@assistant-ui/react-google-adk 0.0.7 → 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.
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +1 -26
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +10 -20
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/contentToParts.d.ts +3 -0
- package/dist/contentToParts.d.ts.map +1 -0
- package/dist/contentToParts.js +36 -0
- package/dist/contentToParts.js.map +1 -0
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +16 -0
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +3 -27
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +2 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +8 -5
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +3 -3
- package/src/AdkClient.test.ts +99 -0
- package/src/AdkClient.ts +4 -31
- package/src/AdkEventAccumulator.test.ts +125 -2
- package/src/AdkEventAccumulator.ts +30 -20
- package/src/contentToParts.ts +38 -0
- package/src/convertAdkMessages.test.ts +53 -0
- package/src/convertAdkMessages.ts +17 -0
- package/src/types.ts +11 -0
- package/src/useAdkMessages.test.ts +75 -0
- package/src/useAdkMessages.ts +3 -31
- package/src/useAdkRuntime.test.ts +74 -1
- package/src/useAdkRuntime.ts +10 -5
package/src/AdkClient.test.ts
CHANGED
|
@@ -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:
|
|
123
|
+
parts: AdkEventPart[];
|
|
123
124
|
} {
|
|
124
|
-
const parts:
|
|
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: [
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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(
|
|
502
|
-
|
|
503
|
-
mimeType
|
|
504
|
-
|
|
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(
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
});
|
package/src/useAdkMessages.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
};
|