@frockbot/plugin-voice 0.0.0 → 0.3.21
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/frockbot.json +25 -0
- package/package.json +45 -6
- package/src/ask.test.ts +129 -0
- package/src/ask.ts +191 -0
- package/src/backend.ts +63 -0
- package/src/bot.test.ts +78 -0
- package/src/bot.ts +158 -0
- package/src/client/VoiceSurface.vue +70 -0
- package/src/client/VoiceToggle.vue +32 -0
- package/src/client/index.test.ts +95 -0
- package/src/client/index.ts +330 -0
- package/src/client/pending-notifications.test.ts +63 -0
- package/src/client/pending-notifications.ts +80 -0
- package/src/client/playback.test.ts +13 -0
- package/src/client/playback.ts +79 -0
- package/src/client/state.ts +25 -0
- package/src/client/styles.css +137 -0
- package/src/index.ts +6 -0
- package/src/ledger.test.ts +246 -0
- package/src/ledger.ts +499 -0
- package/src/manifest.ts +3 -0
- package/src/prompt.ts +18 -0
- package/src/shared.ts +509 -0
- package/src/tools.test.ts +79 -0
- package/src/tools.ts +254 -0
- package/src/user.ts +145 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
package/src/tools.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { VoicePendingAnswerV1, VoiceToolNameV1 } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
export interface VoiceBotSummaryV1 {
|
|
4
|
+
botId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: "active" | "archived";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface VoiceBotActivityV1 {
|
|
10
|
+
botId: string;
|
|
11
|
+
since: string;
|
|
12
|
+
runs: Array<{
|
|
13
|
+
runId: string;
|
|
14
|
+
status: string;
|
|
15
|
+
startedAt: string;
|
|
16
|
+
partialText?: string;
|
|
17
|
+
}>;
|
|
18
|
+
tasks: Array<{ taskId: string; title: string; status: string }>;
|
|
19
|
+
pendingInbox: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VoiceMemoryHitV1 {
|
|
23
|
+
scope: "user" | "bot";
|
|
24
|
+
botId?: string;
|
|
25
|
+
path: string;
|
|
26
|
+
snippet: string;
|
|
27
|
+
score: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VoiceToolHostV1 {
|
|
31
|
+
listBots(): Promise<readonly VoiceBotSummaryV1[]>;
|
|
32
|
+
botActivity(botId: string, since?: string): Promise<VoiceBotActivityV1>;
|
|
33
|
+
memorySearch(input: {
|
|
34
|
+
query: string;
|
|
35
|
+
botId?: string;
|
|
36
|
+
}): Promise<readonly VoiceMemoryHitV1[]>;
|
|
37
|
+
pendingAnswers(): Promise<readonly VoicePendingAnswerV1[]>;
|
|
38
|
+
askBot(input: {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
callId: string;
|
|
41
|
+
bot: string;
|
|
42
|
+
question: string;
|
|
43
|
+
at: string;
|
|
44
|
+
}): Promise<unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface VoiceToolExecutionContextV1 {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
callId: string;
|
|
50
|
+
at: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GeminiFunctionDeclarationV1 {
|
|
54
|
+
name: VoiceToolNameV1;
|
|
55
|
+
description: string;
|
|
56
|
+
parameters: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface VoiceToolSpecV1 {
|
|
60
|
+
declaration: GeminiFunctionDeclarationV1;
|
|
61
|
+
label(args: Record<string, unknown>): string;
|
|
62
|
+
execute(
|
|
63
|
+
host: VoiceToolHostV1,
|
|
64
|
+
args: Record<string, unknown>,
|
|
65
|
+
context: VoiceToolExecutionContextV1,
|
|
66
|
+
): Promise<unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function noExtraArgs(args: Record<string, unknown>): void {
|
|
70
|
+
if (Object.keys(args).length > 0)
|
|
71
|
+
throw new Error("tool arguments are invalid");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stringArg(
|
|
75
|
+
args: Record<string, unknown>,
|
|
76
|
+
name: string,
|
|
77
|
+
max: number,
|
|
78
|
+
required = true,
|
|
79
|
+
): string | undefined {
|
|
80
|
+
const value = args[name];
|
|
81
|
+
if (value === undefined && !required) return undefined;
|
|
82
|
+
if (
|
|
83
|
+
typeof value !== "string" ||
|
|
84
|
+
value.trim().length === 0 ||
|
|
85
|
+
value.length > max
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(`${name} must be a bounded string`);
|
|
88
|
+
}
|
|
89
|
+
return value.trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function only(args: Record<string, unknown>, names: readonly string[]): void {
|
|
93
|
+
const allowed = new Set(names);
|
|
94
|
+
if (Object.keys(args).some((name) => !allowed.has(name))) {
|
|
95
|
+
throw new Error("tool arguments are invalid");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function boundedJson(value: unknown): unknown {
|
|
100
|
+
const json = JSON.stringify(value);
|
|
101
|
+
if (json.length <= 16_000) return value;
|
|
102
|
+
return { truncated: true, summary: json.slice(0, 15_000) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The transport remains table-driven as Voice adds effectful tools. */
|
|
106
|
+
export const VOICE_TOOL_TABLE_V1: Readonly<
|
|
107
|
+
Record<VoiceToolNameV1, VoiceToolSpecV1>
|
|
108
|
+
> = {
|
|
109
|
+
list_bots: {
|
|
110
|
+
declaration: {
|
|
111
|
+
name: "list_bots",
|
|
112
|
+
description: "List this person's Bots and whether each is active.",
|
|
113
|
+
parameters: { type: "OBJECT", properties: {} },
|
|
114
|
+
},
|
|
115
|
+
label: () => "Checked your Bots",
|
|
116
|
+
async execute(host, args) {
|
|
117
|
+
noExtraArgs(args);
|
|
118
|
+
return { bots: (await host.listBots()).slice(0, 50) };
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
bot_activity: {
|
|
122
|
+
declaration: {
|
|
123
|
+
name: "bot_activity",
|
|
124
|
+
description:
|
|
125
|
+
"Read one Bot's recent runs, in-progress reply, running tasks, and waiting inbox items.",
|
|
126
|
+
parameters: {
|
|
127
|
+
type: "OBJECT",
|
|
128
|
+
properties: {
|
|
129
|
+
bot: { type: "STRING", description: "The Bot id." },
|
|
130
|
+
since: {
|
|
131
|
+
type: "STRING",
|
|
132
|
+
description: "Optional ISO timestamp for the oldest activity.",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
required: ["bot"],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
label: (args) => `Checked ${String(args.bot ?? "a Bot")}'s activity`,
|
|
139
|
+
async execute(host, args) {
|
|
140
|
+
only(args, ["bot", "since"]);
|
|
141
|
+
const botId = stringArg(args, "bot", 128)!;
|
|
142
|
+
const since = stringArg(args, "since", 64, false);
|
|
143
|
+
if (since && !Number.isFinite(Date.parse(since))) {
|
|
144
|
+
throw new Error("since must be an ISO timestamp");
|
|
145
|
+
}
|
|
146
|
+
return boundedJson(await host.botActivity(botId, since));
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
memory_search: {
|
|
150
|
+
declaration: {
|
|
151
|
+
name: "memory_search",
|
|
152
|
+
description:
|
|
153
|
+
"Search durable User memory and Bot memory without waking a Computer.",
|
|
154
|
+
parameters: {
|
|
155
|
+
type: "OBJECT",
|
|
156
|
+
properties: {
|
|
157
|
+
query: { type: "STRING", description: "What to look for." },
|
|
158
|
+
bot: {
|
|
159
|
+
type: "STRING",
|
|
160
|
+
description: "Optional Bot id. Omit to search all Bot tiers.",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["query"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
label: () => "Searched memory",
|
|
167
|
+
async execute(host, args) {
|
|
168
|
+
only(args, ["query", "bot"]);
|
|
169
|
+
return {
|
|
170
|
+
results: (
|
|
171
|
+
await host.memorySearch({
|
|
172
|
+
query: stringArg(args, "query", 512)!,
|
|
173
|
+
...(stringArg(args, "bot", 128, false)
|
|
174
|
+
? { botId: stringArg(args, "bot", 128, false)! }
|
|
175
|
+
: {}),
|
|
176
|
+
})
|
|
177
|
+
).slice(0, 24),
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
pending_answers: {
|
|
182
|
+
declaration: {
|
|
183
|
+
name: "pending_answers",
|
|
184
|
+
description: "Read Bot answers waiting to be spoken to this person.",
|
|
185
|
+
parameters: { type: "OBJECT", properties: {} },
|
|
186
|
+
},
|
|
187
|
+
label: () => "Checked answers waiting for you",
|
|
188
|
+
async execute(host, args) {
|
|
189
|
+
noExtraArgs(args);
|
|
190
|
+
return { answers: (await host.pendingAnswers()).slice(0, 32) };
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
ask_bot: {
|
|
194
|
+
declaration: {
|
|
195
|
+
name: "ask_bot",
|
|
196
|
+
description:
|
|
197
|
+
"Ask one active Bot a question on this person's behalf. The Bot answers later; do not wait for it.",
|
|
198
|
+
parameters: {
|
|
199
|
+
type: "OBJECT",
|
|
200
|
+
properties: {
|
|
201
|
+
bot: {
|
|
202
|
+
type: "STRING",
|
|
203
|
+
description: "The Bot id or exact Bot name.",
|
|
204
|
+
},
|
|
205
|
+
question: {
|
|
206
|
+
type: "STRING",
|
|
207
|
+
description: "The complete question to send to the Bot.",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
required: ["bot", "question"],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
label: (args) => `Asked ${String(args.bot ?? "a Bot")}`,
|
|
214
|
+
async execute(host, args, context) {
|
|
215
|
+
only(args, ["bot", "question"]);
|
|
216
|
+
return host.askBot({
|
|
217
|
+
...context,
|
|
218
|
+
bot: stringArg(args, "bot", 128)!,
|
|
219
|
+
question: stringArg(args, "question", 2_000)!,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const VOICE_FUNCTION_DECLARATIONS_V1 = Object.values(
|
|
226
|
+
VOICE_TOOL_TABLE_V1,
|
|
227
|
+
).map((tool) => tool.declaration);
|
|
228
|
+
|
|
229
|
+
export async function executeVoiceToolV1(
|
|
230
|
+
host: VoiceToolHostV1,
|
|
231
|
+
input: {
|
|
232
|
+
name: string;
|
|
233
|
+
args?: unknown;
|
|
234
|
+
context?: VoiceToolExecutionContextV1;
|
|
235
|
+
},
|
|
236
|
+
): Promise<{ name: VoiceToolNameV1; label: string; result: unknown }> {
|
|
237
|
+
const tool = VOICE_TOOL_TABLE_V1[input.name as VoiceToolNameV1];
|
|
238
|
+
if (!tool) throw new Error("That Voice tool is unavailable.");
|
|
239
|
+
const args = input.args ?? {};
|
|
240
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
241
|
+
throw new Error("tool arguments must be an object");
|
|
242
|
+
}
|
|
243
|
+
const record = args as Record<string, unknown>;
|
|
244
|
+
const context = input.context ?? {
|
|
245
|
+
sessionId: "voice-tool",
|
|
246
|
+
callId: "voice-tool-call",
|
|
247
|
+
at: new Date(0).toISOString(),
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
name: input.name as VoiceToolNameV1,
|
|
251
|
+
label: tool.label(record).slice(0, 160),
|
|
252
|
+
result: await tool.execute(host, record, context),
|
|
253
|
+
};
|
|
254
|
+
}
|
package/src/user.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { defineUserBackendContribution } from "@frockbot/kernel-contracts/contributions";
|
|
2
|
+
import type { Plugin } from "cordis";
|
|
3
|
+
import { askBotFromVoiceV1, type VoiceAskHostV1 } from "./ask.js";
|
|
4
|
+
import { VoiceLedgerV1, type VoiceLedgerStorageV1 } from "./ledger.js";
|
|
5
|
+
import { executeVoiceToolV1, type VoiceToolHostV1 } from "./tools.js";
|
|
6
|
+
import type {
|
|
7
|
+
VoiceOfflineReasonV1,
|
|
8
|
+
VoicePendingAnswerV1,
|
|
9
|
+
VoiceToolCallEntryV1,
|
|
10
|
+
VoiceTranscriptEntryV1,
|
|
11
|
+
} from "./shared.js";
|
|
12
|
+
|
|
13
|
+
export interface VoiceUserBackendHostV1
|
|
14
|
+
extends Omit<VoiceToolHostV1, "askBot">, VoiceAskHostV1 {
|
|
15
|
+
storage: VoiceLedgerStorageV1;
|
|
16
|
+
userId(): string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class VoiceUserBackendContributionV1 {
|
|
20
|
+
readonly packageId = "voice";
|
|
21
|
+
readonly ledger: VoiceLedgerV1;
|
|
22
|
+
|
|
23
|
+
constructor(private readonly host: VoiceUserBackendHostV1) {
|
|
24
|
+
this.ledger = new VoiceLedgerV1(host.storage);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
start(input: { sessionId: string; deviceId: string; at: string }) {
|
|
28
|
+
return this.ledger.start(input);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
end(input: {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
at: string;
|
|
34
|
+
reason: VoiceOfflineReasonV1;
|
|
35
|
+
seconds: number;
|
|
36
|
+
}) {
|
|
37
|
+
return this.ledger.end(input);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
saveResumptionHandle(input: {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
handle: string;
|
|
43
|
+
at: string;
|
|
44
|
+
}) {
|
|
45
|
+
return this.ledger.saveResumptionHandle(input);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
appendTranscript(sessionId: string, entry: VoiceTranscriptEntryV1) {
|
|
49
|
+
return this.ledger.appendTranscript(sessionId, entry);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async executeTool(input: {
|
|
53
|
+
sessionId: string;
|
|
54
|
+
callId: string;
|
|
55
|
+
name: string;
|
|
56
|
+
args?: unknown;
|
|
57
|
+
at: string;
|
|
58
|
+
}) {
|
|
59
|
+
const executed = await executeVoiceToolV1(
|
|
60
|
+
{
|
|
61
|
+
...this.host,
|
|
62
|
+
askBot: (ask) =>
|
|
63
|
+
askBotFromVoiceV1(this.ledger, this.host, {
|
|
64
|
+
userId: this.host.userId(),
|
|
65
|
+
...ask,
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: input.name,
|
|
70
|
+
args: input.args,
|
|
71
|
+
context: {
|
|
72
|
+
sessionId: input.sessionId,
|
|
73
|
+
callId: input.callId,
|
|
74
|
+
at: input.at,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
await this.ledger.appendToolCall(input.sessionId, {
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
id: input.callId,
|
|
81
|
+
name: executed.name,
|
|
82
|
+
label: executed.label,
|
|
83
|
+
at: input.at,
|
|
84
|
+
} satisfies VoiceToolCallEntryV1);
|
|
85
|
+
return executed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
recordPendingAnswer(answer: VoicePendingAnswerV1) {
|
|
89
|
+
return this.ledger.recordPendingAnswer(answer);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
recordAnswerDelivery(delivery: import("./shared.js").VoiceAnswerDeliveryV1) {
|
|
93
|
+
return delivery.outcome === "answered"
|
|
94
|
+
? this.ledger.recordAnswered({
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
type: "voice/answered",
|
|
97
|
+
askId: delivery.askId,
|
|
98
|
+
botId: delivery.botId,
|
|
99
|
+
runId: delivery.runId,
|
|
100
|
+
answer: delivery.answer,
|
|
101
|
+
answeredAt: delivery.at,
|
|
102
|
+
})
|
|
103
|
+
: this.ledger.recordFailed({
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
type: "voice/failed",
|
|
106
|
+
askId: delivery.askId,
|
|
107
|
+
botId: delivery.botId,
|
|
108
|
+
runId: delivery.runId,
|
|
109
|
+
reason: delivery.reason,
|
|
110
|
+
failedAt: delivery.at,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
markBriefed(input: {
|
|
115
|
+
askIds: readonly string[];
|
|
116
|
+
sessionId: string;
|
|
117
|
+
at: string;
|
|
118
|
+
}) {
|
|
119
|
+
return this.ledger.markBriefed(input);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
view() {
|
|
123
|
+
return this.ledger.view();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface VoiceUserApplicationHostV1 {
|
|
128
|
+
voice: VoiceUserBackendHostV1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createVoiceUserBackendPluginV1(
|
|
132
|
+
host: VoiceUserBackendHostV1,
|
|
133
|
+
lifecycle: { mount(value: VoiceUserBackendContributionV1): () => void },
|
|
134
|
+
): Plugin {
|
|
135
|
+
return () => lifecycle.mount(new VoiceUserBackendContributionV1(host));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const userContribution = defineUserBackendContribution<
|
|
139
|
+
VoiceUserApplicationHostV1,
|
|
140
|
+
VoiceUserBackendContributionV1
|
|
141
|
+
>({
|
|
142
|
+
specifier: "@frockbot/plugin-voice/user",
|
|
143
|
+
create: (host, lifecycle) =>
|
|
144
|
+
createVoiceUserBackendPluginV1(host.voice, lifecycle),
|
|
145
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
12
|
+
"types": ["bun", "vite/client"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts", "src/**/*.vue"]
|
|
15
|
+
}
|
package/README.md
DELETED