Skip to content

Commit 097dbe4

Browse files
authored
Add support for optional telemetry plugin (#1018)
* remove console log accidentally merged with #1013 * set metadata on stream messages in the chat_history array * implement support for optional telemetry plugin * anonymize message & code details * export telemetry hook from NPM package entry point
1 parent 636d5e9 commit 097dbe4

File tree

11 files changed

+322
-113
lines changed

11 files changed

+322
-113
lines changed

packages/jupyter-ai/jupyter_ai/handlers.py

+1
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ def broadcast_message(self, message: Message):
249249
):
250250
stream_message: AgentStreamMessage = history_message
251251
stream_message.body += chunk.content
252+
stream_message.metadata = chunk.metadata
252253
stream_message.complete = chunk.stream_complete
253254
break
254255
elif isinstance(message, PendingMessage):

packages/jupyter-ai/src/components/chat-messages.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ function sortMessages(
7474
export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
7575
const collaborators = useCollaboratorsContext();
7676

77-
if (props.message.type === 'agent-stream' && props.message.complete) {
78-
console.log(props.message.metadata);
79-
}
80-
8177
const sharedStyles: SxProps<Theme> = {
8278
height: '24px',
8379
width: '24px'
@@ -228,8 +224,9 @@ export function ChatMessages(props: ChatMessagesProps): JSX.Element {
228224
sx={{ marginBottom: 3 }}
229225
/>
230226
<RendermimeMarkdown
231-
rmRegistry={props.rmRegistry}
232227
markdownStr={message.body}
228+
rmRegistry={props.rmRegistry}
229+
parentMessage={message}
233230
complete={
234231
message.type === 'agent-stream' ? !!message.complete : true
235232
}

packages/jupyter-ai/src/components/chat.tsx

+69-61
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ import { SelectionContextProvider } from '../contexts/selection-context';
1919
import { SelectionWatcher } from '../selection-watcher';
2020
import { ChatHandler } from '../chat_handler';
2121
import { CollaboratorsContextProvider } from '../contexts/collaborators-context';
22-
import { IJaiCompletionProvider, IJaiMessageFooter } from '../tokens';
22+
import {
23+
IJaiCompletionProvider,
24+
IJaiMessageFooter,
25+
IJaiTelemetryHandler
26+
} from '../tokens';
2327
import {
2428
ActiveCellContextProvider,
2529
ActiveCellManager
2630
} from '../contexts/active-cell-context';
2731
import { ScrollContainer } from './scroll-container';
2832
import { TooltippedIconButton } from './mui-extras/tooltipped-icon-button';
33+
import { TelemetryContextProvider } from '../contexts/telemetry-context';
2934

3035
type ChatBodyProps = {
3136
chatHandler: ChatHandler;
@@ -178,6 +183,7 @@ export type ChatProps = {
178183
activeCellManager: ActiveCellManager;
179184
focusInputSignal: ISignal<unknown, void>;
180185
messageFooter: IJaiMessageFooter | null;
186+
telemetryHandler: IJaiTelemetryHandler | null;
181187
};
182188

183189
enum ChatView {
@@ -201,69 +207,71 @@ export function Chat(props: ChatProps): JSX.Element {
201207
<ActiveCellContextProvider
202208
activeCellManager={props.activeCellManager}
203209
>
204-
<Box
205-
// root box should not include padding as it offsets the vertical
206-
// scrollbar to the left
207-
sx={{
208-
width: '100%',
209-
height: '100%',
210-
boxSizing: 'border-box',
211-
background: 'var(--jp-layout-color0)',
212-
display: 'flex',
213-
flexDirection: 'column'
214-
}}
215-
>
216-
{/* top bar */}
217-
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
218-
{view !== ChatView.Chat ? (
219-
<IconButton onClick={() => setView(ChatView.Chat)}>
220-
<ArrowBackIcon />
221-
</IconButton>
222-
) : (
223-
<Box />
224-
)}
225-
{view === ChatView.Chat ? (
226-
<Box sx={{ display: 'flex' }}>
227-
{!showWelcomeMessage && (
228-
<TooltippedIconButton
229-
onClick={() =>
230-
props.chatHandler.sendMessage({ type: 'clear' })
231-
}
232-
tooltip="New chat"
233-
>
234-
<AddIcon />
235-
</TooltippedIconButton>
236-
)}
237-
<IconButton onClick={() => openSettingsView()}>
238-
<SettingsIcon />
210+
<TelemetryContextProvider telemetryHandler={props.telemetryHandler}>
211+
<Box
212+
// root box should not include padding as it offsets the vertical
213+
// scrollbar to the left
214+
sx={{
215+
width: '100%',
216+
height: '100%',
217+
boxSizing: 'border-box',
218+
background: 'var(--jp-layout-color0)',
219+
display: 'flex',
220+
flexDirection: 'column'
221+
}}
222+
>
223+
{/* top bar */}
224+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
225+
{view !== ChatView.Chat ? (
226+
<IconButton onClick={() => setView(ChatView.Chat)}>
227+
<ArrowBackIcon />
239228
</IconButton>
240-
</Box>
241-
) : (
242-
<Box />
229+
) : (
230+
<Box />
231+
)}
232+
{view === ChatView.Chat ? (
233+
<Box sx={{ display: 'flex' }}>
234+
{!showWelcomeMessage && (
235+
<TooltippedIconButton
236+
onClick={() =>
237+
props.chatHandler.sendMessage({ type: 'clear' })
238+
}
239+
tooltip="New chat"
240+
>
241+
<AddIcon />
242+
</TooltippedIconButton>
243+
)}
244+
<IconButton onClick={() => openSettingsView()}>
245+
<SettingsIcon />
246+
</IconButton>
247+
</Box>
248+
) : (
249+
<Box />
250+
)}
251+
</Box>
252+
{/* body */}
253+
{view === ChatView.Chat && (
254+
<ChatBody
255+
chatHandler={props.chatHandler}
256+
openSettingsView={openSettingsView}
257+
showWelcomeMessage={showWelcomeMessage}
258+
setShowWelcomeMessage={setShowWelcomeMessage}
259+
rmRegistry={props.rmRegistry}
260+
focusInputSignal={props.focusInputSignal}
261+
messageFooter={props.messageFooter}
262+
/>
263+
)}
264+
{view === ChatView.Settings && (
265+
<ChatSettings
266+
rmRegistry={props.rmRegistry}
267+
completionProvider={props.completionProvider}
268+
openInlineCompleterSettings={
269+
props.openInlineCompleterSettings
270+
}
271+
/>
243272
)}
244273
</Box>
245-
{/* body */}
246-
{view === ChatView.Chat && (
247-
<ChatBody
248-
chatHandler={props.chatHandler}
249-
openSettingsView={openSettingsView}
250-
showWelcomeMessage={showWelcomeMessage}
251-
setShowWelcomeMessage={setShowWelcomeMessage}
252-
rmRegistry={props.rmRegistry}
253-
focusInputSignal={props.focusInputSignal}
254-
messageFooter={props.messageFooter}
255-
/>
256-
)}
257-
{view === ChatView.Settings && (
258-
<ChatSettings
259-
rmRegistry={props.rmRegistry}
260-
completionProvider={props.completionProvider}
261-
openInlineCompleterSettings={
262-
props.openInlineCompleterSettings
263-
}
264-
/>
265-
)}
266-
</Box>
274+
</TelemetryContextProvider>
267275
</ActiveCellContextProvider>
268276
</CollaboratorsContextProvider>
269277
</SelectionContextProvider>
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React from 'react';
22
import { Box } from '@mui/material';
3-
import { addAboveIcon, addBelowIcon } from '@jupyterlab/ui-components';
4-
5-
import { CopyButton } from './copy-button';
3+
import {
4+
addAboveIcon,
5+
addBelowIcon,
6+
copyIcon
7+
} from '@jupyterlab/ui-components';
68
import { replaceCellIcon } from '../../icons';
79

810
import {
@@ -11,20 +13,29 @@ import {
1113
} from '../../contexts/active-cell-context';
1214
import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
1315
import { useReplace } from '../../hooks/use-replace';
16+
import { useCopy } from '../../hooks/use-copy';
17+
import { AiService } from '../../handler';
18+
import { useTelemetry } from '../../contexts/telemetry-context';
19+
import { TelemetryEvent } from '../../tokens';
1420

1521
export type CodeToolbarProps = {
1622
/**
1723
* The content of the Markdown code block this component is attached to.
1824
*/
19-
content: string;
25+
code: string;
26+
/**
27+
* Parent message which contains the code referenced by `content`.
28+
*/
29+
parentMessage?: AiService.ChatMessage;
2030
};
2131

2232
export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
2333
const activeCell = useActiveCellContext();
24-
const sharedToolbarButtonProps = {
25-
content: props.content,
34+
const sharedToolbarButtonProps: ToolbarButtonProps = {
35+
code: props.code,
2636
activeCellManager: activeCell.manager,
27-
activeCellExists: activeCell.exists
37+
activeCellExists: activeCell.exists,
38+
parentMessage: props.parentMessage
2839
};
2940

3041
return (
@@ -41,27 +52,68 @@ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
4152
>
4253
<InsertAboveButton {...sharedToolbarButtonProps} />
4354
<InsertBelowButton {...sharedToolbarButtonProps} />
44-
<ReplaceButton value={props.content} />
45-
<CopyButton value={props.content} />
55+
<ReplaceButton {...sharedToolbarButtonProps} />
56+
<CopyButton {...sharedToolbarButtonProps} />
4657
</Box>
4758
);
4859
}
4960

5061
type ToolbarButtonProps = {
51-
content: string;
62+
code: string;
5263
activeCellExists: boolean;
5364
activeCellManager: ActiveCellManager;
65+
parentMessage?: AiService.ChatMessage;
66+
// TODO: parentMessage should always be defined, but this can be undefined
67+
// when the code toolbar appears in Markdown help messages in the Settings
68+
// UI. The Settings UI should use a different component to render Markdown,
69+
// and should never render code toolbars within it.
5470
};
5571

72+
function buildTelemetryEvent(
73+
type: string,
74+
props: ToolbarButtonProps
75+
): TelemetryEvent {
76+
const charCount = props.code.length;
77+
// number of lines = number of newlines + 1
78+
const lineCount = (props.code.match(/\n/g) ?? []).length + 1;
79+
80+
return {
81+
type,
82+
message: {
83+
id: props.parentMessage?.id ?? '',
84+
type: props.parentMessage?.type ?? 'human',
85+
time: props.parentMessage?.time ?? 0,
86+
metadata:
87+
props.parentMessage && 'metadata' in props.parentMessage
88+
? props.parentMessage.metadata
89+
: {}
90+
},
91+
code: {
92+
charCount,
93+
lineCount
94+
}
95+
};
96+
}
97+
5698
function InsertAboveButton(props: ToolbarButtonProps) {
99+
const telemetryHandler = useTelemetry();
57100
const tooltip = props.activeCellExists
58101
? 'Insert above active cell'
59102
: 'Insert above active cell (no active cell)';
60103

61104
return (
62105
<TooltippedIconButton
63106
tooltip={tooltip}
64-
onClick={() => props.activeCellManager.insertAbove(props.content)}
107+
onClick={() => {
108+
props.activeCellManager.insertAbove(props.code);
109+
110+
try {
111+
telemetryHandler.onEvent(buildTelemetryEvent('insert-above', props));
112+
} catch (e) {
113+
console.error(e);
114+
return;
115+
}
116+
}}
65117
disabled={!props.activeCellExists}
66118
>
67119
<addAboveIcon.react height="16px" width="16px" />
@@ -70,6 +122,7 @@ function InsertAboveButton(props: ToolbarButtonProps) {
70122
}
71123

72124
function InsertBelowButton(props: ToolbarButtonProps) {
125+
const telemetryHandler = useTelemetry();
73126
const tooltip = props.activeCellExists
74127
? 'Insert below active cell'
75128
: 'Insert below active cell (no active cell)';
@@ -78,23 +131,67 @@ function InsertBelowButton(props: ToolbarButtonProps) {
78131
<TooltippedIconButton
79132
tooltip={tooltip}
80133
disabled={!props.activeCellExists}
81-
onClick={() => props.activeCellManager.insertBelow(props.content)}
134+
onClick={() => {
135+
props.activeCellManager.insertBelow(props.code);
136+
137+
try {
138+
telemetryHandler.onEvent(buildTelemetryEvent('insert-below', props));
139+
} catch (e) {
140+
console.error(e);
141+
return;
142+
}
143+
}}
82144
>
83145
<addBelowIcon.react height="16px" width="16px" />
84146
</TooltippedIconButton>
85147
);
86148
}
87149

88-
function ReplaceButton(props: { value: string }) {
150+
function ReplaceButton(props: ToolbarButtonProps) {
151+
const telemetryHandler = useTelemetry();
89152
const { replace, replaceDisabled, replaceLabel } = useReplace();
90153

91154
return (
92155
<TooltippedIconButton
93156
tooltip={replaceLabel}
94157
disabled={replaceDisabled}
95-
onClick={() => replace(props.value)}
158+
onClick={() => {
159+
replace(props.code);
160+
161+
try {
162+
telemetryHandler.onEvent(buildTelemetryEvent('replace', props));
163+
} catch (e) {
164+
console.error(e);
165+
return;
166+
}
167+
}}
96168
>
97169
<replaceCellIcon.react height="16px" width="16px" />
98170
</TooltippedIconButton>
99171
);
100172
}
173+
174+
export function CopyButton(props: ToolbarButtonProps): JSX.Element {
175+
const telemetryHandler = useTelemetry();
176+
const { copy, copyLabel } = useCopy();
177+
178+
return (
179+
<TooltippedIconButton
180+
tooltip={copyLabel}
181+
placement="top"
182+
onClick={() => {
183+
copy(props.code);
184+
185+
try {
186+
telemetryHandler.onEvent(buildTelemetryEvent('copy', props));
187+
} catch (e) {
188+
console.error(e);
189+
return;
190+
}
191+
}}
192+
aria-label="Copy to clipboard"
193+
>
194+
<copyIcon.react height="16px" width="16px" />
195+
</TooltippedIconButton>
196+
);
197+
}

0 commit comments

Comments
 (0)