@assistant-ui/react-google-adk 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/AdkClient.d.ts.map +1 -1
  2. package/dist/AdkClient.js +1 -26
  3. package/dist/AdkClient.js.map +1 -1
  4. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  5. package/dist/AdkEventAccumulator.js +10 -20
  6. package/dist/AdkEventAccumulator.js.map +1 -1
  7. package/dist/AdkSessionAdapter.js +2 -2
  8. package/dist/AdkSessionAdapter.js.map +1 -1
  9. package/dist/contentToParts.d.ts +3 -0
  10. package/dist/contentToParts.d.ts.map +1 -0
  11. package/dist/contentToParts.js +36 -0
  12. package/dist/contentToParts.js.map +1 -0
  13. package/dist/convertAdkMessages.d.ts +1 -1
  14. package/dist/convertAdkMessages.d.ts.map +1 -1
  15. package/dist/convertAdkMessages.js +16 -0
  16. package/dist/convertAdkMessages.js.map +1 -1
  17. package/dist/types.d.ts +9 -0
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/types.js.map +1 -1
  20. package/dist/useAdkMessages.d.ts.map +1 -1
  21. package/dist/useAdkMessages.js +3 -27
  22. package/dist/useAdkMessages.js.map +1 -1
  23. package/dist/useAdkRuntime.d.ts +2 -1
  24. package/dist/useAdkRuntime.d.ts.map +1 -1
  25. package/dist/useAdkRuntime.js +25 -7
  26. package/dist/useAdkRuntime.js.map +1 -1
  27. package/package.json +7 -7
  28. package/src/AdkClient.test.ts +99 -0
  29. package/src/AdkClient.ts +4 -31
  30. package/src/AdkEventAccumulator.test.ts +125 -2
  31. package/src/AdkEventAccumulator.ts +30 -20
  32. package/src/AdkSessionAdapter.test.ts +1 -1
  33. package/src/AdkSessionAdapter.ts +3 -3
  34. package/src/contentToParts.ts +38 -0
  35. package/src/convertAdkMessages.test.ts +53 -0
  36. package/src/convertAdkMessages.ts +18 -1
  37. package/src/types.ts +11 -0
  38. package/src/useAdkMessages.test.ts +75 -0
  39. package/src/useAdkMessages.ts +3 -31
  40. package/src/useAdkRuntime.test.ts +74 -1
  41. package/src/useAdkRuntime.ts +25 -6
@@ -78,6 +78,59 @@ describe("convertAdkMessage - ai messages", () => {
78
78
  });
79
79
  });
80
80
 
