@assistant-ui/react-google-adk 0.0.7 → 0.0.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-google-adk",
3
- "version": "0.0.7",
3
+ "version": "0.0.10",
4
4
  "description": "Google ADK adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "google-adk",
@@ -34,10 +34,10 @@
34
34
  ],
35
35
  "sideEffects": false,
36
36
  "dependencies": {
37
- "@assistant-ui/core": "^0.1.15",
38
- "@assistant-ui/store": "^0.2.8",
37
+ "@assistant-ui/core": "^0.2.0",
38
+ "@assistant-ui/store": "^0.2.10",
39
39
  "assistant-cloud": "*",
40
- "assistant-stream": "^0.3.12",
40
+ "assistant-stream": "^0.3.13",
41
41
  "uuid": "^14.0.0"
42
42
  },
43
43
  "peerDependencies": {
@@ -58,7 +58,7 @@
58
58
  "@types/uuid": "^11.0.0",
59
59
  "react": "^19.2.5",
60
60
  "vitest": "^4.1.5",
61
- "@assistant-ui/x-buildutils": "0.0.6"
61
+ "@assistant-ui/x-buildutils": "0.0.7"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public",
@@ -137,6 +137,37 @@ describe("createAdkStream - proxy mode", () => {
137
137
  });
138
138
  });
139
139
 
140
+ it("sends file parts as inlineData in proxy mode", async () => {
141
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
142
+
143
+ const stream = createAdkStream({ api: "/api/adk" });
144
+ const messages: AdkMessage[] = [
145
+ {
146
+ id: "m1",
147
+ type: "human",
148
+ content: [
149
+ { type: "text", text: "see attached" },
150
+ {
151
+ type: "file",
152
+ mimeType: "application/pdf",
153
+ data: "JVBERi0xLjQK",
154
+ filename: "report.pdf",
155
+ },
156
+ ],
157
+ },
158
+ ];
159
+ const gen = await stream(messages, makeConfig());
160
+ for await (const _ of gen) {
161
+ /* noop */
162
+ }
163
+
164
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
165
+ expect(body.parts).toHaveLength(2);
166
+ expect(body.parts[1]).toEqual({
167
+ inlineData: { mimeType: "application/pdf", data: "JVBERi0xLjQK" },
168
+ });
169
+ });
170
+
140
171
  it("sends parts array when multiple messages are provided", async () => {
141
172
  mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
142
173
 
@@ -608,6 +639,74 @@ describe("createAdkStream - content conversion", () => {
608
639
  });
609
640
  });
610
641
 
