@unctad-ai/voice-agent-server 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtinTools.d.ts +81 -0
- package/dist/builtinTools.d.ts.map +1 -0
- package/dist/builtinTools.js +156 -0
- package/dist/builtinTools.js.map +1 -0
- package/dist/createChatHandler.d.ts +9 -0
- package/dist/createChatHandler.d.ts.map +1 -0
- package/dist/createChatHandler.js +47 -0
- package/dist/createChatHandler.js.map +1 -0
- package/dist/createSttHandler.d.ts +8 -0
- package/dist/createSttHandler.d.ts.map +1 -0
- package/dist/createSttHandler.js +125 -0
- package/dist/createSttHandler.js.map +1 -0
- package/dist/createTtsHandler.d.ts +13 -0
- package/dist/createTtsHandler.d.ts.map +1 -0
- package/dist/createTtsHandler.js +364 -0
- package/dist/createTtsHandler.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/systemPrompt.d.ts +25 -0
- package/dist/systemPrompt.d.ts.map +1 -0
- package/dist/systemPrompt.js +46 -0
- package/dist/systemPrompt.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { SiteConfig, ServiceBase } from '@unctad-ai/voice-agent-core';
|
|
2
|
+
export declare function buildSynonymMap(synonyms: Record<string, string[]>): Record<string, string[]>;
|
|
3
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
4
|
+
export declare function fuzzySearch(query: string, services: ServiceBase[], synonymMap: Record<string, string[]>): ServiceBase[];
|
|
5
|
+
export declare function createBuiltinTools(config: SiteConfig): {
|
|
6
|
+
serverTools: {
|
|
7
|
+
searchServices: import("ai").Tool<{
|
|
8
|
+
query: string;
|
|
9
|
+
}, {
|
|
10
|
+
totalResults: number;
|
|
11
|
+
services: {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
category: string;
|
|
15
|
+
duration: string | undefined;
|
|
16
|
+
cost: string | undefined;
|
|
17
|
+
}[];
|
|
18
|
+
}>;
|
|
19
|
+
getServiceDetails: import("ai").Tool<{
|
|
20
|
+
serviceId: string;
|
|
21
|
+
}, {
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
title: string;
|
|
24
|
+
category: string;
|
|
25
|
+
overview?: string;
|
|
26
|
+
duration?: string;
|
|
27
|
+
cost?: string;
|
|
28
|
+
requirements?: string[];
|
|
29
|
+
eligibility?: string[];
|
|
30
|
+
steps?: Array<{
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}>;
|
|
34
|
+
} | {
|
|
35
|
+
error: string;
|
|
36
|
+
}>;
|
|
37
|
+
listServicesByCategory: import("ai").Tool<{
|
|
38
|
+
category: string;
|
|
39
|
+
}, {
|
|
40
|
+
id: string;
|
|
41
|
+
title: string;
|
|
42
|
+
duration: string | undefined;
|
|
43
|
+
cost: string | undefined;
|
|
44
|
+
}[] | {
|
|
45
|
+
error: string;
|
|
46
|
+
}>;
|
|
47
|
+
compareServices: import("ai").Tool<{
|
|
48
|
+
serviceIds: string[];
|
|
49
|
+
}, "Could not find enough valid services to compare." | {
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
duration: string | undefined;
|
|
53
|
+
cost: string | undefined;
|
|
54
|
+
requirements: string[] | undefined;
|
|
55
|
+
eligibility: string[] | undefined;
|
|
56
|
+
}[]>;
|
|
57
|
+
};
|
|
58
|
+
clientTools: {
|
|
59
|
+
navigateTo: import("ai").Tool<{
|
|
60
|
+
page: string;
|
|
61
|
+
}, never>;
|
|
62
|
+
viewService: import("ai").Tool<{
|
|
63
|
+
serviceId: string;
|
|
64
|
+
}, never>;
|
|
65
|
+
startApplication: import("ai").Tool<{
|
|
66
|
+
serviceId: string;
|
|
67
|
+
}, never>;
|
|
68
|
+
performUIAction: import("ai").Tool<{
|
|
69
|
+
actionId: string;
|
|
70
|
+
paramsJson?: string | undefined;
|
|
71
|
+
}, never>;
|
|
72
|
+
getFormSchema: import("ai").Tool<Record<string, never>, never>;
|
|
73
|
+
fillFormFields: import("ai").Tool<{
|
|
74
|
+
fields: {
|
|
75
|
+
fieldId: string;
|
|
76
|
+
value: string;
|
|
77
|
+
}[];
|
|
78
|
+
}, never>;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=builtinTools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builtinTools.d.ts","sourceRoot":"","sources":["../src/builtinTools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAI3E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAW5F;AAID,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAexD;AAID,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,WAAW,EAAE,EACvB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GACnC,WAAW,EAAE,CAqBf;AAID,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoGpD"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
// --- Synonym map builder ---
|
|
4
|
+
export function buildSynonymMap(synonyms) {
|
|
5
|
+
const map = {};
|
|
6
|
+
for (const [key, values] of Object.entries(synonyms)) {
|
|
7
|
+
if (!map[key])
|
|
8
|
+
map[key] = [];
|
|
9
|
+
map[key].push(...values);
|
|
10
|
+
for (const v of values) {
|
|
11
|
+
if (!map[v])
|
|
12
|
+
map[v] = [];
|
|
13
|
+
if (!map[v].includes(key))
|
|
14
|
+
map[v].push(key);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return map;
|
|
18
|
+
}
|
|
19
|
+
// --- Levenshtein distance ---
|
|
20
|
+
export function levenshtein(a, b) {
|
|
21
|
+
const m = a.length, n = b.length;
|
|
22
|
+
if (m === 0)
|
|
23
|
+
return n;
|
|
24
|
+
if (n === 0)
|
|
25
|
+
return m;
|
|
26
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
27
|
+
for (let i = 1; i <= m; i++) {
|
|
28
|
+
for (let j = 1; j <= n; j++) {
|
|
29
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
30
|
+
? dp[i - 1][j - 1]
|
|
31
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return dp[m][n];
|
|
35
|
+
}
|
|
36
|
+
// --- Fuzzy search ---
|
|
37
|
+
export function fuzzySearch(query, services, synonymMap) {
|
|
38
|
+
const q = query.toLowerCase();
|
|
39
|
+
const searchTerms = [q];
|
|
40
|
+
for (const [term, synonyms] of Object.entries(synonymMap)) {
|
|
41
|
+
if (q.includes(term))
|
|
42
|
+
searchTerms.push(...synonyms);
|
|
43
|
+
}
|
|
44
|
+
const queryWords = q.split(/\s+/).filter((w) => w.length >= 3);
|
|
45
|
+
return services.filter((s) => {
|
|
46
|
+
const titleLower = s.title.toLowerCase();
|
|
47
|
+
const overviewLower = s.overview?.toLowerCase() || '';
|
|
48
|
+
const categoryLower = s.category.toLowerCase();
|
|
49
|
+
const corpus = `${titleLower} ${overviewLower} ${categoryLower}`;
|
|
50
|
+
const substringMatch = searchTerms.some((term) => titleLower.includes(term) || overviewLower.includes(term) || categoryLower.includes(term));
|
|
51
|
+
if (substringMatch)
|
|
52
|
+
return true;
|
|
53
|
+
const corpusWords = corpus.split(/\s+/);
|
|
54
|
+
return queryWords.some((qw) => qw.length >= 5 && corpusWords.some((cw) => cw.length >= 5 && levenshtein(qw, cw) <= 2));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// --- Tool factory ---
|
|
58
|
+
export function createBuiltinTools(config) {
|
|
59
|
+
const synonymMap = buildSynonymMap(config.synonyms);
|
|
60
|
+
// Helper: find service by ID
|
|
61
|
+
function getServiceById(id) {
|
|
62
|
+
return config.services.find((s) => s.id === id);
|
|
63
|
+
}
|
|
64
|
+
// --- Server-side tools (have execute) ---
|
|
65
|
+
const serverTools = {
|
|
66
|
+
searchServices: tool({
|
|
67
|
+
description: `Search ${config.siteTitle} services by keyword. Supports synonyms. When the search returns a single clear match, immediately follow up with viewService to show the page.`,
|
|
68
|
+
inputSchema: z.object({ query: z.string().describe('Search query') }),
|
|
69
|
+
execute: async ({ query }) => {
|
|
70
|
+
const results = fuzzySearch(query, config.services, synonymMap);
|
|
71
|
+
return {
|
|
72
|
+
totalResults: results.length,
|
|
73
|
+
services: results.map((s) => ({
|
|
74
|
+
id: s.id, title: s.title, category: s.category, duration: s.duration, cost: s.cost,
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
getServiceDetails: tool({
|
|
80
|
+
description: 'Get full details about a service so you can answer verbally. Call searchServices first to get the id. Use alongside viewService.',
|
|
81
|
+
inputSchema: z.object({ serviceId: z.string() }),
|
|
82
|
+
execute: async ({ serviceId }) => {
|
|
83
|
+
const service = getServiceById(serviceId);
|
|
84
|
+
if (!service)
|
|
85
|
+
return { error: 'Service not found' };
|
|
86
|
+
const { id, ...details } = service;
|
|
87
|
+
return details;
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
listServicesByCategory: tool({
|
|
91
|
+
description: 'List all services in a category. Use when the user asks what services are available or wants to browse.',
|
|
92
|
+
inputSchema: z.object({
|
|
93
|
+
category: z.enum(Object.keys(config.categoryMap)).describe('Category to list'),
|
|
94
|
+
}),
|
|
95
|
+
execute: async ({ category }) => {
|
|
96
|
+
const categoryTitle = config.categoryMap[category];
|
|
97
|
+
const cat = config.categories.find((c) => c.title.toLowerCase() === categoryTitle?.toLowerCase());
|
|
98
|
+
if (!cat)
|
|
99
|
+
return { error: 'Category not found' };
|
|
100
|
+
return cat.services.map((s) => ({ id: s.id, title: s.title, duration: s.duration, cost: s.cost }));
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
compareServices: tool({
|
|
104
|
+
description: 'Compare two or more services side by side.',
|
|
105
|
+
inputSchema: z.object({ serviceIds: z.array(z.string()).min(2) }),
|
|
106
|
+
execute: async ({ serviceIds }) => {
|
|
107
|
+
const services = serviceIds.map((id) => getServiceById(id)).filter(Boolean);
|
|
108
|
+
if (services.length < 2)
|
|
109
|
+
return 'Could not find enough valid services to compare.';
|
|
110
|
+
return services.map((s) => ({
|
|
111
|
+
id: s.id, title: s.title, duration: s.duration, cost: s.cost,
|
|
112
|
+
requirements: s.requirements, eligibility: s.eligibility,
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
// --- Client-side tools (NO execute — handled via onToolCall in useChat) ---
|
|
118
|
+
const clientTools = {
|
|
119
|
+
navigateTo: tool({
|
|
120
|
+
description: `Navigate to a page in ${config.siteTitle}.`,
|
|
121
|
+
inputSchema: z.object({
|
|
122
|
+
page: z.enum(Object.keys(config.routeMap)).describe('Page key from route map'),
|
|
123
|
+
}),
|
|
124
|
+
}),
|
|
125
|
+
viewService: tool({
|
|
126
|
+
description: 'Navigate to a specific service detail page. Use the id from searchServices.',
|
|
127
|
+
inputSchema: z.object({ serviceId: z.string() }),
|
|
128
|
+
}),
|
|
129
|
+
startApplication: tool({
|
|
130
|
+
description: 'Navigate to an application form for a service. Use when the user wants to apply or register.',
|
|
131
|
+
inputSchema: z.object({ serviceId: z.string() }),
|
|
132
|
+
}),
|
|
133
|
+
performUIAction: tool({
|
|
134
|
+
description: 'Execute a UI action on the current page such as clicking a button, switching a tab, or toggling a view.',
|
|
135
|
+
inputSchema: z.object({
|
|
136
|
+
actionId: z.string().describe('The action id from UI_ACTIONS context'),
|
|
137
|
+
paramsJson: z.string().optional().describe('JSON params string, e.g. {"tab":"taxes"}'),
|
|
138
|
+
}),
|
|
139
|
+
}),
|
|
140
|
+
getFormSchema: tool({
|
|
141
|
+
description: 'Get available form field IDs and types. Call this ONCE before fillFormFields.',
|
|
142
|
+
inputSchema: z.object({}),
|
|
143
|
+
}),
|
|
144
|
+
fillFormFields: tool({
|
|
145
|
+
description: 'Fill one or more form fields. Use field IDs from getFormSchema.',
|
|
146
|
+
inputSchema: z.object({
|
|
147
|
+
fields: z.array(z.object({
|
|
148
|
+
fieldId: z.string().describe('The field ID from getFormSchema'),
|
|
149
|
+
value: z.string().describe('The value to set'),
|
|
150
|
+
})),
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
return { serverTools, clientTools };
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=builtinTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builtinTools.js","sourceRoot":"","sources":["../src/builtinTools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SiteConfig } from '@unctad-ai/voice-agent-core';
|
|
2
|
+
import type { Request, Response } from 'express';
|
|
3
|
+
export interface ChatHandlerOptions {
|
|
4
|
+
config: SiteConfig;
|
|
5
|
+
groqApiKey: string;
|
|
6
|
+
groqModel?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createChatHandler(options: ChatHandlerOptions): (req: Request, res: Response) => Promise<void>;
|
|
9
|
+
//# sourceMappingURL=createChatHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createChatHandler.d.ts","sourceRoot":"","sources":["../src/createChatHandler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMjD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CA+C7G"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { streamText, convertToModelMessages, stepCountIs } from 'ai';
|
|
2
|
+
import { createGroq } from '@ai-sdk/groq';
|
|
3
|
+
import { buildSystemPrompt } from './systemPrompt.js';
|
|
4
|
+
import { createBuiltinTools } from './builtinTools.js';
|
|
5
|
+
export function createChatHandler(options) {
|
|
6
|
+
const { config, groqApiKey, groqModel } = options;
|
|
7
|
+
const groq = createGroq({ apiKey: groqApiKey });
|
|
8
|
+
const { serverTools, clientTools } = createBuiltinTools(config);
|
|
9
|
+
// Merge extra server tools from config if provided
|
|
10
|
+
const allServerTools = config.extraServerTools
|
|
11
|
+
? { ...serverTools, ...config.extraServerTools }
|
|
12
|
+
: serverTools;
|
|
13
|
+
return async function chatHandler(req, res) {
|
|
14
|
+
try {
|
|
15
|
+
const { messages, clientState } = req.body;
|
|
16
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
17
|
+
res.status(400).json({ error: 'messages array is required' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// useChat sends UIMessage format; convert to model messages for streamText
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
const modelMessages = await convertToModelMessages(messages);
|
|
23
|
+
const result = streamText({
|
|
24
|
+
model: groq(groqModel || 'openai/gpt-oss-120b'),
|
|
25
|
+
system: buildSystemPrompt(config, clientState),
|
|
26
|
+
messages: modelMessages,
|
|
27
|
+
tools: { ...allServerTools, ...clientTools },
|
|
28
|
+
stopWhen: stepCountIs(5),
|
|
29
|
+
temperature: 0,
|
|
30
|
+
});
|
|
31
|
+
result.pipeUIMessageStreamToResponse(res);
|
|
32
|
+
// Consume the full text promise to catch async stream errors that would
|
|
33
|
+
// otherwise crash the Node process as unhandled rejections.
|
|
34
|
+
Promise.resolve(result.text).catch((err) => {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
console.error('Stream error:', msg);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('Chat error:', error);
|
|
41
|
+
if (!res.headersSent) {
|
|
42
|
+
res.status(500).json({ error: 'Chat request failed' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=createChatHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createChatHandler.js","sourceRoot":"","sources":["../src/createChatHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AASvD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAClD,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAChD,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAEhE,mDAAmD;IACnD,MAAM,cAAc,GAAG,MAAM,CAAC,gBAAgB;QAC5C,CAAC,CAAC,EAAE,GAAG,WAAW,EAAE,GAAI,MAAM,CAAC,gBAAyE,EAAE;QAC1G,CAAC,CAAC,WAAW,CAAC;IAEhB,OAAO,KAAK,UAAU,WAAW,CAAC,GAAY,EAAE,GAAa;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,GAAG,CAAC,IAA2D,CAAC;YAElG,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACnE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC,CAAC;gBAC9D,OAAO;YACT,CAAC;YAED,2EAA2E;YAC3E,8DAA8D;YAC9D,MAAM,aAAa,GAAG,MAAM,sBAAsB,CAAC,QAAe,CAAC,CAAC;YAEpE,MAAM,MAAM,GAAG,UAAU,CAAC;gBACxB,KAAK,EAAE,IAAI,CAAC,SAAS,IAAI,qBAAqB,CAAC;gBAC/C,MAAM,EAAE,iBAAiB,CAAC,MAAM,EAAE,WAAW,CAAC;gBAC9C,QAAQ,EAAE,aAAa;gBACvB,KAAK,EAAE,EAAE,GAAG,cAAc,EAAE,GAAG,WAAW,EAAE;gBAC5C,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;gBACxB,WAAW,EAAE,CAAC;aACf,CAAC,CAAC;YAEH,MAAM,CAAC,6BAA6B,CAAC,GAAgC,CAAC,CAAC;YAEvE,wEAAwE;YACxE,4DAA4D;YAC5D,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;gBAClD,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC7D,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YACpC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export interface SttHandlerOptions {
|
|
3
|
+
groqApiKey?: string;
|
|
4
|
+
sttProvider?: string;
|
|
5
|
+
kyutaiSttUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createSttHandler(options: SttHandlerOptions): Router;
|
|
8
|
+
//# sourceMappingURL=createSttHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createSttHandler.d.ts","sourceRoot":"","sources":["../src/createSttHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAIjC,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAUD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAiJnE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import Groq from 'groq-sdk';
|
|
3
|
+
import multer from 'multer';
|
|
4
|
+
export function createSttHandler(options) {
|
|
5
|
+
const router = Router();
|
|
6
|
+
const upload = multer({
|
|
7
|
+
storage: multer.memoryStorage(),
|
|
8
|
+
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
|
|
9
|
+
});
|
|
10
|
+
const sttProvider = options.sttProvider || 'kyutai';
|
|
11
|
+
const kyutaiSttUrl = options.kyutaiSttUrl || 'http://localhost:8003';
|
|
12
|
+
// Groq client — only initialized when needed
|
|
13
|
+
let groq = null;
|
|
14
|
+
function getGroq() {
|
|
15
|
+
if (!groq) {
|
|
16
|
+
if (!options.groqApiKey) {
|
|
17
|
+
throw new Error('groqApiKey not set — cannot use Groq STT');
|
|
18
|
+
}
|
|
19
|
+
groq = new Groq({ apiKey: options.groqApiKey });
|
|
20
|
+
}
|
|
21
|
+
return groq;
|
|
22
|
+
}
|
|
23
|
+
// --- Kyutai STT ---
|
|
24
|
+
async function transcribeWithKyutai(wavBuffer) {
|
|
25
|
+
const formData = new FormData();
|
|
26
|
+
const blob = new Blob([new Uint8Array(wavBuffer)], { type: 'audio/wav' });
|
|
27
|
+
formData.append('file', blob, 'audio.wav');
|
|
28
|
+
const res = await fetch(`${kyutaiSttUrl}/v1/audio/transcriptions`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: formData,
|
|
31
|
+
signal: AbortSignal.timeout(15_000),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const body = await res.text().catch(() => '');
|
|
35
|
+
throw new Error(`Kyutai STT HTTP ${res.status}: ${body}`);
|
|
36
|
+
}
|
|
37
|
+
const data = (await res.json());
|
|
38
|
+
// Map Kyutai VAD to Whisper-compatible format:
|
|
39
|
+
// vadProbs[2] = P(no voice activity in 2s) — maps to Whisper's no_speech_prob
|
|
40
|
+
const noSpeechProb = data.vadProbs?.[2] ?? 0;
|
|
41
|
+
// Derive confidence proxy from aggregate VAD probabilities.
|
|
42
|
+
// Low mean VAD probability -> audio is likely not speech -> mapped to negative logprob
|
|
43
|
+
const vadProbs = data.vadProbs ?? [];
|
|
44
|
+
const meanVadProb = vadProbs.length > 0
|
|
45
|
+
? vadProbs.reduce((s, v) => s + v, 0) / vadProbs.length
|
|
46
|
+
: 1; // no probs = assume speech
|
|
47
|
+
const avgLogprob = vadProbs.length > 0 ? -1.0 * (1 - meanVadProb) : 0;
|
|
48
|
+
return {
|
|
49
|
+
text: data.text,
|
|
50
|
+
language: 'en',
|
|
51
|
+
noSpeechProb,
|
|
52
|
+
avgLogprob,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// --- Groq Whisper STT ---
|
|
56
|
+
async function transcribeWithGroq(wavBuffer) {
|
|
57
|
+
const uint8 = new Uint8Array(wavBuffer);
|
|
58
|
+
const file = new File([uint8], 'audio.wav', { type: 'audio/wav' });
|
|
59
|
+
const transcription = await getGroq().audio.transcriptions.create({
|
|
60
|
+
file,
|
|
61
|
+
model: 'whisper-large-v3-turbo',
|
|
62
|
+
temperature: 0,
|
|
63
|
+
response_format: 'verbose_json',
|
|
64
|
+
});
|
|
65
|
+
// verbose_json includes per-segment quality signals the type doesn't expose
|
|
66
|
+
const verbose = transcription;
|
|
67
|
+
const segments = verbose.segments ?? [];
|
|
68
|
+
const noSpeechProb = segments.length > 0
|
|
69
|
+
? Math.max(...segments.map((s) => s.no_speech_prob ?? 0))
|
|
70
|
+
: 0;
|
|
71
|
+
const avgLogprob = segments.length > 0
|
|
72
|
+
? segments.reduce((sum, s) => sum + (s.avg_logprob ?? 0), 0) / segments.length
|
|
73
|
+
: 0;
|
|
74
|
+
return {
|
|
75
|
+
text: transcription.text,
|
|
76
|
+
language: verbose.language ?? 'en',
|
|
77
|
+
noSpeechProb,
|
|
78
|
+
avgLogprob,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// --- Route handler ---
|
|
82
|
+
router.post('/', upload.single('audio'), async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
if (!req.file) {
|
|
85
|
+
res.status(400).json({ error: 'No audio file provided' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let response;
|
|
89
|
+
let provider = sttProvider;
|
|
90
|
+
const t0 = performance.now();
|
|
91
|
+
if (sttProvider === 'kyutai') {
|
|
92
|
+
try {
|
|
93
|
+
response = await transcribeWithKyutai(req.file.buffer);
|
|
94
|
+
if (!response.text)
|
|
95
|
+
throw new Error('empty transcription');
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.warn('[STT] Kyutai failed, falling back to Groq:', err.message);
|
|
99
|
+
provider = 'groq (fallback)';
|
|
100
|
+
response = await transcribeWithGroq(req.file.buffer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
response = await transcribeWithGroq(req.file.buffer);
|
|
105
|
+
}
|
|
106
|
+
const durationMs = Math.round(performance.now() - t0);
|
|
107
|
+
const audioSizeKB = Math.round(req.file.buffer.length / 1024);
|
|
108
|
+
console.log('[STT]', JSON.stringify({
|
|
109
|
+
provider,
|
|
110
|
+
text: response.text,
|
|
111
|
+
durationMs,
|
|
112
|
+
audioSizeKB,
|
|
113
|
+
noSpeechProb: response.noSpeechProb.toFixed(3),
|
|
114
|
+
avgLogprob: response.avgLogprob.toFixed(3),
|
|
115
|
+
}));
|
|
116
|
+
res.json({ ...response, durationMs });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error('STT error:', error);
|
|
120
|
+
res.status(500).json({ error: 'Transcription failed' });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return router;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=createSttHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createSttHandler.js","sourceRoot":"","sources":["../src/createSttHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export interface TtsHandlerOptions {
|
|
3
|
+
ttsProvider?: string;
|
|
4
|
+
qwen3TtsUrl?: string;
|
|
5
|
+
chatterboxTurboUrl?: string;
|
|
6
|
+
cosyVoiceTtsUrl?: string;
|
|
7
|
+
pocketTtsUrl?: string;
|
|
8
|
+
resembleApiKey?: string;
|
|
9
|
+
resembleModel?: string;
|
|
10
|
+
resembleVoiceUuid?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createTtsHandler(options: TtsHandlerOptions): Router;
|
|
13
|
+
//# sourceMappingURL=createTtsHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createTtsHandler.d.ts","sourceRoot":"","sources":["../src/createTtsHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAsLD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAoNnE"}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Strip reasoning-model chain-of-thought from LLM output.
|
|
4
|
+
*
|
|
5
|
+
* Reasoning models (gpt-oss-120b, DeepSeek, QwQ) emit their thinking
|
|
6
|
+
* before the answer. Two patterns:
|
|
7
|
+
*
|
|
8
|
+
* 1. Tagged: <think>reasoning</think>actual answer
|
|
9
|
+
* The sanitizer's < > stripping removes the tags but leaves reasoning
|
|
10
|
+
* content — must strip BEFORE other sanitization.
|
|
11
|
+
*
|
|
12
|
+
* 2. Untagged: garbled preamble + \n\n + reasoning + actual answer
|
|
13
|
+
* Detected by meta-reasoning phrases ("we need to", "according to rules").
|
|
14
|
+
* The actual answer is always the LAST paragraph.
|
|
15
|
+
*/
|
|
16
|
+
function stripChainOfThought(raw) {
|
|
17
|
+
let text = raw;
|
|
18
|
+
// Tagged CoT: <think>...</think> (may span multiple lines)
|
|
19
|
+
text = text.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
|
20
|
+
// Untagged CoT: split on double-newline, check for reasoning patterns
|
|
21
|
+
const paragraphs = text.split(/\n\n+/).map(p => p.trim()).filter(Boolean);
|
|
22
|
+
if (paragraphs.length > 1) {
|
|
23
|
+
const reasoningPatterns = /\b(we need to|we should|we must|according to rules|the user says|ensure no|two sentences|under \d+ words|no markdown|no contractions|let me think|so we|that'?s \d+ sentences)\b/i;
|
|
24
|
+
const hasReasoning = paragraphs.slice(0, -1).some(p => reasoningPatterns.test(p));
|
|
25
|
+
if (hasReasoning) {
|
|
26
|
+
text = paragraphs[paragraphs.length - 1];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return text.trim();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize text for TTS engines.
|
|
33
|
+
* Strips CoT reasoning, markdown/emoji, escapes SSML chars, caps length.
|
|
34
|
+
*/
|
|
35
|
+
function sanitizeForTTS(raw, maxWords = 60) {
|
|
36
|
+
// Strip chain-of-thought FIRST — before < > removal destroys the tags
|
|
37
|
+
let text = stripChainOfThought(raw)
|
|
38
|
+
// Strip emoji
|
|
39
|
+
.replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{E0020}-\u{E007F}\u{2705}\u{274C}\u{2714}\u{2716}]/gu, '')
|
|
40
|
+
// Normalize Unicode dashes
|
|
41
|
+
.replace(/[\u{2010}\u{2011}\u{2012}\u{2013}\u{2014}\u{2015}]/gu, '-')
|
|
42
|
+
// Strip markdown formatting
|
|
43
|
+
.replace(/\*{1,3}([^*]+)\*{1,3}/g, '$1')
|
|
44
|
+
.replace(/^\|.*\|$/gm, '')
|
|
45
|
+
.replace(/^\|[-:| ]+\|$/gm, '')
|
|
46
|
+
.replace(/\|/g, ',')
|
|
47
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
48
|
+
.replace(/`{1,3}[^`]*`{1,3}/g, '')
|
|
49
|
+
.replace(/^[\s]*[-*]\s+/gm, '')
|
|
50
|
+
.replace(/^[\s]*\d+\.\s+/gm, '')
|
|
51
|
+
// Strip bracketed stage directions ([Awaiting response], [END_SESSION], etc.)
|
|
52
|
+
.replace(/\[[^\]]{2,}\]/g, '')
|
|
53
|
+
// SSML-breaking characters
|
|
54
|
+
.replace(/&/g, 'and')
|
|
55
|
+
.replace(/</g, '')
|
|
56
|
+
.replace(/>/g, '')
|
|
57
|
+
// Collapse whitespace and double periods
|
|
58
|
+
.replace(/\n{2,}/g, '. ')
|
|
59
|
+
.replace(/\n/g, ' ')
|
|
60
|
+
.replace(/\s{2,}/g, ' ')
|
|
61
|
+
.replace(/\.{2,}/g, '.')
|
|
62
|
+
.replace(/\.\s*\./g, '.')
|
|
63
|
+
.trim();
|
|
64
|
+
// Cap at ~maxWords words for listening UX — cut at sentence boundary.
|
|
65
|
+
const words = text.split(/\s+/);
|
|
66
|
+
if (words.length > maxWords) {
|
|
67
|
+
const joined = words.slice(0, maxWords).join(' ');
|
|
68
|
+
const lastSentence = Math.max(joined.lastIndexOf('. '), joined.lastIndexOf('? '));
|
|
69
|
+
text = lastSentence > 0 ? joined.slice(0, lastSentence + 1) : joined.replace(/[,;:\s]+$/, '') + '.';
|
|
70
|
+
}
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
// --- TTS provider functions ---
|
|
74
|
+
async function synthesizeWithQwen3TTS(text, url, signal, opts) {
|
|
75
|
+
const formData = new URLSearchParams();
|
|
76
|
+
formData.append('text', text);
|
|
77
|
+
if (opts?.temperature != null)
|
|
78
|
+
formData.append('temperature', String(opts.temperature));
|
|
79
|
+
// Uses /tts-pipeline for token-level streaming: audio chunks yielded as codec
|
|
80
|
+
// tokens are generated. TTFA ~200ms with two-phase emission + Hann crossfade.
|
|
81
|
+
// 50s timeout: accounts for GPU lock wait (up to 15s) + generation (up to 25s).
|
|
82
|
+
const providerTimeout = AbortSignal.timeout(50_000);
|
|
83
|
+
return fetch(`${url}/tts-pipeline`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
86
|
+
body: formData.toString(),
|
|
87
|
+
signal: signal ? AbortSignal.any([signal, providerTimeout]) : providerTimeout,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function synthesizeWithChatterboxTurbo(text, url, signal) {
|
|
91
|
+
const formData = new URLSearchParams();
|
|
92
|
+
formData.append('text', text);
|
|
93
|
+
// Use /tts-pipeline for sentence-level pipelining: splits text into sentences,
|
|
94
|
+
// generates each sequentially, streams PCM as each sentence completes.
|
|
95
|
+
const providerTimeout = AbortSignal.timeout(30_000);
|
|
96
|
+
return fetch(`${url}/tts-pipeline`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
99
|
+
body: formData.toString(),
|
|
100
|
+
signal: signal ? AbortSignal.any([signal, providerTimeout]) : providerTimeout,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function synthesizeWithCosyVoice(text, url, signal) {
|
|
104
|
+
const formData = new URLSearchParams();
|
|
105
|
+
formData.append('text', text);
|
|
106
|
+
const providerTimeout = AbortSignal.timeout(15_000);
|
|
107
|
+
return fetch(`${url}/tts`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
110
|
+
body: formData.toString(),
|
|
111
|
+
signal: signal ? AbortSignal.any([signal, providerTimeout]) : providerTimeout,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function synthesizeWithPocketTTS(text, url, signal) {
|
|
115
|
+
const formData = new URLSearchParams();
|
|
116
|
+
formData.append('text', text);
|
|
117
|
+
const providerTimeout = AbortSignal.timeout(30_000); // Pocket TTS generates at ~0.5x RT on CPU
|
|
118
|
+
return fetch(`${url}/tts`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
121
|
+
body: formData.toString(),
|
|
122
|
+
signal: signal ? AbortSignal.any([signal, providerTimeout]) : providerTimeout,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function synthesizeWithResemble(text, apiKey, model, voiceUuid, signal) {
|
|
126
|
+
const providerTimeout = AbortSignal.timeout(10_000);
|
|
127
|
+
return fetch('https://f.cluster.resemble.ai/stream', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
'User-Agent': 'voice-agent-kit/1.0',
|
|
132
|
+
Authorization: `Bearer ${apiKey}`,
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
model,
|
|
136
|
+
voice_uuid: voiceUuid,
|
|
137
|
+
data: text,
|
|
138
|
+
output_format: 'wav',
|
|
139
|
+
}),
|
|
140
|
+
signal: signal ? AbortSignal.any([signal, providerTimeout]) : providerTimeout,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export function createTtsHandler(options) {
|
|
144
|
+
const router = Router();
|
|
145
|
+
const ttsProvider = options.ttsProvider || 'resemble';
|
|
146
|
+
const qwen3TtsUrl = options.qwen3TtsUrl || 'http://localhost:8005';
|
|
147
|
+
const chatterboxTurboUrl = options.chatterboxTurboUrl || 'http://localhost:8004';
|
|
148
|
+
const cosyVoiceTtsUrl = options.cosyVoiceTtsUrl || 'http://localhost:8004';
|
|
149
|
+
const pocketTtsUrl = options.pocketTtsUrl || 'http://pocket-tts:8002';
|
|
150
|
+
const resembleApiKey = options.resembleApiKey || '';
|
|
151
|
+
const resembleModel = options.resembleModel || '';
|
|
152
|
+
const resembleVoiceUuid = options.resembleVoiceUuid || '';
|
|
153
|
+
// Helper to call Resemble with options closure
|
|
154
|
+
function callResemble(text, signal) {
|
|
155
|
+
return synthesizeWithResemble(text, resembleApiKey, resembleModel, resembleVoiceUuid, signal);
|
|
156
|
+
}
|
|
157
|
+
router.post('/', async (req, res) => {
|
|
158
|
+
// Global AbortController: aborts upstream fetch on client disconnect or overall timeout.
|
|
159
|
+
// 60s cap prevents the fallback cascade (up to 90s worst-case) from running unchecked.
|
|
160
|
+
const globalAc = new AbortController();
|
|
161
|
+
const globalTimeout = AbortSignal.timeout(60_000);
|
|
162
|
+
const signal = AbortSignal.any([globalAc.signal, globalTimeout]);
|
|
163
|
+
// Abort upstream fetch when client disconnects (barge-in, navigation, tab close).
|
|
164
|
+
// MUST use res.on('close'), NOT req.on('close'): in Node 22, req 'close' fires
|
|
165
|
+
// when the request body is consumed (auto-destroy on Readable), not on disconnect.
|
|
166
|
+
// res 'close' fires when the response connection actually closes.
|
|
167
|
+
res.on('close', () => {
|
|
168
|
+
if (!res.writableFinished) {
|
|
169
|
+
console.debug('[TTS] client disconnected before response complete, aborting upstream');
|
|
170
|
+
globalAc.abort();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
const { text, temperature: rawTemp, maxWords: rawMaxWords } = req.body;
|
|
175
|
+
if (!text) {
|
|
176
|
+
res.status(400).json({ error: 'No text provided' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (typeof text !== 'string' || text.length > 2000) {
|
|
180
|
+
res.status(400).json({ error: 'Text must be a string of 2000 characters or less' });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Validate optional temperature (0-1)
|
|
184
|
+
let temperature;
|
|
185
|
+
if (rawTemp != null) {
|
|
186
|
+
temperature = Number(rawTemp);
|
|
187
|
+
if (isNaN(temperature) || temperature < 0 || temperature > 1) {
|
|
188
|
+
res.status(400).json({ error: 'temperature must be between 0 and 1' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Validate optional maxWords (10-200)
|
|
193
|
+
let maxWords;
|
|
194
|
+
if (rawMaxWords != null) {
|
|
195
|
+
maxWords = Number(rawMaxWords);
|
|
196
|
+
if (isNaN(maxWords) || maxWords < 10 || maxWords > 200) {
|
|
197
|
+
res.status(400).json({ error: 'maxWords must be between 10 and 200' });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const sanitized = sanitizeForTTS(text, maxWords);
|
|
202
|
+
console.debug('[TTS] provider:', ttsProvider);
|
|
203
|
+
console.debug('[TTS] raw:', JSON.stringify(text));
|
|
204
|
+
console.debug('[TTS] sanitized:', JSON.stringify(sanitized));
|
|
205
|
+
const ttsStartTime = performance.now();
|
|
206
|
+
let response;
|
|
207
|
+
if (ttsProvider === 'qwen3-tts') {
|
|
208
|
+
try {
|
|
209
|
+
response = await synthesizeWithQwen3TTS(sanitized, qwen3TtsUrl, signal, { temperature });
|
|
210
|
+
if (!response.ok)
|
|
211
|
+
throw new Error(`qwen3-tts ${response.status}`);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
if (signal.aborted)
|
|
215
|
+
throw err; // Don't fallback if globally aborted
|
|
216
|
+
console.warn('[TTS] qwen3-tts failed, falling back to pocket-tts:', err);
|
|
217
|
+
try {
|
|
218
|
+
response = await synthesizeWithPocketTTS(sanitized, pocketTtsUrl, signal);
|
|
219
|
+
if (!response.ok)
|
|
220
|
+
throw new Error(`pocket-tts ${response.status}`);
|
|
221
|
+
}
|
|
222
|
+
catch (err2) {
|
|
223
|
+
if (signal.aborted)
|
|
224
|
+
throw err2;
|
|
225
|
+
console.warn('[TTS] pocket-tts failed, falling back to Resemble:', err2);
|
|
226
|
+
response = await callResemble(sanitized, signal);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (ttsProvider === 'chatterbox-turbo') {
|
|
231
|
+
try {
|
|
232
|
+
response = await synthesizeWithChatterboxTurbo(sanitized, chatterboxTurboUrl, signal);
|
|
233
|
+
if (!response.ok)
|
|
234
|
+
throw new Error(`chatterbox-turbo ${response.status}`);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (signal.aborted)
|
|
238
|
+
throw err;
|
|
239
|
+
console.warn('[TTS] chatterbox-turbo failed, falling back to pocket-tts:', err);
|
|
240
|
+
try {
|
|
241
|
+
response = await synthesizeWithPocketTTS(sanitized, pocketTtsUrl, signal);
|
|
242
|
+
if (!response.ok)
|
|
243
|
+
throw new Error(`pocket-tts ${response.status}`);
|
|
244
|
+
}
|
|
245
|
+
catch (err2) {
|
|
246
|
+
if (signal.aborted)
|
|
247
|
+
throw err2;
|
|
248
|
+
console.warn('[TTS] pocket-tts failed, falling back to Resemble:', err2);
|
|
249
|
+
response = await callResemble(sanitized, signal);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (ttsProvider === 'cosyvoice') {
|
|
254
|
+
try {
|
|
255
|
+
response = await synthesizeWithCosyVoice(sanitized, cosyVoiceTtsUrl, signal);
|
|
256
|
+
if (!response.ok)
|
|
257
|
+
throw new Error(`cosyvoice ${response.status}`);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
if (signal.aborted)
|
|
261
|
+
throw err;
|
|
262
|
+
console.warn('[TTS] cosyvoice failed, falling back to pocket-tts:', err);
|
|
263
|
+
try {
|
|
264
|
+
response = await synthesizeWithPocketTTS(sanitized, pocketTtsUrl, signal);
|
|
265
|
+
if (!response.ok)
|
|
266
|
+
throw new Error(`pocket-tts ${response.status}`);
|
|
267
|
+
}
|
|
268
|
+
catch (err2) {
|
|
269
|
+
if (signal.aborted)
|
|
270
|
+
throw err2;
|
|
271
|
+
console.warn('[TTS] pocket-tts failed, falling back to Resemble:', err2);
|
|
272
|
+
response = await callResemble(sanitized, signal);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (ttsProvider === 'pocket-tts') {
|
|
277
|
+
try {
|
|
278
|
+
response = await synthesizeWithPocketTTS(sanitized, pocketTtsUrl, signal);
|
|
279
|
+
if (!response.ok)
|
|
280
|
+
throw new Error(`pocket-tts ${response.status}`);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (signal.aborted)
|
|
284
|
+
throw err;
|
|
285
|
+
console.warn('[TTS] pocket-tts failed, falling back to Resemble:', err);
|
|
286
|
+
response = await callResemble(sanitized, signal);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
response = await callResemble(sanitized, signal);
|
|
291
|
+
}
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const errorText = await response.text();
|
|
294
|
+
console.error('[TTS] API error:', response.status, errorText);
|
|
295
|
+
// Normalize all upstream errors to 502 Bad Gateway
|
|
296
|
+
res.status(502).json({ error: 'TTS request failed' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
res.setHeader('Content-Type', 'audio/wav');
|
|
300
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
301
|
+
const reader = response.body?.getReader();
|
|
302
|
+
if (!reader) {
|
|
303
|
+
res.status(500).json({ error: 'No response body from TTS' });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Forward chunks as they arrive from upstream TTS provider.
|
|
307
|
+
// Handles backpressure (pause reading when Express buffer is full)
|
|
308
|
+
// and client disconnect (cancel reader to stop upstream consumption).
|
|
309
|
+
let firstChunkSent = false;
|
|
310
|
+
let ttfaMs = 0;
|
|
311
|
+
try {
|
|
312
|
+
while (true) {
|
|
313
|
+
const { done, value } = await reader.read();
|
|
314
|
+
if (done)
|
|
315
|
+
break;
|
|
316
|
+
if (!firstChunkSent) {
|
|
317
|
+
firstChunkSent = true;
|
|
318
|
+
ttfaMs = Math.round(performance.now() - ttsStartTime);
|
|
319
|
+
// Add Server-Timing header before first write so client can read TTFA
|
|
320
|
+
res.setHeader('Server-Timing', `ttfa;dur=${ttfaMs}`);
|
|
321
|
+
// Flush headers now that Server-Timing is set
|
|
322
|
+
res.flushHeaders();
|
|
323
|
+
}
|
|
324
|
+
const canContinue = res.write(value);
|
|
325
|
+
if (!canContinue) {
|
|
326
|
+
// Express buffer is full — wait for drain before reading more
|
|
327
|
+
await new Promise((resolve) => res.once('drain', resolve));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (streamErr) {
|
|
332
|
+
// Reader cancelled (client disconnect) or global timeout — clean up silently
|
|
333
|
+
reader.cancel().catch(() => { });
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
const totalMs = Math.round(performance.now() - ttsStartTime);
|
|
337
|
+
// Update Server-Timing with total duration (only works if headers not yet flushed,
|
|
338
|
+
// otherwise the TTFA-only header was already sent — that's fine)
|
|
339
|
+
if (!firstChunkSent) {
|
|
340
|
+
res.setHeader('Server-Timing', `ttfa;dur=0, total;dur=${totalMs}`);
|
|
341
|
+
res.flushHeaders();
|
|
342
|
+
}
|
|
343
|
+
console.log('[TTS]', JSON.stringify({
|
|
344
|
+
provider: ttsProvider,
|
|
345
|
+
ttfaMs,
|
|
346
|
+
totalMs,
|
|
347
|
+
textLen: sanitized.length,
|
|
348
|
+
}));
|
|
349
|
+
res.end();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
console.error('[TTS] error:', error);
|
|
354
|
+
if (!res.headersSent) {
|
|
355
|
+
res.status(500).json({ error: 'TTS request failed' });
|
|
356
|
+
}
|
|
357
|
+
else if (!res.writableEnded) {
|
|
358
|
+
res.end();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
return router;
|
|
363
|
+
}
|
|
364
|
+
//# sourceMappingURL=createTtsHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createTtsHandler.js","sourceRoot":"","sources":["../src/createTtsHandler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SiteConfig } from '@unctad-ai/voice-agent-core';
|
|
2
|
+
import type { Router } from 'express';
|
|
3
|
+
export interface VoiceServerOptions {
|
|
4
|
+
config: SiteConfig;
|
|
5
|
+
groqApiKey: string;
|
|
6
|
+
groqModel?: string;
|
|
7
|
+
sttProvider?: string;
|
|
8
|
+
kyutaiSttUrl?: string;
|
|
9
|
+
ttsProvider?: string;
|
|
10
|
+
qwen3TtsUrl?: string;
|
|
11
|
+
chatterboxTurboUrl?: string;
|
|
12
|
+
cosyVoiceTtsUrl?: string;
|
|
13
|
+
pocketTtsUrl?: string;
|
|
14
|
+
resembleApiKey?: string;
|
|
15
|
+
resembleModel?: string;
|
|
16
|
+
resembleVoiceUuid?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function createVoiceRoutes(options: VoiceServerOptions): {
|
|
19
|
+
chat: (req: import('express').Request, res: import('express').Response) => Promise<void>;
|
|
20
|
+
stt: Router;
|
|
21
|
+
tts: Router;
|
|
22
|
+
};
|
|
23
|
+
export { createChatHandler } from './createChatHandler.js';
|
|
24
|
+
export { createSttHandler } from './createSttHandler.js';
|
|
25
|
+
export { createTtsHandler } from './createTtsHandler.js';
|
|
26
|
+
export { buildSystemPrompt } from './systemPrompt.js';
|
|
27
|
+
export { createBuiltinTools } from './builtinTools.js';
|
|
28
|
+
export { buildSynonymMap, fuzzySearch } from './builtinTools.js';
|
|
29
|
+
export type { ClientState } from './systemPrompt.js';
|
|
30
|
+
export type { ChatHandlerOptions } from './createChatHandler.js';
|
|
31
|
+
export type { SttHandlerOptions } from './createSttHandler.js';
|
|
32
|
+
export type { TtsHandlerOptions } from './createTtsHandler.js';
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAKtC,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG;IAC9D,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,SAAS,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAMA;AAED,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createChatHandler } from './createChatHandler.js';
|
|
2
|
+
import { createSttHandler } from './createSttHandler.js';
|
|
3
|
+
import { createTtsHandler } from './createTtsHandler.js';
|
|
4
|
+
export function createVoiceRoutes(options) {
|
|
5
|
+
return {
|
|
6
|
+
chat: createChatHandler(options),
|
|
7
|
+
stt: createSttHandler(options),
|
|
8
|
+
tts: createTtsHandler(options),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export { createChatHandler } from './createChatHandler.js';
|
|
12
|
+
export { createSttHandler } from './createSttHandler.js';
|
|
13
|
+
export { createTtsHandler } from './createTtsHandler.js';
|
|
14
|
+
export { buildSystemPrompt } from './systemPrompt.js';
|
|
15
|
+
export { createBuiltinTools } from './builtinTools.js';
|
|
16
|
+
export { buildSynonymMap, fuzzySearch } from './builtinTools.js';
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAkBzD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAK3D,OAAO;QACL,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;QAChC,GAAG,EAAE,gBAAgB,CAAC,OAAO,CAAC;QAC9B,GAAG,EAAE,gBAAgB,CAAC,OAAO,CAAC;KAC/B,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SiteConfig } from '@unctad-ai/voice-agent-core';
|
|
2
|
+
export interface ClientState {
|
|
3
|
+
route?: string;
|
|
4
|
+
currentService?: {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
category: string;
|
|
8
|
+
} | null;
|
|
9
|
+
categories?: Array<{
|
|
10
|
+
category: string;
|
|
11
|
+
count: number;
|
|
12
|
+
}>;
|
|
13
|
+
uiActions?: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
description: string;
|
|
16
|
+
category?: string;
|
|
17
|
+
params?: unknown;
|
|
18
|
+
}>;
|
|
19
|
+
formStatus?: {
|
|
20
|
+
fieldCount: number;
|
|
21
|
+
groups: string[];
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function buildSystemPrompt(config: SiteConfig, clientState?: ClientState): string;
|
|
25
|
+
//# sourceMappingURL=systemPrompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemPrompt.d.ts","sourceRoot":"","sources":["../src/systemPrompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxE,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5F,UAAU,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;CAC9D;AAqBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,MAAM,CA6BvF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const BASE_RULES = `RULES:
|
|
2
|
+
1. Two sentences max, under 40 words. Plain spoken English — no markdown, lists, formatting, or bracketed tags like [Awaiting response]. Never use contractions (say "you would" not "you'd", "I am" not "I'm", "do not" not "don't").
|
|
3
|
+
2. Summarize, never enumerate. Say "three categories like investor services and permits" — never list every item.
|
|
4
|
+
3. After a tool call, confirm what you did in one sentence.
|
|
5
|
+
4. Never fabricate information. Never say you lack a capability your tools provide.
|
|
6
|
+
5. Say exactly [SILENT] if the speaker is not addressing you — side conversations, background noise, or filler words. When unsure, choose [SILENT].
|
|
7
|
+
|
|
8
|
+
PROACTIVE NAVIGATION: When the user asks about a service, call searchServices first. Then call BOTH viewService (to show the page) AND getServiceDetails (to get data you can speak about) — do not call one without the other. When the user wants to APPLY, call startApplication instead of viewService.
|
|
9
|
+
|
|
10
|
+
TOOL SELECTION: Use searchServices when the user has a specific keyword or service in mind. Use listServicesByCategory when the user wants to BROWSE or see ALL services in a category.
|
|
11
|
+
|
|
12
|
+
PAGE TYPES:
|
|
13
|
+
- /service/:id pages are INFORMATIONAL — they show overview, requirements, and steps. After viewService, briefly describe the service. Do NOT call getFormSchema or fillFormFields on these pages.
|
|
14
|
+
- /dashboard/* pages MAY have fillable forms. Only call getFormSchema when the user explicitly asks to fill or start an application.
|
|
15
|
+
|
|
16
|
+
FORMS: When on a /dashboard/* page, ALWAYS call getFormSchema to see what fields are actually visible — NEVER guess or fabricate form content. The schema is the single source of truth for what the user sees. Ask conversationally for a few details at a time — never dump all field names at once. Batch-fill with fillFormFields once you have the information. When getFormSchema returns sections, guide the user through the FIRST section only. More sections appear automatically as the user answers questions — call getFormSchema again to see newly visible fields.
|
|
17
|
+
|
|
18
|
+
GOODBYE: When the user says goodbye or is done, respond with a warm farewell and append [END_SESSION] at the end. Example: "Happy to help, goodbye! [END_SESSION]"`;
|
|
19
|
+
export function buildSystemPrompt(config, clientState) {
|
|
20
|
+
// Identity layer — from config
|
|
21
|
+
let prompt = `You are ${config.copilotName}, a friendly voice assistant for ${config.siteTitle}. ${config.systemPromptIntro} Your name is ${config.copilotName}.\n\n`;
|
|
22
|
+
// Base rules layer — package-owned
|
|
23
|
+
prompt += BASE_RULES;
|
|
24
|
+
// Dynamic context layer — per-request clientState
|
|
25
|
+
if (!clientState)
|
|
26
|
+
return prompt;
|
|
27
|
+
if (clientState.route) {
|
|
28
|
+
prompt += `\n\nCurrent page: ${clientState.route}`;
|
|
29
|
+
}
|
|
30
|
+
if (clientState.currentService) {
|
|
31
|
+
const s = clientState.currentService;
|
|
32
|
+
prompt += `\nViewing service: ${s.title} (${s.category}). Call getServiceDetails for full info.`;
|
|
33
|
+
}
|
|
34
|
+
if (clientState.categories) {
|
|
35
|
+
prompt += `\nService categories: ${JSON.stringify(clientState.categories)}`;
|
|
36
|
+
}
|
|
37
|
+
if (clientState.uiActions && clientState.uiActions.length > 0) {
|
|
38
|
+
prompt += `\n\nUI_ACTIONS available on this page:\n${JSON.stringify(clientState.uiActions)}`;
|
|
39
|
+
}
|
|
40
|
+
if (clientState.formStatus) {
|
|
41
|
+
const f = clientState.formStatus;
|
|
42
|
+
prompt += `\n\nForm: ${f.fieldCount} fields in ${f.groups.length} sections. Call getFormSchema before fillFormFields.`;
|
|
43
|
+
}
|
|
44
|
+
return prompt;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=systemPrompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemPrompt.js","sourceRoot":"","sources":["../src/systemPrompt.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;mKAiBgJ,CAAC;AAEpK,MAAM,UAAU,iBAAiB,CAAC,MAAkB,EAAE,WAAyB;IAC7E,+BAA+B;IAC/B,IAAI,MAAM,GAAG,WAAW,MAAM,CAAC,WAAW,oCAAoC,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC,iBAAiB,iBAAiB,MAAM,CAAC,WAAW,OAAO,CAAC;IAEtK,mCAAmC;IACnC,MAAM,IAAI,UAAU,CAAC;IAErB,kDAAkD;IAClD,IAAI,CAAC,WAAW;QAAE,OAAO,MAAM,CAAC;IAEhC,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,qBAAqB,WAAW,CAAC,KAAK,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,WAAW,CAAC,cAAc,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,WAAW,CAAC,cAAc,CAAC;QACrC,MAAM,IAAI,sBAAsB,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,QAAQ,0CAA0C,CAAC;IACnG,CAAC;IACD,IAAI,WAAW,CAAC,UAAU,EAAE,CAAC;QAC3B,MAAM,IAAI,yBAAyB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;IAC9E,CAAC;IACD,IAAI,WAAW,CAAC,SAAS,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,2CAA2C,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;IAC/F,CAAC;IACD,IAAI,WAAW,CAAC,UAAU,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,WAAW,CAAC,UAAU,CAAC;QACjC,MAAM,IAAI,aAAa,CAAC,CAAC,UAAU,cAAc,CAAC,CAAC,MAAM,CAAC,MAAM,sDAAsD,CAAC;IACzH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unctad-ai/voice-agent-server",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/unctad-ai/voice-agent-kit.git",
|
|
17
|
+
"directory": "packages/server"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@unctad-ai/voice-agent-core": "0.1.1"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@ai-sdk/groq": "^3.0.0",
|
|
24
|
+
"ai": "^6.0.0",
|
|
25
|
+
"express": ">=5",
|
|
26
|
+
"zod": "^3.0.0 || ^4.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@ai-sdk/groq": "^3.0.24",
|
|
30
|
+
"@types/express": "^5.0.6",
|
|
31
|
+
"@types/multer": "^2.0.0",
|
|
32
|
+
"ai": "^6.0.112",
|
|
33
|
+
"express": "^5.2.1",
|
|
34
|
+
"groq-sdk": "^0.37.0",
|
|
35
|
+
"multer": "^2.1.1",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"zod": "^4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"import": "./dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|