81
+ it("converts file content parts to file message parts", () => {
82
+ const msg: AdkMessage = {
83
+ id: "m1",
84
+ type: "ai",
85
+ content: [
86
+ {
87
+ type: "file",
88
+ mimeType: "application/pdf",
89
+ data: "JVBERi0xLjQK",
90
+ filename: "report.pdf",
91
+ },
92
+ ],
93
+ };
94
+ const result = convertAdkMessage(msg);
95
+ expect(result).toMatchObject({
96
+ content: [
97
+ {
98
+ type: "file",
99
+ mimeType: "application/pdf",
100
+ data: "JVBERi0xLjQK",
101
+ filename: "report.pdf",
102
+ },
103
+ ],
104
+ });
105
+ });
106
+
107
+ it("converts file_url content parts to data parts", () => {
108
+ const msg: AdkMessage = {
109
+ id: "m1",
110
+ type: "ai",
111
+ content: [
112
+ {
113
+ type: "file_url",
114
+ url: "gs://bucket/report.pdf",
115
+ mimeType: "application/pdf",
116
+ },
117
+ ],
118
+ };
119
+ const result = convertAdkMessage(msg);
120
+ expect(result).toMatchObject({
121
+ content: [
122
+ {
123
+ type: "data",
124
+ name: "file_url",
125
+ data: {
126
+ url: "gs://bucket/report.pdf",
127
+ mimeType: "application/pdf",
128
+ },
129
+ },
130
+ ],
131
+ });
132
+ });
133
+
81
134
  it("converts code content parts to data parts", () => {
82
135
  const msg: AdkMessage = {
83
136
  id: "m1",
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
 
3
3
  import type { ToolCallMessagePart } from "@assistant-ui/core";
4
- import { useExternalMessageConverter } from "@assistant-ui/core/react";
4
+ import type { useExternalMessageConverter } from "@assistant-ui/core/react";
5
5
  import type { AdkMessage, AdkMessageContentPart } from "./types";
6
6
 
7
7
  type ContentPart =
8
8
  | { type: "text"; text: string }
9
9
  | { type: "reasoning"; text: string }
10
10
  | { type: "image"; image: string }
11
+ | { type: "file"; data: string; mimeType: string; filename?: string }
11
12
  | { type: "data"; name: string; data: unknown };
12
13
 
13
14
  const contentToParts = (content: AdkMessage["content"]): ContentPart[] => {
@@ -28,6 +29,22 @@ const contentToParts = (content: AdkMessage["content"]): ContentPart[] => {
28
29
  };
29
30
  case "image_url":
30
31
  return { type: "image", image: part.url };
32
+ case "file":
33
+ return {
34
+ type: "file",
35
+ data: part.data,
36
+ mimeType: part.mimeType,
37
+ ...(part.filename != null && { filename: part.filename }),
38
+ };
39
+ case "file_url":
40
+ return {
41
+ type: "data",
42
+ name: "file_url",
43
+ data: {
44
+ url: part.url,
45
+ ...(part.mimeType != null && { mimeType: part.mimeType }),
46
+ },
47
+ };
31
48
  case "code":
32
49
  return {
33
50
  type: "data",
package/src/types.ts CHANGED
@@ -109,6 +109,17 @@ export type AdkMessageContentPart =
109
109
  | { type: "reasoning"; text: string }
110
110
  | { type: "image"; mimeType: string; data: string }
111
111
  | { type: "image_url"; url: string }
112
+ | {
113
+ type: "file";
114
+ mimeType: string;
115
+ data: string;
116
+ filename?: string | undefined;
117
+ }
118
+ | {
119
+ type: "file_url";
120
+ url: string;
121
+ mimeType?: string | undefined;
122
+ }
112
123
  | { type: "code"; code: string; language: string }
113
124
  | { type: "code_result"; output: string; outcome: string }
114
125
  | { type: "activity"; message: string };
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { messageToEvent } from "./useAdkMessages";
3
+ import type { AdkMessage } from "./types";
4
+
5
+ describe("messageToEvent (contentToParts)", () => {
6
+ it("serializes a file content part as inlineData", () => {
7
+ const msg: AdkMessage = {
8
+ id: "m1",
9
+ type: "human",
10
+ content: [
11
+ {
12
+ type: "file",
13
+ mimeType: "application/pdf",
14
+ data: "JVBERi0xLjQK",
15
+ filename: "report.pdf",
16
+ },
17
+ ],
18
+ };
19
+ const event = messageToEvent(msg);
20
+ expect(event.content?.parts).toEqual([
21
+ { inlineData: { mimeType: "application/pdf", data: "JVBERi0xLjQK" } },
22
+ ]);
23
+ });
24
+
25
+ it("serializes a file_url content part as fileData with mimeType", () => {
26
+ const msg: AdkMessage = {
27
+ id: "m1",
28
+ type: "human",
29
+ content: [
30
+ {
31
+ type: "file_url",
32
+ url: "gs://bucket/report.pdf",
33
+ mimeType: "application/pdf",
34
+ },
35
+ ],
36
+ };
37
+ const event = messageToEvent(msg);
38
+ expect(event.content?.parts).toEqual([
39
+ {
40
+ fileData: {
41
+ fileUri: "gs://bucket/report.pdf",
42
+ mimeType: "application/pdf",
43
+ },
44
+ },
45
+ ]);
46
+ });
47
+
48
+ it("serializes a file_url without mimeType as bare fileData", () => {
49
+ const msg: AdkMessage = {
50
+ id: "m1",
51
+ type: "human",
52
+ content: [{ type: "file_url", url: "gs://bucket/unknown" }],
53
+ };
54
+ const event = messageToEvent(msg);
55
+ expect(event.content?.parts).toEqual([
56
+ { fileData: { fileUri: "gs://bucket/unknown" } },
57
+ ]);
58
+ });
59
+
60
+ it("serializes mixed text + file content as multiple parts", () => {
61
+ const msg: AdkMessage = {
62
+ id: "m1",
63
+ type: "human",
64
+ content: [
65
+ { type: "text", text: "see attached" },
66
+ { type: "file", mimeType: "image/png", data: "AAAA" },
67
+ ],
68
+ };
69
+ const event = messageToEvent(msg);
70
+ expect(event.content?.parts).toEqual([
71
+ { text: "see attached" },
72
+ { inlineData: { mimeType: "image/png", data: "AAAA" } },
73
+ ]);
74
+ });
75
+ });
@@ -3,11 +3,10 @@ import { useState, useCallback, useRef, useMemo } from "react";
3
3
  import { v4 as uuidv4 } from "uuid";
4
4
  import { useAui } from "@assistant-ui/store";
5
5
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
6
+ import { contentToParts } from "./contentToParts";
6
7
  import type {
7
8
  AdkEvent,
8
- AdkEventPart,
9
9
  AdkMessage,
10
- AdkMessageContentPart,
11
10
  AdkMessageMetadata,
12
11
  AdkSendMessageConfig,
13
12
  AdkStreamCallback,
@@ -200,7 +199,8 @@ export const useAdkMessages = ({
200
199
  };
201
200
  };
202
201
 
203
- const messageToEvent = (msg: AdkMessage): AdkEvent => {
202
+ /** @internal — exported for unit tests. */
203
+ export const messageToEvent = (msg: AdkMessage): AdkEvent => {
204
204
  if (msg.type === "human") {
205
205
  return {
206
206
  id: msg.id ?? uuidv4(),
@@ -246,31 +246,3 @@ const messageToEvent = (msg: AdkMessage): AdkEvent => {
246
246
  };
247
247
  return result;
248
248
  };
249
-
250
- const contentToParts = (
251
- content: string | AdkMessageContentPart[],
252
- ): AdkEventPart[] => {
253
- if (typeof content === "string") return [{ text: content }];
254
- return content.map((part) => {
255
- switch (part.type) {
256
- case "text":
257
- return { text: part.text };
258
- case "reasoning":
259
- return { text: part.text, thought: true };
260
- case "image":
261
- return { inlineData: { mimeType: part.mimeType, data: part.data } };
262
- case "image_url":
263
- return { fileData: { fileUri: part.url } };
264
- case "code":
265
- return {
266
- executableCode: { code: part.code, language: part.language },
267
- };
268
- case "code_result":
269
- return {
270
- codeExecutionResult: { output: part.output, outcome: part.outcome },
271
- };
272
- default:
273
- return { text: "" };
274
- }
275
- });
276
- };
@@ -1,7 +1,23 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { getPendingCancellations, getPendingToolCalls } from "./useAdkRuntime";
2
+ import {
3
+ getMessageContent,
4
+ getPendingCancellations,
5
+ getPendingToolCalls,
6
+ } from "./useAdkRuntime";
7
+ import type { AppendMessage } from "@assistant-ui/core";
3
8
  import type { AdkMessage } from "./types";
4
9
 
10
+ const makeAppendMessage = (content: AppendMessage["content"]): AppendMessage =>
11
+ ({
12
+ role: "user",
13
+ content,
14
+ attachments: [],
15
+ parentId: null,
16
+ sourceId: null,
17
+ runConfig: undefined,
18
+ metadata: { custom: {} },
19
+ }) as unknown as AppendMessage;
20
+
5
21
  const aiWithToolCalls = (
6
22
  id: string,
7
23
  toolCalls: Array<{ id: string; name: string }>,
@@ -136,3 +152,60 @@ describe("getPendingCancellations", () => {
136
152
  });
137
153
  });
138
154
  });
155
+
156
+ describe("getMessageContent", () => {
157
+ it("preserves file part data and mimeType end-to-end", () => {
158
+ const result = getMessageContent(
159
+ makeAppendMessage([
160
+ { type: "text", text: "see attached" },
161
+ {
162
+ type: "file",
163
+ mimeType: "application/pdf",
164
+ data: "JVBERi0xLjQK",
165
+ filename: "report.pdf",
166
+ },
167
+ ]),
168
+ );
169
+ expect(result).toEqual([
170
+ { type: "text", text: "see attached" },
171
+ {
172
+ type: "file",
173
+ mimeType: "application/pdf",
174
+ data: "JVBERi0xLjQK",
175
+ filename: "report.pdf",
176
+ },
177
+ ]);
178
+ });
179
+
180
+ it("keeps file-only content as an array (does not collapse to a text marker)", () => {
181
+ const result = getMessageContent(
182
+ makeAppendMessage([
183
+ {
184
+ type: "file",
185
+ mimeType: "image/png",
186
+ data: "AAAA",
187
+ filename: "x.png",
188
+ },
189
+ ]),
190
+ );
191
+ expect(result).toEqual([
192
+ {
193
+ type: "file",
194
+ mimeType: "image/png",
195
+ data: "AAAA",
196
+ filename: "x.png",
197
+ },
198
+ ]);
199
+ });
200
+
201
+ it("omits filename when not provided", () => {
202
+ const result = getMessageContent(
203
+ makeAppendMessage([
204
+ { type: "file", mimeType: "application/pdf", data: "AAAA" },
205
+ ]),
206
+ );
207
+ expect(result).toEqual([
208
+ { type: "file", mimeType: "application/pdf", data: "AAAA" },
209
+ ]);
210
+ });
211
+ });
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
3
3
  import {
4
4
  getExternalStoreMessages,
5
5
  type AttachmentAdapter,
6
+ type DictationAdapter,
6
7
  type FeedbackAdapter,
7
8
  type SpeechSynthesisAdapter,
8
9
  type AppendMessage,
@@ -32,7 +33,8 @@ import { convertAdkMessage } from "./convertAdkMessages";
32
33
  import { symbolAdkRuntimeExtras, type AdkRuntimeExtras } from "./hooks";
33
34
  import { v4 as uuidv4 } from "uuid";
34
35
 
35
- const getMessageContent = (msg: AppendMessage) => {
36
+ /** @internal — exported for unit tests. */
37
+ export const getMessageContent = (msg: AppendMessage) => {
36
38
  const allContent = [
37
39
  ...msg.content,
38
40
  ...(msg.attachments?.flatMap((a) => a.content) ?? []),
@@ -46,19 +48,22 @@ const getMessageContent = (msg: AppendMessage) => {
46
48
  return { type: "image_url" as const, url: part.image };
47
49
  case "file":
48
50
  return {
49
- type: "text" as const,
50
- text: `[File: ${part.filename ?? "file"}]`,
51
+ type: "file" as const,
52
+ mimeType: part.mimeType,
53
+ data: part.data,
54
+ ...(part.filename != null && { filename: part.filename }),
51
55
  };
52
56
 
53
57
  case "tool-call":
54
58
  throw new Error("Tool call appends are not supported.");
55
59
 
56
- default:
60
+ default: {
57
61
  const _exhaustiveCheck: "reasoning" | "source" | "audio" | "data" =
58
62
  type;
59
63
  throw new Error(
60
64
  `Unsupported append message part type: ${_exhaustiveCheck}`,
61
65
  );
66
+ }
62
67
  }
63
68
  });
64
69
 
@@ -143,6 +148,7 @@ export type UseAdkRuntimeOptions = {
143
148
  | {
144
149
  attachments?: AttachmentAdapter;
145
150
  speech?: SpeechSynthesisAdapter;
151
+ dictation?: DictationAdapter;
146
152
  feedback?: FeedbackAdapter;
147
153
  }
148
154
  | undefined;
@@ -163,13 +169,14 @@ export type UseAdkRuntimeOptions = {
163
169
 
164
170
  const useAdkRuntimeImpl = ({
165
171
  autoCancelPendingToolCalls,
166
- adapters: { attachments, feedback, speech } = {},
172
+ adapters: { attachments, dictation, feedback, speech } = {},
167
173
  unstable_allowCancellation,
168
174
  stream,
169
175
  load,
170
176
  getCheckpointId,
171
177
  eventHandlers,
172
178
  }: UseAdkRuntimeOptions) => {
179
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
173
180
  const aui = useAui();
174
181
  const {
175
182
  messages,
@@ -184,12 +191,15 @@ const useAdkRuntimeImpl = ({
184
191
  sendMessage,
185
192
  cancel,
186
193
  replaceMessages,
194
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
187
195
  } = useAdkMessages({
188
196
  stream,
189
197
  ...(eventHandlers && { eventHandlers }),
190
198
  });
191
199
 
200
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
192
201
  const [isRunning, setIsRunning] = useState(false);
202
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
193
203
  const [toolStatuses, setToolStatuses] = useState<
194
204
  Record<string, ToolExecutionStatus>
195
205
  >({});
@@ -210,21 +220,25 @@ const useAdkRuntimeImpl = ({
210
220
  }
211
221
  };
212
222
 
223
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
213
224
  const threadMessages = useExternalMessageConverter({
214
225
  callback: convertAdkMessage,
215
226
  messages,
216
227
  isRunning: effectiveIsRunning,
217
228
  });
218
229
 
230
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
219
231
  const threadMessagesRef = useRef(threadMessages);
220
232
  threadMessagesRef.current = threadMessages;
221
233
 
234
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
222
235
  const [runtimeRef] = useState(() => ({
223
236
  get current() {
224
237
  return runtime;
225
238
  },
226
239
  }));
227
240
 
241
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
228
242
  const toolInvocations = useToolInvocations({
229
243
  state: { messages: threadMessages, isRunning: effectiveIsRunning },
230
244
  getTools: () => runtimeRef.current.thread.getModelContext().tools,
@@ -248,10 +262,11 @@ const useAdkRuntimeImpl = ({
248
262
  setToolStatuses,
249
263
  });
250
264
 
265
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
251
266
  const runtime = useExternalStoreRuntime({
252
267
  isRunning: effectiveIsRunning,
253
268
  messages: threadMessages,
254
- adapters: { attachments, feedback, speech },
269
+ adapters: { attachments, dictation, feedback, speech },
255
270
  extras: {
256
271
  [symbolAdkRuntimeExtras]: true,
257
272
  agentInfo,
@@ -362,11 +377,14 @@ const useAdkRuntimeImpl = ({
362
377
  });
363
378
 
364
379
  {
380
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
365
381
  const loadRef = useRef(load);
382
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
366
383
  useEffect(() => {
367
384
  loadRef.current = load;
368
385
  });
369
386
 
387
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
370
388
  useEffect(() => {
371
389
  const loadFn = loadRef.current;
372
390
  if (!loadFn) return;
@@ -410,6 +428,7 @@ export const useAdkRuntime = ({
410
428
 
411
429
  return useRemoteThreadListRuntime({
412
430
  runtimeHook: function RuntimeHook() {
431
+ // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
413
432
  return useAdkRuntimeImpl(options);
414
433
  },
415
434
  adapter,