@ai-ecoverse/pi-audio 1.20.1 → 1.21.0
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/README.md +3 -1
- package/dist/fish-agents/events.js +94 -0
- package/dist/fish-agents/events.js.map +7 -0
- package/dist/fish-agents/index.js +57 -0
- package/dist/fish-agents/index.js.map +7 -0
- package/dist/fish-agents/provision.js +178 -0
- package/dist/fish-agents/provision.js.map +7 -0
- package/dist/fish-agents/room.js +80 -0
- package/dist/fish-agents/room.js.map +7 -0
- package/dist/fish-agents/session.js +304 -0
- package/dist/fish-agents/session.js.map +7 -0
- package/dist/realtime.js.map +2 -2
- package/dist/tokens.js +11 -1
- package/dist/tokens.js.map +2 -2
- package/dist/types/fish-agents/events.d.ts +21 -0
- package/dist/types/fish-agents/index.d.ts +11 -0
- package/dist/types/fish-agents/provision.d.ts +11 -0
- package/dist/types/fish-agents/room.d.ts +46 -0
- package/dist/types/fish-agents/session.d.ts +19 -0
- package/dist/types/realtime.d.ts +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -52,10 +52,12 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
52
52
|
|
|
53
53
|
`@ai-ecoverse/pi-audio/gemini-live` registers `gemini-3.8-live`, `gemini-live-2.5-flash-native-audio`, and `gemini-3.1-flash-live-preview`. Setup enables sliding-window compression and session resumption. `gemini-3.8-live` leaves function behavior at the model default and accepts `vocabulary` as `customVocabulary`. The 2.5 model declares `NON_BLOCKING`. The 3.1 preview declares `BLOCKING`. `respond` maps `Delivery` onto `scheduling` (`INTERRUPT`, `WHEN_IDLE`, `SILENT`) and returns `status`, `retryable`, and `message`. A `goAway` reconnects with the latest resumable handle. There is no truncate call. The adapter takes a recorded `wire` and never calls Google.
|
|
54
54
|
|
|
55
|
-
`mintRealtimeToken(model, constraints, options)` returns `{ value, expiresAt, model }` and calls `options.fetch` (or `globalThis.fetch`). OpenAI `POST /v1/realtime/client_secrets` sends a realtime session with instructions, voice, and tools. xAI `POST /v1/realtime/client_secrets` sends only `expires_after.seconds` of 300. Gemini `POST /v1beta/auth_tokens` locks `models/<id>`, instructions, voice, and tools in `liveConnectConstraints` and returns `name`, which the live adapter sends as `access_token`. ElevenLabs `GET /v1/convai/conversation/get-signed-url` takes `agent_id` (or `GET /conversation/token` when the only transport is WebRTC) and expires in 15 minutes. Fish `POST /v1/agent/sessions` sends `agent_id` plus `overrides.system_prompt` and `overrides.voice_id`. `value` is the LiveKit participant token
|
|
55
|
+
`mintRealtimeToken(model, constraints, options)` returns `{ value, expiresAt, model, url? }` and calls `options.fetch` (or `globalThis.fetch`). OpenAI `POST /v1/realtime/client_secrets` sends a realtime session with instructions, voice, and tools. xAI `POST /v1/realtime/client_secrets` sends only `expires_after.seconds` of 300. Gemini `POST /v1beta/auth_tokens` locks `models/<id>`, instructions, voice, and tools in `liveConnectConstraints` and returns `name`, which the live adapter sends as `access_token`. ElevenLabs `GET /v1/convai/conversation/get-signed-url` takes `agent_id` (or `GET /conversation/token` when the only transport is WebRTC) and expires in 15 minutes. Fish `POST /v1/agent/sessions` sends `agent_id` plus `overrides.system_prompt` and `overrides.voice_id`. `value` is the LiveKit participant token and `url` is `livekit_url`. `expiresAt` is milliseconds. A numeric `expires_at` below `1e12` is unix seconds.
|
|
56
56
|
|
|
57
57
|
`@ai-ecoverse/pi-audio/elevenlabs-agents` registers `elevenlabs-agent`, a hosted cascade (`provisioning: "agent"`, `truncation: "server"`, `liveToolUpdate: false`). `provisionAgent` creates or updates the agent and reuses it when the spec hash matches. pi tools become client tools. A string `llm` is a vendor model name. A `CustomLlm` sets `llm` to `custom-llm` and sends `custom_llm.url`, `model_id`, `api_type`, and `request_headers` (ElevenLabs `api_key` is a workspace secret reference, not a raw token). `connectRealtime` uses a `RealtimeToken` from `mintRealtimeToken` (`get-signed-url`) when one is passed. Otherwise an API key provisions the agent and mints the signed URL. Silent `inject` sends `contextual_update` with `context_id` `pi-audio`. `close()` prices `audio.inputSeconds` from the wall-clock session at $0.08 per minute.
|
|
58
58
|
|
|
59
|
+
`@ai-ecoverse/pi-audio/fish-agents` registers `fish-agent` (`transports: ["webrtc"]`, `delivery.silent: "approximate"`, `delivery.interrupt: "native"`). `provisionAgent` creates client tools, writes the draft, and publishes it. A `RealtimeToken` with `url` is the LiveKit room from `mintRealtimeToken`; connect does not open a second session. Without a token, connect creates the session and fails on an unknown `transport`. Silent `inject` sends `user.message` with `audio: false` and drops that reply's transcript. The microphone is published after `openEchoMic`. The catalog `audioCost` is zero because Fish has not published a per-minute session rate. The example voice id is the one in Fish's quickstart.
|
|
60
|
+
|
|
59
61
|
`talkerTools` is `ask_agent` and `read`. pi-audio defines `askAgent` (a pi `Tool` whose parameters are `request` and optional `context`) and the `Thinker` interface. The host implements `Thinker` and supplies `read`. `bindThinker` runs direct tools with `directTimeoutMs` and truncates their text to `directMaxChars`. On a blocking `ask_agent` it answers with `ack` immediately and injects the result later. On an async call it `respond`s with the result and `options.result`. Progress is injected with `options.progress`. `tool_call_cancelled` aborts the thinker and any direct tool. A blocking failure is injected after the acknowledgement, with a `thinker_result` caption. `Thinker.ask` receives the optional `context` string. Closing the session aborts work that is still running. The binding also yields `thinker_progress` / `thinker_result` captions and `thinking` / `tool` / `idle` phases.
|
|
60
62
|
|
|
61
63
|
## Development
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { blankUsage } from "../blank-usage.js";
|
|
2
|
+
function none() {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function text(value) {
|
|
9
|
+
return typeof value === "string" ? value : "";
|
|
10
|
+
}
|
|
11
|
+
function done(itemId, interrupted) {
|
|
12
|
+
return { type: "response_done", itemId, interrupted, usage: blankUsage() };
|
|
13
|
+
}
|
|
14
|
+
function ingestFishData(message) {
|
|
15
|
+
const type = message.type;
|
|
16
|
+
if (typeof type !== "string") return none();
|
|
17
|
+
if (type === "client_tool.call") return toolCall(message);
|
|
18
|
+
if (type === "error")
|
|
19
|
+
return [{ type: "error", error: text(message.code) || "error", fatal: false }];
|
|
20
|
+
if (type === "session.ended") {
|
|
21
|
+
const reason = text(message.reason);
|
|
22
|
+
return [{ type: "error", error: reason.length > 0 ? reason : "session ended", fatal: false }];
|
|
23
|
+
}
|
|
24
|
+
if (type === "tool.started" || type === "tool.completed" || type === "tool.failed") return none();
|
|
25
|
+
return none();
|
|
26
|
+
}
|
|
27
|
+
function toolCall(message) {
|
|
28
|
+
const name = text(message.toolName);
|
|
29
|
+
const id = text(message.callId);
|
|
30
|
+
if (name.length === 0 || id.length === 0) return none();
|
|
31
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
32
|
+
const call = { type: "toolCall", id, name, arguments: params };
|
|
33
|
+
return [{ type: "tool_call", call, blocking: message.expectsResponse !== false }];
|
|
34
|
+
}
|
|
35
|
+
function ingestFishText(turn, packet, localIdentity) {
|
|
36
|
+
const itemId = text(packet.attributes["lk.segment_id"]);
|
|
37
|
+
const id = itemId.length > 0 ? itemId : "segment";
|
|
38
|
+
const final = packet.attributes["lk.transcription_final"] === "true";
|
|
39
|
+
if (packet.identity === localIdentity) {
|
|
40
|
+
return [{ type: "input_transcript", itemId: id, text: packet.text, final }];
|
|
41
|
+
}
|
|
42
|
+
if (turn.discardFinals > 0) {
|
|
43
|
+
if (final) turn.discardFinals -= 1;
|
|
44
|
+
return none();
|
|
45
|
+
}
|
|
46
|
+
turn.turnOpen = true;
|
|
47
|
+
return [{ type: "output_transcript", itemId: id, text: packet.text, final }];
|
|
48
|
+
}
|
|
49
|
+
function ingestFishAudio(turn, chunk) {
|
|
50
|
+
if (turn.dropAudio) return none();
|
|
51
|
+
turn.turnOpen = true;
|
|
52
|
+
return [{ type: "output_audio", itemId: "agent", chunk }];
|
|
53
|
+
}
|
|
54
|
+
function ingestFishState(turn, value) {
|
|
55
|
+
if (value === "speaking" || value === "thinking") {
|
|
56
|
+
turn.turnOpen = true;
|
|
57
|
+
return none();
|
|
58
|
+
}
|
|
59
|
+
if (value !== "listening") return none();
|
|
60
|
+
turn.dropAudio = false;
|
|
61
|
+
if (!turn.turnOpen) return none();
|
|
62
|
+
turn.turnOpen = false;
|
|
63
|
+
return [done("agent", false)];
|
|
64
|
+
}
|
|
65
|
+
function ingestSpeakers(turn, identities) {
|
|
66
|
+
const active = identities.length > 0;
|
|
67
|
+
if (!active) {
|
|
68
|
+
turn.speaking = false;
|
|
69
|
+
return none();
|
|
70
|
+
}
|
|
71
|
+
if (turn.speaking) return none();
|
|
72
|
+
turn.speaking = true;
|
|
73
|
+
return [{ type: "input_speech_start" }];
|
|
74
|
+
}
|
|
75
|
+
function userMessage(value, speak) {
|
|
76
|
+
const message = { type: "user.message", text: value };
|
|
77
|
+
if (!speak) message.audio = false;
|
|
78
|
+
return message;
|
|
79
|
+
}
|
|
80
|
+
function toolResult(callId, result, isError) {
|
|
81
|
+
const message = { type: "client_tool.result", callId, result };
|
|
82
|
+
if (isError) message.isError = true;
|
|
83
|
+
return message;
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
ingestFishAudio,
|
|
87
|
+
ingestFishData,
|
|
88
|
+
ingestFishState,
|
|
89
|
+
ingestFishText,
|
|
90
|
+
ingestSpeakers,
|
|
91
|
+
toolResult,
|
|
92
|
+
userMessage
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish-agents/events.ts"],
|
|
4
|
+
"sourcesContent": ["import type { JsonObject, ToolCall } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport { blankUsage } from '../blank-usage.ts';\nimport type { RealtimeEvent } from '../realtime.ts';\n\nexport interface FishTurn {\n dropAudio: boolean;\n discardFinals: number;\n speaking: boolean;\n turnOpen: boolean;\n}\n\nexport interface FishText {\n identity: string;\n text: string;\n attributes: Record<string, string>;\n}\n\nfunction none(): RealtimeEvent[] {\n return [];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction done(itemId: string, interrupted: boolean): RealtimeEvent {\n return { type: 'response_done', itemId, interrupted, usage: blankUsage() };\n}\n\nexport function ingestFishData(message: Record<string, unknown>): RealtimeEvent[] {\n const type = message.type;\n if (typeof type !== 'string') return none();\n if (type === 'client_tool.call') return toolCall(message);\n if (type === 'error')\n return [{ type: 'error', error: text(message.code) || 'error', fatal: false }];\n if (type === 'session.ended') {\n const reason = text(message.reason);\n return [{ type: 'error', error: reason.length > 0 ? reason : 'session ended', fatal: false }];\n }\n if (type === 'tool.started' || type === 'tool.completed' || type === 'tool.failed') return none();\n return none();\n}\n\nfunction toolCall(message: Record<string, unknown>): RealtimeEvent[] {\n const name = text(message.toolName);\n const id = text(message.callId);\n if (name.length === 0 || id.length === 0) return none();\n const params = isRecord(message.params) ? message.params : {};\n const call: ToolCall = { type: 'toolCall', id, name, arguments: params as JsonObject };\n return [{ type: 'tool_call', call, blocking: message.expectsResponse !== false }];\n}\n\nexport function ingestFishText(\n turn: FishTurn,\n packet: FishText,\n localIdentity: string\n): RealtimeEvent[] {\n const itemId = text(packet.attributes['lk.segment_id']);\n const id = itemId.length > 0 ? itemId : 'segment';\n const final = packet.attributes['lk.transcription_final'] === 'true';\n if (packet.identity === localIdentity) {\n return [{ type: 'input_transcript', itemId: id, text: packet.text, final }];\n }\n if (turn.discardFinals > 0) {\n if (final) turn.discardFinals -= 1;\n return none();\n }\n turn.turnOpen = true;\n return [{ type: 'output_transcript', itemId: id, text: packet.text, final }];\n}\n\nexport function ingestFishAudio(turn: FishTurn, chunk: AudioChunk): RealtimeEvent[] {\n if (turn.dropAudio) return none();\n turn.turnOpen = true;\n return [{ type: 'output_audio', itemId: 'agent', chunk }];\n}\n\nexport function ingestFishState(turn: FishTurn, value: string): RealtimeEvent[] {\n if (value === 'speaking' || value === 'thinking') {\n turn.turnOpen = true;\n return none();\n }\n if (value !== 'listening') return none();\n turn.dropAudio = false;\n if (!turn.turnOpen) return none();\n turn.turnOpen = false;\n return [done('agent', false)];\n}\n\nexport function ingestSpeakers(turn: FishTurn, identities: string[]): RealtimeEvent[] {\n const active = identities.length > 0;\n if (!active) {\n turn.speaking = false;\n return none();\n }\n if (turn.speaking) return none();\n turn.speaking = true;\n return [{ type: 'input_speech_start' }];\n}\n\nexport function userMessage(value: string, speak: boolean): JsonObject {\n const message: JsonObject = { type: 'user.message', text: value };\n if (!speak) message.audio = false;\n return message;\n}\n\nexport function toolResult(callId: string, result: string, isError: boolean): JsonObject {\n const message: JsonObject = { type: 'client_tool.result', callId, result };\n if (isError) message.isError = true;\n return message;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { registerVoiceModel } from "../models.js";
|
|
2
|
+
import { registerRealtimeApiProvider } from "../registry.js";
|
|
3
|
+
import { connectFish } from "./session.js";
|
|
4
|
+
const voices = [{ id: "802e3bc2b27e49c2995d23ef70e6ac89", name: "Example" }];
|
|
5
|
+
function pcm16(sampleRate) {
|
|
6
|
+
return { encoding: "pcm16", sampleRate, channels: 1 };
|
|
7
|
+
}
|
|
8
|
+
const fishAgent = {
|
|
9
|
+
id: "fish-agent",
|
|
10
|
+
name: "Fish Agent",
|
|
11
|
+
api: "fish-agents",
|
|
12
|
+
provider: "fish",
|
|
13
|
+
baseUrl: "https://api.fish.audio",
|
|
14
|
+
type: "realtime",
|
|
15
|
+
transports: ["webrtc"],
|
|
16
|
+
toolCalls: "blocking",
|
|
17
|
+
inputFormat: pcm16(24e3),
|
|
18
|
+
outputFormat: pcm16(24e3),
|
|
19
|
+
maxSessionMs: 18e5,
|
|
20
|
+
resumable: false,
|
|
21
|
+
ephemeralAuth: true,
|
|
22
|
+
voices,
|
|
23
|
+
cascade: { stt: "deepgram:nova-3", llm: "fish", tts: "s2.1-pro" },
|
|
24
|
+
provisioning: "agent",
|
|
25
|
+
delivery: { interrupt: "native", when_idle: "approximate", silent: "approximate" },
|
|
26
|
+
truncation: "server",
|
|
27
|
+
liveToolUpdate: false,
|
|
28
|
+
toolTimeoutMs: { min: 1e3, max: 12e4 },
|
|
29
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
30
|
+
audioCost: { perInputMinute: 0 }
|
|
31
|
+
};
|
|
32
|
+
function registerFishAgents(options = {}) {
|
|
33
|
+
const unregisterModel = registerVoiceModel(fishAgent);
|
|
34
|
+
const unregisterApi = registerRealtimeApiProvider({
|
|
35
|
+
api: "fish-agents",
|
|
36
|
+
connect: (model, context, connectOptions) => connectFish(model, context, connectOptions, options)
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
unregisterModel();
|
|
40
|
+
unregisterApi();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
import { hashHostedSpec, memoryHostedCache } from "../hosted-agent.js";
|
|
44
|
+
import { agentConfig, httpsOrigin, provisionAgent } from "./provision.js";
|
|
45
|
+
import { importFishSdk, openFishRoom } from "./room.js";
|
|
46
|
+
export {
|
|
47
|
+
agentConfig,
|
|
48
|
+
fishAgent,
|
|
49
|
+
hashHostedSpec,
|
|
50
|
+
httpsOrigin,
|
|
51
|
+
importFishSdk,
|
|
52
|
+
memoryHostedCache,
|
|
53
|
+
openFishRoom,
|
|
54
|
+
provisionAgent,
|
|
55
|
+
registerFishAgents
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish-agents/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { RealtimeModel } from '../models.ts';\nimport { registerVoiceModel } from '../models.ts';\nimport { registerRealtimeApiProvider } from '../registry.ts';\nimport type { FishOptions } from './session.ts';\nimport { connectFish } from './session.ts';\n\nconst voices = [{ id: '802e3bc2b27e49c2995d23ef70e6ac89', name: 'Example' }];\n\nfunction pcm16(sampleRate: number) {\n return { encoding: 'pcm16' as const, sampleRate, channels: 1 as const };\n}\n\nexport const fishAgent: RealtimeModel<'fish-agents'> = {\n id: 'fish-agent',\n name: 'Fish Agent',\n api: 'fish-agents',\n provider: 'fish',\n baseUrl: 'https://api.fish.audio',\n type: 'realtime',\n transports: ['webrtc'],\n toolCalls: 'blocking',\n inputFormat: pcm16(24000),\n outputFormat: pcm16(24000),\n maxSessionMs: 1_800_000,\n resumable: false,\n ephemeralAuth: true,\n voices,\n cascade: { stt: 'deepgram:nova-3', llm: 'fish', tts: 's2.1-pro' },\n provisioning: 'agent',\n delivery: { interrupt: 'native', when_idle: 'approximate', silent: 'approximate' },\n truncation: 'server',\n liveToolUpdate: false,\n toolTimeoutMs: { min: 1000, max: 120_000 },\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n audioCost: { perInputMinute: 0 },\n};\n\nexport function registerFishAgents(options: FishOptions = {}): () => void {\n const unregisterModel = registerVoiceModel(fishAgent);\n const unregisterApi = registerRealtimeApiProvider({\n api: 'fish-agents',\n connect: (model, context, connectOptions) =>\n connectFish(model, context, connectOptions, options),\n });\n return () => {\n unregisterModel();\n unregisterApi();\n };\n}\n\nexport type { CustomLlm, HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';\nexport { hashHostedSpec, memoryHostedCache } from '../hosted-agent.ts';\nexport type { ProvisionOptions } from './provision.ts';\nexport { agentConfig, httpsOrigin, provisionAgent } from './provision.ts';\nexport type { FishRoom, FishRoomFactory, FishSdk } from './room.ts';\nexport { importFishSdk, openFishRoom } from './room.ts';\nexport type { FishOptions } from './session.ts';\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,mCAAmC;AAE5C,SAAS,mBAAmB;AAE5B,MAAM,SAAS,CAAC,EAAE,IAAI,oCAAoC,MAAM,UAAU,CAAC;AAE3E,SAAS,MAAM,YAAoB;AACjC,SAAO,EAAE,UAAU,SAAkB,YAAY,UAAU,EAAW;AACxE;AAEO,MAAM,YAA0C;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,KAAK;AAAA,EACL,UAAU;AAAA,EACV,SAAS;AAAA,EACT,MAAM;AAAA,EACN,YAAY,CAAC,QAAQ;AAAA,EACrB,WAAW;AAAA,EACX,aAAa,MAAM,IAAK;AAAA,EACxB,cAAc,MAAM,IAAK;AAAA,EACzB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,eAAe;AAAA,EACf;AAAA,EACA,SAAS,EAAE,KAAK,mBAAmB,KAAK,QAAQ,KAAK,WAAW;AAAA,EAChE,cAAc;AAAA,EACd,UAAU,EAAE,WAAW,UAAU,WAAW,eAAe,QAAQ,cAAc;AAAA,EACjF,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe,EAAE,KAAK,KAAM,KAAK,KAAQ;AAAA,EACzC,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,EAAE;AAAA,EACzD,WAAW,EAAE,gBAAgB,EAAE;AACjC;AAEO,SAAS,mBAAmB,UAAuB,CAAC,GAAe;AACxE,QAAM,kBAAkB,mBAAmB,SAAS;AACpD,QAAM,gBAAgB,4BAA4B;AAAA,IAChD,KAAK;AAAA,IACL,SAAS,CAAC,OAAO,SAAS,mBACxB,YAAY,OAAO,SAAS,gBAAgB,OAAO;AAAA,EACvD,CAAC;AACD,SAAO,MAAM;AACX,oBAAgB;AAChB,kBAAc;AAAA,EAChB;AACF;AAGA,SAAS,gBAAgB,yBAAyB;AAElD,SAAS,aAAa,aAAa,sBAAsB;AAEzD,SAAS,eAAe,oBAAoB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { VoiceRequestError } from "../errors.js";
|
|
2
|
+
import {
|
|
3
|
+
hashHostedSpec,
|
|
4
|
+
hostedCacheKey,
|
|
5
|
+
memoryHostedCache
|
|
6
|
+
} from "../hosted-agent.js";
|
|
7
|
+
const defaultCache = memoryHostedCache();
|
|
8
|
+
const toolName = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function asVoice(error) {
|
|
13
|
+
if (error instanceof VoiceRequestError) return error;
|
|
14
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
15
|
+
return new VoiceRequestError("aborted", "session aborted");
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) return new VoiceRequestError("error", error.message);
|
|
18
|
+
return new VoiceRequestError("error", String(error));
|
|
19
|
+
}
|
|
20
|
+
function httpsOrigin(baseUrl) {
|
|
21
|
+
const url = new URL(baseUrl);
|
|
22
|
+
let protocol = url.protocol;
|
|
23
|
+
if (protocol === "wss:") protocol = "https:";
|
|
24
|
+
if (protocol === "ws:") protocol = "http:";
|
|
25
|
+
return `${protocol}//${url.host}`;
|
|
26
|
+
}
|
|
27
|
+
function mergedHeaders(options, defaults) {
|
|
28
|
+
const headers = { ...defaults };
|
|
29
|
+
const extra = options.headers;
|
|
30
|
+
if (extra === void 0) return headers;
|
|
31
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
32
|
+
if (value === null) delete headers[key];
|
|
33
|
+
else headers[key] = value;
|
|
34
|
+
}
|
|
35
|
+
return headers;
|
|
36
|
+
}
|
|
37
|
+
function apiKey(options) {
|
|
38
|
+
const key = options.apiKey;
|
|
39
|
+
if (typeof key === "string" && key.length > 0) return key;
|
|
40
|
+
throw new VoiceRequestError("error", "missing realtime credential");
|
|
41
|
+
}
|
|
42
|
+
function timeoutSeconds(timeoutMs) {
|
|
43
|
+
if (timeoutMs === void 0) return 30;
|
|
44
|
+
return Math.min(120, Math.max(1, Math.round(timeoutMs / 1e3)));
|
|
45
|
+
}
|
|
46
|
+
function argumentsOf(tool) {
|
|
47
|
+
const parameters = tool.parameters;
|
|
48
|
+
if (!isRecord(parameters) || !isRecord(parameters.properties)) return [];
|
|
49
|
+
const args = [];
|
|
50
|
+
for (const [name, schema] of Object.entries(parameters.properties)) {
|
|
51
|
+
const description = isRecord(schema) && typeof schema.description === "string" ? schema.description : "";
|
|
52
|
+
args.push({ name, description });
|
|
53
|
+
}
|
|
54
|
+
return args;
|
|
55
|
+
}
|
|
56
|
+
function clientTool(tool, timeoutMs) {
|
|
57
|
+
if (!toolName.test(tool.name)) {
|
|
58
|
+
throw new VoiceRequestError("error", `invalid client tool name: ${tool.name}`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
tool_type: "client",
|
|
62
|
+
name: tool.name,
|
|
63
|
+
description: tool.description,
|
|
64
|
+
arguments: argumentsOf(tool),
|
|
65
|
+
expects_response: true,
|
|
66
|
+
timeout_seconds: timeoutSeconds(timeoutMs)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function bearer(headers) {
|
|
70
|
+
if (headers === void 0) return void 0;
|
|
71
|
+
const value = headers.Authorization ?? headers.authorization;
|
|
72
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
73
|
+
if (value.startsWith("Bearer ")) return value.slice("Bearer ".length);
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function llmFields(llm) {
|
|
77
|
+
if (typeof llm === "string") return { model: llm };
|
|
78
|
+
if (llm.api === "openai-responses") {
|
|
79
|
+
throw new VoiceRequestError("error", "fish custom llm accepts openai-completions only");
|
|
80
|
+
}
|
|
81
|
+
const custom = { base_url: llm.baseUrl, model: llm.id };
|
|
82
|
+
const key = bearer(llm.headers);
|
|
83
|
+
if (key !== void 0) custom.api_key = key;
|
|
84
|
+
return { custom };
|
|
85
|
+
}
|
|
86
|
+
function agentConfig(spec, toolIds) {
|
|
87
|
+
const prompt = { system_prompt: spec.instructions };
|
|
88
|
+
if (spec.firstMessage !== void 0) prompt.first_message = spec.firstMessage;
|
|
89
|
+
const config = {
|
|
90
|
+
prompt,
|
|
91
|
+
voice: { voice_id: spec.voice },
|
|
92
|
+
llm: llmFields(spec.llm)
|
|
93
|
+
};
|
|
94
|
+
if (toolIds.length > 0) config.tools = { enabled: true, tool_ids: toolIds };
|
|
95
|
+
return config;
|
|
96
|
+
}
|
|
97
|
+
async function requestJson(fetchImpl, url, init) {
|
|
98
|
+
let response;
|
|
99
|
+
try {
|
|
100
|
+
response = await fetchImpl(url, init);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw asVoice(error);
|
|
103
|
+
}
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new VoiceRequestError("error", `agent request failed (${response.status})`);
|
|
106
|
+
}
|
|
107
|
+
if (response.status === 204) return {};
|
|
108
|
+
let json;
|
|
109
|
+
try {
|
|
110
|
+
json = await response.json();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw asVoice(error);
|
|
113
|
+
}
|
|
114
|
+
if (!isRecord(json)) throw new VoiceRequestError("error", "agent response was not an object");
|
|
115
|
+
return json;
|
|
116
|
+
}
|
|
117
|
+
function textId(json, field, fallback) {
|
|
118
|
+
const id = json[field];
|
|
119
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
120
|
+
if (fallback !== void 0) return fallback;
|
|
121
|
+
throw new VoiceRequestError("error", `agent response had no ${field}`);
|
|
122
|
+
}
|
|
123
|
+
async function createTools(origin, spec, options, fetchImpl) {
|
|
124
|
+
const ids = [];
|
|
125
|
+
for (const tool of spec.tools) {
|
|
126
|
+
const json = await requestJson(fetchImpl, `${origin}/v1/agent/tools`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: mergedHeaders(options, {
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
Authorization: `Bearer ${apiKey(options)}`
|
|
131
|
+
}),
|
|
132
|
+
body: JSON.stringify(clientTool(tool, spec.clientToolTimeoutMs)),
|
|
133
|
+
signal: options.signal
|
|
134
|
+
});
|
|
135
|
+
ids.push(textId(json, "tool_id", void 0));
|
|
136
|
+
}
|
|
137
|
+
return ids;
|
|
138
|
+
}
|
|
139
|
+
async function provisionAgent(model, spec, options = {}) {
|
|
140
|
+
if (options.signal?.aborted === true) throw new VoiceRequestError("aborted", "session aborted");
|
|
141
|
+
const cache = options.cache ?? defaultCache;
|
|
142
|
+
const now = options.now ?? Date.now;
|
|
143
|
+
const key = hostedCacheKey(model.provider, spec.name);
|
|
144
|
+
const hash = hashHostedSpec(spec);
|
|
145
|
+
const cached = cache.get(key);
|
|
146
|
+
if (cached !== void 0 && cached.specHash === hash) return cached;
|
|
147
|
+
const origin = httpsOrigin(model.baseUrl);
|
|
148
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
149
|
+
const toolIds = await createTools(origin, spec, options, fetchImpl);
|
|
150
|
+
const config = agentConfig(spec, toolIds);
|
|
151
|
+
const existing = cached?.agentId ?? options.agentId;
|
|
152
|
+
const headers = mergedHeaders(options, {
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
Authorization: `Bearer ${apiKey(options)}`
|
|
155
|
+
});
|
|
156
|
+
const url = existing === void 0 ? `${origin}/v1/agent/agents` : `${origin}/v1/agent/agents/${encodeURIComponent(existing)}/config`;
|
|
157
|
+
const json = await requestJson(fetchImpl, url, {
|
|
158
|
+
method: existing === void 0 ? "POST" : "PATCH",
|
|
159
|
+
headers,
|
|
160
|
+
body: JSON.stringify(existing === void 0 ? { name: spec.name, config } : config),
|
|
161
|
+
signal: options.signal
|
|
162
|
+
});
|
|
163
|
+
const agentId = textId(json, "agent_id", existing);
|
|
164
|
+
await requestJson(fetchImpl, `${origin}/v1/agent/agents/${encodeURIComponent(agentId)}/publish`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: mergedHeaders(options, { Authorization: `Bearer ${apiKey(options)}` }),
|
|
167
|
+
signal: options.signal
|
|
168
|
+
});
|
|
169
|
+
const agent = { agentId, specHash: hash, publishedAt: now() };
|
|
170
|
+
cache.set(key, agent);
|
|
171
|
+
return agent;
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
agentConfig,
|
|
175
|
+
httpsOrigin,
|
|
176
|
+
provisionAgent
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=provision.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish-agents/provision.ts"],
|
|
4
|
+
"sourcesContent": ["import type { JsonObject, ProviderRequestOptions, Tool } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from '../errors.ts';\nimport {\n type CustomLlm,\n type HostedAgent,\n type HostedAgentSpec,\n type HostedCache,\n hashHostedSpec,\n hostedCacheKey,\n memoryHostedCache,\n} from '../hosted-agent.ts';\nimport type { RealtimeModel } from '../models.ts';\n\nexport interface ProvisionOptions extends ProviderRequestOptions {\n agentId?: string;\n now?: () => number;\n cache?: HostedCache;\n}\n\nconst defaultCache = memoryHostedCache();\nconst toolName = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asVoice(error: unknown): VoiceRequestError {\n if (error instanceof VoiceRequestError) return error;\n if (error instanceof Error && error.name === 'AbortError') {\n return new VoiceRequestError('aborted', 'session aborted');\n }\n if (error instanceof Error) return new VoiceRequestError('error', error.message);\n return new VoiceRequestError('error', String(error));\n}\n\nexport function httpsOrigin(baseUrl: string): string {\n const url = new URL(baseUrl);\n let protocol = url.protocol;\n if (protocol === 'wss:') protocol = 'https:';\n if (protocol === 'ws:') protocol = 'http:';\n return `${protocol}//${url.host}`;\n}\n\nfunction mergedHeaders(\n options: ProvisionOptions,\n defaults: Record<string, string>\n): Record<string, string> {\n const headers = { ...defaults };\n const extra = options.headers;\n if (extra === undefined) return headers;\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n return headers;\n}\n\nfunction apiKey(options: ProvisionOptions): string {\n const key = options.apiKey;\n if (typeof key === 'string' && key.length > 0) return key;\n throw new VoiceRequestError('error', 'missing realtime credential');\n}\n\nfunction timeoutSeconds(timeoutMs: number | undefined): number {\n if (timeoutMs === undefined) return 30;\n return Math.min(120, Math.max(1, Math.round(timeoutMs / 1000)));\n}\n\nfunction argumentsOf(tool: Tool): JsonObject[] {\n const parameters = tool.parameters as unknown;\n if (!isRecord(parameters) || !isRecord(parameters.properties)) return [];\n const args: JsonObject[] = [];\n for (const [name, schema] of Object.entries(parameters.properties)) {\n const description =\n isRecord(schema) && typeof schema.description === 'string' ? schema.description : '';\n args.push({ name, description });\n }\n return args;\n}\n\nfunction clientTool(tool: Tool, timeoutMs: number | undefined): JsonObject {\n if (!toolName.test(tool.name)) {\n throw new VoiceRequestError('error', `invalid client tool name: ${tool.name}`);\n }\n return {\n tool_type: 'client',\n name: tool.name,\n description: tool.description,\n arguments: argumentsOf(tool),\n expects_response: true,\n timeout_seconds: timeoutSeconds(timeoutMs),\n };\n}\n\nfunction bearer(headers: CustomLlm['headers']): string | undefined {\n if (headers === undefined) return undefined;\n const value = headers.Authorization ?? headers.authorization;\n if (typeof value !== 'string' || value.length === 0) return undefined;\n if (value.startsWith('Bearer ')) return value.slice('Bearer '.length);\n return value;\n}\n\nfunction llmFields(llm: string | CustomLlm): JsonObject {\n if (typeof llm === 'string') return { model: llm };\n if (llm.api === 'openai-responses') {\n throw new VoiceRequestError('error', 'fish custom llm accepts openai-completions only');\n }\n const custom: JsonObject = { base_url: llm.baseUrl, model: llm.id };\n const key = bearer(llm.headers);\n if (key !== undefined) custom.api_key = key;\n return { custom };\n}\n\nexport function agentConfig(spec: HostedAgentSpec, toolIds: string[]): JsonObject {\n const prompt: JsonObject = { system_prompt: spec.instructions };\n if (spec.firstMessage !== undefined) prompt.first_message = spec.firstMessage;\n const config: JsonObject = {\n prompt,\n voice: { voice_id: spec.voice },\n llm: llmFields(spec.llm),\n };\n if (toolIds.length > 0) config.tools = { enabled: true, tool_ids: toolIds };\n return config;\n}\n\nasync function requestJson(\n fetchImpl: typeof fetch,\n url: string,\n init: RequestInit\n): Promise<Record<string, unknown>> {\n let response: Response;\n try {\n response = await fetchImpl(url, init);\n } catch (error) {\n throw asVoice(error);\n }\n if (!response.ok) {\n throw new VoiceRequestError('error', `agent request failed (${response.status})`);\n }\n if (response.status === 204) return {};\n let json: unknown;\n try {\n json = await response.json();\n } catch (error) {\n throw asVoice(error);\n }\n if (!isRecord(json)) throw new VoiceRequestError('error', 'agent response was not an object');\n return json;\n}\n\nfunction textId(\n json: Record<string, unknown>,\n field: string,\n fallback: string | undefined\n): string {\n const id = json[field];\n if (typeof id === 'string' && id.length > 0) return id;\n if (fallback !== undefined) return fallback;\n throw new VoiceRequestError('error', `agent response had no ${field}`);\n}\n\nasync function createTools(\n origin: string,\n spec: HostedAgentSpec,\n options: ProvisionOptions,\n fetchImpl: typeof fetch\n): Promise<string[]> {\n const ids: string[] = [];\n for (const tool of spec.tools) {\n const json = await requestJson(fetchImpl, `${origin}/v1/agent/tools`, {\n method: 'POST',\n headers: mergedHeaders(options, {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey(options)}`,\n }),\n body: JSON.stringify(clientTool(tool, spec.clientToolTimeoutMs)),\n signal: options.signal,\n });\n ids.push(textId(json, 'tool_id', undefined));\n }\n return ids;\n}\n\nexport async function provisionAgent(\n model: RealtimeModel<string>,\n spec: HostedAgentSpec,\n options: ProvisionOptions = {}\n): Promise<HostedAgent> {\n if (options.signal?.aborted === true) throw new VoiceRequestError('aborted', 'session aborted');\n const cache = options.cache ?? defaultCache;\n const now = options.now ?? Date.now;\n const key = hostedCacheKey(model.provider, spec.name);\n const hash = hashHostedSpec(spec);\n const cached = cache.get(key);\n if (cached !== undefined && cached.specHash === hash) return cached;\n const origin = httpsOrigin(model.baseUrl);\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const toolIds = await createTools(origin, spec, options, fetchImpl);\n const config = agentConfig(spec, toolIds);\n const existing = cached?.agentId ?? options.agentId;\n const headers = mergedHeaders(options, {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey(options)}`,\n });\n const url =\n existing === undefined\n ? `${origin}/v1/agent/agents`\n : `${origin}/v1/agent/agents/${encodeURIComponent(existing)}/config`;\n const json = await requestJson(fetchImpl, url, {\n method: existing === undefined ? 'POST' : 'PATCH',\n headers,\n body: JSON.stringify(existing === undefined ? { name: spec.name, config } : config),\n signal: options.signal,\n });\n const agentId = textId(json, 'agent_id', existing);\n await requestJson(fetchImpl, `${origin}/v1/agent/agents/${encodeURIComponent(agentId)}/publish`, {\n method: 'POST',\n headers: mergedHeaders(options, { Authorization: `Bearer ${apiKey(options)}` }),\n signal: options.signal,\n });\n const agent: HostedAgent = { agentId, specHash: hash, publishedAt: now() };\n cache.set(key, agent);\n return agent;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const loadSpecifier = new Function("specifier", "return import(specifier)");
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
async function importFishSdk(load = loadSpecifier) {
|
|
6
|
+
try {
|
|
7
|
+
return await load("livekit-client");
|
|
8
|
+
} catch (browserError) {
|
|
9
|
+
try {
|
|
10
|
+
return await load("@livekit/rtc-node");
|
|
11
|
+
} catch {
|
|
12
|
+
if (browserError instanceof Error) throw browserError;
|
|
13
|
+
throw new Error(String(browserError));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function decodePacket(payload) {
|
|
18
|
+
const raw = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
function openFishRoom(sdk, handlers) {
|
|
22
|
+
const room = new sdk.Room();
|
|
23
|
+
room.on("dataReceived", (payload, participant, _kind, topic) => {
|
|
24
|
+
if (topic !== "agent-event") return;
|
|
25
|
+
const identity = isRecord(participant) ? text(participant.identity) : void 0;
|
|
26
|
+
try {
|
|
27
|
+
handlers.data(String(topic), decodePacket(payload), identity);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
handlers.fail(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
room.registerTextStreamHandler("lk.transcription", (reader, participant) => {
|
|
33
|
+
void reader.readAll().then(
|
|
34
|
+
(value) => {
|
|
35
|
+
handlers.text(participant.identity, value, reader.info.attributes ?? {});
|
|
36
|
+
},
|
|
37
|
+
(error) => handlers.fail(error)
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
room.on("participantAttributesChanged", (changed, _participant) => {
|
|
41
|
+
if (!isRecord(changed)) return;
|
|
42
|
+
const value = changed["lk.agent.state"];
|
|
43
|
+
if (typeof value === "string") handlers.attribute("lk.agent.state", value);
|
|
44
|
+
});
|
|
45
|
+
room.on("activeSpeakersChanged", (speakers) => {
|
|
46
|
+
const identities = Array.isArray(speakers) ? speakers.filter(isRecord).map((speaker) => text(speaker.identity)) : [];
|
|
47
|
+
handlers.speakers(identities);
|
|
48
|
+
});
|
|
49
|
+
room.on("trackSubscribed", (track) => {
|
|
50
|
+
if (!isRecord(track) || typeof track.on !== "function") return;
|
|
51
|
+
track.on("audio", (chunk) => handlers.audio(chunk));
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
identity: "",
|
|
55
|
+
async connect(url, token) {
|
|
56
|
+
await room.connect(url, token);
|
|
57
|
+
this.identity = room.localParticipant.identity ?? "";
|
|
58
|
+
},
|
|
59
|
+
async publishMic(source, open) {
|
|
60
|
+
await room.localParticipant.publishTrack(source, { echoCancellation: open.echoCancellation });
|
|
61
|
+
},
|
|
62
|
+
async publishData(payload) {
|
|
63
|
+
await room.localParticipant.publishData(new TextEncoder().encode(payload), {
|
|
64
|
+
reliable: true,
|
|
65
|
+
topic: "client-event"
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
close() {
|
|
69
|
+
room.disconnect();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function text(value) {
|
|
74
|
+
return typeof value === "string" ? value : "";
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
importFishSdk,
|
|
78
|
+
openFishRoom
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=room.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish-agents/room.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioChunk, AudioSource } from '../audio.ts';\nimport type { MicOpen } from '../echo.ts';\n\nconst loadSpecifier = new Function('specifier', 'return import(specifier)') as (\n specifier: string\n) => Promise<FishSdk>;\n\nexport interface FishSdkRoom {\n connect(url: string, token: string): Promise<void>;\n disconnect(): void;\n localParticipant: {\n identity?: string;\n publishTrack(track: unknown, options?: unknown): Promise<unknown>;\n publishData(data: Uint8Array, options: { reliable: boolean; topic: string }): Promise<void>;\n };\n on(event: string, listener: (...args: unknown[]) => void): void;\n registerTextStreamHandler(\n topic: string,\n handler: (\n reader: { readAll(): Promise<string>; info: { attributes?: Record<string, string> } },\n participant: { identity: string }\n ) => void\n ): void;\n}\n\nexport interface FishSdk {\n Room: new () => FishSdkRoom;\n}\n\nexport interface FishHandlers {\n data(topic: string, json: unknown, identity: string | undefined): void;\n text(identity: string, value: string, attributes: Record<string, string>): void;\n attribute(key: string, value: string): void;\n speakers(identities: string[]): void;\n audio(chunk: AudioChunk): void;\n fail(error: unknown): void;\n}\n\nexport interface FishRoom {\n url?: string;\n token?: string;\n identity: string;\n connect(url: string, token: string): Promise<void>;\n publishMic(source: AudioSource, open: MicOpen): Promise<void>;\n publishData(payload: string): Promise<void>;\n close(): void;\n}\n\nexport type FishRoomFactory = (handlers: FishHandlers) => FishRoom;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport async function importFishSdk(\n load: (specifier: string) => Promise<FishSdk> = loadSpecifier\n): Promise<FishSdk> {\n try {\n return await load('livekit-client');\n } catch (browserError) {\n try {\n return await load('@livekit/rtc-node');\n } catch {\n if (browserError instanceof Error) throw browserError;\n throw new Error(String(browserError));\n }\n }\n}\n\nfunction decodePacket(payload: unknown): unknown {\n const raw =\n typeof payload === 'string' ? payload : new TextDecoder().decode(payload as Uint8Array);\n return JSON.parse(raw);\n}\n\nexport function openFishRoom(sdk: FishSdk, handlers: FishHandlers): FishRoom {\n const room = new sdk.Room();\n room.on('dataReceived', (payload, participant, _kind, topic) => {\n if (topic !== 'agent-event') return;\n const identity = isRecord(participant) ? text(participant.identity) : undefined;\n try {\n handlers.data(String(topic), decodePacket(payload), identity);\n } catch (error) {\n handlers.fail(error);\n }\n });\n room.registerTextStreamHandler('lk.transcription', (reader, participant) => {\n void reader.readAll().then(\n (value) => {\n handlers.text(participant.identity, value, reader.info.attributes ?? {});\n },\n (error: unknown) => handlers.fail(error)\n );\n });\n room.on('participantAttributesChanged', (changed, _participant) => {\n if (!isRecord(changed)) return;\n const value = changed['lk.agent.state'];\n if (typeof value === 'string') handlers.attribute('lk.agent.state', value);\n });\n room.on('activeSpeakersChanged', (speakers) => {\n const identities = Array.isArray(speakers)\n ? speakers.filter(isRecord).map((speaker) => text(speaker.identity))\n : [];\n handlers.speakers(identities);\n });\n room.on('trackSubscribed', (track) => {\n if (!isRecord(track) || typeof track.on !== 'function') return;\n track.on('audio', (chunk: AudioChunk) => handlers.audio(chunk));\n });\n return {\n identity: '',\n async connect(url, token) {\n await room.connect(url, token);\n this.identity = room.localParticipant.identity ?? '';\n },\n async publishMic(source, open) {\n await room.localParticipant.publishTrack(source, { echoCancellation: open.echoCancellation });\n },\n async publishData(payload) {\n await room.localParticipant.publishData(new TextEncoder().encode(payload), {\n reliable: true,\n topic: 'client-event',\n });\n },\n close() {\n room.disconnect();\n },\n };\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n"],
|
|
5
|
+
"mappings": "AAGA,MAAM,gBAAgB,IAAI,SAAS,aAAa,0BAA0B;AA+C1E,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,eAAsB,cACpB,OAAgD,eAC9B;AAClB,MAAI;AACF,WAAO,MAAM,KAAK,gBAAgB;AAAA,EACpC,SAAS,cAAc;AACrB,QAAI;AACF,aAAO,MAAM,KAAK,mBAAmB;AAAA,IACvC,QAAQ;AACN,UAAI,wBAAwB,MAAO,OAAM;AACzC,YAAM,IAAI,MAAM,OAAO,YAAY,CAAC;AAAA,IACtC;AAAA,EACF;AACF;AAEA,SAAS,aAAa,SAA2B;AAC/C,QAAM,MACJ,OAAO,YAAY,WAAW,UAAU,IAAI,YAAY,EAAE,OAAO,OAAqB;AACxF,SAAO,KAAK,MAAM,GAAG;AACvB;AAEO,SAAS,aAAa,KAAc,UAAkC;AAC3E,QAAM,OAAO,IAAI,IAAI,KAAK;AAC1B,OAAK,GAAG,gBAAgB,CAAC,SAAS,aAAa,OAAO,UAAU;AAC9D,QAAI,UAAU,cAAe;AAC7B,UAAM,WAAW,SAAS,WAAW,IAAI,KAAK,YAAY,QAAQ,IAAI;AACtE,QAAI;AACF,eAAS,KAAK,OAAO,KAAK,GAAG,aAAa,OAAO,GAAG,QAAQ;AAAA,IAC9D,SAAS,OAAO;AACd,eAAS,KAAK,KAAK;AAAA,IACrB;AAAA,EACF,CAAC;AACD,OAAK,0BAA0B,oBAAoB,CAAC,QAAQ,gBAAgB;AAC1E,SAAK,OAAO,QAAQ,EAAE;AAAA,MACpB,CAAC,UAAU;AACT,iBAAS,KAAK,YAAY,UAAU,OAAO,OAAO,KAAK,cAAc,CAAC,CAAC;AAAA,MACzE;AAAA,MACA,CAAC,UAAmB,SAAS,KAAK,KAAK;AAAA,IACzC;AAAA,EACF,CAAC;AACD,OAAK,GAAG,gCAAgC,CAAC,SAAS,iBAAiB;AACjE,QAAI,CAAC,SAAS,OAAO,EAAG;AACxB,UAAM,QAAQ,QAAQ,gBAAgB;AACtC,QAAI,OAAO,UAAU,SAAU,UAAS,UAAU,kBAAkB,KAAK;AAAA,EAC3E,CAAC;AACD,OAAK,GAAG,yBAAyB,CAAC,aAAa;AAC7C,UAAM,aAAa,MAAM,QAAQ,QAAQ,IACrC,SAAS,OAAO,QAAQ,EAAE,IAAI,CAAC,YAAY,KAAK,QAAQ,QAAQ,CAAC,IACjE,CAAC;AACL,aAAS,SAAS,UAAU;AAAA,EAC9B,CAAC;AACD,OAAK,GAAG,mBAAmB,CAAC,UAAU;AACpC,QAAI,CAAC,SAAS,KAAK,KAAK,OAAO,MAAM,OAAO,WAAY;AACxD,UAAM,GAAG,SAAS,CAAC,UAAsB,SAAS,MAAM,KAAK,CAAC;AAAA,EAChE,CAAC;AACD,SAAO;AAAA,IACL,UAAU;AAAA,IACV,MAAM,QAAQ,KAAK,OAAO;AACxB,YAAM,KAAK,QAAQ,KAAK,KAAK;AAC7B,WAAK,WAAW,KAAK,iBAAiB,YAAY;AAAA,IACpD;AAAA,IACA,MAAM,WAAW,QAAQ,MAAM;AAC7B,YAAM,KAAK,iBAAiB,aAAa,QAAQ,EAAE,kBAAkB,KAAK,iBAAiB,CAAC;AAAA,IAC9F;AAAA,IACA,MAAM,YAAY,SAAS;AACzB,YAAM,KAAK,iBAAiB,YAAY,IAAI,YAAY,EAAE,OAAO,OAAO,GAAG;AAAA,QACzE,UAAU;AAAA,QACV,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,QAAQ;AACN,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AACF;AAEA,SAAS,KAAK,OAAwB;AACpC,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { micOpen, openEchoMic } from "../echo.js";
|
|
2
|
+
import { VoiceRequestError } from "../errors.js";
|
|
3
|
+
import { pushQueue } from "../queue.js";
|
|
4
|
+
import { planTruncation, RealtimeLedger } from "../realtime.js";
|
|
5
|
+
import { priceAudio } from "../usage.js";
|
|
6
|
+
import {
|
|
7
|
+
ingestFishAudio,
|
|
8
|
+
ingestFishData,
|
|
9
|
+
ingestFishState,
|
|
10
|
+
ingestFishText,
|
|
11
|
+
ingestSpeakers,
|
|
12
|
+
toolResult,
|
|
13
|
+
userMessage
|
|
14
|
+
} from "./events.js";
|
|
15
|
+
import { provisionAgent } from "./provision.js";
|
|
16
|
+
import {
|
|
17
|
+
importFishSdk,
|
|
18
|
+
openFishRoom
|
|
19
|
+
} from "./room.js";
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function resultText(result) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
for (const part of result.content) {
|
|
26
|
+
if (part.type === "text") parts.push(part.text);
|
|
27
|
+
}
|
|
28
|
+
return parts.join("\n");
|
|
29
|
+
}
|
|
30
|
+
function hasKey(options) {
|
|
31
|
+
return typeof options?.apiKey === "string" && options.apiKey.length > 0;
|
|
32
|
+
}
|
|
33
|
+
function talkerLlm(model) {
|
|
34
|
+
const cascade = model.cascade;
|
|
35
|
+
if (cascade === void 0) return "fish";
|
|
36
|
+
return cascade.llm;
|
|
37
|
+
}
|
|
38
|
+
function specFromContext(model, context) {
|
|
39
|
+
return {
|
|
40
|
+
name: model.name,
|
|
41
|
+
instructions: context.instructions,
|
|
42
|
+
tools: context.tools,
|
|
43
|
+
voice: context.voice,
|
|
44
|
+
llm: talkerLlm(model)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function placeholder(agentId) {
|
|
48
|
+
return { agentId, specHash: "", publishedAt: 0 };
|
|
49
|
+
}
|
|
50
|
+
async function ensureAgent(model, context, options, settings) {
|
|
51
|
+
if (!hasKey(options)) {
|
|
52
|
+
const id = settings.agentId;
|
|
53
|
+
if (id === void 0 || id.length === 0) {
|
|
54
|
+
throw new VoiceRequestError("error", "missing realtime credential");
|
|
55
|
+
}
|
|
56
|
+
return placeholder(id);
|
|
57
|
+
}
|
|
58
|
+
const provision = {
|
|
59
|
+
...options,
|
|
60
|
+
agentId: settings.agentId,
|
|
61
|
+
cache: settings.cache,
|
|
62
|
+
now: settings.now
|
|
63
|
+
};
|
|
64
|
+
return provisionAgent(model, specFromContext(model, context), provision);
|
|
65
|
+
}
|
|
66
|
+
function sessionBody(agentId, context, firstMessage) {
|
|
67
|
+
const overrides = {};
|
|
68
|
+
if (context.instructions.length > 0) overrides.system_prompt = context.instructions;
|
|
69
|
+
if (context.voice.length > 0) overrides.voice_id = context.voice;
|
|
70
|
+
if (firstMessage !== void 0) overrides.first_message = firstMessage;
|
|
71
|
+
const body = { agent_id: agentId };
|
|
72
|
+
if (Object.keys(overrides).length > 0) body.overrides = overrides;
|
|
73
|
+
return body;
|
|
74
|
+
}
|
|
75
|
+
function authHeaders(options) {
|
|
76
|
+
const headers = { "Content-Type": "application/json" };
|
|
77
|
+
const key = options?.apiKey;
|
|
78
|
+
if (typeof key === "string" && key.length > 0) headers.Authorization = `Bearer ${key}`;
|
|
79
|
+
const extra = options?.headers;
|
|
80
|
+
if (extra === void 0) return headers;
|
|
81
|
+
for (const [key2, value] of Object.entries(extra)) {
|
|
82
|
+
if (value === null) delete headers[key2];
|
|
83
|
+
else headers[key2] = value;
|
|
84
|
+
}
|
|
85
|
+
return headers;
|
|
86
|
+
}
|
|
87
|
+
async function createSession(model, agentId, context, options, firstMessage) {
|
|
88
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
89
|
+
const response = await fetchImpl(`${new URL(model.baseUrl).origin}/v1/agent/sessions`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: authHeaders(options),
|
|
92
|
+
body: JSON.stringify(sessionBody(agentId, context, firstMessage)),
|
|
93
|
+
signal: options?.signal
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
throw new VoiceRequestError("error", `session request failed (${response.status})`);
|
|
97
|
+
const json = await response.json();
|
|
98
|
+
if (!isRecord(json)) throw new VoiceRequestError("error", "session response was not an object");
|
|
99
|
+
if (json.transport !== "livekit") throw new VoiceRequestError("error", "unsupported transport");
|
|
100
|
+
const url = json.livekit_url;
|
|
101
|
+
const token = json.token;
|
|
102
|
+
const sessionId = json.session_id;
|
|
103
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
104
|
+
throw new VoiceRequestError("error", "missing livekit url");
|
|
105
|
+
}
|
|
106
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
107
|
+
throw new VoiceRequestError("error", "missing livekit token");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
url,
|
|
111
|
+
token,
|
|
112
|
+
sessionId: typeof sessionId === "string" && sessionId.length > 0 ? sessionId : "session"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function joinTarget(model, context, options, settings) {
|
|
116
|
+
const minted = options?.token;
|
|
117
|
+
if (minted !== void 0 && minted.value.length > 0) {
|
|
118
|
+
const url = minted.url;
|
|
119
|
+
if (url === void 0 || url.length === 0)
|
|
120
|
+
throw new VoiceRequestError("error", "missing livekit url");
|
|
121
|
+
return { url, token: minted.value, sessionId: "session" };
|
|
122
|
+
}
|
|
123
|
+
const agent = await ensureAgent(model, context, options, settings);
|
|
124
|
+
return createSession(model, agent.agentId, context, options, settings.firstMessage);
|
|
125
|
+
}
|
|
126
|
+
async function openRoom(settings, handlers) {
|
|
127
|
+
if (settings.room !== void 0) return settings.room(handlers);
|
|
128
|
+
if (settings.sdk !== void 0) return openFishRoom(settings.sdk, handlers);
|
|
129
|
+
return openFishRoom(await importFishSdk(settings.loadSdk), handlers);
|
|
130
|
+
}
|
|
131
|
+
async function publishMicrophone(room, settings) {
|
|
132
|
+
const mic = settings.microphone;
|
|
133
|
+
if (mic === void 0) return;
|
|
134
|
+
const echo = settings.echo;
|
|
135
|
+
const mode = echo === void 0 ? "browser" : echo.mode;
|
|
136
|
+
const source = echo === void 0 ? mic : openEchoMic(echo, mic);
|
|
137
|
+
await room.publishMic(source, micOpen(mode, false));
|
|
138
|
+
}
|
|
139
|
+
class FishSession {
|
|
140
|
+
model;
|
|
141
|
+
#context;
|
|
142
|
+
#room;
|
|
143
|
+
#now;
|
|
144
|
+
#started;
|
|
145
|
+
#ledger = new RealtimeLedger();
|
|
146
|
+
#events = pushQueue();
|
|
147
|
+
#turn = {
|
|
148
|
+
dropAudio: false,
|
|
149
|
+
discardFinals: 0,
|
|
150
|
+
speaking: false,
|
|
151
|
+
turnOpen: false
|
|
152
|
+
};
|
|
153
|
+
#closed = false;
|
|
154
|
+
#usage;
|
|
155
|
+
constructor(model, context, room, now) {
|
|
156
|
+
this.model = model;
|
|
157
|
+
this.#context = context;
|
|
158
|
+
this.#room = room;
|
|
159
|
+
this.#now = now;
|
|
160
|
+
this.#started = now();
|
|
161
|
+
}
|
|
162
|
+
start(sessionId) {
|
|
163
|
+
this.#emit({ type: "session_start", sessionId });
|
|
164
|
+
}
|
|
165
|
+
#assertOpen() {
|
|
166
|
+
if (this.#closed) throw new VoiceRequestError("error", "session is closed");
|
|
167
|
+
}
|
|
168
|
+
#send(event) {
|
|
169
|
+
void Promise.resolve(this.#room.publishData(JSON.stringify(event))).catch((error) => {
|
|
170
|
+
this.#fail(error);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
#emit(event) {
|
|
174
|
+
if (this.#closed) return;
|
|
175
|
+
this.#ledger.observe(event);
|
|
176
|
+
this.#events.push(event);
|
|
177
|
+
}
|
|
178
|
+
appendAudio(chunk) {
|
|
179
|
+
this.#assertOpen();
|
|
180
|
+
this.#ledger.addInput(chunk);
|
|
181
|
+
const source = (async function* source2() {
|
|
182
|
+
yield chunk;
|
|
183
|
+
})();
|
|
184
|
+
void publishMicrophone(this.#room, { microphone: source }).catch(
|
|
185
|
+
(error) => this.#fail(error)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
sendText(value) {
|
|
189
|
+
this.#assertOpen();
|
|
190
|
+
this.#ledger.addText();
|
|
191
|
+
this.#send(userMessage(value, true));
|
|
192
|
+
}
|
|
193
|
+
inject(value, delivery) {
|
|
194
|
+
this.#assertOpen();
|
|
195
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
196
|
+
this.#assertOpen();
|
|
197
|
+
this.#ledger.addText();
|
|
198
|
+
if (delivery === "silent") this.#turn.discardFinals += 1;
|
|
199
|
+
if (delivery === "interrupt") this.#send({ type: "user.interrupt" });
|
|
200
|
+
this.#send(userMessage(value, delivery !== "silent"));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
respond(callId, result, delivery) {
|
|
204
|
+
this.#assertOpen();
|
|
205
|
+
const send = () => {
|
|
206
|
+
this.#send(toolResult(callId, resultText(result), result.isError));
|
|
207
|
+
};
|
|
208
|
+
if (delivery === void 0) {
|
|
209
|
+
send();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
213
|
+
this.#assertOpen();
|
|
214
|
+
send();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
interrupt(heard) {
|
|
218
|
+
this.#assertOpen();
|
|
219
|
+
planTruncation(this.model.truncation, heard);
|
|
220
|
+
this.#turn.dropAudio = true;
|
|
221
|
+
this.#send({ type: "user.interrupt" });
|
|
222
|
+
this.#ledger.settle();
|
|
223
|
+
}
|
|
224
|
+
update(patch) {
|
|
225
|
+
this.#assertOpen();
|
|
226
|
+
if (patch.tools !== void 0 && this.model.liveToolUpdate === false) {
|
|
227
|
+
throw new VoiceRequestError("error", "live tool update is unsupported");
|
|
228
|
+
}
|
|
229
|
+
if (patch.tools !== void 0) this.#context.tools = patch.tools;
|
|
230
|
+
if (patch.instructions !== void 0) this.#context.instructions = patch.instructions;
|
|
231
|
+
if (patch.voice !== void 0) this.#context.voice = patch.voice;
|
|
232
|
+
}
|
|
233
|
+
async close() {
|
|
234
|
+
if (this.#usage !== void 0) return { usage: this.#usage };
|
|
235
|
+
this.#ledger.settle();
|
|
236
|
+
this.#closed = true;
|
|
237
|
+
this.#room.close();
|
|
238
|
+
this.#events.end();
|
|
239
|
+
const inputSeconds = Math.max(0, this.#now() - this.#started) / 1e3;
|
|
240
|
+
this.#usage = priceAudio(this.model.audioCost, {
|
|
241
|
+
inputSeconds,
|
|
242
|
+
outputSeconds: this.#ledger.outputSeconds,
|
|
243
|
+
textMessages: this.#ledger.textMessages
|
|
244
|
+
});
|
|
245
|
+
return { usage: this.#usage };
|
|
246
|
+
}
|
|
247
|
+
[Symbol.asyncIterator]() {
|
|
248
|
+
return this.#events.iterable[Symbol.asyncIterator]();
|
|
249
|
+
}
|
|
250
|
+
handlers() {
|
|
251
|
+
return {
|
|
252
|
+
data: (topic, json) => {
|
|
253
|
+
if (topic !== "agent-event" || !isRecord(json)) return;
|
|
254
|
+
for (const event of ingestFishData(json)) this.#emit(event);
|
|
255
|
+
},
|
|
256
|
+
text: (identity, value, attributes) => {
|
|
257
|
+
const packet = { identity, text: value, attributes };
|
|
258
|
+
for (const event of ingestFishText(this.#turn, packet, this.#room.identity))
|
|
259
|
+
this.#emit(event);
|
|
260
|
+
},
|
|
261
|
+
attribute: (key, value) => {
|
|
262
|
+
if (key !== "lk.agent.state") return;
|
|
263
|
+
for (const event of ingestFishState(this.#turn, value)) this.#emit(event);
|
|
264
|
+
},
|
|
265
|
+
speakers: (identities) => {
|
|
266
|
+
for (const event of ingestSpeakers(this.#turn, identities)) this.#emit(event);
|
|
267
|
+
},
|
|
268
|
+
audio: (chunk) => {
|
|
269
|
+
for (const event of ingestFishAudio(this.#turn, chunk)) this.#emit(event);
|
|
270
|
+
},
|
|
271
|
+
fail: (error) => this.#fail(error)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
#fail(error) {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
this.#emit({ type: "error", error: message, fatal: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function connectFish(model, context, options, settings = {}) {
|
|
280
|
+
if (options?.signal?.aborted === true) throw new VoiceRequestError("aborted", "session aborted");
|
|
281
|
+
const target = await joinTarget(model, context, options, settings);
|
|
282
|
+
const sessionHolder = {};
|
|
283
|
+
const room = await openRoom(settings, {
|
|
284
|
+
data: (topic, json) => sessionHolder.session?.handlers().data(topic, json, void 0),
|
|
285
|
+
text: (identity, value, attributes) => sessionHolder.session?.handlers().text(identity, value, attributes),
|
|
286
|
+
attribute: (key, value) => sessionHolder.session?.handlers().attribute(key, value),
|
|
287
|
+
speakers: (identities) => sessionHolder.session?.handlers().speakers(identities),
|
|
288
|
+
audio: (chunk) => sessionHolder.session?.handlers().audio(chunk),
|
|
289
|
+
fail: (error) => sessionHolder.session?.handlers().fail(error)
|
|
290
|
+
});
|
|
291
|
+
const now = settings.now !== void 0 ? settings.now : Date.now;
|
|
292
|
+
const session = new FishSession(model, context, room, now);
|
|
293
|
+
sessionHolder.session = session;
|
|
294
|
+
await room.connect(target.url, target.token);
|
|
295
|
+
if (room.identity.length === 0) room.identity = "user";
|
|
296
|
+
await publishMicrophone(room, settings);
|
|
297
|
+
session.start(target.sessionId);
|
|
298
|
+
return session;
|
|
299
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
connectFish,
|
|
302
|
+
specFromContext
|
|
303
|
+
};
|
|
304
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish-agents/session.ts"],
|
|
4
|
+
"sourcesContent": ["import type { JsonObject, ToolResultMessage, Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk, AudioSource } from '../audio.ts';\nimport { type EchoControl, micOpen, openEchoMic } from '../echo.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';\nimport type { Delivery, RealtimeModel } from '../models.ts';\nimport { pushQueue } from '../queue.ts';\nimport type {\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n} from '../realtime.ts';\nimport { planTruncation, RealtimeLedger } from '../realtime.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n type FishText,\n type FishTurn,\n ingestFishAudio,\n ingestFishData,\n ingestFishState,\n ingestFishText,\n ingestSpeakers,\n toolResult,\n userMessage,\n} from './events.ts';\nimport { type ProvisionOptions, provisionAgent } from './provision.ts';\nimport {\n type FishHandlers,\n type FishRoom,\n type FishRoomFactory,\n type FishSdk,\n importFishSdk,\n openFishRoom,\n} from './room.ts';\n\nexport interface FishOptions {\n room?: FishRoomFactory;\n sdk?: FishSdk;\n cache?: HostedCache;\n agentId?: string;\n now?: () => number;\n firstMessage?: string;\n microphone?: AudioSource;\n echo?: EchoControl;\n loadSdk?: (specifier: string) => Promise<FishSdk>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction resultText(result: ToolResultMessage): string {\n const parts: string[] = [];\n for (const part of result.content) {\n if (part.type === 'text') parts.push(part.text);\n }\n return parts.join('\\n');\n}\n\nfunction hasKey(options: RealtimeOptions | undefined): boolean {\n return typeof options?.apiKey === 'string' && options.apiKey.length > 0;\n}\n\nfunction talkerLlm(model: RealtimeModel<string>): string {\n const cascade = model.cascade;\n if (cascade === undefined) return 'fish';\n return cascade.llm;\n}\n\nexport function specFromContext(\n model: RealtimeModel<string>,\n context: RealtimeContext\n): HostedAgentSpec {\n return {\n name: model.name,\n instructions: context.instructions,\n tools: context.tools,\n voice: context.voice,\n llm: talkerLlm(model),\n };\n}\n\nfunction placeholder(agentId: string): HostedAgent {\n return { agentId, specHash: '', publishedAt: 0 };\n}\n\nasync function ensureAgent(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: FishOptions\n): Promise<HostedAgent> {\n if (!hasKey(options)) {\n const id = settings.agentId;\n if (id === undefined || id.length === 0) {\n throw new VoiceRequestError('error', 'missing realtime credential');\n }\n return placeholder(id);\n }\n const provision: ProvisionOptions = {\n ...options,\n agentId: settings.agentId,\n cache: settings.cache,\n now: settings.now,\n };\n return provisionAgent(model, specFromContext(model, context), provision);\n}\n\nfunction sessionBody(\n agentId: string,\n context: RealtimeContext,\n firstMessage: string | undefined\n): JsonObject {\n const overrides: JsonObject = {};\n if (context.instructions.length > 0) overrides.system_prompt = context.instructions;\n if (context.voice.length > 0) overrides.voice_id = context.voice;\n if (firstMessage !== undefined) overrides.first_message = firstMessage;\n const body: JsonObject = { agent_id: agentId };\n if (Object.keys(overrides).length > 0) body.overrides = overrides;\n return body;\n}\n\nfunction authHeaders(options: RealtimeOptions | undefined): Record<string, string> {\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n const key = options?.apiKey;\n if (typeof key === 'string' && key.length > 0) headers.Authorization = `Bearer ${key}`;\n const extra = options?.headers;\n if (extra === undefined) return headers;\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n return headers;\n}\n\nasync function createSession(\n model: RealtimeModel<string>,\n agentId: string,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n firstMessage: string | undefined\n): Promise<{ url: string; token: string; sessionId: string }> {\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const response = await fetchImpl(`${new URL(model.baseUrl).origin}/v1/agent/sessions`, {\n method: 'POST',\n headers: authHeaders(options),\n body: JSON.stringify(sessionBody(agentId, context, firstMessage)),\n signal: options?.signal,\n });\n if (!response.ok)\n throw new VoiceRequestError('error', `session request failed (${response.status})`);\n const json: unknown = await response.json();\n if (!isRecord(json)) throw new VoiceRequestError('error', 'session response was not an object');\n if (json.transport !== 'livekit') throw new VoiceRequestError('error', 'unsupported transport');\n const url = json.livekit_url;\n const token = json.token;\n const sessionId = json.session_id;\n if (typeof url !== 'string' || url.length === 0) {\n throw new VoiceRequestError('error', 'missing livekit url');\n }\n if (typeof token !== 'string' || token.length === 0) {\n throw new VoiceRequestError('error', 'missing livekit token');\n }\n return {\n url,\n token,\n sessionId: typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : 'session',\n };\n}\n\nasync function joinTarget(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: FishOptions\n): Promise<{ url: string; token: string; sessionId: string }> {\n const minted = options?.token;\n if (minted !== undefined && minted.value.length > 0) {\n const url = minted.url;\n if (url === undefined || url.length === 0)\n throw new VoiceRequestError('error', 'missing livekit url');\n return { url, token: minted.value, sessionId: 'session' };\n }\n const agent = await ensureAgent(model, context, options, settings);\n return createSession(model, agent.agentId, context, options, settings.firstMessage);\n}\n\nasync function openRoom(settings: FishOptions, handlers: FishHandlers): Promise<FishRoom> {\n if (settings.room !== undefined) return settings.room(handlers);\n if (settings.sdk !== undefined) return openFishRoom(settings.sdk, handlers);\n return openFishRoom(await importFishSdk(settings.loadSdk), handlers);\n}\n\nasync function publishMicrophone(room: FishRoom, settings: FishOptions): Promise<void> {\n const mic = settings.microphone;\n if (mic === undefined) return;\n const echo = settings.echo;\n const mode = echo === undefined ? 'browser' : echo.mode;\n const source = echo === undefined ? mic : openEchoMic(echo, mic);\n await room.publishMic(source, micOpen(mode, false));\n}\n\nclass FishSession implements RealtimeSession {\n readonly model: RealtimeModel<string>;\n readonly #context: RealtimeContext;\n readonly #room: FishRoom;\n readonly #now: () => number;\n readonly #started: number;\n readonly #ledger = new RealtimeLedger();\n readonly #events = pushQueue<RealtimeEvent>();\n readonly #turn: FishTurn = {\n dropAudio: false,\n discardFinals: 0,\n speaking: false,\n turnOpen: false,\n };\n #closed = false;\n #usage: Usage | undefined;\n\n constructor(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n room: FishRoom,\n now: () => number\n ) {\n this.model = model;\n this.#context = context;\n this.#room = room;\n this.#now = now;\n this.#started = now();\n }\n\n start(sessionId: string): void {\n this.#emit({ type: 'session_start', sessionId });\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new VoiceRequestError('error', 'session is closed');\n }\n\n #send(event: JsonObject): void {\n void Promise.resolve(this.#room.publishData(JSON.stringify(event))).catch((error: unknown) => {\n this.#fail(error);\n });\n }\n\n #emit(event: RealtimeEvent): void {\n if (this.#closed) return;\n this.#ledger.observe(event);\n this.#events.push(event);\n }\n\n appendAudio(chunk: AudioChunk): void {\n this.#assertOpen();\n this.#ledger.addInput(chunk);\n const source = (async function* source() {\n yield chunk;\n })();\n void publishMicrophone(this.#room, { microphone: source }).catch((error: unknown) =>\n this.#fail(error)\n );\n }\n\n sendText(value: string): void {\n this.#assertOpen();\n this.#ledger.addText();\n this.#send(userMessage(value, true));\n }\n\n inject(value: string, delivery: Delivery): void {\n this.#assertOpen();\n this.#ledger.submit(this.model.delivery, delivery, () => {\n this.#assertOpen();\n this.#ledger.addText();\n if (delivery === 'silent') this.#turn.discardFinals += 1;\n if (delivery === 'interrupt') this.#send({ type: 'user.interrupt' });\n this.#send(userMessage(value, delivery !== 'silent'));\n });\n }\n\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void {\n this.#assertOpen();\n const send = () => {\n this.#send(toolResult(callId, resultText(result), result.isError));\n };\n if (delivery === undefined) {\n send();\n return;\n }\n this.#ledger.submit(this.model.delivery, delivery, () => {\n this.#assertOpen();\n send();\n });\n }\n\n interrupt(heard?: { itemId: string; heardMs: number }): void {\n this.#assertOpen();\n planTruncation(this.model.truncation, heard);\n this.#turn.dropAudio = true;\n this.#send({ type: 'user.interrupt' });\n this.#ledger.settle();\n }\n\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void {\n this.#assertOpen();\n if (patch.tools !== undefined && this.model.liveToolUpdate === false) {\n throw new VoiceRequestError('error', 'live tool update is unsupported');\n }\n if (patch.tools !== undefined) this.#context.tools = patch.tools;\n if (patch.instructions !== undefined) this.#context.instructions = patch.instructions;\n if (patch.voice !== undefined) this.#context.voice = patch.voice;\n }\n\n async close(): Promise<{ usage: Usage }> {\n if (this.#usage !== undefined) return { usage: this.#usage };\n this.#ledger.settle();\n this.#closed = true;\n this.#room.close();\n this.#events.end();\n const inputSeconds = Math.max(0, this.#now() - this.#started) / 1000;\n this.#usage = priceAudio(this.model.audioCost, {\n inputSeconds,\n outputSeconds: this.#ledger.outputSeconds,\n textMessages: this.#ledger.textMessages,\n });\n return { usage: this.#usage };\n }\n\n [Symbol.asyncIterator](): AsyncIterator<RealtimeEvent> {\n return this.#events.iterable[Symbol.asyncIterator]();\n }\n\n handlers(): FishHandlers {\n return {\n data: (topic, json) => {\n if (topic !== 'agent-event' || !isRecord(json)) return;\n for (const event of ingestFishData(json)) this.#emit(event);\n },\n text: (identity, value, attributes) => {\n const packet: FishText = { identity, text: value, attributes };\n for (const event of ingestFishText(this.#turn, packet, this.#room.identity))\n this.#emit(event);\n },\n attribute: (key, value) => {\n if (key !== 'lk.agent.state') return;\n for (const event of ingestFishState(this.#turn, value)) this.#emit(event);\n },\n speakers: (identities) => {\n for (const event of ingestSpeakers(this.#turn, identities)) this.#emit(event);\n },\n audio: (chunk) => {\n for (const event of ingestFishAudio(this.#turn, chunk)) this.#emit(event);\n },\n fail: (error) => this.#fail(error),\n };\n }\n\n #fail(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error);\n this.#emit({ type: 'error', error: message, fatal: true });\n }\n}\n\nexport async function connectFish(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: FishOptions = {}\n): Promise<RealtimeSession> {\n if (options?.signal?.aborted === true) throw new VoiceRequestError('aborted', 'session aborted');\n const target = await joinTarget(model, context, options, settings);\n const sessionHolder: { session?: FishSession } = {};\n const room = await openRoom(settings, {\n data: (topic, json) => sessionHolder.session?.handlers().data(topic, json, undefined),\n text: (identity, value, attributes) =>\n sessionHolder.session?.handlers().text(identity, value, attributes),\n attribute: (key, value) => sessionHolder.session?.handlers().attribute(key, value),\n speakers: (identities) => sessionHolder.session?.handlers().speakers(identities),\n audio: (chunk) => sessionHolder.session?.handlers().audio(chunk),\n fail: (error) => sessionHolder.session?.handlers().fail(error),\n });\n const now = settings.now !== undefined ? settings.now : Date.now;\n const session = new FishSession(model, context, room, now);\n sessionHolder.session = session;\n await room.connect(target.url, target.token);\n if (room.identity.length === 0) room.identity = 'user';\n await publishMicrophone(room, settings);\n session.start(target.sessionId);\n return session;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["key", "source"]
|
|
7
|
+
}
|
package/dist/realtime.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/realtime.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n Message,\n ProviderRequestOptions,\n Tool,\n ToolCall,\n ToolResultMessage,\n Usage,\n} from '@earendil-works/pi-ai';\nimport {\n type AudioChunk,\n alignmentSeconds,\n type CharAlignment,\n chunkSeconds,\n type TextSpan,\n} from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { Delivery, DeliverySupport, RealtimeModel } from './models.ts';\nimport { priceAudio, type RealtimeAudioCost } from './usage.ts';\n\nexport interface RealtimeContext {\n instructions: string;\n tools: Tool[];\n voice: string;\n history?: Message[];\n}\n\nexport interface RealtimeToken {\n value: string;\n expiresAt: number;\n model: string;\n}\n\nexport interface RealtimeOptions extends ProviderRequestOptions {\n token?: RealtimeToken;\n transport?: 'websocket' | 'webrtc';\n gateway?: { url: string; token: string };\n resumeHandle?: string;\n}\n\nexport type RealtimeEvent =\n | { type: 'session_start'; sessionId: string }\n | { type: 'input_speech_start' }\n | { type: 'input_transcript'; itemId: string; text: string; final: boolean }\n | {\n type: 'output_audio';\n itemId: string;\n chunk: AudioChunk;\n span?: TextSpan;\n alignment?: CharAlignment;\n }\n | { type: 'output_transcript'; itemId: string; text: string; final: boolean }\n | { type: 'tool_call'; call: ToolCall; blocking: boolean }\n | { type: 'tool_call_cancelled'; callId: string }\n | { type: 'response_done'; itemId: string; interrupted: boolean; usage: Usage }\n | { type: 'go_away'; timeLeftMs: number }\n | { type: 'resumption'; handle: string }\n | { type: 'error'; error: string; fatal: boolean };\n\nexport interface RealtimeSession extends AsyncIterable<RealtimeEvent> {\n readonly model: RealtimeModel<string>;\n appendAudio(chunk: AudioChunk): void;\n appendVideoFrame?(jpeg: Uint8Array): void;\n sendText(text: string): void;\n inject(text: string, delivery: Delivery): void;\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void;\n interrupt(heard?: { itemId: string; heardMs: number }): void;\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void;\n close(): Promise<{ usage: Usage }>;\n}\n\nexport interface RealtimeApiProvider<TApi extends string = string> {\n api: TApi;\n connect: (\n model: RealtimeModel<TApi>,\n context: RealtimeContext,\n options?: RealtimeOptions\n ) => Promise<RealtimeSession>;\n}\n\nexport interface DeliveryPlan {\n delivery: Delivery;\n support: DeliverySupport;\n waitForIdle: boolean;\n}\n\nexport interface TruncationPlan {\n item?: { itemId: string; audioEndMs: number };\n}\n\nexport interface RealtimeSocket {\n url: string;\n protocols: string[];\n}\n\nexport function planDelivery(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery\n): DeliveryPlan {\n const support = delivery[requested];\n if (support !== 'native' && support !== 'approximate') {\n throw new VoiceRequestError('error', `unsupported delivery: ${requested}`);\n }\n return {\n delivery: requested,\n support,\n waitForIdle: requested === 'when_idle' && support === 'approximate',\n };\n}\n\nexport function planTruncation(\n truncation: RealtimeModel<string>['truncation'],\n heard?: { itemId: string; heardMs: number }\n): TruncationPlan {\n if (truncation === 'client' && heard !== undefined) {\n return { item: { itemId: heard.itemId, audioEndMs: heard.heardMs } };\n }\n return {};\n}\n\nexport function realtimeSocket(\n url: string,\n options?: Pick<RealtimeOptions, 'gateway'>,\n protocols: readonly string[] = []\n): RealtimeSocket {\n const gateway = options?.gateway;\n if (gateway === undefined) return { url, protocols: [...protocols] };\n return {\n url: gateway.url,\n protocols: [`cf-aig-authorization.${gateway.token}`, ...protocols],\n };\n}\n\nexport class RealtimeLedger {\n #inputSeconds = 0;\n #outputSeconds = 0;\n #textMessages = 0;\n #busy = false;\n #held: Array<() => void> = [];\n\n get inputSeconds(): number {\n return this.#inputSeconds;\n }\n\n get outputSeconds(): number {\n return this.#outputSeconds;\n }\n\n get textMessages(): number {\n return this.#textMessages;\n }\n\n get busy(): boolean {\n return this.#busy;\n }\n\n addInput(chunk: AudioChunk): void {\n this.#inputSeconds += chunkSeconds(chunk);\n }\n\n addOutput(chunk: AudioChunk, alignment?: CharAlignment): void {\n const seconds = chunkSeconds(chunk);\n this.#outputSeconds += seconds > 0 ? seconds : alignmentSeconds(alignment);\n }\n\n addText(): void {\n this.#textMessages += 1;\n }\n\n observe(event: RealtimeEvent): void {\n if (event.type === 'output_audio') this.addOutput(event.chunk, event.alignment);\n if (event.type === 'output_audio' || event.type === 'output_transcript') this.#busy = true;\n if (event.type === 'response_done') this.#release();\n }\n\n submit(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery,\n run: () => void\n ): DeliveryPlan {\n const plan = planDelivery(delivery, requested);\n if (plan.waitForIdle && this.#busy) this.#held.push(run);\n else run();\n return plan;\n }\n\n settle(): void {\n this.#release();\n }\n\n usage(rates: RealtimeAudioCost): Usage {\n return priceAudio(rates, {\n inputSeconds: this.#inputSeconds,\n outputSeconds: this.#outputSeconds,\n textMessages: this.#textMessages,\n });\n }\n\n #release(): void {\n this.#busy = false;\n const held = this.#held;\n this.#held = [];\n for (const run of held) run();\n }\n}\n"],
|
|
5
|
-
"mappings": "AAQA;AAAA,EAEE;AAAA,EAEA;AAAA,OAEK;AACP,SAAS,yBAAyB;AAElC,SAAS,kBAA0C;
|
|
4
|
+
"sourcesContent": ["import type {\n Message,\n ProviderRequestOptions,\n Tool,\n ToolCall,\n ToolResultMessage,\n Usage,\n} from '@earendil-works/pi-ai';\nimport {\n type AudioChunk,\n alignmentSeconds,\n type CharAlignment,\n chunkSeconds,\n type TextSpan,\n} from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { Delivery, DeliverySupport, RealtimeModel } from './models.ts';\nimport { priceAudio, type RealtimeAudioCost } from './usage.ts';\n\nexport interface RealtimeContext {\n instructions: string;\n tools: Tool[];\n voice: string;\n history?: Message[];\n}\n\nexport interface RealtimeToken {\n value: string;\n expiresAt: number;\n model: string;\n url?: string;\n}\n\nexport interface RealtimeOptions extends ProviderRequestOptions {\n token?: RealtimeToken;\n transport?: 'websocket' | 'webrtc';\n gateway?: { url: string; token: string };\n resumeHandle?: string;\n}\n\nexport type RealtimeEvent =\n | { type: 'session_start'; sessionId: string }\n | { type: 'input_speech_start' }\n | { type: 'input_transcript'; itemId: string; text: string; final: boolean }\n | {\n type: 'output_audio';\n itemId: string;\n chunk: AudioChunk;\n span?: TextSpan;\n alignment?: CharAlignment;\n }\n | { type: 'output_transcript'; itemId: string; text: string; final: boolean }\n | { type: 'tool_call'; call: ToolCall; blocking: boolean }\n | { type: 'tool_call_cancelled'; callId: string }\n | { type: 'response_done'; itemId: string; interrupted: boolean; usage: Usage }\n | { type: 'go_away'; timeLeftMs: number }\n | { type: 'resumption'; handle: string }\n | { type: 'error'; error: string; fatal: boolean };\n\nexport interface RealtimeSession extends AsyncIterable<RealtimeEvent> {\n readonly model: RealtimeModel<string>;\n appendAudio(chunk: AudioChunk): void;\n appendVideoFrame?(jpeg: Uint8Array): void;\n sendText(text: string): void;\n inject(text: string, delivery: Delivery): void;\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void;\n interrupt(heard?: { itemId: string; heardMs: number }): void;\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void;\n close(): Promise<{ usage: Usage }>;\n}\n\nexport interface RealtimeApiProvider<TApi extends string = string> {\n api: TApi;\n connect: (\n model: RealtimeModel<TApi>,\n context: RealtimeContext,\n options?: RealtimeOptions\n ) => Promise<RealtimeSession>;\n}\n\nexport interface DeliveryPlan {\n delivery: Delivery;\n support: DeliverySupport;\n waitForIdle: boolean;\n}\n\nexport interface TruncationPlan {\n item?: { itemId: string; audioEndMs: number };\n}\n\nexport interface RealtimeSocket {\n url: string;\n protocols: string[];\n}\n\nexport function planDelivery(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery\n): DeliveryPlan {\n const support = delivery[requested];\n if (support !== 'native' && support !== 'approximate') {\n throw new VoiceRequestError('error', `unsupported delivery: ${requested}`);\n }\n return {\n delivery: requested,\n support,\n waitForIdle: requested === 'when_idle' && support === 'approximate',\n };\n}\n\nexport function planTruncation(\n truncation: RealtimeModel<string>['truncation'],\n heard?: { itemId: string; heardMs: number }\n): TruncationPlan {\n if (truncation === 'client' && heard !== undefined) {\n return { item: { itemId: heard.itemId, audioEndMs: heard.heardMs } };\n }\n return {};\n}\n\nexport function realtimeSocket(\n url: string,\n options?: Pick<RealtimeOptions, 'gateway'>,\n protocols: readonly string[] = []\n): RealtimeSocket {\n const gateway = options?.gateway;\n if (gateway === undefined) return { url, protocols: [...protocols] };\n return {\n url: gateway.url,\n protocols: [`cf-aig-authorization.${gateway.token}`, ...protocols],\n };\n}\n\nexport class RealtimeLedger {\n #inputSeconds = 0;\n #outputSeconds = 0;\n #textMessages = 0;\n #busy = false;\n #held: Array<() => void> = [];\n\n get inputSeconds(): number {\n return this.#inputSeconds;\n }\n\n get outputSeconds(): number {\n return this.#outputSeconds;\n }\n\n get textMessages(): number {\n return this.#textMessages;\n }\n\n get busy(): boolean {\n return this.#busy;\n }\n\n addInput(chunk: AudioChunk): void {\n this.#inputSeconds += chunkSeconds(chunk);\n }\n\n addOutput(chunk: AudioChunk, alignment?: CharAlignment): void {\n const seconds = chunkSeconds(chunk);\n this.#outputSeconds += seconds > 0 ? seconds : alignmentSeconds(alignment);\n }\n\n addText(): void {\n this.#textMessages += 1;\n }\n\n observe(event: RealtimeEvent): void {\n if (event.type === 'output_audio') this.addOutput(event.chunk, event.alignment);\n if (event.type === 'output_audio' || event.type === 'output_transcript') this.#busy = true;\n if (event.type === 'response_done') this.#release();\n }\n\n submit(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery,\n run: () => void\n ): DeliveryPlan {\n const plan = planDelivery(delivery, requested);\n if (plan.waitForIdle && this.#busy) this.#held.push(run);\n else run();\n return plan;\n }\n\n settle(): void {\n this.#release();\n }\n\n usage(rates: RealtimeAudioCost): Usage {\n return priceAudio(rates, {\n inputSeconds: this.#inputSeconds,\n outputSeconds: this.#outputSeconds,\n textMessages: this.#textMessages,\n });\n }\n\n #release(): void {\n this.#busy = false;\n const held = this.#held;\n this.#held = [];\n for (const run of held) run();\n }\n}\n"],
|
|
5
|
+
"mappings": "AAQA;AAAA,EAEE;AAAA,EAEA;AAAA,OAEK;AACP,SAAS,yBAAyB;AAElC,SAAS,kBAA0C;AA8E5C,SAAS,aACd,UACA,WACc;AACd,QAAM,UAAU,SAAS,SAAS;AAClC,MAAI,YAAY,YAAY,YAAY,eAAe;AACrD,UAAM,IAAI,kBAAkB,SAAS,yBAAyB,SAAS,EAAE;AAAA,EAC3E;AACA,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA,aAAa,cAAc,eAAe,YAAY;AAAA,EACxD;AACF;AAEO,SAAS,eACd,YACA,OACgB;AAChB,MAAI,eAAe,YAAY,UAAU,QAAW;AAClD,WAAO,EAAE,MAAM,EAAE,QAAQ,MAAM,QAAQ,YAAY,MAAM,QAAQ,EAAE;AAAA,EACrE;AACA,SAAO,CAAC;AACV;AAEO,SAAS,eACd,KACA,SACA,YAA+B,CAAC,GAChB;AAChB,QAAM,UAAU,SAAS;AACzB,MAAI,YAAY,OAAW,QAAO,EAAE,KAAK,WAAW,CAAC,GAAG,SAAS,EAAE;AACnE,SAAO;AAAA,IACL,KAAK,QAAQ;AAAA,IACb,WAAW,CAAC,wBAAwB,QAAQ,KAAK,IAAI,GAAG,SAAS;AAAA,EACnE;AACF;AAEO,MAAM,eAAe;AAAA,EAC1B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,QAA2B,CAAC;AAAA,EAE5B,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,gBAAwB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,OAAgB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS,OAAyB;AAChC,SAAK,iBAAiB,aAAa,KAAK;AAAA,EAC1C;AAAA,EAEA,UAAU,OAAmB,WAAiC;AAC5D,UAAM,UAAU,aAAa,KAAK;AAClC,SAAK,kBAAkB,UAAU,IAAI,UAAU,iBAAiB,SAAS;AAAA,EAC3E;AAAA,EAEA,UAAgB;AACd,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,QAAQ,OAA4B;AAClC,QAAI,MAAM,SAAS,eAAgB,MAAK,UAAU,MAAM,OAAO,MAAM,SAAS;AAC9E,QAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,oBAAqB,MAAK,QAAQ;AACtF,QAAI,MAAM,SAAS,gBAAiB,MAAK,SAAS;AAAA,EACpD;AAAA,EAEA,OACE,UACA,WACA,KACc;AACd,UAAM,OAAO,aAAa,UAAU,SAAS;AAC7C,QAAI,KAAK,eAAe,KAAK,MAAO,MAAK,MAAM,KAAK,GAAG;AAAA,QAClD,KAAI;AACT,WAAO;AAAA,EACT;AAAA,EAEA,SAAe;AACb,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,OAAiC;AACrC,WAAO,WAAW,OAAO;AAAA,MACvB,cAAc,KAAK;AAAA,MACnB,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,IACrB,CAAC;AAAA,EACH;AAAA,EAEA,WAAiB;AACf,SAAK,QAAQ;AACb,UAAM,OAAO,KAAK;AAClB,SAAK,QAAQ,CAAC;AACd,eAAW,OAAO,KAAM,KAAI;AAAA,EAC9B;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/tokens.js
CHANGED
|
@@ -159,6 +159,7 @@ function mintCall(model, constraints, options, key, now) {
|
|
|
159
159
|
headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),
|
|
160
160
|
body: JSON.stringify(fishBody(constraints)),
|
|
161
161
|
field: "token",
|
|
162
|
+
urlField: "livekit_url",
|
|
162
163
|
expires: fromField("expires_at"),
|
|
163
164
|
fallbackExpires: now
|
|
164
165
|
};
|
|
@@ -174,6 +175,12 @@ function readValue(json, field) {
|
|
|
174
175
|
if (typeof value === "string" && value.length > 0) return value;
|
|
175
176
|
throw new VoiceRequestError("error", "mint failed (empty token)");
|
|
176
177
|
}
|
|
178
|
+
function readUrl(json, field) {
|
|
179
|
+
if (field === void 0) return void 0;
|
|
180
|
+
const value = json[field];
|
|
181
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
182
|
+
throw new VoiceRequestError("error", "mint failed (missing livekit url)");
|
|
183
|
+
}
|
|
177
184
|
async function mintRealtimeToken(model, constraints, options) {
|
|
178
185
|
const settings = options ?? {};
|
|
179
186
|
const key = apiKey(settings);
|
|
@@ -188,11 +195,14 @@ async function mintRealtimeToken(model, constraints, options) {
|
|
|
188
195
|
});
|
|
189
196
|
if (!response.ok) throw new VoiceRequestError("error", `mint failed (${response.status})`);
|
|
190
197
|
const json = await response.json();
|
|
191
|
-
|
|
198
|
+
const token = {
|
|
192
199
|
value: readValue(json, call.field),
|
|
193
200
|
expiresAt: call.expires(json, call.fallbackExpires),
|
|
194
201
|
model: model.id
|
|
195
202
|
};
|
|
203
|
+
const url = readUrl(json, call.urlField);
|
|
204
|
+
if (url !== void 0) token.url = url;
|
|
205
|
+
return token;
|
|
196
206
|
}
|
|
197
207
|
export {
|
|
198
208
|
mintRealtimeToken
|
package/dist/tokens.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tokens.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ProviderRequestOptions, Tool } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from './errors.ts';\nimport type { RealtimeModel } from './models.ts';\nimport type { RealtimeToken } from './realtime.ts';\n\nexport interface RealtimeConstraints {\n instructions?: string;\n tools?: Tool[];\n voice?: string;\n uses?: number;\n agentId?: string;\n}\n\nconst openaiSecrets = 'https://api.openai.com/v1/realtime/client_secrets';\nconst xaiSecrets = 'https://api.x.ai/v1/realtime/client_secrets';\nconst geminiTokens = 'https://generativelanguage.googleapis.com/v1beta/auth_tokens';\nconst elevenSigned = 'https://api.elevenlabs.io/v1/convai/conversation/get-signed-url';\nconst elevenWebRtc = 'https://api.elevenlabs.io/v1/convai/conversation/token';\nconst fishSessions = 'https://api.fish.audio/v1/agent/sessions';\nconst fifteenMinutes = 15 * 60 * 1000;\nconst thirtyMinutes = 30 * 60 * 1000;\nconst oneMinute = 60 * 1000;\nconst xaiLifetimeSeconds = 300;\n\ntype Json = Record<string, unknown>;\n\ninterface MintCall {\n url: string;\n method: 'GET' | 'POST';\n headers: Record<string, string>;\n body?: string;\n field: string;\n expires: (json: Json, fallback: number) => number;\n fallbackExpires: number;\n}\n\nfunction apiKey(options: ProviderRequestOptions): string {\n const key = options.apiKey;\n if (typeof key === 'string' && key.length > 0) return key;\n throw new VoiceRequestError('error', 'missing realtime credential');\n}\n\nfunction mergedHeaders(\n options: ProviderRequestOptions,\n defaults: Record<string, string>\n): Record<string, string> {\n const headers = { ...defaults };\n const extra = options.headers;\n if (extra === undefined) return headers;\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n return headers;\n}\n\nfunction omitUndefined(source: Json): Json {\n const out: Json = {};\n for (const [key, value] of Object.entries(source)) {\n if (value !== undefined) out[key] = value;\n }\n return out;\n}\n\nfunction functionTool(tool: Tool): Json {\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction declaration(tool: Tool): Json {\n return {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction requiredAgent(constraints: RealtimeConstraints): string {\n const id = constraints.agentId;\n if (typeof id === 'string' && id.length > 0) return id;\n throw new VoiceRequestError('error', 'missing agent id');\n}\n\nfunction expiresAt(value: unknown, fallback: number): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value < 1e12 ? value * 1000 : value;\n }\n if (typeof value === 'string') {\n const parsed = Date.parse(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return fallback;\n}\n\nfunction fromField(field: string): (json: Json, fallback: number) => number {\n return (json, fallback) => expiresAt(json[field], fallback);\n}\n\nfunction openaiBody(model: RealtimeModel<string>, constraints: RealtimeConstraints): Json {\n const session = omitUndefined({\n type: 'realtime',\n model: model.id,\n instructions: constraints.instructions,\n tools: constraints.tools?.map(functionTool),\n });\n if (constraints.voice !== undefined) session.audio = { output: { voice: constraints.voice } };\n return { session };\n}\n\nfunction geminiBody(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n now: number\n): Json {\n const instructions = constraints.instructions;\n const voice = constraints.voice;\n const tools = constraints.tools;\n const config = omitUndefined({\n responseModalities: ['AUDIO'],\n sessionResumption: {},\n systemInstruction: instructions === undefined ? undefined : { parts: [{ text: instructions }] },\n speechConfig:\n voice === undefined\n ? undefined\n : { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } },\n tools: tools === undefined ? undefined : [{ functionDeclarations: tools.map(declaration) }],\n });\n return {\n uses: constraints.uses ?? 1,\n expireTime: new Date(now + thirtyMinutes).toISOString(),\n newSessionExpireTime: new Date(now + oneMinute).toISOString(),\n liveConnectConstraints: { model: `models/${model.id}`, config },\n };\n}\n\nfunction fishBody(constraints: RealtimeConstraints): Json {\n const overrides = omitUndefined({\n system_prompt: constraints.instructions,\n voice_id: constraints.voice,\n });\n return omitUndefined({\n agent_id: requiredAgent(constraints),\n overrides: Object.keys(overrides).length > 0 ? overrides : undefined,\n });\n}\n\nfunction webrtcOnly(model: RealtimeModel<string>): boolean {\n return model.transports.length === 1 && model.transports[0] === 'webrtc';\n}\n\nfunction elevenUrl(model: RealtimeModel<string>, constraints: RealtimeConstraints): string {\n const url = new URL(webrtcOnly(model) ? elevenWebRtc : elevenSigned);\n url.searchParams.set('agent_id', requiredAgent(constraints));\n return url.toString();\n}\n\nfunction mintCall(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options: ProviderRequestOptions,\n key: string,\n now: number\n): MintCall {\n const jsonHeaders = { 'Content-Type': 'application/json' };\n switch (model.api) {\n case 'openai-realtime':\n return {\n url: openaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(openaiBody(model, constraints)),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n case 'xai-realtime':\n return {\n url: xaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify({ expires_after: { seconds: xaiLifetimeSeconds } }),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now + xaiLifetimeSeconds * 1000,\n };\n case 'gemini-live':\n return {\n url: geminiTokens,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, 'x-goog-api-key': key }),\n body: JSON.stringify(geminiBody(model, constraints, now)),\n field: 'name',\n expires: fromField('expireTime'),\n fallbackExpires: now + thirtyMinutes,\n };\n case 'elevenlabs-agents':\n return {\n url: elevenUrl(model, constraints),\n method: 'GET',\n headers: mergedHeaders(options, { 'xi-api-key': key }),\n field: webrtcOnly(model) ? 'token' : 'signed_url',\n expires: fromField('expires_at'),\n fallbackExpires: now + fifteenMinutes,\n };\n case 'fish-agents':\n return {\n url: fishSessions,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(fishBody(constraints)),\n field: 'token',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n default:\n throw new VoiceRequestError('error', 'unsupported realtime api');\n }\n}\n\nfunction readValue(json: unknown, field: string): string {\n if (typeof json !== 'object' || json === null) {\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n }\n const value = (json as Json)[field];\n if (typeof value === 'string' && value.length > 0) return value;\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n}\n\nexport async function mintRealtimeToken(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options?: ProviderRequestOptions\n): Promise<RealtimeToken> {\n const settings = options ?? {};\n const key = apiKey(settings);\n const now = Date.now();\n const call = mintCall(model, constraints, settings, key, now);\n const fetch = settings.fetch ?? globalThis.fetch;\n const response = await fetch(call.url, {\n method: call.method,\n headers: call.headers,\n body: call.body,\n signal: settings.signal,\n });\n if (!response.ok) throw new VoiceRequestError('error', `mint failed (${response.status})`);\n const json: unknown = await response.json();\n
|
|
5
|
-
"mappings": "AACA,SAAS,yBAAyB;AAYlC,MAAM,gBAAgB;AACtB,MAAM,aAAa;AACnB,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,iBAAiB,KAAK,KAAK;AACjC,MAAM,gBAAgB,KAAK,KAAK;AAChC,MAAM,YAAY,KAAK;AACvB,MAAM,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions, Tool } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from './errors.ts';\nimport type { RealtimeModel } from './models.ts';\nimport type { RealtimeToken } from './realtime.ts';\n\nexport interface RealtimeConstraints {\n instructions?: string;\n tools?: Tool[];\n voice?: string;\n uses?: number;\n agentId?: string;\n}\n\nconst openaiSecrets = 'https://api.openai.com/v1/realtime/client_secrets';\nconst xaiSecrets = 'https://api.x.ai/v1/realtime/client_secrets';\nconst geminiTokens = 'https://generativelanguage.googleapis.com/v1beta/auth_tokens';\nconst elevenSigned = 'https://api.elevenlabs.io/v1/convai/conversation/get-signed-url';\nconst elevenWebRtc = 'https://api.elevenlabs.io/v1/convai/conversation/token';\nconst fishSessions = 'https://api.fish.audio/v1/agent/sessions';\nconst fifteenMinutes = 15 * 60 * 1000;\nconst thirtyMinutes = 30 * 60 * 1000;\nconst oneMinute = 60 * 1000;\nconst xaiLifetimeSeconds = 300;\n\ntype Json = Record<string, unknown>;\n\ninterface MintCall {\n url: string;\n method: 'GET' | 'POST';\n headers: Record<string, string>;\n body?: string;\n field: string;\n urlField?: string;\n expires: (json: Json, fallback: number) => number;\n fallbackExpires: number;\n}\n\nfunction apiKey(options: ProviderRequestOptions): string {\n const key = options.apiKey;\n if (typeof key === 'string' && key.length > 0) return key;\n throw new VoiceRequestError('error', 'missing realtime credential');\n}\n\nfunction mergedHeaders(\n options: ProviderRequestOptions,\n defaults: Record<string, string>\n): Record<string, string> {\n const headers = { ...defaults };\n const extra = options.headers;\n if (extra === undefined) return headers;\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n return headers;\n}\n\nfunction omitUndefined(source: Json): Json {\n const out: Json = {};\n for (const [key, value] of Object.entries(source)) {\n if (value !== undefined) out[key] = value;\n }\n return out;\n}\n\nfunction functionTool(tool: Tool): Json {\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction declaration(tool: Tool): Json {\n return {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction requiredAgent(constraints: RealtimeConstraints): string {\n const id = constraints.agentId;\n if (typeof id === 'string' && id.length > 0) return id;\n throw new VoiceRequestError('error', 'missing agent id');\n}\n\nfunction expiresAt(value: unknown, fallback: number): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value < 1e12 ? value * 1000 : value;\n }\n if (typeof value === 'string') {\n const parsed = Date.parse(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return fallback;\n}\n\nfunction fromField(field: string): (json: Json, fallback: number) => number {\n return (json, fallback) => expiresAt(json[field], fallback);\n}\n\nfunction openaiBody(model: RealtimeModel<string>, constraints: RealtimeConstraints): Json {\n const session = omitUndefined({\n type: 'realtime',\n model: model.id,\n instructions: constraints.instructions,\n tools: constraints.tools?.map(functionTool),\n });\n if (constraints.voice !== undefined) session.audio = { output: { voice: constraints.voice } };\n return { session };\n}\n\nfunction geminiBody(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n now: number\n): Json {\n const instructions = constraints.instructions;\n const voice = constraints.voice;\n const tools = constraints.tools;\n const config = omitUndefined({\n responseModalities: ['AUDIO'],\n sessionResumption: {},\n systemInstruction: instructions === undefined ? undefined : { parts: [{ text: instructions }] },\n speechConfig:\n voice === undefined\n ? undefined\n : { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } },\n tools: tools === undefined ? undefined : [{ functionDeclarations: tools.map(declaration) }],\n });\n return {\n uses: constraints.uses ?? 1,\n expireTime: new Date(now + thirtyMinutes).toISOString(),\n newSessionExpireTime: new Date(now + oneMinute).toISOString(),\n liveConnectConstraints: { model: `models/${model.id}`, config },\n };\n}\n\nfunction fishBody(constraints: RealtimeConstraints): Json {\n const overrides = omitUndefined({\n system_prompt: constraints.instructions,\n voice_id: constraints.voice,\n });\n return omitUndefined({\n agent_id: requiredAgent(constraints),\n overrides: Object.keys(overrides).length > 0 ? overrides : undefined,\n });\n}\n\nfunction webrtcOnly(model: RealtimeModel<string>): boolean {\n return model.transports.length === 1 && model.transports[0] === 'webrtc';\n}\n\nfunction elevenUrl(model: RealtimeModel<string>, constraints: RealtimeConstraints): string {\n const url = new URL(webrtcOnly(model) ? elevenWebRtc : elevenSigned);\n url.searchParams.set('agent_id', requiredAgent(constraints));\n return url.toString();\n}\n\nfunction mintCall(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options: ProviderRequestOptions,\n key: string,\n now: number\n): MintCall {\n const jsonHeaders = { 'Content-Type': 'application/json' };\n switch (model.api) {\n case 'openai-realtime':\n return {\n url: openaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(openaiBody(model, constraints)),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n case 'xai-realtime':\n return {\n url: xaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify({ expires_after: { seconds: xaiLifetimeSeconds } }),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now + xaiLifetimeSeconds * 1000,\n };\n case 'gemini-live':\n return {\n url: geminiTokens,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, 'x-goog-api-key': key }),\n body: JSON.stringify(geminiBody(model, constraints, now)),\n field: 'name',\n expires: fromField('expireTime'),\n fallbackExpires: now + thirtyMinutes,\n };\n case 'elevenlabs-agents':\n return {\n url: elevenUrl(model, constraints),\n method: 'GET',\n headers: mergedHeaders(options, { 'xi-api-key': key }),\n field: webrtcOnly(model) ? 'token' : 'signed_url',\n expires: fromField('expires_at'),\n fallbackExpires: now + fifteenMinutes,\n };\n case 'fish-agents':\n return {\n url: fishSessions,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(fishBody(constraints)),\n field: 'token',\n urlField: 'livekit_url',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n default:\n throw new VoiceRequestError('error', 'unsupported realtime api');\n }\n}\n\nfunction readValue(json: unknown, field: string): string {\n if (typeof json !== 'object' || json === null) {\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n }\n const value = (json as Json)[field];\n if (typeof value === 'string' && value.length > 0) return value;\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n}\n\nfunction readUrl(json: unknown, field: string | undefined): string | undefined {\n if (field === undefined) return undefined;\n const value = (json as Json)[field];\n if (typeof value === 'string' && value.length > 0) return value;\n throw new VoiceRequestError('error', 'mint failed (missing livekit url)');\n}\n\nexport async function mintRealtimeToken(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options?: ProviderRequestOptions\n): Promise<RealtimeToken> {\n const settings = options ?? {};\n const key = apiKey(settings);\n const now = Date.now();\n const call = mintCall(model, constraints, settings, key, now);\n const fetch = settings.fetch ?? globalThis.fetch;\n const response = await fetch(call.url, {\n method: call.method,\n headers: call.headers,\n body: call.body,\n signal: settings.signal,\n });\n if (!response.ok) throw new VoiceRequestError('error', `mint failed (${response.status})`);\n const json: unknown = await response.json();\n const token: RealtimeToken = {\n value: readValue(json, call.field),\n expiresAt: call.expires(json as Json, call.fallbackExpires),\n model: model.id,\n };\n const url = readUrl(json, call.urlField);\n if (url !== undefined) token.url = url;\n return token;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JsonObject } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { AudioChunk } from '../audio.ts';
|
|
3
|
+
import type { RealtimeEvent } from '../realtime.ts';
|
|
4
|
+
export interface FishTurn {
|
|
5
|
+
dropAudio: boolean;
|
|
6
|
+
discardFinals: number;
|
|
7
|
+
speaking: boolean;
|
|
8
|
+
turnOpen: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface FishText {
|
|
11
|
+
identity: string;
|
|
12
|
+
text: string;
|
|
13
|
+
attributes: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export declare function ingestFishData(message: Record<string, unknown>): RealtimeEvent[];
|
|
16
|
+
export declare function ingestFishText(turn: FishTurn, packet: FishText, localIdentity: string): RealtimeEvent[];
|
|
17
|
+
export declare function ingestFishAudio(turn: FishTurn, chunk: AudioChunk): RealtimeEvent[];
|
|
18
|
+
export declare function ingestFishState(turn: FishTurn, value: string): RealtimeEvent[];
|
|
19
|
+
export declare function ingestSpeakers(turn: FishTurn, identities: string[]): RealtimeEvent[];
|
|
20
|
+
export declare function userMessage(value: string, speak: boolean): JsonObject;
|
|
21
|
+
export declare function toolResult(callId: string, result: string, isError: boolean): JsonObject;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RealtimeModel } from '../models.ts';
|
|
2
|
+
import type { FishOptions } from './session.ts';
|
|
3
|
+
export declare const fishAgent: RealtimeModel<'fish-agents'>;
|
|
4
|
+
export declare function registerFishAgents(options?: FishOptions): () => void;
|
|
5
|
+
export type { CustomLlm, HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
|
|
6
|
+
export { hashHostedSpec, memoryHostedCache } from '../hosted-agent.ts';
|
|
7
|
+
export type { ProvisionOptions } from './provision.ts';
|
|
8
|
+
export { agentConfig, httpsOrigin, provisionAgent } from './provision.ts';
|
|
9
|
+
export type { FishRoom, FishRoomFactory, FishSdk } from './room.ts';
|
|
10
|
+
export { importFishSdk, openFishRoom } from './room.ts';
|
|
11
|
+
export type { FishOptions } from './session.ts';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { JsonObject, ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import { type HostedAgent, type HostedAgentSpec, type HostedCache } from '../hosted-agent.ts';
|
|
3
|
+
import type { RealtimeModel } from '../models.ts';
|
|
4
|
+
export interface ProvisionOptions extends ProviderRequestOptions {
|
|
5
|
+
agentId?: string;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
cache?: HostedCache;
|
|
8
|
+
}
|
|
9
|
+
export declare function httpsOrigin(baseUrl: string): string;
|
|
10
|
+
export declare function agentConfig(spec: HostedAgentSpec, toolIds: string[]): JsonObject;
|
|
11
|
+
export declare function provisionAgent(model: RealtimeModel<string>, spec: HostedAgentSpec, options?: ProvisionOptions): Promise<HostedAgent>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AudioChunk, AudioSource } from '../audio.ts';
|
|
2
|
+
import type { MicOpen } from '../echo.ts';
|
|
3
|
+
export interface FishSdkRoom {
|
|
4
|
+
connect(url: string, token: string): Promise<void>;
|
|
5
|
+
disconnect(): void;
|
|
6
|
+
localParticipant: {
|
|
7
|
+
identity?: string;
|
|
8
|
+
publishTrack(track: unknown, options?: unknown): Promise<unknown>;
|
|
9
|
+
publishData(data: Uint8Array, options: {
|
|
10
|
+
reliable: boolean;
|
|
11
|
+
topic: string;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
15
|
+
registerTextStreamHandler(topic: string, handler: (reader: {
|
|
16
|
+
readAll(): Promise<string>;
|
|
17
|
+
info: {
|
|
18
|
+
attributes?: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
}, participant: {
|
|
21
|
+
identity: string;
|
|
22
|
+
}) => void): void;
|
|
23
|
+
}
|
|
24
|
+
export interface FishSdk {
|
|
25
|
+
Room: new () => FishSdkRoom;
|
|
26
|
+
}
|
|
27
|
+
export interface FishHandlers {
|
|
28
|
+
data(topic: string, json: unknown, identity: string | undefined): void;
|
|
29
|
+
text(identity: string, value: string, attributes: Record<string, string>): void;
|
|
30
|
+
attribute(key: string, value: string): void;
|
|
31
|
+
speakers(identities: string[]): void;
|
|
32
|
+
audio(chunk: AudioChunk): void;
|
|
33
|
+
fail(error: unknown): void;
|
|
34
|
+
}
|
|
35
|
+
export interface FishRoom {
|
|
36
|
+
url?: string;
|
|
37
|
+
token?: string;
|
|
38
|
+
identity: string;
|
|
39
|
+
connect(url: string, token: string): Promise<void>;
|
|
40
|
+
publishMic(source: AudioSource, open: MicOpen): Promise<void>;
|
|
41
|
+
publishData(payload: string): Promise<void>;
|
|
42
|
+
close(): void;
|
|
43
|
+
}
|
|
44
|
+
export type FishRoomFactory = (handlers: FishHandlers) => FishRoom;
|
|
45
|
+
export declare function importFishSdk(load?: (specifier: string) => Promise<FishSdk>): Promise<FishSdk>;
|
|
46
|
+
export declare function openFishRoom(sdk: FishSdk, handlers: FishHandlers): FishRoom;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AudioSource } from '../audio.ts';
|
|
2
|
+
import { type EchoControl } from '../echo.ts';
|
|
3
|
+
import type { HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
|
|
4
|
+
import type { RealtimeModel } from '../models.ts';
|
|
5
|
+
import type { RealtimeContext, RealtimeOptions, RealtimeSession } from '../realtime.ts';
|
|
6
|
+
import { type FishRoomFactory, type FishSdk } from './room.ts';
|
|
7
|
+
export interface FishOptions {
|
|
8
|
+
room?: FishRoomFactory;
|
|
9
|
+
sdk?: FishSdk;
|
|
10
|
+
cache?: HostedCache;
|
|
11
|
+
agentId?: string;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
firstMessage?: string;
|
|
14
|
+
microphone?: AudioSource;
|
|
15
|
+
echo?: EchoControl;
|
|
16
|
+
loadSdk?: (specifier: string) => Promise<FishSdk>;
|
|
17
|
+
}
|
|
18
|
+
export declare function specFromContext(model: RealtimeModel<string>, context: RealtimeContext): HostedAgentSpec;
|
|
19
|
+
export declare function connectFish(model: RealtimeModel<string>, context: RealtimeContext, options: RealtimeOptions | undefined, settings?: FishOptions): Promise<RealtimeSession>;
|
package/dist/types/realtime.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -73,6 +73,10 @@
|
|
|
73
73
|
"./elevenlabs-agents": {
|
|
74
74
|
"types": "./dist/types/elevenlabs-agents/index.d.ts",
|
|
75
75
|
"default": "./dist/elevenlabs-agents/index.js"
|
|
76
|
+
},
|
|
77
|
+
"./fish-agents": {
|
|
78
|
+
"types": "./dist/types/fish-agents/index.d.ts",
|
|
79
|
+
"default": "./dist/fish-agents/index.js"
|
|
76
80
|
}
|
|
77
81
|
},
|
|
78
82
|
"files": [
|