642
+ it("converts file content parts to inlineData in direct mode", async () => {
643
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
644
+
645
+ const stream = createAdkStream({
646
+ api: "http://localhost:8000",
647
+ appName: "app",
648
+ userId: "u",
649
+ });
650
+ const messages: AdkMessage[] = [
651
+ {
652
+ id: "m1",
653
+ type: "human",
654
+ content: [
655
+ {
656
+ type: "file",
657
+ mimeType: "application/pdf",
658
+ data: "JVBERi0xLjQK",
659
+ filename: "report.pdf",
660
+ },
661
+ ],
662
+ },
663
+ ];
664
+ const gen = await stream(messages, makeConfig());
665
+ for await (const _ of gen) {
666
+ /* noop */
667
+ }
668
+
669
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
670
+ expect(body.newMessage.parts[0]).toEqual({
671
+ inlineData: { mimeType: "application/pdf", data: "JVBERi0xLjQK" },
672
+ });
673
+ });
674
+
675
+ it("converts file_url content parts to fileData with mimeType in direct mode", async () => {
676
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
677
+
678
+ const stream = createAdkStream({
679
+ api: "http://localhost:8000",
680
+ appName: "app",
681
+ userId: "u",
682
+ });
683
+ const messages: AdkMessage[] = [
684
+ {
685
+ id: "m1",
686
+ type: "human",
687
+ content: [
688
+ {
689
+ type: "file_url",
690
+ url: "gs://bucket/report.pdf",
691
+ mimeType: "application/pdf",
692
+ },
693
+ ],
694
+ },
695
+ ];
696
+ const gen = await stream(messages, makeConfig());
697
+ for await (const _ of gen) {
698
+ /* noop */
699
+ }
700
+
701
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
702
+ expect(body.newMessage.parts[0]).toEqual({
703
+ fileData: {
704
+ fileUri: "gs://bucket/report.pdf",
705
+ mimeType: "application/pdf",
706
+ },
707
+ });
708
+ });
709
+
611
710
  it("converts code content parts to executableCode", async () => {
612
711
  mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
613
712
 
package/src/AdkClient.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { contentToParts } from "./contentToParts";
1
2
  import type {
2
3
  AdkEvent,
4
+ AdkEventPart,
3
5
  AdkMessage,
4
- AdkMessageContentPart,
5
6
  AdkStreamCallback,
6
7
  } from "./types";
7
8
 
@@ -119,9 +120,9 @@ async function resolveHeaders(
119
120
  */
120
121
  function messagesToContent(messages: AdkMessage[]): {
121
122
  role: string;
122
- parts: Array<Record<string, unknown>>;
123
+ parts: AdkEventPart[];
123
124
  } {
124
- const parts: Array<Record<string, unknown>> = [];
125
+ const parts: AdkEventPart[] = [];
125
126
 
126
127
  for (const msg of messages) {
127
128
  if (msg.type === "human") {
@@ -209,34 +210,6 @@ function messagesToProxyBody(
209
210
  return body;
210
211
  }
211
212
 
212
- function contentToParts(
213
- content: string | AdkMessageContentPart[],
214
- ): Array<Record<string, unknown>> {
215
- if (typeof content === "string") return [{ text: content }];
216
- return content.map((part) => {
217
- switch (part.type) {
218
- case "text":
219
- return { text: part.text };
220
- case "reasoning":
221
- return { text: part.text, thought: true };
222
- case "image":
223
- return { inlineData: { mimeType: part.mimeType, data: part.data } };
224
- case "image_url":
225
- return { fileData: { fileUri: part.url } };
226
- case "code":
227
- return {
228
- executableCode: { code: part.code, language: part.language },
229
- };
230
- case "code_result":
231
- return {
232
- codeExecutionResult: { output: part.output, outcome: part.outcome },
233
- };
234
- default:
235
- return { text: "" };
236
- }
237
- });
238
- }
239
-
240
213
  async function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {
241
214
  const reader = response.body!.getReader();
242
215
  const decoder = new TextDecoder();
@@ -1085,14 +1085,21 @@ describe("AdkEventAccumulator - user message handling", () => {
1085
1085
  });
1086
1086
  });
1087
1087
 
1088
- it("creates human message with fileData content", () => {
1088
+ it("creates human message with image fileData content", () => {
1089
1089
  const acc = new AdkEventAccumulator();
1090
1090
  const msgs = acc.processEvent(
1091
1091
  makeEvent({
1092
1092
  author: "user",
1093
1093
  content: {
1094
1094
  role: "user",
1095
- parts: [{ fileData: { fileUri: "gs://bucket/image.png" } }],
1095
+ parts: [
1096
+ {
1097
+ fileData: {
1098
+ fileUri: "gs://bucket/image.png",
1099
+ mimeType: "image/png",
1100
+ },
1101
+ },
1102
+ ],
1096
1103
  },
1097
1104
  }),
1098
1105
  );
@@ -1103,6 +1110,122 @@ describe("AdkEventAccumulator - user message handling", () => {
1103
1110
  });
1104
1111
  });
1105
1112
 
1113
+ it("routes non-image inlineData to a file part", () => {
1114
+ const acc = new AdkEventAccumulator();
1115
+ const msgs = acc.processEvent(
1116
+ makeEvent({
1117
+ author: "user",
1118
+ content: {
1119
+ role: "user",
1120
+ parts: [
1121
+ {
1122
+ inlineData: { mimeType: "application/pdf", data: "JVBERi0xLjQK" },
1123
+ },
1124
+ ],
1125
+ },
1126
+ }),
1127
+ );
1128
+ expect(msgs).toHaveLength(1);
1129
+ expect(msgs[0]).toMatchObject({
1130
+ type: "human",
1131
+ content: [
1132
+ { type: "file", mimeType: "application/pdf", data: "JVBERi0xLjQK" },
1133
+ ],
1134
+ });
1135
+ });
1136
+
1137
+ it("routes non-image fileData to a file_url part", () => {
1138
+ const acc = new AdkEventAccumulator();
1139
+ const msgs = acc.processEvent(
1140
+ makeEvent({
1141
+ author: "user",
1142
+ content: {
1143
+ role: "user",
1144
+ parts: [
1145
+ {
1146
+ fileData: {
1147
+ fileUri: "gs://bucket/report.pdf",
1148
+ mimeType: "application/pdf",
1149
+ },
1150
+ },
1151
+ ],
1152
+ },
1153
+ }),
1154
+ );
1155
+ expect(msgs).toHaveLength(1);
1156
+ expect(msgs[0]).toMatchObject({
1157
+ type: "human",
1158
+ content: [
1159
+ {
1160
+ type: "file_url",
1161
+ url: "gs://bucket/report.pdf",
1162
+ mimeType: "application/pdf",
1163
+ },
1164
+ ],
1165
+ });
1166
+ });
1167
+
1168
+ it("coerces fileData with image mime to image_url even if it originated as file_url (intentional, image mime is canonical)", () => {
1169
+ const acc = new AdkEventAccumulator();
1170
+ const msgs = acc.processEvent(
1171
+ makeEvent({
1172
+ author: "user",
1173
+ content: {
1174
+ role: "user",
1175
+ parts: [
1176
+ {
1177
+ fileData: {
1178
+ fileUri: "https://example.com/photo.jpg",
1179
+ mimeType: "image/jpeg",
1180
+ },
1181
+ },
1182
+ ],
1183
+ },
1184
+ }),
1185
+ );
1186
+ expect(msgs).toHaveLength(1);
1187
+ expect(msgs[0]).toMatchObject({
1188
+ type: "human",
1189
+ content: [{ type: "image_url", url: "https://example.com/photo.jpg" }],
1190
+ });
1191
+ });
1192
+
1193
+ it("falls back to image_url for fileData with no mimeType (preserves legacy round-trip)", () => {
1194
+ const acc = new AdkEventAccumulator();
1195
+ const msgs = acc.processEvent(
1196
+ makeEvent({
1197
+ author: "user",
1198
+ content: {
1199
+ role: "user",
1200
+ parts: [{ fileData: { fileUri: "gs://bucket/unknown" } }],
1201
+ },
1202
+ }),
1203
+ );
1204
+ expect(msgs).toHaveLength(1);
1205
+ expect(msgs[0]).toMatchObject({
1206
+ type: "human",
1207
+ content: [{ type: "image_url", url: "gs://bucket/unknown" }],
1208
+ });
1209
+ });
1210
+
1211
+ it("routes assistant non-image inlineData to a file part", () => {
1212
+ const acc = new AdkEventAccumulator();
1213
+ const msgs = acc.processEvent(
1214
+ makeEvent({
1215
+ author: "assistant",
1216
+ content: {
1217
+ role: "model",
1218
+ parts: [{ inlineData: { mimeType: "audio/mpeg", data: "QUJDRA==" } }],
1219
+ },
1220
+ }),
1221
+ );
1222
+ expect(msgs).toHaveLength(1);
1223
+ expect(msgs[0]).toMatchObject({
1224
+ type: "ai",
1225
+ content: [{ type: "file", mimeType: "audio/mpeg", data: "QUJDRA==" }],
1226
+ });
1227
+ });
1228
+
1106
1229
  it("tool result events (no author, role:'user') still create tool messages", () => {
1107
1230
  // Regression: the user-author check must not hijack tool events.
1108
1231
  // messageToEvent for `type:'tool'` emits events without `author`,
@@ -80,6 +80,22 @@ const finishReasonToStatus = (
80
80
  return { type: "complete", reason: "stop" };
81
81
  };
82
82
 
83
+ const inlineDataToPart = (
84
+ mimeType: string,
85
+ data: string,
86
+ ): AdkMessageContentPart =>
87
+ mimeType.startsWith("image/")
88
+ ? { type: "image", mimeType, data }
89
+ : { type: "file", mimeType, data };
90
+
91
+ const fileDataToPart = (
92
+ fileUri: string,
93
+ mimeType: string | undefined,
94
+ ): AdkMessageContentPart =>
95
+ mimeType == null || mimeType.startsWith("image/")
96
+ ? { type: "image_url", url: fileUri }
97
+ : { type: "file_url", url: fileUri, mimeType };
98
+
83
99
  // ── Snake_case normalization ──
84
100
 
85
101
  const normalizeEventPart = (part: AdkEventPart): AdkEventPart => {
@@ -299,16 +315,13 @@ export class AdkEventAccumulator {
299
315
  if (part.text != null && !part.thought) {
300
316
  humanParts.push({ type: "text", text: part.text });
301
317
  } else if (part.inlineData) {
302
- humanParts.push({
303
- type: "image",
304
- mimeType: part.inlineData.mimeType,
305
- data: part.inlineData.data,
306
- });
318
+ humanParts.push(
319
+ inlineDataToPart(part.inlineData.mimeType, part.inlineData.data),
320
+ );
307
321
  } else if (part.fileData) {
308
- humanParts.push({
309
- type: "image_url",
310
- url: part.fileData.fileUri,
311
- });
322
+ humanParts.push(
323
+ fileDataToPart(part.fileData.fileUri, part.fileData.mimeType),
324
+ );
312
325
  }
313
326
  }
314
327
  if (humanParts.length > 0) {
@@ -495,24 +508,21 @@ export class AdkEventAccumulator {
495
508
  return;
496
509
  }
497
510
 
498
- // Inline data (images etc)
499
511
  if (part.inlineData) {
500
512
  const msg = this.getOrCreateAiMessage(event);
501
- this.appendContent(msg, {
502
- type: "image",
503
- mimeType: part.inlineData.mimeType,
504
- data: part.inlineData.data,
505
- });
513
+ this.appendContent(
514
+ msg,
515
+ inlineDataToPart(part.inlineData.mimeType, part.inlineData.data),
516
+ );
506
517
  return;
507
518
  }
508
519
 
509
- // File data (URI reference)
510
520
  if (part.fileData) {
511
521
  const msg = this.getOrCreateAiMessage(event);
512
- this.appendContent(msg, {
513
- type: "image_url",
514
- url: part.fileData.fileUri,
515
- });
522
+ this.appendContent(
523
+ msg,
524
+ fileDataToPart(part.fileData.fileUri, part.fileData.mimeType),
525
+ );
516
526
  }
517
527
  }
518
528
 
@@ -0,0 +1,38 @@
1
+ import type { AdkEventPart, AdkMessage, AdkMessageContentPart } from "./types";
2
+
3
+ export const contentToParts = (
4
+ content: AdkMessage["content"],
5
+ ): AdkEventPart[] => {
6
+ if (typeof content === "string") return [{ text: content }];
7
+ return (content as AdkMessageContentPart[]).map((part): AdkEventPart => {
8
+ switch (part.type) {
9
+ case "text":
10
+ return { text: part.text };
11
+ case "reasoning":
12
+ return { text: part.text, thought: true };
13
+ case "image":
14
+ return { inlineData: { mimeType: part.mimeType, data: part.data } };
15
+ case "image_url":
16
+ return { fileData: { fileUri: part.url } };
17
+ case "file":
18
+ return { inlineData: { mimeType: part.mimeType, data: part.data } };
19
+ case "file_url":
20
+ return {
21
+ fileData: {
22
+ fileUri: part.url,
23
+ ...(part.mimeType != null && { mimeType: part.mimeType }),
24
+ },
25
+ };
26
+ case "code":
27
+ return {
28
+ executableCode: { code: part.code, language: part.language },
29
+ };
30
+ case "code_result":
31
+ return {
32
+ codeExecutionResult: { output: part.output, outcome: part.outcome },
33
+ };
34
+ default:
35
+ return { text: "" };
36
+ }
37
+ });
38
+ };
@@ -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",
@@ -8,6 +8,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
+ });