@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/google",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Google Gemini provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"access": "public"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"effect": "4.0.0-
|
|
63
|
-
"typescript": "^
|
|
64
|
-
"@effect-uai/core": "0.
|
|
62
|
+
"effect": "4.0.0-rc.111",
|
|
63
|
+
"typescript": "^7.0.2",
|
|
64
|
+
"@effect-uai/core": "0.12.1"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@effect-uai/core": ">=0.2.0 <1",
|
|
68
|
-
"effect": ">=4.0.0-
|
|
68
|
+
"effect": ">=4.0.0-rc.111 <5.0.0"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsdown",
|
package/src/Gemini.ts
CHANGED
|
@@ -330,13 +330,10 @@ export const layer = (
|
|
|
330
330
|
const typed = Layer.effect(Gemini, make(cfg))
|
|
331
331
|
const generic = Layer.effect(
|
|
332
332
|
LanguageModel,
|
|
333
|
-
Effect.map(
|
|
334
|
-
|
|
335
|
-
(
|
|
336
|
-
|
|
337
|
-
turn: (request) => s.turn(request as GeminiRequest),
|
|
338
|
-
}),
|
|
339
|
-
),
|
|
333
|
+
Effect.map(make(cfg), (s): LanguageModelService => ({
|
|
334
|
+
streamTurn: (request) => s.streamTurn(request as GeminiRequest),
|
|
335
|
+
turn: (request) => s.turn(request as GeminiRequest),
|
|
336
|
+
})),
|
|
340
337
|
)
|
|
341
338
|
return Layer.merge(typed, generic)
|
|
342
339
|
}
|
package/src/GeminiEmbedding.ts
CHANGED
|
@@ -121,21 +121,16 @@ const urlRejected = (param: string): AiError.AiError =>
|
|
|
121
121
|
*/
|
|
122
122
|
const imageSourceToPart = (param: string) =>
|
|
123
123
|
Match.type<ImageSource>().pipe(
|
|
124
|
-
Match.tag(
|
|
125
|
-
|
|
126
|
-
(s): Effect.Effect<WirePart, AiError.AiError> =>
|
|
127
|
-
Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),
|
|
124
|
+
Match.tag("base64", (s): Effect.Effect<WirePart, AiError.AiError> =>
|
|
125
|
+
Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),
|
|
128
126
|
),
|
|
129
|
-
Match.tag(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },
|
|
134
|
-
}),
|
|
127
|
+
Match.tag("bytes", (s): Effect.Effect<WirePart, AiError.AiError> =>
|
|
128
|
+
Effect.succeed({
|
|
129
|
+
inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },
|
|
130
|
+
}),
|
|
135
131
|
),
|
|
136
|
-
Match.tag(
|
|
137
|
-
|
|
138
|
-
(): Effect.Effect<WirePart, AiError.AiError> => Effect.fail(urlRejected(param)),
|
|
132
|
+
Match.tag("url", (): Effect.Effect<WirePart, AiError.AiError> =>
|
|
133
|
+
Effect.fail(urlRejected(param)),
|
|
139
134
|
),
|
|
140
135
|
Match.exhaustive,
|
|
141
136
|
)
|
|
@@ -291,12 +286,10 @@ const embedImpl =
|
|
|
291
286
|
postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:embedContent`, body),
|
|
292
287
|
),
|
|
293
288
|
Effect.flatMap(decodeSingle),
|
|
294
|
-
Effect.map(
|
|
295
|
-
(decoded)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}),
|
|
299
|
-
),
|
|
289
|
+
Effect.map((decoded): EmbedResponse => ({
|
|
290
|
+
embedding: valuesToEmbedding(decoded.embedding.values),
|
|
291
|
+
usage: emptyUsage,
|
|
292
|
+
})),
|
|
300
293
|
)
|
|
301
294
|
|
|
302
295
|
const embedManyImpl =
|
|
@@ -309,12 +302,10 @@ const embedManyImpl =
|
|
|
309
302
|
postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:batchEmbedContents`, body),
|
|
310
303
|
),
|
|
311
304
|
Effect.flatMap(decodeBatch),
|
|
312
|
-
Effect.map(
|
|
313
|
-
(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}),
|
|
317
|
-
),
|
|
305
|
+
Effect.map((decoded): EmbedManyResponse => ({
|
|
306
|
+
embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),
|
|
307
|
+
usage: emptyUsage,
|
|
308
|
+
})),
|
|
318
309
|
)
|
|
319
310
|
|
|
320
311
|
// ---------------------------------------------------------------------------
|
|
@@ -346,29 +337,26 @@ export const layer = (
|
|
|
346
337
|
const typed = Layer.effect(GeminiEmbedding, make(cfg))
|
|
347
338
|
const generic = Layer.effect(
|
|
348
339
|
EmbeddingModel,
|
|
349
|
-
Effect.map(
|
|
350
|
-
|
|
351
|
-
(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,
|
|
370
|
-
}),
|
|
371
|
-
),
|
|
340
|
+
Effect.map(make(cfg), (s): EmbeddingModelService => ({
|
|
341
|
+
// Gemini's embed endpoints emit float32 only. Reject a non-float32
|
|
342
|
+
// `encoding` (bucket 1) so a float32 vector isn't handed back
|
|
343
|
+
// mislabeled as the requested type. `task` is left silent: it's a
|
|
344
|
+
// per-model gap (honored by gemini-embedding-001, ignored by
|
|
345
|
+
// gemini-embedding-2 server-side), and per capabilities.md §2.3 we
|
|
346
|
+
// don't maintain per-model tables; there's no wire error to translate.
|
|
347
|
+
embed: <E extends EmbedEncoding | undefined = undefined>(
|
|
348
|
+
req: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
|
|
349
|
+
) =>
|
|
350
|
+
assertEncoding(req.encoding, ["float32"], "gemini").pipe(
|
|
351
|
+
Effect.andThen(s.embed(req as GeminiEmbedRequest)),
|
|
352
|
+
) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,
|
|
353
|
+
embedMany: <E extends EmbedEncoding | undefined = undefined>(
|
|
354
|
+
req: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
|
|
355
|
+
) =>
|
|
356
|
+
assertEncoding(req.encoding, ["float32"], "gemini").pipe(
|
|
357
|
+
Effect.andThen(s.embedMany(req as GeminiEmbedManyRequest)),
|
|
358
|
+
) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,
|
|
359
|
+
})),
|
|
372
360
|
)
|
|
373
361
|
return Layer.merge(typed, generic)
|
|
374
362
|
}
|
package/src/GeminiSynthesizer.ts
CHANGED
|
@@ -222,21 +222,18 @@ const unsupportedStreamDialogue: SpeechSynthesizerService["streamSynthesizeDialo
|
|
|
222
222
|
// ---------------------------------------------------------------------------
|
|
223
223
|
|
|
224
224
|
export const make = (cfg: Config) =>
|
|
225
|
-
Effect.map(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Stream.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
streamSynthesizeDialogue: unsupportedStreamDialogue,
|
|
238
|
-
}),
|
|
239
|
-
)
|
|
225
|
+
Effect.map(HttpClient.HttpClient, (client): GeminiSynthesizerService => ({
|
|
226
|
+
synthesize: (r) =>
|
|
227
|
+
synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
228
|
+
streamSynthesis: (r) =>
|
|
229
|
+
Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(
|
|
230
|
+
Stream.map((b): AudioChunk => ({ bytes: b.bytes })),
|
|
231
|
+
Stream.provideService(HttpClient.HttpClient, client),
|
|
232
|
+
),
|
|
233
|
+
streamSynthesisFrom: unsupportedStreamFrom,
|
|
234
|
+
synthesizeDialogue: unsupportedDialogue,
|
|
235
|
+
streamSynthesizeDialogue: unsupportedStreamDialogue,
|
|
236
|
+
}))
|
|
240
237
|
|
|
241
238
|
/**
|
|
242
239
|
* Layer registers both `GeminiSynthesizer` and the generic
|
|
@@ -249,17 +246,13 @@ export const layer = (cfg: Config) =>
|
|
|
249
246
|
Layer.effect(GeminiSynthesizer, make(cfg)),
|
|
250
247
|
Layer.effect(
|
|
251
248
|
SpeechSynthesizer,
|
|
252
|
-
Effect.map(
|
|
253
|
-
|
|
254
|
-
(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
synthesizeDialogue: s.synthesizeDialogue,
|
|
261
|
-
streamSynthesizeDialogue: s.streamSynthesizeDialogue,
|
|
262
|
-
}),
|
|
263
|
-
),
|
|
249
|
+
Effect.map(make(cfg), (s): SpeechSynthesizerService => ({
|
|
250
|
+
synthesize: (req: CommonSynthesizeRequest) => s.synthesize(req as GeminiSynthesizeRequest),
|
|
251
|
+
streamSynthesis: (req: CommonSynthesizeRequest) =>
|
|
252
|
+
s.streamSynthesis(req as GeminiSynthesizeRequest),
|
|
253
|
+
streamSynthesisFrom: s.streamSynthesisFrom,
|
|
254
|
+
synthesizeDialogue: s.synthesizeDialogue,
|
|
255
|
+
streamSynthesizeDialogue: s.streamSynthesizeDialogue,
|
|
256
|
+
})),
|
|
264
257
|
),
|
|
265
258
|
)
|
package/src/GeminiTools.ts
CHANGED
|
@@ -77,12 +77,10 @@ const renderOne = (
|
|
|
77
77
|
})
|
|
78
78
|
: decodeConfig(tool.config).pipe(
|
|
79
79
|
Result.map(configToWire),
|
|
80
|
-
Result.mapError(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}),
|
|
85
|
-
),
|
|
80
|
+
Result.mapError((): ToolRenderError => ({
|
|
81
|
+
capability: "providerTool",
|
|
82
|
+
reason: `unrecognized Gemini provider tool config on "${tool.name}"`,
|
|
83
|
+
})),
|
|
86
84
|
)
|
|
87
85
|
|
|
88
86
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import * as Turn from "@effect-uai/core/Turn"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import { WireInteraction, jobStateOf } from "./GoogleDeepResearch.js"
|
|
5
|
+
|
|
6
|
+
const decode = Schema.decodeUnknownSync(WireInteraction)
|
|
7
|
+
|
|
8
|
+
// Extract the `Turn` from a completed interaction the way the service does.
|
|
9
|
+
const turnOf = (raw: unknown): Turn.Turn => {
|
|
10
|
+
const state = jobStateOf(decode(raw))
|
|
11
|
+
if (state._tag !== "Succeeded") throw new Error(`expected Succeeded, got ${state._tag}`)
|
|
12
|
+
return state.result
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("GoogleDeepResearch grounding", () => {
|
|
16
|
+
it("maps grounding chunks on a completed interaction to url_citation annotations", () => {
|
|
17
|
+
const turn = turnOf({
|
|
18
|
+
id: "int_1",
|
|
19
|
+
status: "completed",
|
|
20
|
+
outputs: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: "Report body [1][2].",
|
|
24
|
+
groundingMetadata: {
|
|
25
|
+
groundingChunks: [
|
|
26
|
+
{
|
|
27
|
+
web: {
|
|
28
|
+
uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/abc",
|
|
29
|
+
title: "example.com",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
web: { uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/def" },
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
})
|
|
40
|
+
expect(Turn.assistantText(turn)).toBe("Report body [1][2].")
|
|
41
|
+
expect(Turn.citations(turn)).toEqual([
|
|
42
|
+
{
|
|
43
|
+
type: "url_citation",
|
|
44
|
+
url: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/abc",
|
|
45
|
+
title: "example.com",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "url_citation",
|
|
49
|
+
url: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/def",
|
|
50
|
+
title: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/def",
|
|
51
|
+
},
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("gathers and de-dupes grounding across interaction / output / step levels", () => {
|
|
56
|
+
const turn = turnOf({
|
|
57
|
+
id: "int_2",
|
|
58
|
+
status: "completed",
|
|
59
|
+
groundingMetadata: {
|
|
60
|
+
groundingChunks: [{ web: { uri: "https://a.example", title: "a" } }],
|
|
61
|
+
},
|
|
62
|
+
outputs: [{ type: "text", text: "Body." }],
|
|
63
|
+
steps: [
|
|
64
|
+
{
|
|
65
|
+
type: "research",
|
|
66
|
+
groundingMetadata: {
|
|
67
|
+
groundingChunks: [
|
|
68
|
+
{ web: { uri: "https://a.example", title: "a" } },
|
|
69
|
+
{ web: { uri: "https://b.example", title: "b" } },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
})
|
|
75
|
+
expect(Turn.citations(turn).map((c) => c.type === "url_citation" && c.url)).toEqual([
|
|
76
|
+
"https://a.example",
|
|
77
|
+
"https://b.example",
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Array as Arr,
|
|
3
|
+
Context,
|
|
4
|
+
Effect,
|
|
5
|
+
Layer,
|
|
6
|
+
Match,
|
|
7
|
+
Option,
|
|
8
|
+
Redacted,
|
|
9
|
+
Ref,
|
|
10
|
+
Result,
|
|
11
|
+
Schema,
|
|
12
|
+
Stream,
|
|
13
|
+
pipe,
|
|
14
|
+
} from "effect"
|
|
2
15
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
3
16
|
import * as AiError from "@effect-uai/core/AiError"
|
|
17
|
+
import type { Source } from "@effect-uai/core/Citation"
|
|
4
18
|
import {
|
|
5
19
|
DeepResearch,
|
|
6
20
|
type DeepResearchService,
|
|
@@ -13,6 +27,7 @@ import * as Job from "@effect-uai/core/Job"
|
|
|
13
27
|
import type { ResearchJobRef, ResearchRequest, ResearchState } from "@effect-uai/core/Research"
|
|
14
28
|
import * as SSE from "@effect-uai/core/SSE"
|
|
15
29
|
import { type Turn, TurnEvent } from "@effect-uai/core/Turn"
|
|
30
|
+
import { GroundingMetadata, groundingToAnnotations } from "./codec.js"
|
|
16
31
|
import type { GoogleDeepResearchModel } from "./models.js"
|
|
17
32
|
|
|
18
33
|
// ---------------------------------------------------------------------------
|
|
@@ -61,23 +76,28 @@ const decodeSubmit = Schema.decodeUnknownEffect(WireSubmit)
|
|
|
61
76
|
|
|
62
77
|
// A completed interaction's text lives at `outputs[].text` (SDK `outputs`) or,
|
|
63
78
|
// in some doc examples, `steps[].content[].text`. Decode both defensively and
|
|
64
|
-
// extract from whichever is populated.
|
|
65
|
-
// preview surface
|
|
66
|
-
//
|
|
79
|
+
// extract from whichever is populated. Grounding sources ride Google's
|
|
80
|
+
// `groundingMetadata.groundingChunks[]`; this preview surface is undocumented,
|
|
81
|
+
// so decode it at every plausible level (interaction / output / step) and map
|
|
82
|
+
// whichever is populated onto `url_citation` annotations. The report text also
|
|
83
|
+
// carries the sources as an inline `[n]` / `**Sources:**` list.
|
|
67
84
|
const WireTextPart = Schema.Struct({
|
|
68
85
|
type: Schema.optional(Schema.NullOr(Schema.String)),
|
|
69
86
|
text: Schema.optional(Schema.NullOr(Schema.String)),
|
|
87
|
+
groundingMetadata: Schema.optional(Schema.NullOr(GroundingMetadata)),
|
|
70
88
|
})
|
|
71
89
|
const WireStep = Schema.Struct({
|
|
72
90
|
type: Schema.optional(Schema.NullOr(Schema.String)),
|
|
73
91
|
text: Schema.optional(Schema.NullOr(Schema.String)),
|
|
74
92
|
content: Schema.optional(Schema.NullOr(Schema.Array(WireTextPart))),
|
|
93
|
+
groundingMetadata: Schema.optional(Schema.NullOr(GroundingMetadata)),
|
|
75
94
|
})
|
|
76
95
|
export const WireInteraction = Schema.Struct({
|
|
77
96
|
id: Schema.optional(Schema.NullOr(Schema.String)),
|
|
78
97
|
status: Schema.optional(Schema.NullOr(Schema.String)),
|
|
79
98
|
outputs: Schema.optional(Schema.NullOr(Schema.Array(WireTextPart))),
|
|
80
99
|
steps: Schema.optional(Schema.NullOr(Schema.Array(WireStep))),
|
|
100
|
+
groundingMetadata: Schema.optional(Schema.NullOr(GroundingMetadata)),
|
|
81
101
|
error: Schema.optional(
|
|
82
102
|
Schema.NullOr(
|
|
83
103
|
Schema.Struct({
|
|
@@ -104,44 +124,111 @@ const reportText = (wire: WireInteraction): string => {
|
|
|
104
124
|
return fromStep.length > 0 ? fromStep : (lastStep?.text ?? "")
|
|
105
125
|
}
|
|
106
126
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The subset of the domain `Citation.Source` this provider can populate.
|
|
129
|
+
* Declared here rather than derived from the wire schema, so the exported
|
|
130
|
+
* shape is ours and a wire change cannot silently alter it.
|
|
131
|
+
*/
|
|
132
|
+
const SourceSchema = Schema.Struct({
|
|
133
|
+
url: Schema.String,
|
|
134
|
+
title: Schema.optional(Schema.String),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The research trace: what the model did at each step and which sources it
|
|
139
|
+
* consulted there. The `Turn` keeps only the final report and the deduped
|
|
140
|
+
* union of all sources, so the per-step breakdown lives here or nowhere.
|
|
141
|
+
*
|
|
142
|
+
* Lands on `providerData.gemini`; read it with {@link researchDataOf}.
|
|
143
|
+
*/
|
|
144
|
+
export const GeminiResearchData = Schema.Struct({
|
|
145
|
+
steps: Schema.Array(
|
|
146
|
+
Schema.Struct({
|
|
147
|
+
text: Schema.String,
|
|
148
|
+
sources: Schema.Array(SourceSchema),
|
|
149
|
+
}),
|
|
150
|
+
),
|
|
118
151
|
})
|
|
152
|
+
export type GeminiResearchData = typeof GeminiResearchData.Type
|
|
153
|
+
|
|
154
|
+
const decodeResearchData = Schema.decodeUnknownResult(Schema.Struct({ gemini: GeminiResearchData }))
|
|
155
|
+
|
|
156
|
+
/** Read this provider's data off an item, if it is there and ours. */
|
|
157
|
+
export const researchDataOf = (item: Items.HistoryItem): Option.Option<GeminiResearchData> =>
|
|
158
|
+
pipe(
|
|
159
|
+
decodeResearchData(item.providerData),
|
|
160
|
+
Result.match({
|
|
161
|
+
onSuccess: (d) => Option.some(d.gemini),
|
|
162
|
+
onFailure: () => Option.none<GeminiResearchData>(),
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const sourcesOf = (meta: GroundingMetadata | null | undefined): ReadonlyArray<Source> =>
|
|
167
|
+
(meta?.groundingChunks ?? []).flatMap((chunk) =>
|
|
168
|
+
chunk.web?.uri == null
|
|
169
|
+
? []
|
|
170
|
+
: [{ url: chunk.web.uri, ...(chunk.web.title != null && { title: chunk.web.title }) }],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
// A step contributes to the trace if it said something or consulted something.
|
|
174
|
+
const researchSteps = (wire: WireInteraction): GeminiResearchData["steps"] =>
|
|
175
|
+
(wire.steps ?? []).flatMap((step) => {
|
|
176
|
+
const text = textOfParts(step.content) || (step.text ?? "")
|
|
177
|
+
const sources = [
|
|
178
|
+
...sourcesOf(step.groundingMetadata),
|
|
179
|
+
...(step.content ?? []).flatMap((p) => sourcesOf(p.groundingMetadata)),
|
|
180
|
+
]
|
|
181
|
+
return text.length === 0 && sources.length === 0 ? [] : [{ text, sources }]
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Grounding can live at any level of the interaction envelope; gather it from
|
|
185
|
+
// all three (interaction / output part / step) and let `groundingToAnnotations`
|
|
186
|
+
// de-dupe by url.
|
|
187
|
+
const interactionAnnotations = (wire: WireInteraction): ReadonlyArray<Items.Annotation> =>
|
|
188
|
+
groundingToAnnotations(
|
|
189
|
+
wire.groundingMetadata,
|
|
190
|
+
...Arr.map(wire.outputs ?? [], (o) => o.groundingMetadata),
|
|
191
|
+
...Arr.map(wire.steps ?? [], (s) => s.groundingMetadata),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const turnFromInteraction = (wire: WireInteraction): Turn => {
|
|
195
|
+
const annotations = interactionAnnotations(wire)
|
|
196
|
+
return {
|
|
197
|
+
items: [
|
|
198
|
+
{
|
|
199
|
+
type: "message",
|
|
200
|
+
role: "assistant",
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: "output_text",
|
|
204
|
+
text: reportText(wire),
|
|
205
|
+
...(annotations.length > 0 && { annotations }),
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
providerData: { gemini: { steps: researchSteps(wire) } },
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
usage: {},
|
|
212
|
+
stop_reason: "stop",
|
|
213
|
+
}
|
|
214
|
+
}
|
|
119
215
|
|
|
120
216
|
// Map the interaction status onto the generic `JobState`.
|
|
121
217
|
export const jobStateOf = (wire: WireInteraction): ResearchState =>
|
|
122
218
|
Match.value(wire.status ?? "").pipe(
|
|
123
219
|
Match.whenOr("queued", "pending", (): ResearchState => ({ _tag: "Pending" })),
|
|
124
|
-
Match.whenOr(
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"failed",
|
|
137
|
-
"error",
|
|
138
|
-
"cancelled",
|
|
139
|
-
(): ResearchState => ({
|
|
140
|
-
_tag: "Failed",
|
|
141
|
-
...(wire.error?.message != null && { reason: wire.error.message }),
|
|
142
|
-
raw: wire,
|
|
143
|
-
}),
|
|
144
|
-
),
|
|
220
|
+
Match.whenOr("in_progress", "running", "processing", (): ResearchState => ({
|
|
221
|
+
_tag: "Running",
|
|
222
|
+
})),
|
|
223
|
+
Match.whenOr("completed", "succeeded", (): ResearchState => ({
|
|
224
|
+
_tag: "Succeeded",
|
|
225
|
+
result: turnFromInteraction(wire),
|
|
226
|
+
})),
|
|
227
|
+
Match.whenOr("failed", "error", "cancelled", (): ResearchState => ({
|
|
228
|
+
_tag: "Failed",
|
|
229
|
+
...(wire.error?.message != null && { reason: wire.error.message }),
|
|
230
|
+
raw: wire,
|
|
231
|
+
})),
|
|
145
232
|
Match.orElse((): ResearchState => ({ _tag: "Running" })),
|
|
146
233
|
)
|
|
147
234
|
|
|
@@ -226,6 +313,9 @@ const pollJob = (
|
|
|
226
313
|
return yield* httpStatusError(response.status, text)
|
|
227
314
|
}
|
|
228
315
|
const json = yield* response.json.pipe(Effect.mapError(transportFailure))
|
|
316
|
+
// Preview surface: log the raw interaction so the (undocumented) grounding
|
|
317
|
+
// path can be confirmed against a live completed job at LOG_LEVEL=Debug.
|
|
318
|
+
yield* Effect.logDebug("google interactions poll response", { interaction: json })
|
|
229
319
|
const wire = yield* decodeInteraction(json).pipe(Effect.mapError(transportFailure))
|
|
230
320
|
return jobStateOf(wire)
|
|
231
321
|
})
|
|
@@ -284,7 +374,7 @@ const completedTurn = (wire: WireInteraction | null | undefined, streamed: strin
|
|
|
284
374
|
type: "message",
|
|
285
375
|
role: "assistant",
|
|
286
376
|
content: [{ type: "output_text", text: streamed }],
|
|
287
|
-
...(wire != null && { providerData: wire }),
|
|
377
|
+
...(wire != null && { providerData: { gemini: { steps: researchSteps(wire) } } }),
|
|
288
378
|
},
|
|
289
379
|
],
|
|
290
380
|
usage: {},
|
package/src/LyriaGenerator.ts
CHANGED
|
@@ -105,19 +105,18 @@ const isClipModel = (model: string): boolean => model.includes("clip")
|
|
|
105
105
|
|
|
106
106
|
export const realizedFormat: (mime: LyriaResponseMimeType) => AudioFormat =
|
|
107
107
|
Match.type<LyriaResponseMimeType>().pipe(
|
|
108
|
-
Match.when(
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
),
|
|
108
|
+
Match.when("audio/mp3", (): AudioFormat => ({
|
|
109
|
+
container: "mp3",
|
|
110
|
+
encoding: "mp3",
|
|
111
|
+
sampleRate: 44100,
|
|
112
|
+
channels: 2,
|
|
113
|
+
})),
|
|
114
|
+
Match.when("audio/wav", (): AudioFormat => ({
|
|
115
|
+
container: "wav",
|
|
116
|
+
encoding: "pcm_s16le",
|
|
117
|
+
sampleRate: 44100,
|
|
118
|
+
channels: 2,
|
|
119
|
+
})),
|
|
121
120
|
Match.exhaustive,
|
|
122
121
|
)
|
|
123
122
|
|
|
@@ -395,15 +394,12 @@ export const layer = (
|
|
|
395
394
|
Layer.effect(LyriaGenerator, make(cfg)),
|
|
396
395
|
Layer.effect(
|
|
397
396
|
MusicGenerator,
|
|
398
|
-
Effect.map(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
streamGenerationFrom: s.streamGenerationFrom,
|
|
406
|
-
}),
|
|
407
|
-
),
|
|
397
|
+
Effect.map(make(cfg), (s): MusicGeneratorService => ({
|
|
398
|
+
generate: (req: CommonGenerateMusicRequest) =>
|
|
399
|
+
s.generate({ ...req, model: req.model as LyriaModel }),
|
|
400
|
+
streamGeneration: (req: CommonStreamGenerateMusicRequest) =>
|
|
401
|
+
s.streamGeneration({ ...req, model: req.model as LyriaModel }),
|
|
402
|
+
streamGenerationFrom: s.streamGenerationFrom,
|
|
403
|
+
})),
|
|
408
404
|
),
|
|
409
405
|
)
|