@ai-ecoverse/pi-audio 1.25.0 → 1.26.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 +2 -0
- package/dist/brain/call.js +36 -0
- package/dist/brain/call.js.map +7 -0
- package/dist/brain/hash.js +12 -0
- package/dist/brain/hash.js.map +7 -0
- package/dist/brain/index.js +38 -0
- package/dist/brain/index.js.map +7 -0
- package/dist/brain/options.js +1 -0
- package/dist/brain/options.js.map +7 -0
- package/dist/brain/parse.js +102 -0
- package/dist/brain/parse.js.map +7 -0
- package/dist/brain/sse.js +25 -0
- package/dist/brain/sse.js.map +7 -0
- package/dist/brain/turn.js +135 -0
- package/dist/brain/turn.js.map +7 -0
- package/dist/types/brain/call.d.ts +7 -0
- package/dist/types/brain/hash.d.ts +1 -0
- package/dist/types/brain/index.d.ts +3 -0
- package/dist/types/brain/options.d.ts +12 -0
- package/dist/types/brain/parse.d.ts +22 -0
- package/dist/types/brain/sse.d.ts +3 -0
- package/dist/types/brain/turn.d.ts +5 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
64
64
|
|
|
65
65
|
`@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. `fishCustomLlm` sends `llm.custom.base_url`, `model`, and a raw `api_key`, and rejects `openai-responses`. 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.
|
|
66
66
|
|
|
67
|
+
`@ai-ecoverse/pi-audio/brain` exports `chatCompletionsBrain`, a fetch handler for a pi agent behind `CustomLlm.baseUrl`. The session key is Fish `session_id`, or `session_id` inside `fishaudio_extra_body`, `llm_extra_body`, `elevenlabs_extra_body`, or `custom_llm_extra_body` (a string extra body is the key). A hash of the streamed reply prompts only the new user text when it matches. The same user text with a different hash replays that stream. A new user text with a different hash is prompted as `The caller heard:` plus the vendor's copy. Final-answer text is streamed. Request tools are terminal `tool_calls` when `vendorTools` is `terminal`. `filler` is written when no text arrives within `firstTokenMs`. A turn still open at `hardDeadlineMs` ends as an error. Fish recommends a first token under 800 ms and caps a generation at 10 s.
|
|
68
|
+
|
|
67
69
|
`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.
|
|
68
70
|
|
|
69
71
|
## Development
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function contentBlocks(event) {
|
|
5
|
+
const direct = event.message?.content;
|
|
6
|
+
if (Array.isArray(direct)) return direct;
|
|
7
|
+
const model = event.entry?.model;
|
|
8
|
+
if (!Array.isArray(model) || model.length === 0) return [];
|
|
9
|
+
const first = model[0];
|
|
10
|
+
if (!isRecord(first) || !Array.isArray(first.content)) return [];
|
|
11
|
+
return first.content;
|
|
12
|
+
}
|
|
13
|
+
function callId(id, name) {
|
|
14
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
15
|
+
return name;
|
|
16
|
+
}
|
|
17
|
+
function vendorCall(event, tools) {
|
|
18
|
+
if (event.type === "tool_execution_start" && event.toolName && tools.has(event.toolName)) {
|
|
19
|
+
return {
|
|
20
|
+
id: callId(event.toolCallId, event.toolName),
|
|
21
|
+
name: event.toolName,
|
|
22
|
+
arguments: JSON.stringify(event.args ?? {})
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
for (const block of contentBlocks(event)) {
|
|
26
|
+
if (!isRecord(block) || block.type !== "toolCall" || typeof block.name !== "string") continue;
|
|
27
|
+
if (!tools.has(block.name)) continue;
|
|
28
|
+
const args = isRecord(block.arguments) ? block.arguments : {};
|
|
29
|
+
return { id: callId(block.id, block.name), name: block.name, arguments: JSON.stringify(args) };
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
vendorCall
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=call.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/call.ts"],
|
|
4
|
+
"sourcesContent": ["import type { VoiceAgentEvent } from '../spoken.ts';\n\nexport interface VendorCall {\n id: string;\n name: string;\n arguments: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction contentBlocks(event: VoiceAgentEvent): readonly unknown[] {\n const direct = event.message?.content;\n if (Array.isArray(direct)) return direct;\n const model = event.entry?.model;\n if (!Array.isArray(model) || model.length === 0) return [];\n const first = model[0];\n if (!isRecord(first) || !Array.isArray(first.content)) return [];\n return first.content;\n}\n\nfunction callId(id: unknown, name: string): string {\n if (typeof id === 'string' && id.length > 0) return id;\n return name;\n}\n\nexport function vendorCall(event: VoiceAgentEvent, tools: Set<string>): VendorCall | undefined {\n if (event.type === 'tool_execution_start' && event.toolName && tools.has(event.toolName)) {\n return {\n id: callId(event.toolCallId, event.toolName),\n name: event.toolName,\n arguments: JSON.stringify(event.args ?? {}),\n };\n }\n for (const block of contentBlocks(event)) {\n if (!isRecord(block) || block.type !== 'toolCall' || typeof block.name !== 'string') continue;\n if (!tools.has(block.name)) continue;\n const args = isRecord(block.arguments) ? block.arguments : {};\n return { id: callId(block.id, block.name), name: block.name, arguments: JSON.stringify(args) };\n }\n return undefined;\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,cAAc,OAA4C;AACjE,QAAM,SAAS,MAAM,SAAS;AAC9B,MAAI,MAAM,QAAQ,MAAM,EAAG,QAAO;AAClC,QAAM,QAAQ,MAAM,OAAO;AAC3B,MAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AACzD,QAAM,QAAQ,MAAM,CAAC;AACrB,MAAI,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,MAAM,OAAO,EAAG,QAAO,CAAC;AAC/D,SAAO,MAAM;AACf;AAEA,SAAS,OAAO,IAAa,MAAsB;AACjD,MAAI,OAAO,OAAO,YAAY,GAAG,SAAS,EAAG,QAAO;AACpD,SAAO;AACT;AAEO,SAAS,WAAW,OAAwB,OAA4C;AAC7F,MAAI,MAAM,SAAS,0BAA0B,MAAM,YAAY,MAAM,IAAI,MAAM,QAAQ,GAAG;AACxF,WAAO;AAAA,MACL,IAAI,OAAO,MAAM,YAAY,MAAM,QAAQ;AAAA,MAC3C,MAAM,MAAM;AAAA,MACZ,WAAW,KAAK,UAAU,MAAM,QAAQ,CAAC,CAAC;AAAA,IAC5C;AAAA,EACF;AACA,aAAW,SAAS,cAAc,KAAK,GAAG;AACxC,QAAI,CAAC,SAAS,KAAK,KAAK,MAAM,SAAS,cAAc,OAAO,MAAM,SAAS,SAAU;AACrF,QAAI,CAAC,MAAM,IAAI,MAAM,IAAI,EAAG;AAC5B,UAAM,OAAO,SAAS,MAAM,SAAS,IAAI,MAAM,YAAY,CAAC;AAC5D,WAAO,EAAE,IAAI,OAAO,MAAM,IAAI,MAAM,IAAI,GAAG,MAAM,MAAM,MAAM,WAAW,KAAK,UAAU,IAAI,EAAE;AAAA,EAC/F;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function replyHash(text) {
|
|
2
|
+
let hash = 2166136261;
|
|
3
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
4
|
+
hash ^= text.charCodeAt(index);
|
|
5
|
+
hash = Math.imul(hash, 16777619);
|
|
6
|
+
}
|
|
7
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
replyHash
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=hash.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/hash.ts"],
|
|
4
|
+
"sourcesContent": ["export function replyHash(text: string): string {\n let hash = 0x811c9dc5;\n for (let index = 0; index < text.length; index += 1) {\n hash ^= text.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n"],
|
|
5
|
+
"mappings": "AAAO,SAAS,UAAU,MAAsB;AAC9C,MAAI,OAAO;AACX,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;AACnD,YAAQ,KAAK,WAAW,KAAK;AAC7B,WAAO,KAAK,KAAK,MAAM,QAAU;AAAA,EACnC;AACA,UAAQ,SAAS,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAClD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseTurn, turnPlan } from "./parse.js";
|
|
2
|
+
import { runTurn } from "./turn.js";
|
|
3
|
+
function jsonError(message) {
|
|
4
|
+
return Response.json({ error: { message } }, { status: 400 });
|
|
5
|
+
}
|
|
6
|
+
async function readBody(request) {
|
|
7
|
+
try {
|
|
8
|
+
return await request.json();
|
|
9
|
+
} catch {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function chatCompletionsBrain(options) {
|
|
14
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
15
|
+
return async (request) => {
|
|
16
|
+
const body = await readBody(request);
|
|
17
|
+
if (body === void 0) return jsonError("invalid chat completion");
|
|
18
|
+
const parsed = parseTurn(body);
|
|
19
|
+
if (typeof parsed === "string") return jsonError(parsed);
|
|
20
|
+
const plan = turnPlan(sessions.get(parsed.sessionKey), parsed);
|
|
21
|
+
const stream = new ReadableStream({
|
|
22
|
+
async start(controller) {
|
|
23
|
+
try {
|
|
24
|
+
await runTurn(options, sessions, parsed, plan, request, controller);
|
|
25
|
+
} finally {
|
|
26
|
+
controller.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return new Response(stream, {
|
|
31
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" }
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
chatCompletionsBrain
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { BrainOptions } from './options.ts';\nimport type { Memory } from './parse.ts';\nimport { parseTurn, turnPlan } from './parse.ts';\nimport { runTurn } from './turn.ts';\n\nexport type { BrainBudget, BrainOptions } from './options.ts';\n\nfunction jsonError(message: string): Response {\n return Response.json({ error: { message } }, { status: 400 });\n}\n\nasync function readBody(request: Request): Promise<unknown> {\n try {\n return await request.json();\n } catch {\n return undefined;\n }\n}\n\nexport function chatCompletionsBrain(\n options: BrainOptions\n): (request: Request) => Promise<Response> {\n const sessions = new Map<string, Memory>();\n return async (request) => {\n const body = await readBody(request);\n if (body === undefined) return jsonError('invalid chat completion');\n const parsed = parseTurn(body);\n if (typeof parsed === 'string') return jsonError(parsed);\n const plan = turnPlan(sessions.get(parsed.sessionKey), parsed);\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n await runTurn(options, sessions, parsed, plan, request, controller);\n } finally {\n controller.close();\n }\n },\n });\n return new Response(stream, {\n headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' },\n });\n };\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe;AAIxB,SAAS,UAAU,SAA2B;AAC5C,SAAO,SAAS,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9D;AAEA,eAAe,SAAS,SAAoC;AAC1D,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,qBACd,SACyC;AACzC,QAAM,WAAW,oBAAI,IAAoB;AACzC,SAAO,OAAO,YAAY;AACxB,UAAM,OAAO,MAAM,SAAS,OAAO;AACnC,QAAI,SAAS,OAAW,QAAO,UAAU,yBAAyB;AAClE,UAAM,SAAS,UAAU,IAAI;AAC7B,QAAI,OAAO,WAAW,SAAU,QAAO,UAAU,MAAM;AACvD,UAAM,OAAO,SAAS,SAAS,IAAI,OAAO,UAAU,GAAG,MAAM;AAC7D,UAAM,SAAS,IAAI,eAA2B;AAAA,MAC5C,MAAM,MAAM,YAAY;AACtB,YAAI;AACF,gBAAM,QAAQ,SAAS,UAAU,QAAQ,MAAM,SAAS,UAAU;AAAA,QACpE,UAAE;AACA,qBAAW,MAAM;AAAA,QACnB;AAAA,MACF;AAAA,IACF,CAAC;AACD,WAAO,IAAI,SAAS,QAAQ;AAAA,MAC1B,SAAS,EAAE,gBAAgB,qBAAqB,iBAAiB,WAAW;AAAA,IAC9E,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { replyHash } from "./hash.js";
|
|
2
|
+
const extraNames = [
|
|
3
|
+
"fishaudio_extra_body",
|
|
4
|
+
"llm_extra_body",
|
|
5
|
+
"elevenlabs_extra_body",
|
|
6
|
+
"custom_llm_extra_body"
|
|
7
|
+
];
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function textOf(content) {
|
|
12
|
+
if (typeof content === "string") return content;
|
|
13
|
+
if (!Array.isArray(content)) return "";
|
|
14
|
+
let text = "";
|
|
15
|
+
for (const part of content) {
|
|
16
|
+
if (isRecord(part) && typeof part.text === "string") text += part.text;
|
|
17
|
+
}
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
function namedSession(extra) {
|
|
21
|
+
if (typeof extra === "string") return extra.length > 0 ? extra : void 0;
|
|
22
|
+
if (!isRecord(extra)) return void 0;
|
|
23
|
+
const id = extra.session_id;
|
|
24
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
function sessionOf(body) {
|
|
28
|
+
if (typeof body.session_id === "string" && body.session_id.length > 0) return body.session_id;
|
|
29
|
+
for (const name of extraNames) {
|
|
30
|
+
const key = namedSession(body[name]);
|
|
31
|
+
if (key !== void 0) return key;
|
|
32
|
+
}
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
function toolNames(body) {
|
|
36
|
+
const names = /* @__PURE__ */ new Set();
|
|
37
|
+
if (!Array.isArray(body.tools)) return names;
|
|
38
|
+
for (const tool of body.tools) {
|
|
39
|
+
if (!isRecord(tool)) continue;
|
|
40
|
+
const fn = tool.function;
|
|
41
|
+
if (!isRecord(fn)) continue;
|
|
42
|
+
if (typeof fn.name !== "string" || fn.name.length === 0) continue;
|
|
43
|
+
names.add(fn.name);
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
47
|
+
function lastTurn(messages) {
|
|
48
|
+
if (!Array.isArray(messages)) return void 0;
|
|
49
|
+
let prior;
|
|
50
|
+
let priorTools = false;
|
|
51
|
+
let user;
|
|
52
|
+
for (const message of messages) {
|
|
53
|
+
if (!isRecord(message) || message.role === "system") continue;
|
|
54
|
+
if (message.role === "assistant") {
|
|
55
|
+
prior = textOf(message.content);
|
|
56
|
+
priorTools = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
57
|
+
user = void 0;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (message.role === "user" || message.role === "tool") user = textOf(message.content);
|
|
61
|
+
}
|
|
62
|
+
return { prior, priorTools, user };
|
|
63
|
+
}
|
|
64
|
+
function parseTurn(body) {
|
|
65
|
+
if (!isRecord(body)) return "invalid chat completion";
|
|
66
|
+
if (body.stream !== true) return "stream is required";
|
|
67
|
+
const sessionKey = sessionOf(body);
|
|
68
|
+
if (sessionKey === void 0) return "missing session id";
|
|
69
|
+
const turn = lastTurn(body.messages);
|
|
70
|
+
if (turn?.user === void 0) return "missing user turn";
|
|
71
|
+
const model = typeof body.model === "string" && body.model.length > 0 ? body.model : "brain";
|
|
72
|
+
return {
|
|
73
|
+
model,
|
|
74
|
+
sessionKey,
|
|
75
|
+
tools: toolNames(body),
|
|
76
|
+
prior: turn.prior,
|
|
77
|
+
priorTools: turn.priorTools,
|
|
78
|
+
user: turn.user
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function sameReply(memory, prior) {
|
|
82
|
+
if (prior === void 0) return false;
|
|
83
|
+
return replyHash(prior) === memory.hash;
|
|
84
|
+
}
|
|
85
|
+
function turnPlan(memory, turn) {
|
|
86
|
+
if (memory === void 0) return { replay: false, content: turn.user };
|
|
87
|
+
const matches = sameReply(memory, turn.prior);
|
|
88
|
+
if (turn.user === memory.user && !matches && !turn.priorTools) {
|
|
89
|
+
return { replay: true, text: memory.text };
|
|
90
|
+
}
|
|
91
|
+
if (!matches && turn.prior !== void 0 && !turn.priorTools) {
|
|
92
|
+
return { replay: false, content: `The caller heard: ${turn.prior}
|
|
93
|
+
|
|
94
|
+
${turn.user}` };
|
|
95
|
+
}
|
|
96
|
+
return { replay: false, content: turn.user };
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
parseTurn,
|
|
100
|
+
turnPlan
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/parse.ts"],
|
|
4
|
+
"sourcesContent": ["import { replyHash } from './hash.ts';\n\nexport interface Memory {\n hash: string;\n text: string;\n user: string;\n}\n\nexport interface ParsedTurn {\n model: string;\n sessionKey: string;\n tools: Set<string>;\n prior?: string;\n priorTools: boolean;\n user: string;\n}\n\nexport type TurnPlan = { replay: true; text: string } | { replay: false; content: string };\n\nconst extraNames = [\n 'fishaudio_extra_body',\n 'llm_extra_body',\n 'elevenlabs_extra_body',\n 'custom_llm_extra_body',\n] as const;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n let text = '';\n for (const part of content) {\n if (isRecord(part) && typeof part.text === 'string') text += part.text;\n }\n return text;\n}\n\nfunction namedSession(extra: unknown): string | undefined {\n if (typeof extra === 'string') return extra.length > 0 ? extra : undefined;\n if (!isRecord(extra)) return undefined;\n const id = extra.session_id;\n if (typeof id === 'string' && id.length > 0) return id;\n return undefined;\n}\n\nfunction sessionOf(body: Record<string, unknown>): string | undefined {\n if (typeof body.session_id === 'string' && body.session_id.length > 0) return body.session_id;\n for (const name of extraNames) {\n const key = namedSession(body[name]);\n if (key !== undefined) return key;\n }\n return undefined;\n}\n\nfunction toolNames(body: Record<string, unknown>): Set<string> {\n const names = new Set<string>();\n if (!Array.isArray(body.tools)) return names;\n for (const tool of body.tools) {\n if (!isRecord(tool)) continue;\n const fn = tool.function;\n if (!isRecord(fn)) continue;\n if (typeof fn.name !== 'string' || fn.name.length === 0) continue;\n names.add(fn.name);\n }\n return names;\n}\n\nfunction lastTurn(\n messages: unknown\n): { prior?: string; priorTools: boolean; user?: string } | undefined {\n if (!Array.isArray(messages)) return undefined;\n let prior: string | undefined;\n let priorTools = false;\n let user: string | undefined;\n for (const message of messages) {\n if (!isRecord(message) || message.role === 'system') continue;\n if (message.role === 'assistant') {\n prior = textOf(message.content);\n priorTools = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;\n user = undefined;\n continue;\n }\n if (message.role === 'user' || message.role === 'tool') user = textOf(message.content);\n }\n return { prior, priorTools, user };\n}\n\nexport function parseTurn(body: unknown): ParsedTurn | string {\n if (!isRecord(body)) return 'invalid chat completion';\n if (body.stream !== true) return 'stream is required';\n const sessionKey = sessionOf(body);\n if (sessionKey === undefined) return 'missing session id';\n const turn = lastTurn(body.messages);\n if (turn?.user === undefined) return 'missing user turn';\n const model = typeof body.model === 'string' && body.model.length > 0 ? body.model : 'brain';\n return {\n model,\n sessionKey,\n tools: toolNames(body),\n prior: turn.prior,\n priorTools: turn.priorTools,\n user: turn.user,\n };\n}\n\nfunction sameReply(memory: Memory, prior: string | undefined): boolean {\n if (prior === undefined) return false;\n return replyHash(prior) === memory.hash;\n}\n\nexport function turnPlan(memory: Memory | undefined, turn: ParsedTurn): TurnPlan {\n if (memory === undefined) return { replay: false, content: turn.user };\n const matches = sameReply(memory, turn.prior);\n if (turn.user === memory.user && !matches && !turn.priorTools) {\n return { replay: true, text: memory.text };\n }\n if (!matches && turn.prior !== undefined && !turn.priorTools) {\n return { replay: false, content: `The caller heard: ${turn.prior}\\n\\n${turn.user}` };\n }\n return { replay: false, content: turn.user };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function sseChunk(model, created, delta, finish) {
|
|
2
|
+
return `data: ${JSON.stringify({
|
|
3
|
+
id: "chatcmpl-brain",
|
|
4
|
+
object: "chat.completion.chunk",
|
|
5
|
+
created,
|
|
6
|
+
model,
|
|
7
|
+
choices: [{ index: 0, delta, finish_reason: finish }]
|
|
8
|
+
})}
|
|
9
|
+
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
12
|
+
function sseDone() {
|
|
13
|
+
return "data: [DONE]\n\n";
|
|
14
|
+
}
|
|
15
|
+
function sseError(message) {
|
|
16
|
+
return `data: ${JSON.stringify({ error: { message } })}
|
|
17
|
+
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
sseChunk,
|
|
22
|
+
sseDone,
|
|
23
|
+
sseError
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/sse.ts"],
|
|
4
|
+
"sourcesContent": ["export function sseChunk(\n model: string,\n created: number,\n delta: Record<string, unknown>,\n finish: string | null\n): string {\n return `data: ${JSON.stringify({\n id: 'chatcmpl-brain',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta, finish_reason: finish }],\n })}\\n\\n`;\n}\n\nexport function sseDone(): string {\n return 'data: [DONE]\\n\\n';\n}\n\nexport function sseError(message: string): string {\n return `data: ${JSON.stringify({ error: { message } })}\\n\\n`;\n}\n"],
|
|
5
|
+
"mappings": "AAAO,SAAS,SACd,OACA,SACA,OACA,QACQ;AACR,SAAO,SAAS,KAAK,UAAU;AAAA,IAC7B,IAAI;AAAA,IACJ,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,eAAe,OAAO,CAAC;AAAA,EACtD,CAAC,CAAC;AAAA;AAAA;AACJ;AAEO,SAAS,UAAkB;AAChC,SAAO;AACT;AAEO,SAAS,SAAS,SAAyB;AAChD,SAAO,SAAS,KAAK,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;AAAA;AAAA;AACxD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSpokenState,
|
|
3
|
+
spokenPieces
|
|
4
|
+
} from "../spoken.js";
|
|
5
|
+
import { vendorCall } from "./call.js";
|
|
6
|
+
import { replyHash } from "./hash.js";
|
|
7
|
+
import { sseChunk, sseDone, sseError } from "./sse.js";
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
function sender(model, created, clock, controller) {
|
|
10
|
+
return (delta, finish2) => {
|
|
11
|
+
const body = clock.opened ? delta : { role: "assistant", ...delta };
|
|
12
|
+
clock.opened = true;
|
|
13
|
+
controller.enqueue(encoder.encode(sseChunk(model, created, body, finish2)));
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function finish(send, controller, reason) {
|
|
17
|
+
send({}, reason);
|
|
18
|
+
controller.enqueue(encoder.encode(sseDone()));
|
|
19
|
+
}
|
|
20
|
+
function remember(sessions, parsed, clock) {
|
|
21
|
+
sessions.set(parsed.sessionKey, {
|
|
22
|
+
hash: replyHash(clock.streamed),
|
|
23
|
+
text: clock.streamed,
|
|
24
|
+
user: parsed.user
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function writeReplay(parsed, text, sessions, controller) {
|
|
28
|
+
const clock = { cause: void 0, sent: false, streamed: text, opened: false };
|
|
29
|
+
const send = sender(parsed.model, Math.floor(Date.now() / 1e3), clock, controller);
|
|
30
|
+
if (text.length > 0) send({ content: text }, null);
|
|
31
|
+
finish(send, controller, "stop");
|
|
32
|
+
remember(sessions, parsed, clock);
|
|
33
|
+
}
|
|
34
|
+
function bindAbort(request, control, clock, hardMs) {
|
|
35
|
+
const fromClient = () => {
|
|
36
|
+
clock.cause = clock.cause ?? "client";
|
|
37
|
+
control.abort();
|
|
38
|
+
};
|
|
39
|
+
if (request.signal.aborted) fromClient();
|
|
40
|
+
else request.signal.addEventListener("abort", fromClient, { once: true });
|
|
41
|
+
const deadline = setTimeout(() => {
|
|
42
|
+
clock.cause = clock.cause ?? "deadline";
|
|
43
|
+
control.abort();
|
|
44
|
+
}, hardMs);
|
|
45
|
+
return () => {
|
|
46
|
+
clearTimeout(deadline);
|
|
47
|
+
request.signal.removeEventListener("abort", fromClient);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function armFiller(options, message, clock, send) {
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
if (clock.sent) return;
|
|
53
|
+
const text = options.filler(message);
|
|
54
|
+
if (text.length === 0) return;
|
|
55
|
+
clock.sent = true;
|
|
56
|
+
clock.streamed += text;
|
|
57
|
+
send({ content: text }, null);
|
|
58
|
+
}, options.budget.firstTokenMs);
|
|
59
|
+
return () => clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
function writeAnswers(event, state, clock, send) {
|
|
62
|
+
for (const piece of spokenPieces(event, state)) {
|
|
63
|
+
clock.sent = true;
|
|
64
|
+
clock.streamed += piece.text;
|
|
65
|
+
send({ content: piece.text }, null);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function writeTool(send, call) {
|
|
69
|
+
send(
|
|
70
|
+
{
|
|
71
|
+
tool_calls: [
|
|
72
|
+
{
|
|
73
|
+
index: 0,
|
|
74
|
+
id: call.id,
|
|
75
|
+
type: "function",
|
|
76
|
+
function: { name: call.name, arguments: call.arguments }
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
null
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function stopFor(clock, controller) {
|
|
84
|
+
if (clock.cause === "deadline") controller.enqueue(encoder.encode(sseError("llm deadline")));
|
|
85
|
+
}
|
|
86
|
+
function errorText(error) {
|
|
87
|
+
if (error instanceof Error) return error.message;
|
|
88
|
+
return String(error);
|
|
89
|
+
}
|
|
90
|
+
async function liveTurn(options, sessions, parsed, content, request, controller) {
|
|
91
|
+
const clock = { cause: void 0, sent: false, streamed: "", opened: false };
|
|
92
|
+
const send = sender(parsed.model, Math.floor(Date.now() / 1e3), clock, controller);
|
|
93
|
+
const message = { role: "user", content, timestamp: Date.now() };
|
|
94
|
+
const control = new AbortController();
|
|
95
|
+
const releaseAbort = bindAbort(request, control, clock, options.budget.hardDeadlineMs);
|
|
96
|
+
const releaseFiller = armFiller(options, message, clock, send);
|
|
97
|
+
try {
|
|
98
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
99
|
+
const conversation = await options.conversation(parsed.sessionKey, control.signal);
|
|
100
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
101
|
+
const state = createSpokenState();
|
|
102
|
+
const meta = { source: "voice", transcriptionModel: parsed.model };
|
|
103
|
+
for await (const event of conversation.prompt(message, meta, control.signal)) {
|
|
104
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
105
|
+
writeAnswers(event, state, clock, send);
|
|
106
|
+
const call = vendorCall(event, parsed.tools);
|
|
107
|
+
if (call === void 0 || options.vendorTools === "drop") continue;
|
|
108
|
+
writeTool(send, call);
|
|
109
|
+
finish(send, controller, "tool_calls");
|
|
110
|
+
remember(sessions, parsed, clock);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
114
|
+
finish(send, controller, "stop");
|
|
115
|
+
remember(sessions, parsed, clock);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (clock.cause === "client") return;
|
|
118
|
+
const text = clock.cause === "deadline" ? "llm deadline" : errorText(error);
|
|
119
|
+
controller.enqueue(encoder.encode(sseError(text)));
|
|
120
|
+
} finally {
|
|
121
|
+
releaseFiller();
|
|
122
|
+
releaseAbort();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function runTurn(options, sessions, parsed, plan, request, controller) {
|
|
126
|
+
if (plan.replay) {
|
|
127
|
+
writeReplay(parsed, plan.text, sessions, controller);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await liveTurn(options, sessions, parsed, plan.content, request, controller);
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
runTurn
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=turn.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/turn.ts"],
|
|
4
|
+
"sourcesContent": ["import type { UserMessage } from '@earendil-works/pi-ai';\nimport {\n createSpokenState,\n type SpokenState,\n spokenPieces,\n type VoiceAgentEvent,\n} from '../spoken.ts';\nimport { type VendorCall, vendorCall } from './call.ts';\nimport { replyHash } from './hash.ts';\nimport type { BrainOptions } from './options.ts';\nimport type { Memory, ParsedTurn, TurnPlan } from './parse.ts';\nimport { sseChunk, sseDone, sseError } from './sse.ts';\n\nconst encoder = new TextEncoder();\n\ninterface Clock {\n cause: 'client' | 'deadline' | undefined;\n sent: boolean;\n streamed: string;\n opened: boolean;\n}\n\ntype Send = (delta: Record<string, unknown>, finish: string | null) => void;\ntype Controller = ReadableStreamDefaultController<Uint8Array>;\n\nfunction sender(model: string, created: number, clock: Clock, controller: Controller): Send {\n return (delta, finish) => {\n const body = clock.opened ? delta : { role: 'assistant', ...delta };\n clock.opened = true;\n controller.enqueue(encoder.encode(sseChunk(model, created, body, finish)));\n };\n}\n\nfunction finish(send: Send, controller: Controller, reason: string): void {\n send({}, reason);\n controller.enqueue(encoder.encode(sseDone()));\n}\n\nfunction remember(sessions: Map<string, Memory>, parsed: ParsedTurn, clock: Clock): void {\n sessions.set(parsed.sessionKey, {\n hash: replyHash(clock.streamed),\n text: clock.streamed,\n user: parsed.user,\n });\n}\n\nfunction writeReplay(\n parsed: ParsedTurn,\n text: string,\n sessions: Map<string, Memory>,\n controller: Controller\n): void {\n const clock: Clock = { cause: undefined, sent: false, streamed: text, opened: false };\n const send = sender(parsed.model, Math.floor(Date.now() / 1000), clock, controller);\n if (text.length > 0) send({ content: text }, null);\n finish(send, controller, 'stop');\n remember(sessions, parsed, clock);\n}\n\nfunction bindAbort(\n request: Request,\n control: AbortController,\n clock: Clock,\n hardMs: number\n): () => void {\n const fromClient = () => {\n clock.cause = clock.cause ?? 'client';\n control.abort();\n };\n if (request.signal.aborted) fromClient();\n else request.signal.addEventListener('abort', fromClient, { once: true });\n const deadline = setTimeout(() => {\n clock.cause = clock.cause ?? 'deadline';\n control.abort();\n }, hardMs);\n return () => {\n clearTimeout(deadline);\n request.signal.removeEventListener('abort', fromClient);\n };\n}\n\nfunction armFiller(\n options: BrainOptions,\n message: UserMessage,\n clock: Clock,\n send: Send\n): () => void {\n const timer = setTimeout(() => {\n if (clock.sent) return;\n const text = options.filler(message);\n if (text.length === 0) return;\n clock.sent = true;\n clock.streamed += text;\n send({ content: text }, null);\n }, options.budget.firstTokenMs);\n return () => clearTimeout(timer);\n}\n\nfunction writeAnswers(event: VoiceAgentEvent, state: SpokenState, clock: Clock, send: Send): void {\n for (const piece of spokenPieces(event, state)) {\n clock.sent = true;\n clock.streamed += piece.text;\n send({ content: piece.text }, null);\n }\n}\n\nfunction writeTool(send: Send, call: VendorCall): void {\n send(\n {\n tool_calls: [\n {\n index: 0,\n id: call.id,\n type: 'function',\n function: { name: call.name, arguments: call.arguments },\n },\n ],\n },\n null\n );\n}\n\nfunction stopFor(clock: Clock, controller: Controller): void {\n if (clock.cause === 'deadline') controller.enqueue(encoder.encode(sseError('llm deadline')));\n}\n\nfunction errorText(error: unknown): string {\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\nasync function liveTurn(\n options: BrainOptions,\n sessions: Map<string, Memory>,\n parsed: ParsedTurn,\n content: string,\n request: Request,\n controller: Controller\n): Promise<void> {\n const clock: Clock = { cause: undefined, sent: false, streamed: '', opened: false };\n const send = sender(parsed.model, Math.floor(Date.now() / 1000), clock, controller);\n const message: UserMessage = { role: 'user', content, timestamp: Date.now() };\n const control = new AbortController();\n const releaseAbort = bindAbort(request, control, clock, options.budget.hardDeadlineMs);\n const releaseFiller = armFiller(options, message, clock, send);\n try {\n if (clock.cause !== undefined) return stopFor(clock, controller);\n const conversation = await options.conversation(parsed.sessionKey, control.signal);\n if (clock.cause !== undefined) return stopFor(clock, controller);\n const state = createSpokenState();\n const meta = { source: 'voice' as const, transcriptionModel: parsed.model };\n for await (const event of conversation.prompt(message, meta, control.signal)) {\n if (clock.cause !== undefined) return stopFor(clock, controller);\n writeAnswers(event, state, clock, send);\n const call = vendorCall(event, parsed.tools);\n if (call === undefined || options.vendorTools === 'drop') continue;\n writeTool(send, call);\n finish(send, controller, 'tool_calls');\n remember(sessions, parsed, clock);\n return;\n }\n if (clock.cause !== undefined) return stopFor(clock, controller);\n finish(send, controller, 'stop');\n remember(sessions, parsed, clock);\n } catch (error) {\n if (clock.cause === 'client') return;\n const text = clock.cause === 'deadline' ? 'llm deadline' : errorText(error);\n controller.enqueue(encoder.encode(sseError(text)));\n } finally {\n releaseFiller();\n releaseAbort();\n }\n}\n\nexport async function runTurn(\n options: BrainOptions,\n sessions: Map<string, Memory>,\n parsed: ParsedTurn,\n plan: TurnPlan,\n request: Request,\n controller: Controller\n): Promise<void> {\n if (plan.replay) {\n writeReplay(parsed, plan.text, sessions, controller);\n return;\n }\n await liveTurn(options, sessions, parsed, plan.content, request, controller);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["finish"]
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function replyHash(text: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UserMessage } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { VoiceConversation } from '../turns.ts';
|
|
3
|
+
export interface BrainBudget {
|
|
4
|
+
firstTokenMs: number;
|
|
5
|
+
hardDeadlineMs: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BrainOptions {
|
|
8
|
+
conversation(sessionKey: string, signal: AbortSignal): Promise<VoiceConversation>;
|
|
9
|
+
vendorTools: 'terminal' | 'drop';
|
|
10
|
+
budget: BrainBudget;
|
|
11
|
+
filler(user: UserMessage): string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Memory {
|
|
2
|
+
hash: string;
|
|
3
|
+
text: string;
|
|
4
|
+
user: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ParsedTurn {
|
|
7
|
+
model: string;
|
|
8
|
+
sessionKey: string;
|
|
9
|
+
tools: Set<string>;
|
|
10
|
+
prior?: string;
|
|
11
|
+
priorTools: boolean;
|
|
12
|
+
user: string;
|
|
13
|
+
}
|
|
14
|
+
export type TurnPlan = {
|
|
15
|
+
replay: true;
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
replay: false;
|
|
19
|
+
content: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function parseTurn(body: unknown): ParsedTurn | string;
|
|
22
|
+
export declare function turnPlan(memory: Memory | undefined, turn: ParsedTurn): TurnPlan;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BrainOptions } from './options.ts';
|
|
2
|
+
import type { Memory, ParsedTurn, TurnPlan } from './parse.ts';
|
|
3
|
+
type Controller = ReadableStreamDefaultController<Uint8Array>;
|
|
4
|
+
export declare function runTurn(options: BrainOptions, sessions: Map<string, Memory>, parsed: ParsedTurn, plan: TurnPlan, request: Request, controller: Controller): Promise<void>;
|
|
5
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.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",
|
|
@@ -85,6 +85,10 @@
|
|
|
85
85
|
"./fish-agents": {
|
|
86
86
|
"types": "./dist/types/fish-agents/index.d.ts",
|
|
87
87
|
"default": "./dist/fish-agents/index.js"
|
|
88
|
+
},
|
|
89
|
+
"./brain": {
|
|
90
|
+
"types": "./dist/types/brain/index.d.ts",
|
|
91
|
+
"default": "./dist/brain/index.js"
|
|
88
92
|
}
|
|
89
93
|
},
|
|
90
94
|
"files": [
|