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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vad/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vad/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "felona-voice",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Voice agent framework powered by JEV (Joint Embedding Vectors) for dynamic conversation flow",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"voice",
|
|
22
|
+
"agent",
|
|
23
|
+
"ai",
|
|
24
|
+
"jev",
|
|
25
|
+
"embedding",
|
|
26
|
+
"stt",
|
|
27
|
+
"tts",
|
|
28
|
+
"conversational-ai"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"ws": "^8.18.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.14.0",
|
|
36
|
+
"@types/ws": "^8.5.12",
|
|
37
|
+
"typescript": "^5.5.0",
|
|
38
|
+
"vitest": "^2.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"onnxruntime-node": ">=1.18.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"onnxruntime-node": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type {
|
|
3
|
+
FelAgentConfig,
|
|
4
|
+
Session,
|
|
5
|
+
AudioChunk,
|
|
6
|
+
AgentAction,
|
|
7
|
+
AgentHooks,
|
|
8
|
+
LLMInterface,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import { WebSocketTransport } from "./transport/websocket.js";
|
|
11
|
+
import { JEVEngine } from "./jev/engine.js";
|
|
12
|
+
import { OpenAIEmbeddingProvider } from "./jev/embeddings.js";
|
|
13
|
+
import { ConversationMemory } from "./memory/context.js";
|
|
14
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
15
|
+
import { CallLogger } from "./analytics/logger.js";
|
|
16
|
+
import { VoicePipeline } from "./pipeline.js";
|
|
17
|
+
|
|
18
|
+
// Provider imports
|
|
19
|
+
import { DeepgramSTT } from "./stt/deepgram.js";
|
|
20
|
+
import { ElevenLabsTTS } from "./tts/eleven-labs.js";
|
|
21
|
+
import { EnergyVAD } from "./vad/energy.js";
|
|
22
|
+
import { OpenAILLM } from "./llm/openai.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* FelAgent — The main entry point for building a voice agent with Felona Voice.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const agent = new FelAgent({
|
|
30
|
+
* name: "Support Bot",
|
|
31
|
+
* systemPrompt: "You are a helpful support agent.",
|
|
32
|
+
* stt: { provider: "deepgram", apiKey: "..." },
|
|
33
|
+
* tts: { provider: "elevenlabs", apiKey: "..." },
|
|
34
|
+
* llm: { provider: "openai", apiKey: "...", model: "gpt-4o-mini" },
|
|
35
|
+
* actions: [ ... ],
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* agent.listen({ port: 8080 });
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* FelAgent wires together:
|
|
42
|
+
* - Transport (WebSocket server for audio I/O)
|
|
43
|
+
* - Voice Pipeline (STT → JEV → LLM → TTS per session)
|
|
44
|
+
* - JEV Engine (action prediction)
|
|
45
|
+
* - Memory, Tools, Analytics
|
|
46
|
+
*/
|
|
47
|
+
export class FelAgent extends EventEmitter {
|
|
48
|
+
private readonly config: FelAgentConfig;
|
|
49
|
+
private transport: WebSocketTransport;
|
|
50
|
+
private readonly jev: JEVEngine;
|
|
51
|
+
private readonly logger: CallLogger;
|
|
52
|
+
private readonly tools: ToolRegistry;
|
|
53
|
+
private readonly hooks: AgentHooks;
|
|
54
|
+
private readonly sttProvider: DeepgramSTT;
|
|
55
|
+
private readonly ttsProvider: ElevenLabsTTS;
|
|
56
|
+
private readonly vadProvider: EnergyVAD;
|
|
57
|
+
private readonly llmProvider: OpenAILLM;
|
|
58
|
+
private readonly llmInterface: LLMInterface;
|
|
59
|
+
|
|
60
|
+
// Active pipelines (one per session)
|
|
61
|
+
private pipelines: Map<string, VoicePipeline> = new Map();
|
|
62
|
+
|
|
63
|
+
private initialized = false;
|
|
64
|
+
|
|
65
|
+
constructor(config: FelAgentConfig) {
|
|
66
|
+
super();
|
|
67
|
+
this.config = config;
|
|
68
|
+
this.hooks = config.hooks ?? {};
|
|
69
|
+
|
|
70
|
+
// Initialize logger
|
|
71
|
+
this.logger = new CallLogger({
|
|
72
|
+
logDir: config.logging?.logDir,
|
|
73
|
+
level: config.logging?.level,
|
|
74
|
+
enabled: config.logging?.enabled ?? true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Initialize tools
|
|
78
|
+
this.tools = new ToolRegistry();
|
|
79
|
+
if (config.tools) {
|
|
80
|
+
this.tools.registerAll(config.tools);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Initialize providers
|
|
84
|
+
this.sttProvider = this.createSTTProvider();
|
|
85
|
+
this.ttsProvider = this.createTTSProvider();
|
|
86
|
+
this.vadProvider = new EnergyVAD();
|
|
87
|
+
this.llmProvider = this.createLLMProvider();
|
|
88
|
+
this.llmInterface = this.llmProvider.create({
|
|
89
|
+
model: (config.llm.model as string) ?? "gpt-4o-mini",
|
|
90
|
+
temperature: config.llm.temperature as number | undefined,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Initialize JEV engine
|
|
94
|
+
const embeddingProvider = new OpenAIEmbeddingProvider({
|
|
95
|
+
apiKey:
|
|
96
|
+
(config.jev?.embeddingApiKey as string) ??
|
|
97
|
+
(config.llm.apiKey as string) ??
|
|
98
|
+
"",
|
|
99
|
+
});
|
|
100
|
+
this.jev = new JEVEngine({
|
|
101
|
+
embeddingProvider,
|
|
102
|
+
confidenceThreshold: config.jev?.confidenceThreshold,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Initialize transport
|
|
106
|
+
this.transport = new WebSocketTransport();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Start the agent — initialize JEV, start the transport, begin accepting calls.
|
|
111
|
+
*/
|
|
112
|
+
async listen(options?: { port?: number; host?: string }): Promise<void> {
|
|
113
|
+
const port = options?.port ?? this.config.transport?.port ?? 8080;
|
|
114
|
+
const host = options?.host ?? this.config.transport?.host;
|
|
115
|
+
|
|
116
|
+
this.logger.log("info", `Starting Felona Voice agent: "${this.config.name}"`);
|
|
117
|
+
|
|
118
|
+
// Step 1: Initialize JEV — embed all action descriptions
|
|
119
|
+
this.logger.log("info", "Initializing JEV engine...");
|
|
120
|
+
await this.jev.initialize(this.config.actions);
|
|
121
|
+
this.logger.log(
|
|
122
|
+
"info",
|
|
123
|
+
`JEV initialized with ${this.config.actions.length} actions`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Step 2: Load predictor model if specified
|
|
127
|
+
if (this.config.jev?.predictorModel) {
|
|
128
|
+
await this.jev.loadPredictor(this.config.jev.predictorModel);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Step 3: Wire up transport event handlers
|
|
132
|
+
this.transport.onConnect((session) => this.handleConnect(session));
|
|
133
|
+
this.transport.onDisconnect((session) => this.handleDisconnect(session));
|
|
134
|
+
this.transport.onAudioChunk((sessionId, chunk) =>
|
|
135
|
+
this.handleAudio(sessionId, chunk),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Step 4: Start the transport server
|
|
139
|
+
await this.transport.start({ port, host });
|
|
140
|
+
|
|
141
|
+
this.initialized = true;
|
|
142
|
+
this.logger.log("info", `Agent "${this.config.name}" ready on port ${port}`);
|
|
143
|
+
this.logger.log("info", ` STT: ${this.sttProvider.name}`);
|
|
144
|
+
this.logger.log("info", ` TTS: ${this.ttsProvider.name}`);
|
|
145
|
+
this.logger.log("info", ` LLM: ${this.llmProvider.name}`);
|
|
146
|
+
this.logger.log(
|
|
147
|
+
"info",
|
|
148
|
+
` JEV: ${this.jev.hasPredictor ? "trained predictor" : "cold-start mode"}`,
|
|
149
|
+
);
|
|
150
|
+
this.logger.log(
|
|
151
|
+
"info",
|
|
152
|
+
` Actions: ${this.config.actions.map((a) => a.id).join(", ")}`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
this.emit("ready", { port, host });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Stop the agent — shut down all pipelines and the transport.
|
|
160
|
+
*/
|
|
161
|
+
async stop(): Promise<void> {
|
|
162
|
+
this.logger.log("info", "Stopping agent...");
|
|
163
|
+
|
|
164
|
+
// Stop all active pipelines
|
|
165
|
+
for (const [sessionId, pipeline] of this.pipelines) {
|
|
166
|
+
await pipeline.stop();
|
|
167
|
+
}
|
|
168
|
+
this.pipelines.clear();
|
|
169
|
+
|
|
170
|
+
// Stop transport
|
|
171
|
+
await this.transport.stop();
|
|
172
|
+
|
|
173
|
+
this.initialized = false;
|
|
174
|
+
this.logger.log("info", "Agent stopped");
|
|
175
|
+
this.emit("stopped");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Handle a new client connection — create a voice pipeline for this session.
|
|
180
|
+
*/
|
|
181
|
+
private async handleConnect(session: Session): Promise<void> {
|
|
182
|
+
this.logger.log("info", `New call: ${session.id}`);
|
|
183
|
+
|
|
184
|
+
// Create per-session memory
|
|
185
|
+
const memory = new ConversationMemory();
|
|
186
|
+
|
|
187
|
+
// Create a new VAD instance per session (has internal state)
|
|
188
|
+
const vad = new EnergyVAD();
|
|
189
|
+
|
|
190
|
+
// Create the voice pipeline for this session
|
|
191
|
+
const pipeline = new VoicePipeline({
|
|
192
|
+
sessionId: session.id,
|
|
193
|
+
session,
|
|
194
|
+
stt: this.sttProvider,
|
|
195
|
+
tts: this.ttsProvider,
|
|
196
|
+
vad,
|
|
197
|
+
llm: this.llmInterface,
|
|
198
|
+
jev: this.jev,
|
|
199
|
+
memory,
|
|
200
|
+
tools: this.tools,
|
|
201
|
+
logger: this.logger,
|
|
202
|
+
hooks: this.hooks,
|
|
203
|
+
systemPrompt: this.config.systemPrompt,
|
|
204
|
+
sendAudio: (chunk) => this.transport.sendAudio(session.id, chunk),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Forward pipeline events
|
|
208
|
+
pipeline.on("transcript", (data) =>
|
|
209
|
+
this.emit("transcript", { sessionId: session.id, ...data }),
|
|
210
|
+
);
|
|
211
|
+
pipeline.on("error", (error) =>
|
|
212
|
+
this.emit("error", { sessionId: session.id, error }),
|
|
213
|
+
);
|
|
214
|
+
pipeline.on("bargeIn", () =>
|
|
215
|
+
this.emit("bargeIn", { sessionId: session.id }),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
this.pipelines.set(session.id, pipeline);
|
|
219
|
+
await pipeline.start();
|
|
220
|
+
|
|
221
|
+
this.emit("callStarted", session);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Handle a client disconnection — stop and clean up the pipeline.
|
|
226
|
+
*/
|
|
227
|
+
private async handleDisconnect(session: Session): Promise<void> {
|
|
228
|
+
this.logger.log("info", `Call ended: ${session.id}`);
|
|
229
|
+
|
|
230
|
+
const pipeline = this.pipelines.get(session.id);
|
|
231
|
+
if (pipeline) {
|
|
232
|
+
await pipeline.stop();
|
|
233
|
+
this.pipelines.delete(session.id);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.emit("callEnded", session);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Handle incoming audio from a client — route to the correct pipeline.
|
|
241
|
+
*/
|
|
242
|
+
private handleAudio(sessionId: string, chunk: AudioChunk): void {
|
|
243
|
+
const pipeline = this.pipelines.get(sessionId);
|
|
244
|
+
if (pipeline) {
|
|
245
|
+
pipeline.processAudio(chunk);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Create STT provider based on config */
|
|
250
|
+
private createSTTProvider(): DeepgramSTT {
|
|
251
|
+
switch (this.config.stt.provider) {
|
|
252
|
+
case "deepgram":
|
|
253
|
+
return new DeepgramSTT({
|
|
254
|
+
apiKey: (this.config.stt.apiKey as string) ?? "",
|
|
255
|
+
});
|
|
256
|
+
default:
|
|
257
|
+
this.logger.log(
|
|
258
|
+
"warn",
|
|
259
|
+
`Unknown STT provider "${this.config.stt.provider}", falling back to Deepgram`,
|
|
260
|
+
);
|
|
261
|
+
return new DeepgramSTT({
|
|
262
|
+
apiKey: (this.config.stt.apiKey as string) ?? "",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Create TTS provider based on config */
|
|
268
|
+
private createTTSProvider(): ElevenLabsTTS {
|
|
269
|
+
switch (this.config.tts.provider) {
|
|
270
|
+
case "elevenlabs":
|
|
271
|
+
return new ElevenLabsTTS({
|
|
272
|
+
apiKey: (this.config.tts.apiKey as string) ?? "",
|
|
273
|
+
voice: this.config.tts.voice as string | undefined,
|
|
274
|
+
});
|
|
275
|
+
default:
|
|
276
|
+
this.logger.log(
|
|
277
|
+
"warn",
|
|
278
|
+
`Unknown TTS provider "${this.config.tts.provider}", falling back to ElevenLabs`,
|
|
279
|
+
);
|
|
280
|
+
return new ElevenLabsTTS({
|
|
281
|
+
apiKey: (this.config.tts.apiKey as string) ?? "",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Create LLM provider based on config */
|
|
287
|
+
private createLLMProvider(): OpenAILLM {
|
|
288
|
+
switch (this.config.llm.provider) {
|
|
289
|
+
case "openai":
|
|
290
|
+
return new OpenAILLM({
|
|
291
|
+
apiKey: (this.config.llm.apiKey as string) ?? "",
|
|
292
|
+
});
|
|
293
|
+
default:
|
|
294
|
+
this.logger.log(
|
|
295
|
+
"warn",
|
|
296
|
+
`Unknown LLM provider "${this.config.llm.provider}", falling back to OpenAI`,
|
|
297
|
+
);
|
|
298
|
+
return new OpenAILLM({
|
|
299
|
+
apiKey: (this.config.llm.apiKey as string) ?? "",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Get the number of active calls */
|
|
305
|
+
get activeCallCount(): number {
|
|
306
|
+
return this.pipelines.size;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Check if the agent is running */
|
|
310
|
+
get isRunning(): boolean {
|
|
311
|
+
return this.initialized;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Get the agent name */
|
|
315
|
+
get name(): string {
|
|
316
|
+
return this.config.name;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Get the JEV engine (for inspection/testing) */
|
|
320
|
+
get jevEngine(): JEVEngine {
|
|
321
|
+
return this.jev;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Helper to define an action with type safety.
|
|
327
|
+
*/
|
|
328
|
+
export function defineAction(action: AgentAction): AgentAction {
|
|
329
|
+
return action;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Quick-start factory — creates a minimal agent with sensible defaults.
|
|
334
|
+
* Automatically creates a default "respond" action that uses the LLM directly.
|
|
335
|
+
*/
|
|
336
|
+
export function quickStart(config: {
|
|
337
|
+
llm: { provider: string; apiKey: string; model?: string };
|
|
338
|
+
stt: { provider: string; apiKey: string };
|
|
339
|
+
tts: { provider: string; apiKey: string; voice?: string };
|
|
340
|
+
systemPrompt?: string;
|
|
341
|
+
}): FelAgent {
|
|
342
|
+
return new FelAgent({
|
|
343
|
+
name: "Felona Agent",
|
|
344
|
+
systemPrompt:
|
|
345
|
+
config.systemPrompt ??
|
|
346
|
+
"You are a helpful, friendly voice assistant. Keep your responses concise and conversational.",
|
|
347
|
+
stt: config.stt,
|
|
348
|
+
tts: config.tts,
|
|
349
|
+
llm: config.llm,
|
|
350
|
+
actions: [
|
|
351
|
+
defineAction({
|
|
352
|
+
id: "respond",
|
|
353
|
+
description:
|
|
354
|
+
"Respond to the user's message helpfully and conversationally",
|
|
355
|
+
handler: async (ctx) => {
|
|
356
|
+
const response = await ctx.llm.generate(
|
|
357
|
+
`Respond to the user naturally. User said: "${ctx.conversation.currentUtterance}"`,
|
|
358
|
+
{
|
|
359
|
+
systemPrompt: ctx.conversation.systemPrompt,
|
|
360
|
+
history: ctx.memory
|
|
361
|
+
.getRecentTurns(10)
|
|
362
|
+
.map((t) => ({
|
|
363
|
+
role: t.role === "user" ? "user" as const : "assistant" as const,
|
|
364
|
+
content: t.content,
|
|
365
|
+
})),
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
return response;
|
|
369
|
+
},
|
|
370
|
+
}),
|
|
371
|
+
],
|
|
372
|
+
});
|
|
373
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ConversationTurn, Session, ActionMatch } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CallLogger — Logs structured conversation data for JEV training and analytics.
|
|
7
|
+
*
|
|
8
|
+
* Every completed call produces a log file with:
|
|
9
|
+
* - Session metadata
|
|
10
|
+
* - All conversation turns
|
|
11
|
+
* - JEV decisions (action selected, confidence, candidates)
|
|
12
|
+
* - Timing information
|
|
13
|
+
*
|
|
14
|
+
* These logs are the training data for the JEV predictor (Phase 2).
|
|
15
|
+
*/
|
|
16
|
+
export class CallLogger {
|
|
17
|
+
private readonly logDir: string;
|
|
18
|
+
private readonly level: "debug" | "info" | "warn" | "error";
|
|
19
|
+
private readonly enabled: boolean;
|
|
20
|
+
|
|
21
|
+
constructor(options?: {
|
|
22
|
+
logDir?: string;
|
|
23
|
+
level?: "debug" | "info" | "warn" | "error";
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
this.logDir = options?.logDir ?? "./call-logs";
|
|
27
|
+
this.level = options?.level ?? "info";
|
|
28
|
+
this.enabled = options?.enabled ?? true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Log a complete call session.
|
|
33
|
+
* This is called when a call ends.
|
|
34
|
+
*/
|
|
35
|
+
async logCall(data: CallLogEntry): Promise<string | null> {
|
|
36
|
+
if (!this.enabled) return null;
|
|
37
|
+
|
|
38
|
+
await mkdir(this.logDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const filename = `${data.session.id}_${Date.now()}.json`;
|
|
41
|
+
const filepath = join(this.logDir, filename);
|
|
42
|
+
|
|
43
|
+
const logData: CallLogFile = {
|
|
44
|
+
version: "1.0",
|
|
45
|
+
sessionId: data.session.id,
|
|
46
|
+
startedAt: data.session.startedAt.toISOString(),
|
|
47
|
+
endedAt: new Date().toISOString(),
|
|
48
|
+
durationMs: Date.now() - data.session.startedAt.getTime(),
|
|
49
|
+
metadata: data.session.metadata,
|
|
50
|
+
turns: data.turns,
|
|
51
|
+
decisions: data.decisions,
|
|
52
|
+
metrics: data.metrics,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await writeFile(filepath, JSON.stringify(logData, null, 2));
|
|
56
|
+
this.log("info", `Call log saved: ${filepath}`);
|
|
57
|
+
|
|
58
|
+
return filepath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Log a JEV decision for the current session.
|
|
63
|
+
* Used to build (context, action) training pairs.
|
|
64
|
+
*/
|
|
65
|
+
logDecision(decision: JEVDecisionLog): void {
|
|
66
|
+
if (!this.enabled) return;
|
|
67
|
+
this.log(
|
|
68
|
+
"debug",
|
|
69
|
+
`JEV decision: ${decision.selectedAction} (confidence: ${decision.confidence.toFixed(3)})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** General-purpose log */
|
|
74
|
+
log(
|
|
75
|
+
level: "debug" | "info" | "warn" | "error",
|
|
76
|
+
message: string,
|
|
77
|
+
data?: Record<string, unknown>,
|
|
78
|
+
): void {
|
|
79
|
+
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
80
|
+
if (levels[level] < levels[this.level]) return;
|
|
81
|
+
|
|
82
|
+
const prefix = `[Felona/${level.toUpperCase()}]`;
|
|
83
|
+
const timestamp = new Date().toISOString();
|
|
84
|
+
|
|
85
|
+
if (data) {
|
|
86
|
+
console.log(`${prefix} ${timestamp} ${message}`, data);
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`${prefix} ${timestamp} ${message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Data for a complete call log */
|
|
94
|
+
export interface CallLogEntry {
|
|
95
|
+
session: Session;
|
|
96
|
+
turns: ConversationTurn[];
|
|
97
|
+
decisions: JEVDecisionLog[];
|
|
98
|
+
metrics: CallMetrics;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A single JEV decision record */
|
|
102
|
+
export interface JEVDecisionLog {
|
|
103
|
+
/** Timestamp of the decision */
|
|
104
|
+
timestampMs: number;
|
|
105
|
+
/** The context string that was embedded */
|
|
106
|
+
contextSummary: string;
|
|
107
|
+
/** The action that was selected */
|
|
108
|
+
selectedAction: string;
|
|
109
|
+
/** Confidence score */
|
|
110
|
+
confidence: number;
|
|
111
|
+
/** Top N candidates with scores */
|
|
112
|
+
candidates: Array<{ actionId: string; score: number }>;
|
|
113
|
+
/** Time taken for the JEV decision (ms) */
|
|
114
|
+
latencyMs: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Call-level metrics */
|
|
118
|
+
export interface CallMetrics {
|
|
119
|
+
/** Total number of turns */
|
|
120
|
+
totalTurns: number;
|
|
121
|
+
/** Average JEV decision latency (ms) */
|
|
122
|
+
avgJEVLatencyMs: number;
|
|
123
|
+
/** Average STT latency (ms) */
|
|
124
|
+
avgSTTLatencyMs?: number;
|
|
125
|
+
/** Average TTS latency (ms) */
|
|
126
|
+
avgTTSLatencyMs?: number;
|
|
127
|
+
/** Number of barge-in events */
|
|
128
|
+
bargeInCount: number;
|
|
129
|
+
/** Average JEV confidence */
|
|
130
|
+
avgConfidence: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Structure of a call log file */
|
|
134
|
+
export interface CallLogFile {
|
|
135
|
+
version: string;
|
|
136
|
+
sessionId: string;
|
|
137
|
+
startedAt: string;
|
|
138
|
+
endedAt: string;
|
|
139
|
+
durationMs: number;
|
|
140
|
+
metadata: Record<string, unknown>;
|
|
141
|
+
turns: ConversationTurn[];
|
|
142
|
+
decisions: JEVDecisionLog[];
|
|
143
|
+
metrics: CallMetrics;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createCallLogger(options?: {
|
|
147
|
+
logDir?: string;
|
|
148
|
+
level?: "debug" | "info" | "warn" | "error";
|
|
149
|
+
enabled?: boolean;
|
|
150
|
+
}): CallLogger {
|
|
151
|
+
return new CallLogger(options);
|
|
152
|
+
}
|