@effect-uai/google 0.11.0 → 0.12.1
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/Gemini.d.mts +8 -2
- package/dist/Gemini.d.mts.map +1 -1
- package/dist/Gemini.mjs +1 -1
- package/dist/Gemini.mjs.map +1 -1
- package/dist/GeminiEmbedding.d.mts +2 -3
- package/dist/GeminiEmbedding.d.mts.map +1 -1
- package/dist/GeminiEmbedding.mjs +1 -1
- package/dist/GeminiEmbedding.mjs.map +1 -1
- package/dist/GeminiSynthesizer.d.mts +14 -6
- package/dist/GeminiSynthesizer.d.mts.map +1 -1
- package/dist/GeminiSynthesizer.mjs +13 -12
- package/dist/GeminiSynthesizer.mjs.map +1 -1
- package/dist/GeminiSynthesizer.test.d.mts +1 -1
- package/dist/GeminiSynthesizer.test.mjs +1 -1
- package/dist/GeminiSynthesizer.test.mjs.map +1 -1
- package/dist/GeminiTools.d.mts +9 -5
- package/dist/GeminiTools.d.mts.map +1 -1
- package/dist/GeminiTools.mjs +1 -1
- package/dist/GeminiTools.mjs.map +1 -1
- package/dist/GoogleDeepResearch.d.mts +58 -7
- package/dist/GoogleDeepResearch.d.mts.map +1 -1
- package/dist/GoogleDeepResearch.mjs +77 -23
- package/dist/GoogleDeepResearch.mjs.map +1 -1
- package/dist/GoogleDeepResearch.test.d.mts +1 -0
- package/dist/GoogleDeepResearch.test.mjs +66 -0
- package/dist/GoogleDeepResearch.test.mjs.map +1 -0
- package/dist/LyriaGenerator.d.mts +0 -2
- package/dist/LyriaGenerator.d.mts.map +1 -1
- package/dist/LyriaGenerator.mjs +9 -6
- package/dist/LyriaGenerator.mjs.map +1 -1
- package/dist/LyriaGenerator.test.d.mts +1 -1
- package/dist/LyriaGenerator.test.mjs +153 -8
- package/dist/LyriaGenerator.test.mjs.map +1 -1
- package/dist/codec.d.mts +36 -11
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +27 -4
- package/dist/codec.mjs.map +1 -1
- package/dist/geminiSpeechCodec.d.mts +0 -1
- package/dist/geminiSpeechCodec.d.mts.map +1 -1
- package/dist/geminiSpeechCodec.mjs.map +1 -1
- package/dist/geminiSpeechCodec.test.d.mts +1 -1
- package/dist/geminiSpeechCodec.test.mjs +1 -1
- package/dist/geminiSpeechCodec.test.mjs.map +1 -1
- package/dist/geminiTools.test.d.mts +1 -1
- package/dist/geminiTools.test.mjs +44 -19
- package/dist/geminiTools.test.mjs.map +1 -1
- package/dist/{magic-string.es-CaWgF-3p.mjs → magic-string.es-CUAnjKYC.mjs} +14 -13
- package/dist/{magic-string.es-CaWgF-3p.mjs.map → magic-string.es-CUAnjKYC.mjs.map} +1 -1
- package/dist/models.d.mts.map +1 -1
- package/dist/{dist-CWoAuwS9.mjs → test.DNmyFkvJ-N3QqMRwZ.mjs} +257 -381
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
- package/package.json +5 -5
- package/src/Gemini.ts +4 -7
- package/src/GeminiEmbedding.ts +36 -48
- package/src/GeminiSynthesizer.ts +20 -27
- package/src/GeminiTools.ts +4 -6
- package/src/GoogleDeepResearch.test.ts +80 -0
- package/src/GoogleDeepResearch.ts +127 -37
- package/src/LyriaGenerator.ts +19 -23
- package/src/codec.ts +98 -12
- package/src/geminiTools.test.ts +21 -3
- package/dist/dist-CWoAuwS9.mjs.map +0 -1
- /package/dist/{chunk-uyGKjUfl.mjs → rolldown-runtime-EAsCxrDo.mjs} +0 -0
package/src/codec.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
InputImage,
|
|
6
6
|
HistoryItem,
|
|
7
7
|
Message,
|
|
8
|
+
Annotation,
|
|
8
9
|
} from "@effect-uai/core/Items"
|
|
9
10
|
import type { ToolDescriptor } from "@effect-uai/core/Tool"
|
|
10
11
|
import type { Turn } from "@effect-uai/core/Turn"
|
|
@@ -50,8 +51,55 @@ const Content = Schema.Struct({
|
|
|
50
51
|
parts: Schema.optional(Schema.Array(Part)),
|
|
51
52
|
})
|
|
52
53
|
|
|
54
|
+
// Google grounding metadata. On a grounded response the candidate carries
|
|
55
|
+
// `groundingChunks[].web.{uri,title}` - the structured form of the inline `[n]`
|
|
56
|
+
// / trailing `**Sources:**` list the model renders into the answer text.
|
|
57
|
+
// Decoded defensively (every field optional) as it is absent on ungrounded
|
|
58
|
+
// turns. Shared with the Interactions deep-research surface via `codec.ts`.
|
|
59
|
+
const GroundingChunk = Schema.Struct({
|
|
60
|
+
web: Schema.optional(
|
|
61
|
+
Schema.NullOr(
|
|
62
|
+
Schema.Struct({
|
|
63
|
+
uri: Schema.optional(Schema.NullOr(Schema.String)),
|
|
64
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
})
|
|
69
|
+
export const GroundingMetadata = Schema.Struct({
|
|
70
|
+
groundingChunks: Schema.optional(Schema.NullOr(Schema.Array(GroundingChunk))),
|
|
71
|
+
})
|
|
72
|
+
export type GroundingMetadata = typeof GroundingMetadata.Type
|
|
73
|
+
|
|
74
|
+
// Two citations are the same source when they point at the same url.
|
|
75
|
+
const sameUrlCitation = (a: Annotation, b: Annotation): boolean =>
|
|
76
|
+
a.type === "url_citation" && b.type === "url_citation" && a.url === b.url
|
|
77
|
+
|
|
78
|
+
// Grounding chunks → `url_citation` annotations, de-duped by url across every
|
|
79
|
+
// metadata block passed. `web.uri` is a `grounding-api-redirect` URL and
|
|
80
|
+
// `web.title` the bare domain; both are kept as-is (the redirect resolves to
|
|
81
|
+
// the real page).
|
|
82
|
+
export const groundingToAnnotations = (
|
|
83
|
+
...metas: ReadonlyArray<GroundingMetadata | null | undefined>
|
|
84
|
+
): ReadonlyArray<Annotation> =>
|
|
85
|
+
pipe(
|
|
86
|
+
metas,
|
|
87
|
+
Arr.flatMap((meta) => meta?.groundingChunks ?? []),
|
|
88
|
+
Arr.filterMap((chunk) =>
|
|
89
|
+
chunk.web?.uri == null
|
|
90
|
+
? Result.failVoid
|
|
91
|
+
: Result.succeed<Annotation>({
|
|
92
|
+
type: "url_citation",
|
|
93
|
+
url: chunk.web.uri,
|
|
94
|
+
title: chunk.web.title ?? chunk.web.uri,
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
Arr.dedupeWith(sameUrlCitation),
|
|
98
|
+
)
|
|
99
|
+
|
|
53
100
|
const Candidate = Schema.Struct({
|
|
54
101
|
content: Schema.optional(Content),
|
|
102
|
+
groundingMetadata: Schema.optional(GroundingMetadata),
|
|
55
103
|
finishReason: Schema.optional(Schema.String),
|
|
56
104
|
index: Schema.optional(Schema.Number),
|
|
57
105
|
})
|
|
@@ -87,6 +135,7 @@ type RequestPart =
|
|
|
87
135
|
}
|
|
88
136
|
| {
|
|
89
137
|
readonly functionResponse: {
|
|
138
|
+
readonly id?: string
|
|
90
139
|
readonly name: string
|
|
91
140
|
readonly response: Record<string, unknown>
|
|
92
141
|
}
|
|
@@ -169,16 +218,12 @@ const messageText = (message: Message): string => message.content.map(blockText)
|
|
|
169
218
|
*/
|
|
170
219
|
const imageSourceToParts = Match.type<InputImage["source"]>().pipe(
|
|
171
220
|
Match.tag("url", (): ReadonlyArray<RequestPart> => []),
|
|
172
|
-
Match.tag(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
(s): ReadonlyArray<RequestPart> => [
|
|
179
|
-
{ inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) } },
|
|
180
|
-
],
|
|
181
|
-
),
|
|
221
|
+
Match.tag("base64", (s): ReadonlyArray<RequestPart> => [
|
|
222
|
+
{ inlineData: { mimeType: s.mimeType, data: s.base64 } },
|
|
223
|
+
]),
|
|
224
|
+
Match.tag("bytes", (s): ReadonlyArray<RequestPart> => [
|
|
225
|
+
{ inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) } },
|
|
226
|
+
]),
|
|
182
227
|
Match.exhaustive,
|
|
183
228
|
)
|
|
184
229
|
|
|
@@ -292,6 +337,20 @@ const geminiField = (
|
|
|
292
337
|
)
|
|
293
338
|
|
|
294
339
|
const providerIdFor = (item: ToolCall): Option.Option<string> => geminiField(item, (g) => g.id)
|
|
340
|
+
|
|
341
|
+
// The originating call's Gemini id, looked up from history by our `call_id`.
|
|
342
|
+
// Gemini 3 maps a `functionResponse` back to its call by this id, so parallel
|
|
343
|
+
// calls to the same function are mis-paired if we drop it.
|
|
344
|
+
const providerIdForCallId = (
|
|
345
|
+
history: ReadonlyArray<HistoryItem>,
|
|
346
|
+
call_id: string,
|
|
347
|
+
): Option.Option<string> =>
|
|
348
|
+
pipe(
|
|
349
|
+
history,
|
|
350
|
+
Arr.findFirst((item): item is ToolCall => isFunctionCallItem(item) && item.call_id === call_id),
|
|
351
|
+
Option.flatMap(providerIdFor),
|
|
352
|
+
)
|
|
353
|
+
|
|
295
354
|
const signatureFor = (item: ToolCall): Option.Option<string> =>
|
|
296
355
|
geminiField(item, (g) => g.thoughtSignature)
|
|
297
356
|
|
|
@@ -327,6 +386,10 @@ const itemToContent =
|
|
|
327
386
|
parts: [
|
|
328
387
|
{
|
|
329
388
|
functionResponse: {
|
|
389
|
+
...Option.match(providerIdForCallId(history, o.call_id), {
|
|
390
|
+
onSome: (id) => ({ id }),
|
|
391
|
+
onNone: () => ({}),
|
|
392
|
+
}),
|
|
330
393
|
name: Option.getOrElse(nameForCallId(history, o.call_id), () => o.call_id),
|
|
331
394
|
response: parsedResponse(o.output),
|
|
332
395
|
},
|
|
@@ -367,7 +430,9 @@ const sanitizeSchema = (schema: unknown): unknown => {
|
|
|
367
430
|
)
|
|
368
431
|
}
|
|
369
432
|
|
|
370
|
-
const toolDescriptorsToTools = (
|
|
433
|
+
const toolDescriptorsToTools = (
|
|
434
|
+
tools: ReadonlyArray<ToolDescriptor>,
|
|
435
|
+
): ReadonlyArray<RequestTool> =>
|
|
371
436
|
tools.length === 0
|
|
372
437
|
? []
|
|
373
438
|
: [
|
|
@@ -466,6 +531,8 @@ export type Accumulator = {
|
|
|
466
531
|
readonly text: string
|
|
467
532
|
readonly reasoning: string
|
|
468
533
|
readonly functionCalls: ReadonlyArray<AccumulatedFunctionCall>
|
|
534
|
+
/** Grounding citations, accumulated (de-duped by url) across chunks. */
|
|
535
|
+
readonly annotations: ReadonlyArray<Annotation>
|
|
469
536
|
readonly finishReason: Option.Option<string>
|
|
470
537
|
readonly usage: {
|
|
471
538
|
readonly input_tokens?: number
|
|
@@ -480,6 +547,7 @@ export const emptyAccumulator: Accumulator = {
|
|
|
480
547
|
text: "",
|
|
481
548
|
reasoning: "",
|
|
482
549
|
functionCalls: [],
|
|
550
|
+
annotations: [],
|
|
483
551
|
finishReason: Option.none(),
|
|
484
552
|
usage: {},
|
|
485
553
|
}
|
|
@@ -598,6 +666,14 @@ const appendFunctionCalls = (
|
|
|
598
666
|
prior,
|
|
599
667
|
)
|
|
600
668
|
|
|
669
|
+
// Grounding usually arrives on the final chunk, but merge across chunks and
|
|
670
|
+
// de-dupe by url so a mid-stream metadata block is not lost or double-counted.
|
|
671
|
+
const mergeAnnotations = (
|
|
672
|
+
prev: ReadonlyArray<Annotation>,
|
|
673
|
+
next: ReadonlyArray<Annotation>,
|
|
674
|
+
): ReadonlyArray<Annotation> =>
|
|
675
|
+
next.length === 0 ? prev : Arr.dedupeWith(Arr.appendAll(prev, next), sameUrlCitation)
|
|
676
|
+
|
|
601
677
|
export const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult => {
|
|
602
678
|
const parts = chunkParts(chunk)
|
|
603
679
|
const finishReason = Option.fromNullishOr(chunk.candidates?.[0]?.finishReason)
|
|
@@ -608,6 +684,10 @@ export const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult =>
|
|
|
608
684
|
text: acc.text + sumStrings(parts, "text"),
|
|
609
685
|
reasoning: acc.reasoning + sumStrings(parts, "reasoning"),
|
|
610
686
|
functionCalls: appendFunctionCalls(acc.functionCalls, collectFunctionCalls(parts)),
|
|
687
|
+
annotations: mergeAnnotations(
|
|
688
|
+
acc.annotations,
|
|
689
|
+
groundingToAnnotations(chunk.candidates?.[0]?.groundingMetadata),
|
|
690
|
+
),
|
|
611
691
|
finishReason: Option.orElse(finishReason, () => acc.finishReason),
|
|
612
692
|
usage: mergeUsage(acc.usage, chunk.usageMetadata),
|
|
613
693
|
},
|
|
@@ -624,7 +704,13 @@ const assistantMessageItems = (acc: Accumulator): ReadonlyArray<HistoryItem> =>
|
|
|
624
704
|
{
|
|
625
705
|
type: "message",
|
|
626
706
|
role: "assistant",
|
|
627
|
-
content: [
|
|
707
|
+
content: [
|
|
708
|
+
{
|
|
709
|
+
type: "output_text",
|
|
710
|
+
text: acc.text,
|
|
711
|
+
...(acc.annotations.length > 0 && { annotations: acc.annotations }),
|
|
712
|
+
},
|
|
713
|
+
],
|
|
628
714
|
},
|
|
629
715
|
]
|
|
630
716
|
|
package/src/geminiTools.test.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { Effect, Option, Result, Schema } from "effect"
|
|
2
2
|
import * as Tool from "@effect-uai/core/Tool"
|
|
3
3
|
import { descriptorsOf, providerToolsOf } from "@effect-uai/core/Tool"
|
|
4
|
+
import * as Turn from "@effect-uai/core/Turn"
|
|
4
5
|
import { describe, expect, it } from "vitest"
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
WireChunk,
|
|
8
|
+
accumulatorToTurn,
|
|
9
|
+
buildRequestBody,
|
|
10
|
+
emptyAccumulator,
|
|
11
|
+
ingestChunk,
|
|
12
|
+
} from "./codec.js"
|
|
6
13
|
import { codeExecutionTool, googleSearchTool, renderProviderTools } from "./GeminiTools.js"
|
|
7
14
|
|
|
8
15
|
const localTool = Tool.make({
|
|
@@ -69,14 +76,19 @@ describe("Gemini provider tools", () => {
|
|
|
69
76
|
expect(Result.isFailure(result)).toBe(true)
|
|
70
77
|
})
|
|
71
78
|
|
|
72
|
-
it("
|
|
79
|
+
it("maps groundingMetadata chunks to url_citation annotations on the turn", () => {
|
|
73
80
|
const grounded = {
|
|
74
81
|
candidates: [
|
|
75
82
|
{
|
|
76
83
|
content: { role: "model", parts: [{ text: "Grounded answer." }] },
|
|
77
84
|
groundingMetadata: {
|
|
78
85
|
webSearchQueries: ["effect ts"],
|
|
79
|
-
groundingChunks: [
|
|
86
|
+
groundingChunks: [
|
|
87
|
+
{ web: { uri: "https://example.com", title: "Example" } },
|
|
88
|
+
// Duplicate uri (de-duped) and a bare-domain title fallback.
|
|
89
|
+
{ web: { uri: "https://example.com", title: "Example" } },
|
|
90
|
+
{ web: { uri: "https://effect.website" } },
|
|
91
|
+
],
|
|
80
92
|
},
|
|
81
93
|
finishReason: "STOP",
|
|
82
94
|
},
|
|
@@ -84,5 +96,11 @@ describe("Gemini provider tools", () => {
|
|
|
84
96
|
}
|
|
85
97
|
const chunk = Schema.decodeUnknownSync(WireChunk)(grounded)
|
|
86
98
|
expect(chunk.candidates?.[0]?.content?.parts?.[0]).toEqual({ text: "Grounded answer." })
|
|
99
|
+
|
|
100
|
+
const turn = accumulatorToTurn(ingestChunk(emptyAccumulator, chunk).accumulator)
|
|
101
|
+
expect(Turn.citations(turn)).toEqual([
|
|
102
|
+
{ type: "url_citation", url: "https://example.com", title: "Example" },
|
|
103
|
+
{ type: "url_citation", url: "https://effect.website", title: "https://effect.website" },
|
|
104
|
+
])
|
|
87
105
|
})
|
|
88
106
|
})
|