felona-voice 0.1.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/dist/agent.d.ts +105 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +287 -0
- package/dist/agent.js.map +1 -0
- package/dist/analytics/index.d.ts +3 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/index.js +2 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/logger.d.ts +92 -0
- package/dist/analytics/logger.d.ts.map +1 -0
- package/dist/analytics/logger.js +75 -0
- package/dist/analytics/logger.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/jev/action-space.d.ts +38 -0
- package/dist/jev/action-space.d.ts.map +1 -0
- package/dist/jev/action-space.js +92 -0
- package/dist/jev/action-space.js.map +1 -0
- package/dist/jev/embeddings.d.ts +23 -0
- package/dist/jev/embeddings.d.ts.map +1 -0
- package/dist/jev/embeddings.js +63 -0
- package/dist/jev/embeddings.js.map +1 -0
- package/dist/jev/engine.d.ts +101 -0
- package/dist/jev/engine.d.ts.map +1 -0
- package/dist/jev/engine.js +173 -0
- package/dist/jev/engine.js.map +1 -0
- package/dist/jev/index.d.ts +4 -0
- package/dist/jev/index.d.ts.map +1 -0
- package/dist/jev/index.js +4 -0
- package/dist/jev/index.js.map +1 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +2 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/openai.d.ts +22 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +140 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/memory/context.d.ts +53 -0
- package/dist/memory/context.d.ts.map +1 -0
- package/dist/memory/context.js +95 -0
- package/dist/memory/context.js.map +1 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/pipeline.d.ts +93 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +255 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/stt/deepgram.d.ts +27 -0
- package/dist/stt/deepgram.d.ts.map +1 -0
- package/dist/stt/deepgram.js +137 -0
- package/dist/stt/deepgram.js.map +1 -0
- package/dist/stt/index.d.ts +2 -0
- package/dist/stt/index.d.ts.map +1 -0
- package/dist/stt/index.js +2 -0
- package/dist/stt/index.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +38 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +77 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/transport/index.d.ts +2 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +2 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/websocket.d.ts +38 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +180 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/tts/eleven-labs.d.ts +27 -0
- package/dist/tts/eleven-labs.d.ts.map +1 -0
- package/dist/tts/eleven-labs.js +73 -0
- package/dist/tts/eleven-labs.js.map +1 -0
- package/dist/tts/index.d.ts +2 -0
- package/dist/tts/index.d.ts.map +1 -0
- package/dist/tts/index.js +2 -0
- package/dist/tts/index.js.map +1 -0
- package/dist/types.d.ts +370 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/vad/energy.d.ts +49 -0
- package/dist/vad/energy.d.ts.map +1 -0
- package/dist/vad/energy.js +108 -0
- package/dist/vad/energy.js.map +1 -0
- package/dist/vad/index.d.ts +2 -0
- package/dist/vad/index.d.ts.map +1 -0
- package/dist/vad/index.js +2 -0
- package/dist/vad/index.js.map +1 -0
- package/package.json +51 -0
- package/src/agent.ts +373 -0
- package/src/analytics/index.ts +7 -0
- package/src/analytics/logger.ts +152 -0
- package/src/index.ts +121 -0
- package/src/jev/action-space.ts +119 -0
- package/src/jev/embeddings.ts +81 -0
- package/src/jev/engine.ts +225 -0
- package/src/jev/index.ts +3 -0
- package/src/llm/index.ts +1 -0
- package/src/llm/openai.ts +178 -0
- package/src/memory/context.ts +124 -0
- package/src/memory/index.ts +1 -0
- package/src/pipeline.ts +331 -0
- package/src/stt/deepgram.ts +182 -0
- package/src/stt/index.ts +1 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/registry.ts +99 -0
- package/src/transport/index.ts +1 -0
- package/src/transport/websocket.ts +227 -0
- package/src/tts/eleven-labs.ts +100 -0
- package/src/tts/index.ts +1 -0
- package/src/types.ts +429 -0
- package/src/vad/energy.ts +135 -0
- package/src/vad/index.ts +1 -0
- package/tests/jev.test.ts +198 -0
- package/tests/memory.test.ts +169 -0
- package/tests/vad.test.ts +94 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAILLM — LLM provider using OpenAI's chat completions API.
|
|
3
|
+
*
|
|
4
|
+
* The LLM in Felona Voice is used for **content generation**, not flow control.
|
|
5
|
+
* JEV decides what to do; the LLM decides how to say it.
|
|
6
|
+
*/
|
|
7
|
+
export class OpenAILLM {
|
|
8
|
+
name = "openai";
|
|
9
|
+
apiKey;
|
|
10
|
+
baseUrl;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.apiKey = options.apiKey;
|
|
13
|
+
this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
14
|
+
}
|
|
15
|
+
create(options) {
|
|
16
|
+
return new OpenAILLMInterface({
|
|
17
|
+
apiKey: this.apiKey,
|
|
18
|
+
baseUrl: this.baseUrl,
|
|
19
|
+
model: options.model,
|
|
20
|
+
temperature: options.temperature ?? 0.7,
|
|
21
|
+
maxTokens: options.maxTokens ?? 256,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
class OpenAILLMInterface {
|
|
26
|
+
apiKey;
|
|
27
|
+
baseUrl;
|
|
28
|
+
model;
|
|
29
|
+
temperature;
|
|
30
|
+
maxTokens;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.apiKey = options.apiKey;
|
|
33
|
+
this.baseUrl = options.baseUrl;
|
|
34
|
+
this.model = options.model;
|
|
35
|
+
this.temperature = options.temperature;
|
|
36
|
+
this.maxTokens = options.maxTokens;
|
|
37
|
+
}
|
|
38
|
+
async generate(prompt, context) {
|
|
39
|
+
const messages = this.buildMessages(prompt, context);
|
|
40
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model: this.model,
|
|
48
|
+
messages,
|
|
49
|
+
temperature: this.temperature,
|
|
50
|
+
max_tokens: this.maxTokens,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const error = await response.text();
|
|
55
|
+
throw new Error(`OpenAI LLM failed: ${response.status} ${error}`);
|
|
56
|
+
}
|
|
57
|
+
const data = (await response.json());
|
|
58
|
+
return data.choices[0]?.message?.content ?? "";
|
|
59
|
+
}
|
|
60
|
+
async *generateStream(prompt, context) {
|
|
61
|
+
const messages = this.buildMessages(prompt, context);
|
|
62
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
model: this.model,
|
|
70
|
+
messages,
|
|
71
|
+
temperature: this.temperature,
|
|
72
|
+
max_tokens: this.maxTokens,
|
|
73
|
+
stream: true,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const error = await response.text();
|
|
78
|
+
throw new Error(`OpenAI LLM stream failed: ${response.status} ${error}`);
|
|
79
|
+
}
|
|
80
|
+
if (!response.body) {
|
|
81
|
+
throw new Error("OpenAI LLM returned no body");
|
|
82
|
+
}
|
|
83
|
+
const reader = response.body.getReader();
|
|
84
|
+
const decoder = new TextDecoder();
|
|
85
|
+
let buffer = "";
|
|
86
|
+
try {
|
|
87
|
+
while (true) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
if (done)
|
|
90
|
+
break;
|
|
91
|
+
buffer += decoder.decode(value, { stream: true });
|
|
92
|
+
const lines = buffer.split("\n");
|
|
93
|
+
buffer = lines.pop() ?? "";
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
const trimmed = line.trim();
|
|
96
|
+
if (!trimmed || !trimmed.startsWith("data: "))
|
|
97
|
+
continue;
|
|
98
|
+
const jsonStr = trimmed.slice(6);
|
|
99
|
+
if (jsonStr === "[DONE]")
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(jsonStr);
|
|
103
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
104
|
+
if (content) {
|
|
105
|
+
yield content;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignore malformed JSON chunks
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
reader.releaseLock();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
buildMessages(prompt, context) {
|
|
119
|
+
const messages = [];
|
|
120
|
+
// System prompt
|
|
121
|
+
if (context?.systemPrompt) {
|
|
122
|
+
messages.push({ role: "system", content: context.systemPrompt });
|
|
123
|
+
}
|
|
124
|
+
// Additional instructions
|
|
125
|
+
if (context?.instructions) {
|
|
126
|
+
messages.push({ role: "system", content: context.instructions });
|
|
127
|
+
}
|
|
128
|
+
// Conversation history
|
|
129
|
+
if (context?.history) {
|
|
130
|
+
messages.push(...context.history);
|
|
131
|
+
}
|
|
132
|
+
// The actual prompt (what JEV decided the agent should do)
|
|
133
|
+
messages.push({ role: "user", content: prompt });
|
|
134
|
+
return messages;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function createOpenAILLM(options) {
|
|
138
|
+
return new OpenAILLM(options);
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/llm/openai.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { ConversationTurn, MemoryManager, ConversationContext, Session } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* ConversationMemory — Manages the sliding window of conversation turns,
|
|
5
|
+
* extracted slots, and builds the full context for JEV.
|
|
6
|
+
*
|
|
7
|
+
* This is the agent's short-term memory for a single session.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ConversationMemory extends EventEmitter implements MemoryManager {
|
|
10
|
+
private turns;
|
|
11
|
+
private slots;
|
|
12
|
+
private readonly maxTurns;
|
|
13
|
+
constructor(options?: {
|
|
14
|
+
maxTurns?: number;
|
|
15
|
+
});
|
|
16
|
+
addTurn(turn: ConversationTurn): void;
|
|
17
|
+
getTurns(): ConversationTurn[];
|
|
18
|
+
getRecentTurns(n: number): ConversationTurn[];
|
|
19
|
+
setSlot(key: string, value: unknown): void;
|
|
20
|
+
getSlot(key: string): unknown;
|
|
21
|
+
getSlots(): Record<string, unknown>;
|
|
22
|
+
clear(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Build the full conversation context for JEV.
|
|
25
|
+
* This is the primary input to the JEV engine's encode() method.
|
|
26
|
+
*/
|
|
27
|
+
buildContext(session: Session, systemPrompt: string, currentUtterance?: string): ConversationContext;
|
|
28
|
+
/**
|
|
29
|
+
* Serialize the conversation to a format suitable for LLM context.
|
|
30
|
+
* Returns an array of {role, content} messages.
|
|
31
|
+
*/
|
|
32
|
+
toLLMHistory(): Array<{
|
|
33
|
+
role: "user" | "assistant";
|
|
34
|
+
content: string;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Get a text summary of the conversation so far.
|
|
38
|
+
* Useful for embedding into the JEV context vector.
|
|
39
|
+
*/
|
|
40
|
+
toContextString(): string;
|
|
41
|
+
/** Number of turns in memory */
|
|
42
|
+
get length(): number;
|
|
43
|
+
/** Get the last turn (or undefined if empty) */
|
|
44
|
+
get lastTurn(): ConversationTurn | undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create a new ConversationMemory instance.
|
|
48
|
+
* Factory function for convenience.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createMemory(options?: {
|
|
51
|
+
maxTurns?: number;
|
|
52
|
+
}): ConversationMemory;
|
|
53
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/memory/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,OAAO,EACR,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,YAAa,YAAW,aAAa;IAC3E,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAK3C,OAAO,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAYrC,QAAQ,IAAI,gBAAgB,EAAE;IAI9B,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAI7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAM1C,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI7B,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAInC,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,YAAY,CACV,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACxB,mBAAmB;IAUtB;;;OAGG;IACH,YAAY,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAOtE;;;OAGG;IACH,eAAe,IAAI,MAAM;IAMzB,gCAAgC;IAChC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,gDAAgD;IAChD,IAAI,QAAQ,IAAI,gBAAgB,GAAG,SAAS,CAE3C;CACF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9B,kBAAkB,CAEpB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* ConversationMemory — Manages the sliding window of conversation turns,
|
|
4
|
+
* extracted slots, and builds the full context for JEV.
|
|
5
|
+
*
|
|
6
|
+
* This is the agent's short-term memory for a single session.
|
|
7
|
+
*/
|
|
8
|
+
export class ConversationMemory extends EventEmitter {
|
|
9
|
+
turns = [];
|
|
10
|
+
slots = new Map();
|
|
11
|
+
maxTurns;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.maxTurns = options?.maxTurns ?? 50;
|
|
15
|
+
}
|
|
16
|
+
addTurn(turn) {
|
|
17
|
+
this.turns.push(turn);
|
|
18
|
+
// Sliding window — drop oldest turns if we exceed the max
|
|
19
|
+
if (this.turns.length > this.maxTurns) {
|
|
20
|
+
const dropped = this.turns.shift();
|
|
21
|
+
this.emit("turnDropped", dropped);
|
|
22
|
+
}
|
|
23
|
+
this.emit("turnAdded", turn);
|
|
24
|
+
}
|
|
25
|
+
getTurns() {
|
|
26
|
+
return [...this.turns];
|
|
27
|
+
}
|
|
28
|
+
getRecentTurns(n) {
|
|
29
|
+
return this.turns.slice(-n);
|
|
30
|
+
}
|
|
31
|
+
setSlot(key, value) {
|
|
32
|
+
const previous = this.slots.get(key);
|
|
33
|
+
this.slots.set(key, value);
|
|
34
|
+
this.emit("slotUpdated", { key, value, previous });
|
|
35
|
+
}
|
|
36
|
+
getSlot(key) {
|
|
37
|
+
return this.slots.get(key);
|
|
38
|
+
}
|
|
39
|
+
getSlots() {
|
|
40
|
+
return Object.fromEntries(this.slots);
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this.turns = [];
|
|
44
|
+
this.slots.clear();
|
|
45
|
+
this.emit("cleared");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the full conversation context for JEV.
|
|
49
|
+
* This is the primary input to the JEV engine's encode() method.
|
|
50
|
+
*/
|
|
51
|
+
buildContext(session, systemPrompt, currentUtterance) {
|
|
52
|
+
return {
|
|
53
|
+
session,
|
|
54
|
+
turns: this.getTurns(),
|
|
55
|
+
currentUtterance: currentUtterance ?? "",
|
|
56
|
+
slots: this.getSlots(),
|
|
57
|
+
systemPrompt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Serialize the conversation to a format suitable for LLM context.
|
|
62
|
+
* Returns an array of {role, content} messages.
|
|
63
|
+
*/
|
|
64
|
+
toLLMHistory() {
|
|
65
|
+
return this.turns.map((turn) => ({
|
|
66
|
+
role: turn.role === "user" ? "user" : "assistant",
|
|
67
|
+
content: turn.content,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get a text summary of the conversation so far.
|
|
72
|
+
* Useful for embedding into the JEV context vector.
|
|
73
|
+
*/
|
|
74
|
+
toContextString() {
|
|
75
|
+
return this.turns
|
|
76
|
+
.map((t) => `${t.role === "user" ? "User" : "Agent"}: ${t.content}`)
|
|
77
|
+
.join("\n");
|
|
78
|
+
}
|
|
79
|
+
/** Number of turns in memory */
|
|
80
|
+
get length() {
|
|
81
|
+
return this.turns.length;
|
|
82
|
+
}
|
|
83
|
+
/** Get the last turn (or undefined if empty) */
|
|
84
|
+
get lastTurn() {
|
|
85
|
+
return this.turns[this.turns.length - 1];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a new ConversationMemory instance.
|
|
90
|
+
* Factory function for convenience.
|
|
91
|
+
*/
|
|
92
|
+
export function createMemory(options) {
|
|
93
|
+
return new ConversationMemory(options);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/memory/context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/memory/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/memory/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { AudioChunk, Session, STTProvider, TTSProvider, VADProvider, LLMInterface, AgentHooks } from "./types.js";
|
|
3
|
+
import type { JEVEngine } from "./jev/engine.js";
|
|
4
|
+
import type { ConversationMemory } from "./memory/context.js";
|
|
5
|
+
import type { ToolRegistry } from "./tools/registry.js";
|
|
6
|
+
import type { CallLogger } from "./analytics/logger.js";
|
|
7
|
+
/**
|
|
8
|
+
* VoicePipeline — Orchestrates the full voice conversation loop.
|
|
9
|
+
*
|
|
10
|
+
* Audio In → VAD → STT → JEV Decision → LLM Generate → TTS → Audio Out
|
|
11
|
+
*
|
|
12
|
+
* One pipeline instance is created per active session (call).
|
|
13
|
+
* It manages:
|
|
14
|
+
* - Audio buffering and VAD-based turn detection
|
|
15
|
+
* - Streaming STT transcription
|
|
16
|
+
* - JEV-powered action selection
|
|
17
|
+
* - LLM response generation
|
|
18
|
+
* - Streaming TTS playback
|
|
19
|
+
* - Barge-in (user interruption) handling
|
|
20
|
+
*/
|
|
21
|
+
export declare class VoicePipeline extends EventEmitter {
|
|
22
|
+
private readonly sessionId;
|
|
23
|
+
private readonly session;
|
|
24
|
+
private readonly stt;
|
|
25
|
+
private readonly tts;
|
|
26
|
+
private readonly vad;
|
|
27
|
+
private readonly llm;
|
|
28
|
+
private readonly jev;
|
|
29
|
+
private readonly memory;
|
|
30
|
+
private readonly tools;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
private readonly hooks;
|
|
33
|
+
private readonly systemPrompt;
|
|
34
|
+
private sttStream;
|
|
35
|
+
private isSpeaking;
|
|
36
|
+
private isProcessing;
|
|
37
|
+
private currentTranscript;
|
|
38
|
+
private decisions;
|
|
39
|
+
private sendAudioFn;
|
|
40
|
+
constructor(options: {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
session: Session;
|
|
43
|
+
stt: STTProvider;
|
|
44
|
+
tts: TTSProvider;
|
|
45
|
+
vad: VADProvider;
|
|
46
|
+
llm: LLMInterface;
|
|
47
|
+
jev: JEVEngine;
|
|
48
|
+
memory: ConversationMemory;
|
|
49
|
+
tools: ToolRegistry;
|
|
50
|
+
logger: CallLogger;
|
|
51
|
+
hooks: AgentHooks;
|
|
52
|
+
systemPrompt: string;
|
|
53
|
+
sendAudio: (chunk: AudioChunk) => Promise<void>;
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Start the pipeline — initialize STT stream and begin processing.
|
|
57
|
+
*/
|
|
58
|
+
start(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Process incoming audio from the transport.
|
|
61
|
+
* This is called for every audio chunk received from the client.
|
|
62
|
+
*/
|
|
63
|
+
processAudio(chunk: AudioChunk): void;
|
|
64
|
+
/**
|
|
65
|
+
* Stop the pipeline — clean up resources.
|
|
66
|
+
*/
|
|
67
|
+
stop(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Handle a STT transcription result.
|
|
70
|
+
*/
|
|
71
|
+
private handleSTTResult;
|
|
72
|
+
/**
|
|
73
|
+
* Handle user turn completion — the user has stopped speaking.
|
|
74
|
+
* This triggers the JEV → LLM → TTS pipeline.
|
|
75
|
+
*/
|
|
76
|
+
private handleUserTurnComplete;
|
|
77
|
+
/**
|
|
78
|
+
* Speak text through TTS and send audio back to the client.
|
|
79
|
+
*/
|
|
80
|
+
private speak;
|
|
81
|
+
/**
|
|
82
|
+
* Handle barge-in — user interrupts while agent is speaking.
|
|
83
|
+
* Stop TTS playback immediately and re-enter listening mode.
|
|
84
|
+
*/
|
|
85
|
+
private handleBargeIn;
|
|
86
|
+
/** Get current pipeline state */
|
|
87
|
+
get state(): {
|
|
88
|
+
isSpeaking: boolean;
|
|
89
|
+
isProcessing: boolean;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export declare function createPipeline(options: ConstructorParameters<typeof VoicePipeline>[0]): VoicePipeline;
|
|
93
|
+
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EACP,WAAW,EAEX,WAAW,EACX,WAAW,EACX,YAAY,EAGZ,UAAU,EACX,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,uBAAuB,CAAC;AAExE;;;;;;;;;;;;;GAaG;AACH,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,SAAS,CAAwB;IAGzC,OAAO,CAAC,WAAW,CAAuD;gBAE9D,OAAO,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,GAAG,EAAE,WAAW,CAAC;QACjB,GAAG,EAAE,WAAW,CAAC;QACjB,GAAG,EAAE,WAAW,CAAC;QACjB,GAAG,EAAE,YAAY,CAAC;QAClB,GAAG,EAAE,SAAS,CAAC;QACf,MAAM,EAAE,kBAAkB,CAAC;QAC3B,KAAK,EAAE,YAAY,CAAC;QACpB,MAAM,EAAE,UAAU,CAAC;QACnB,KAAK,EAAE,UAAU,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACjD;IAiBD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5B;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAoBrC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B3B;;OAEG;IACH,OAAO,CAAC,eAAe;IAqBvB;;;OAGG;YACW,sBAAsB;IAqFpC;;OAEG;YACW,KAAK;IAwBnB;;;OAGG;IACH,OAAO,CAAC,aAAa;IASrB,iCAAiC;IACjC,IAAI,KAAK,IAAI;QAAE,UAAU,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,CAK1D;CACF;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,GACtD,aAAa,CAEf"}
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* VoicePipeline — Orchestrates the full voice conversation loop.
|
|
4
|
+
*
|
|
5
|
+
* Audio In → VAD → STT → JEV Decision → LLM Generate → TTS → Audio Out
|
|
6
|
+
*
|
|
7
|
+
* One pipeline instance is created per active session (call).
|
|
8
|
+
* It manages:
|
|
9
|
+
* - Audio buffering and VAD-based turn detection
|
|
10
|
+
* - Streaming STT transcription
|
|
11
|
+
* - JEV-powered action selection
|
|
12
|
+
* - LLM response generation
|
|
13
|
+
* - Streaming TTS playback
|
|
14
|
+
* - Barge-in (user interruption) handling
|
|
15
|
+
*/
|
|
16
|
+
export class VoicePipeline extends EventEmitter {
|
|
17
|
+
sessionId;
|
|
18
|
+
session;
|
|
19
|
+
stt;
|
|
20
|
+
tts;
|
|
21
|
+
vad;
|
|
22
|
+
llm;
|
|
23
|
+
jev;
|
|
24
|
+
memory;
|
|
25
|
+
tools;
|
|
26
|
+
logger;
|
|
27
|
+
hooks;
|
|
28
|
+
systemPrompt;
|
|
29
|
+
sttStream = null;
|
|
30
|
+
isSpeaking = false; // Is the agent currently speaking?
|
|
31
|
+
isProcessing = false; // Is the pipeline processing a user turn?
|
|
32
|
+
currentTranscript = "";
|
|
33
|
+
decisions = [];
|
|
34
|
+
// Callback to send audio back to the client
|
|
35
|
+
sendAudioFn = null;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
super();
|
|
38
|
+
this.sessionId = options.sessionId;
|
|
39
|
+
this.session = options.session;
|
|
40
|
+
this.stt = options.stt;
|
|
41
|
+
this.tts = options.tts;
|
|
42
|
+
this.vad = options.vad;
|
|
43
|
+
this.llm = options.llm;
|
|
44
|
+
this.jev = options.jev;
|
|
45
|
+
this.memory = options.memory;
|
|
46
|
+
this.tools = options.tools;
|
|
47
|
+
this.logger = options.logger;
|
|
48
|
+
this.hooks = options.hooks;
|
|
49
|
+
this.systemPrompt = options.systemPrompt;
|
|
50
|
+
this.sendAudioFn = options.sendAudio;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start the pipeline — initialize STT stream and begin processing.
|
|
54
|
+
*/
|
|
55
|
+
async start() {
|
|
56
|
+
this.logger.log("info", `Pipeline started for session ${this.sessionId}`);
|
|
57
|
+
// Create STT stream
|
|
58
|
+
this.sttStream = this.stt.createStream({
|
|
59
|
+
interimResults: true,
|
|
60
|
+
language: "en-US",
|
|
61
|
+
});
|
|
62
|
+
// Handle STT results
|
|
63
|
+
this.sttStream.onResult((result) => {
|
|
64
|
+
this.handleSTTResult(result);
|
|
65
|
+
});
|
|
66
|
+
// Notify hooks
|
|
67
|
+
await this.hooks.onCallStart?.(this.session);
|
|
68
|
+
this.emit("started");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Process incoming audio from the transport.
|
|
72
|
+
* This is called for every audio chunk received from the client.
|
|
73
|
+
*/
|
|
74
|
+
processAudio(chunk) {
|
|
75
|
+
// Step 1: VAD — detect if the user is speaking
|
|
76
|
+
const vadResult = this.vad.process(chunk);
|
|
77
|
+
// Step 2: Handle barge-in — user starts speaking while agent is talking
|
|
78
|
+
if (vadResult.event?.type === "speech_start" && this.isSpeaking) {
|
|
79
|
+
this.handleBargeIn();
|
|
80
|
+
}
|
|
81
|
+
// Step 3: Feed audio to STT (only when speech is detected)
|
|
82
|
+
if (vadResult.isSpeech && this.sttStream) {
|
|
83
|
+
this.sttStream.write(chunk);
|
|
84
|
+
}
|
|
85
|
+
// Step 4: Handle speech end — user stopped speaking, process the turn
|
|
86
|
+
if (vadResult.event?.type === "speech_end" && !this.isProcessing) {
|
|
87
|
+
this.handleUserTurnComplete();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Stop the pipeline — clean up resources.
|
|
92
|
+
*/
|
|
93
|
+
async stop() {
|
|
94
|
+
// Log the call
|
|
95
|
+
await this.logger.logCall({
|
|
96
|
+
session: this.session,
|
|
97
|
+
turns: this.memory.getTurns(),
|
|
98
|
+
decisions: this.decisions,
|
|
99
|
+
metrics: {
|
|
100
|
+
totalTurns: this.memory.length,
|
|
101
|
+
avgJEVLatencyMs: this.decisions.length > 0
|
|
102
|
+
? this.decisions.reduce((sum, d) => sum + d.latencyMs, 0) / this.decisions.length
|
|
103
|
+
: 0,
|
|
104
|
+
bargeInCount: 0, // TODO: track this
|
|
105
|
+
avgConfidence: this.decisions.length > 0
|
|
106
|
+
? this.decisions.reduce((sum, d) => sum + d.confidence, 0) / this.decisions.length
|
|
107
|
+
: 0,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
// Close STT stream
|
|
111
|
+
await this.sttStream?.close();
|
|
112
|
+
// Notify hooks
|
|
113
|
+
await this.hooks.onCallEnd?.(this.session, this.memory.getTurns());
|
|
114
|
+
this.logger.log("info", `Pipeline stopped for session ${this.sessionId}`);
|
|
115
|
+
this.emit("stopped");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Handle a STT transcription result.
|
|
119
|
+
*/
|
|
120
|
+
handleSTTResult(result) {
|
|
121
|
+
if (result.isFinal) {
|
|
122
|
+
// Accumulate final transcripts
|
|
123
|
+
this.currentTranscript += (this.currentTranscript ? " " : "") + result.text;
|
|
124
|
+
this.emit("transcript", {
|
|
125
|
+
text: this.currentTranscript,
|
|
126
|
+
isFinal: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Emit interim for real-time feedback
|
|
131
|
+
this.emit("transcript", {
|
|
132
|
+
text: result.text,
|
|
133
|
+
isFinal: false,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Handle user turn completion — the user has stopped speaking.
|
|
139
|
+
* This triggers the JEV → LLM → TTS pipeline.
|
|
140
|
+
*/
|
|
141
|
+
async handleUserTurnComplete() {
|
|
142
|
+
const userText = this.currentTranscript.trim();
|
|
143
|
+
if (!userText)
|
|
144
|
+
return; // Ignore empty turns
|
|
145
|
+
this.isProcessing = true;
|
|
146
|
+
this.currentTranscript = "";
|
|
147
|
+
try {
|
|
148
|
+
// Add user turn to memory
|
|
149
|
+
this.memory.addTurn({
|
|
150
|
+
role: "user",
|
|
151
|
+
content: userText,
|
|
152
|
+
timestampMs: Date.now() - this.session.startedAt.getTime(),
|
|
153
|
+
});
|
|
154
|
+
// Notify hooks
|
|
155
|
+
await this.hooks.onUserSpoke?.(userText, this.session);
|
|
156
|
+
// Build conversation context for JEV
|
|
157
|
+
const context = this.memory.buildContext(this.session, this.systemPrompt, userText);
|
|
158
|
+
// JEV Decision — what should the agent do?
|
|
159
|
+
const startTime = performance.now();
|
|
160
|
+
const match = await this.jev.decide(context);
|
|
161
|
+
const jevLatencyMs = performance.now() - startTime;
|
|
162
|
+
// Log the decision
|
|
163
|
+
const decisionLog = {
|
|
164
|
+
timestampMs: Date.now() - this.session.startedAt.getTime(),
|
|
165
|
+
contextSummary: userText.slice(0, 200),
|
|
166
|
+
selectedAction: match.action.id,
|
|
167
|
+
confidence: match.confidence,
|
|
168
|
+
candidates: match.candidates.slice(0, 5),
|
|
169
|
+
latencyMs: jevLatencyMs,
|
|
170
|
+
};
|
|
171
|
+
this.decisions.push(decisionLog);
|
|
172
|
+
this.logger.logDecision(decisionLog);
|
|
173
|
+
// Notify hooks
|
|
174
|
+
await this.hooks.onActionSelected?.(match.action, match.confidence, context);
|
|
175
|
+
this.logger.log("info", `JEV selected: "${match.action.id}" (confidence: ${match.confidence.toFixed(3)}, latency: ${jevLatencyMs.toFixed(1)}ms)`);
|
|
176
|
+
// Execute the action handler — this calls the LLM to generate response text
|
|
177
|
+
const actionContext = {
|
|
178
|
+
conversation: context,
|
|
179
|
+
llm: this.llm,
|
|
180
|
+
tools: this.tools,
|
|
181
|
+
memory: this.memory,
|
|
182
|
+
session: this.session,
|
|
183
|
+
};
|
|
184
|
+
const responseText = await match.action.handler(actionContext);
|
|
185
|
+
// Add agent turn to memory
|
|
186
|
+
this.memory.addTurn({
|
|
187
|
+
role: "agent",
|
|
188
|
+
content: responseText,
|
|
189
|
+
timestampMs: Date.now() - this.session.startedAt.getTime(),
|
|
190
|
+
actionId: match.action.id,
|
|
191
|
+
confidence: match.confidence,
|
|
192
|
+
});
|
|
193
|
+
// Notify hooks
|
|
194
|
+
await this.hooks.onAgentSpoke?.(responseText, this.session);
|
|
195
|
+
// TTS — speak the response
|
|
196
|
+
await this.speak(responseText);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
200
|
+
this.logger.log("error", `Pipeline error: ${err.message}`);
|
|
201
|
+
await this.hooks.onError?.(err, this.session);
|
|
202
|
+
this.emit("error", err);
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
this.isProcessing = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Speak text through TTS and send audio back to the client.
|
|
210
|
+
*/
|
|
211
|
+
async speak(text) {
|
|
212
|
+
if (!this.sendAudioFn)
|
|
213
|
+
return;
|
|
214
|
+
this.isSpeaking = true;
|
|
215
|
+
this.emit("agentSpeaking", true);
|
|
216
|
+
try {
|
|
217
|
+
for await (const chunk of this.tts.synthesize(text)) {
|
|
218
|
+
// Check if we've been interrupted (barge-in)
|
|
219
|
+
if (!this.isSpeaking)
|
|
220
|
+
break;
|
|
221
|
+
await this.sendAudioFn(chunk);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
this.logger.log("error", `TTS error: ${error instanceof Error ? error.message : String(error)}`);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
this.isSpeaking = false;
|
|
229
|
+
this.emit("agentSpeaking", false);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Handle barge-in — user interrupts while agent is speaking.
|
|
234
|
+
* Stop TTS playback immediately and re-enter listening mode.
|
|
235
|
+
*/
|
|
236
|
+
handleBargeIn() {
|
|
237
|
+
if (!this.isSpeaking)
|
|
238
|
+
return;
|
|
239
|
+
this.logger.log("info", "Barge-in detected — stopping agent speech");
|
|
240
|
+
this.isSpeaking = false;
|
|
241
|
+
this.hooks.onBargeIn?.(this.session);
|
|
242
|
+
this.emit("bargeIn");
|
|
243
|
+
}
|
|
244
|
+
/** Get current pipeline state */
|
|
245
|
+
get state() {
|
|
246
|
+
return {
|
|
247
|
+
isSpeaking: this.isSpeaking,
|
|
248
|
+
isProcessing: this.isProcessing,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export function createPipeline(options) {
|
|
253
|
+
return new VoicePipeline(options);
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=pipeline.js.map
|