@ai-ecoverse/pi-audio 1.20.1 → 1.22.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 +5 -1
- package/dist/cloudflare/catalog.js +165 -0
- package/dist/cloudflare/catalog.js.map +7 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/cloudflare/index.js.map +7 -0
- package/dist/cloudflare/listen.js +133 -0
- package/dist/cloudflare/listen.js.map +7 -0
- package/dist/cloudflare/speak.js +224 -0
- package/dist/cloudflare/speak.js.map +7 -0
- package/dist/cloudflare/transcribe.js +155 -0
- package/dist/cloudflare/transcribe.js.map +7 -0
- package/dist/cloudflare/wire.js +166 -0
- package/dist/cloudflare/wire.js.map +7 -0
- 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/cloudflare/catalog.d.ts +1 -0
- package/dist/types/cloudflare/index.d.ts +1 -0
- package/dist/types/cloudflare/listen.d.ts +4 -0
- package/dist/types/cloudflare/speak.d.ts +4 -0
- package/dist/types/cloudflare/transcribe.d.ts +4 -0
- package/dist/types/cloudflare/wire.d.ts +27 -0
- 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 +9 -1
|
@@ -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
|
+
}
|