@contractspec/integration.providers-impls 2.10.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/impls/async-event-queue.d.ts +8 -0
- package/dist/impls/async-event-queue.js +47 -0
- package/dist/impls/health/base-health-provider.d.ts +64 -13
- package/dist/impls/health/base-health-provider.js +506 -156
- package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
- package/dist/impls/health/hybrid-health-providers.js +1088 -0
- package/dist/impls/health/official-health-providers.d.ts +78 -0
- package/dist/impls/health/official-health-providers.js +968 -0
- package/dist/impls/health/provider-normalizers.d.ts +28 -0
- package/dist/impls/health/provider-normalizers.js +287 -0
- package/dist/impls/health/providers.d.ts +2 -39
- package/dist/impls/health/providers.js +895 -184
- package/dist/impls/health-provider-factory.js +1009 -196
- package/dist/impls/index.d.ts +6 -0
- package/dist/impls/index.js +1950 -278
- package/dist/impls/messaging-github.d.ts +17 -0
- package/dist/impls/messaging-github.js +110 -0
- package/dist/impls/messaging-slack.d.ts +14 -0
- package/dist/impls/messaging-slack.js +80 -0
- package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
- package/dist/impls/messaging-whatsapp-meta.js +52 -0
- package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
- package/dist/impls/messaging-whatsapp-twilio.js +82 -0
- package/dist/impls/mistral-conversational.d.ts +23 -0
- package/dist/impls/mistral-conversational.js +476 -0
- package/dist/impls/mistral-conversational.session.d.ts +32 -0
- package/dist/impls/mistral-conversational.session.js +206 -0
- package/dist/impls/mistral-stt.d.ts +17 -0
- package/dist/impls/mistral-stt.js +167 -0
- package/dist/impls/provider-factory.d.ts +5 -1
- package/dist/impls/provider-factory.js +1943 -277
- package/dist/impls/stripe-payments.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1953 -278
- package/dist/messaging.d.ts +1 -0
- package/dist/messaging.js +3 -0
- package/dist/node/impls/async-event-queue.js +46 -0
- package/dist/node/impls/health/base-health-provider.js +506 -156
- package/dist/node/impls/health/hybrid-health-providers.js +1087 -0
- package/dist/node/impls/health/official-health-providers.js +967 -0
- package/dist/node/impls/health/provider-normalizers.js +286 -0
- package/dist/node/impls/health/providers.js +895 -184
- package/dist/node/impls/health-provider-factory.js +1009 -196
- package/dist/node/impls/index.js +1950 -278
- package/dist/node/impls/messaging-github.js +109 -0
- package/dist/node/impls/messaging-slack.js +79 -0
- package/dist/node/impls/messaging-whatsapp-meta.js +51 -0
- package/dist/node/impls/messaging-whatsapp-twilio.js +81 -0
- package/dist/node/impls/mistral-conversational.js +475 -0
- package/dist/node/impls/mistral-conversational.session.js +205 -0
- package/dist/node/impls/mistral-stt.js +166 -0
- package/dist/node/impls/provider-factory.js +1943 -277
- package/dist/node/impls/stripe-payments.js +1 -1
- package/dist/node/index.js +1953 -278
- package/dist/node/messaging.js +2 -0
- package/package.json +156 -12
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// src/impls/async-event-queue.ts
|
|
2
|
+
class AsyncEventQueue {
|
|
3
|
+
values = [];
|
|
4
|
+
waiters = [];
|
|
5
|
+
done = false;
|
|
6
|
+
push(value) {
|
|
7
|
+
if (this.done) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const waiter = this.waiters.shift();
|
|
11
|
+
if (waiter) {
|
|
12
|
+
waiter({ value, done: false });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.values.push(value);
|
|
16
|
+
}
|
|
17
|
+
close() {
|
|
18
|
+
if (this.done) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.done = true;
|
|
22
|
+
for (const waiter of this.waiters) {
|
|
23
|
+
waiter({ value: undefined, done: true });
|
|
24
|
+
}
|
|
25
|
+
this.waiters.length = 0;
|
|
26
|
+
}
|
|
27
|
+
[Symbol.asyncIterator]() {
|
|
28
|
+
return {
|
|
29
|
+
next: async () => {
|
|
30
|
+
const value = this.values.shift();
|
|
31
|
+
if (value != null) {
|
|
32
|
+
return { value, done: false };
|
|
33
|
+
}
|
|
34
|
+
if (this.done) {
|
|
35
|
+
return { value: undefined, done: true };
|
|
36
|
+
}
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
this.waiters.push(resolve);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/impls/mistral-stt.ts
|
|
46
|
+
var DEFAULT_BASE_URL = "https://api.mistral.ai/v1";
|
|
47
|
+
var DEFAULT_MODEL = "voxtral-mini-latest";
|
|
48
|
+
var AUDIO_MIME_BY_FORMAT = {
|
|
49
|
+
mp3: "audio/mpeg",
|
|
50
|
+
wav: "audio/wav",
|
|
51
|
+
ogg: "audio/ogg",
|
|
52
|
+
pcm: "audio/pcm",
|
|
53
|
+
opus: "audio/opus"
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
class MistralSttProvider {
|
|
57
|
+
apiKey;
|
|
58
|
+
defaultModel;
|
|
59
|
+
defaultLanguage;
|
|
60
|
+
baseUrl;
|
|
61
|
+
fetchImpl;
|
|
62
|
+
constructor(options) {
|
|
63
|
+
if (!options.apiKey) {
|
|
64
|
+
throw new Error("MistralSttProvider requires an apiKey");
|
|
65
|
+
}
|
|
66
|
+
this.apiKey = options.apiKey;
|
|
67
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
|
|
68
|
+
this.defaultLanguage = options.defaultLanguage;
|
|
69
|
+
this.baseUrl = normalizeBaseUrl(options.serverURL ?? DEFAULT_BASE_URL);
|
|
70
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
71
|
+
}
|
|
72
|
+
async transcribe(input) {
|
|
73
|
+
const formData = new FormData;
|
|
74
|
+
const model = input.model ?? this.defaultModel;
|
|
75
|
+
const mimeType = AUDIO_MIME_BY_FORMAT[input.audio.format] ?? "audio/wav";
|
|
76
|
+
const fileName = `audio.${input.audio.format}`;
|
|
77
|
+
const audioBytes = new Uint8Array(input.audio.data);
|
|
78
|
+
const blob = new Blob([audioBytes], { type: mimeType });
|
|
79
|
+
formData.append("file", blob, fileName);
|
|
80
|
+
formData.append("model", model);
|
|
81
|
+
formData.append("response_format", "verbose_json");
|
|
82
|
+
const language = input.language ?? this.defaultLanguage;
|
|
83
|
+
if (language) {
|
|
84
|
+
formData.append("language", language);
|
|
85
|
+
}
|
|
86
|
+
const response = await this.fetchImpl(`${this.baseUrl}/audio/transcriptions`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
90
|
+
},
|
|
91
|
+
body: formData
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const body = await response.text();
|
|
95
|
+
throw new Error(`Mistral transcription request failed (${response.status}): ${body}`);
|
|
96
|
+
}
|
|
97
|
+
const payload = await response.json();
|
|
98
|
+
return toTranscriptionResult(payload, input);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function toTranscriptionResult(payload, input) {
|
|
102
|
+
const record = asRecord(payload);
|
|
103
|
+
const text = readString(record, "text") ?? "";
|
|
104
|
+
const language = readString(record, "language") ?? input.language ?? "unknown";
|
|
105
|
+
const segments = parseSegments(record);
|
|
106
|
+
if (segments.length === 0 && text.length > 0) {
|
|
107
|
+
segments.push({
|
|
108
|
+
text,
|
|
109
|
+
startMs: 0,
|
|
110
|
+
endMs: input.audio.durationMs ?? 0
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const durationMs = input.audio.durationMs ?? segments.reduce((max, segment) => Math.max(max, segment.endMs), 0);
|
|
114
|
+
const topLevelWords = parseWordTimings(record.words);
|
|
115
|
+
const flattenedWords = segments.flatMap((segment) => segment.wordTimings ?? []);
|
|
116
|
+
const wordTimings = topLevelWords.length > 0 ? topLevelWords : flattenedWords.length > 0 ? flattenedWords : undefined;
|
|
117
|
+
const speakers = dedupeSpeakers(segments);
|
|
118
|
+
return {
|
|
119
|
+
text,
|
|
120
|
+
segments,
|
|
121
|
+
language,
|
|
122
|
+
durationMs,
|
|
123
|
+
speakers: speakers.length > 0 ? speakers : undefined,
|
|
124
|
+
wordTimings
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function parseSegments(record) {
|
|
128
|
+
if (!Array.isArray(record.segments)) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const parsed = [];
|
|
132
|
+
for (const entry of record.segments) {
|
|
133
|
+
const segmentRecord = asRecord(entry);
|
|
134
|
+
const text = readString(segmentRecord, "text");
|
|
135
|
+
if (!text) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const startSeconds = readNumber(segmentRecord, "start") ?? 0;
|
|
139
|
+
const endSeconds = readNumber(segmentRecord, "end") ?? startSeconds;
|
|
140
|
+
parsed.push({
|
|
141
|
+
text,
|
|
142
|
+
startMs: secondsToMs(startSeconds),
|
|
143
|
+
endMs: secondsToMs(endSeconds),
|
|
144
|
+
speakerId: readString(segmentRecord, "speaker") ?? undefined,
|
|
145
|
+
confidence: readNumber(segmentRecord, "confidence"),
|
|
146
|
+
wordTimings: parseWordTimings(segmentRecord.words)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
function parseWordTimings(value) {
|
|
152
|
+
if (!Array.isArray(value)) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const words = [];
|
|
156
|
+
for (const entry of value) {
|
|
157
|
+
const wordRecord = asRecord(entry);
|
|
158
|
+
const word = readString(wordRecord, "word");
|
|
159
|
+
const startSeconds = readNumber(wordRecord, "start");
|
|
160
|
+
const endSeconds = readNumber(wordRecord, "end");
|
|
161
|
+
if (!word || startSeconds == null || endSeconds == null) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
words.push({
|
|
165
|
+
word,
|
|
166
|
+
startMs: secondsToMs(startSeconds),
|
|
167
|
+
endMs: secondsToMs(endSeconds),
|
|
168
|
+
confidence: readNumber(wordRecord, "confidence")
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return words;
|
|
172
|
+
}
|
|
173
|
+
function dedupeSpeakers(segments) {
|
|
174
|
+
const seen = new Set;
|
|
175
|
+
const speakers = [];
|
|
176
|
+
for (const segment of segments) {
|
|
177
|
+
if (!segment.speakerId || seen.has(segment.speakerId)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
seen.add(segment.speakerId);
|
|
181
|
+
speakers.push({
|
|
182
|
+
id: segment.speakerId,
|
|
183
|
+
name: segment.speakerName
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return speakers;
|
|
187
|
+
}
|
|
188
|
+
function normalizeBaseUrl(url) {
|
|
189
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
190
|
+
}
|
|
191
|
+
function asRecord(value) {
|
|
192
|
+
if (value && typeof value === "object") {
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
function readString(record, key) {
|
|
198
|
+
const value = record[key];
|
|
199
|
+
return typeof value === "string" ? value : undefined;
|
|
200
|
+
}
|
|
201
|
+
function readNumber(record, key) {
|
|
202
|
+
const value = record[key];
|
|
203
|
+
return typeof value === "number" ? value : undefined;
|
|
204
|
+
}
|
|
205
|
+
function secondsToMs(value) {
|
|
206
|
+
return Math.round(value * 1000);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/impls/mistral-conversational.session.ts
|
|
210
|
+
class MistralConversationSession {
|
|
211
|
+
events;
|
|
212
|
+
queue = new AsyncEventQueue;
|
|
213
|
+
turns = [];
|
|
214
|
+
history = [];
|
|
215
|
+
sessionId = crypto.randomUUID();
|
|
216
|
+
startedAt = Date.now();
|
|
217
|
+
sessionConfig;
|
|
218
|
+
defaultModel;
|
|
219
|
+
complete;
|
|
220
|
+
sttProvider;
|
|
221
|
+
pending = Promise.resolve();
|
|
222
|
+
closed = false;
|
|
223
|
+
closedSummary;
|
|
224
|
+
constructor(options) {
|
|
225
|
+
this.sessionConfig = options.sessionConfig;
|
|
226
|
+
this.defaultModel = options.defaultModel;
|
|
227
|
+
this.complete = options.complete;
|
|
228
|
+
this.sttProvider = options.sttProvider;
|
|
229
|
+
this.events = this.queue;
|
|
230
|
+
this.queue.push({
|
|
231
|
+
type: "session_started",
|
|
232
|
+
sessionId: this.sessionId
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
sendAudio(chunk) {
|
|
236
|
+
if (this.closed) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.pending = this.pending.then(async () => {
|
|
240
|
+
const transcription = await this.sttProvider.transcribe({
|
|
241
|
+
audio: {
|
|
242
|
+
data: chunk,
|
|
243
|
+
format: this.sessionConfig.inputFormat ?? "pcm",
|
|
244
|
+
sampleRateHz: 16000
|
|
245
|
+
},
|
|
246
|
+
language: this.sessionConfig.language
|
|
247
|
+
});
|
|
248
|
+
const transcriptText = transcription.text.trim();
|
|
249
|
+
if (transcriptText.length > 0) {
|
|
250
|
+
await this.handleUserText(transcriptText);
|
|
251
|
+
}
|
|
252
|
+
}).catch((error) => {
|
|
253
|
+
this.emitError(error);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
sendText(text) {
|
|
257
|
+
if (this.closed) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const normalized = text.trim();
|
|
261
|
+
if (normalized.length === 0) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.pending = this.pending.then(() => this.handleUserText(normalized)).catch((error) => {
|
|
265
|
+
this.emitError(error);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
interrupt() {
|
|
269
|
+
if (this.closed) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.queue.push({
|
|
273
|
+
type: "error",
|
|
274
|
+
error: new Error("Interrupt is not supported for non-streaming sessions.")
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async close() {
|
|
278
|
+
if (this.closedSummary) {
|
|
279
|
+
return this.closedSummary;
|
|
280
|
+
}
|
|
281
|
+
this.closed = true;
|
|
282
|
+
await this.pending;
|
|
283
|
+
const durationMs = Date.now() - this.startedAt;
|
|
284
|
+
const summary = {
|
|
285
|
+
sessionId: this.sessionId,
|
|
286
|
+
durationMs,
|
|
287
|
+
turns: this.turns.map((turn) => ({
|
|
288
|
+
role: turn.role === "assistant" ? "agent" : turn.role,
|
|
289
|
+
text: turn.text,
|
|
290
|
+
startMs: turn.startMs,
|
|
291
|
+
endMs: turn.endMs
|
|
292
|
+
})),
|
|
293
|
+
transcript: this.turns.map((turn) => `${turn.role}: ${turn.text}`).join(`
|
|
294
|
+
`)
|
|
295
|
+
};
|
|
296
|
+
this.closedSummary = summary;
|
|
297
|
+
this.queue.push({
|
|
298
|
+
type: "session_ended",
|
|
299
|
+
reason: "closed_by_client",
|
|
300
|
+
durationMs
|
|
301
|
+
});
|
|
302
|
+
this.queue.close();
|
|
303
|
+
return summary;
|
|
304
|
+
}
|
|
305
|
+
async handleUserText(text) {
|
|
306
|
+
if (this.closed) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const userStart = Date.now();
|
|
310
|
+
this.queue.push({ type: "user_speech_started" });
|
|
311
|
+
this.queue.push({ type: "user_speech_ended", transcript: text });
|
|
312
|
+
this.queue.push({
|
|
313
|
+
type: "transcript",
|
|
314
|
+
role: "user",
|
|
315
|
+
text,
|
|
316
|
+
timestamp: userStart
|
|
317
|
+
});
|
|
318
|
+
this.turns.push({
|
|
319
|
+
role: "user",
|
|
320
|
+
text,
|
|
321
|
+
startMs: userStart,
|
|
322
|
+
endMs: Date.now()
|
|
323
|
+
});
|
|
324
|
+
this.history.push({ role: "user", content: text });
|
|
325
|
+
const assistantStart = Date.now();
|
|
326
|
+
const assistantText = await this.complete(this.history, {
|
|
327
|
+
...this.sessionConfig,
|
|
328
|
+
llmModel: this.sessionConfig.llmModel ?? this.defaultModel
|
|
329
|
+
});
|
|
330
|
+
if (this.closed) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const normalizedAssistantText = assistantText.trim();
|
|
334
|
+
const finalAssistantText = normalizedAssistantText.length > 0 ? normalizedAssistantText : "I was unable to produce a response.";
|
|
335
|
+
this.queue.push({
|
|
336
|
+
type: "agent_speech_started",
|
|
337
|
+
text: finalAssistantText
|
|
338
|
+
});
|
|
339
|
+
this.queue.push({
|
|
340
|
+
type: "transcript",
|
|
341
|
+
role: "agent",
|
|
342
|
+
text: finalAssistantText,
|
|
343
|
+
timestamp: assistantStart
|
|
344
|
+
});
|
|
345
|
+
this.queue.push({ type: "agent_speech_ended" });
|
|
346
|
+
this.turns.push({
|
|
347
|
+
role: "assistant",
|
|
348
|
+
text: finalAssistantText,
|
|
349
|
+
startMs: assistantStart,
|
|
350
|
+
endMs: Date.now()
|
|
351
|
+
});
|
|
352
|
+
this.history.push({ role: "assistant", content: finalAssistantText });
|
|
353
|
+
}
|
|
354
|
+
emitError(error) {
|
|
355
|
+
if (this.closed) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.queue.push({ type: "error", error: toError(error) });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function toError(error) {
|
|
362
|
+
if (error instanceof Error) {
|
|
363
|
+
return error;
|
|
364
|
+
}
|
|
365
|
+
return new Error(String(error));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/impls/mistral-conversational.ts
|
|
369
|
+
var DEFAULT_BASE_URL2 = "https://api.mistral.ai/v1";
|
|
370
|
+
var DEFAULT_MODEL2 = "mistral-small-latest";
|
|
371
|
+
var DEFAULT_VOICE = "default";
|
|
372
|
+
|
|
373
|
+
class MistralConversationalProvider {
|
|
374
|
+
apiKey;
|
|
375
|
+
defaultModel;
|
|
376
|
+
defaultVoiceId;
|
|
377
|
+
baseUrl;
|
|
378
|
+
fetchImpl;
|
|
379
|
+
sttProvider;
|
|
380
|
+
constructor(options) {
|
|
381
|
+
if (!options.apiKey) {
|
|
382
|
+
throw new Error("MistralConversationalProvider requires an apiKey");
|
|
383
|
+
}
|
|
384
|
+
this.apiKey = options.apiKey;
|
|
385
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
|
|
386
|
+
this.defaultVoiceId = options.defaultVoiceId ?? DEFAULT_VOICE;
|
|
387
|
+
this.baseUrl = normalizeBaseUrl2(options.serverURL ?? DEFAULT_BASE_URL2);
|
|
388
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
389
|
+
this.sttProvider = options.sttProvider ?? new MistralSttProvider({
|
|
390
|
+
apiKey: options.apiKey,
|
|
391
|
+
defaultModel: options.sttOptions?.defaultModel,
|
|
392
|
+
defaultLanguage: options.sttOptions?.defaultLanguage,
|
|
393
|
+
serverURL: options.sttOptions?.serverURL ?? options.serverURL,
|
|
394
|
+
fetchImpl: this.fetchImpl
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async startSession(config) {
|
|
398
|
+
return new MistralConversationSession({
|
|
399
|
+
sessionConfig: {
|
|
400
|
+
...config,
|
|
401
|
+
voiceId: config.voiceId || this.defaultVoiceId
|
|
402
|
+
},
|
|
403
|
+
defaultModel: this.defaultModel,
|
|
404
|
+
complete: (history, sessionConfig) => this.completeConversation(history, sessionConfig),
|
|
405
|
+
sttProvider: this.sttProvider
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
async listVoices() {
|
|
409
|
+
return [
|
|
410
|
+
{
|
|
411
|
+
id: this.defaultVoiceId,
|
|
412
|
+
name: "Mistral Default Voice",
|
|
413
|
+
description: "Default conversational voice profile.",
|
|
414
|
+
capabilities: ["conversational"]
|
|
415
|
+
}
|
|
416
|
+
];
|
|
417
|
+
}
|
|
418
|
+
async completeConversation(history, sessionConfig) {
|
|
419
|
+
const model = sessionConfig.llmModel ?? this.defaultModel;
|
|
420
|
+
const messages = [];
|
|
421
|
+
if (sessionConfig.systemPrompt) {
|
|
422
|
+
messages.push({ role: "system", content: sessionConfig.systemPrompt });
|
|
423
|
+
}
|
|
424
|
+
for (const item of history) {
|
|
425
|
+
messages.push({ role: item.role, content: item.content });
|
|
426
|
+
}
|
|
427
|
+
const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
|
|
428
|
+
method: "POST",
|
|
429
|
+
headers: {
|
|
430
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
431
|
+
"Content-Type": "application/json"
|
|
432
|
+
},
|
|
433
|
+
body: JSON.stringify({
|
|
434
|
+
model,
|
|
435
|
+
messages
|
|
436
|
+
})
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
const body = await response.text();
|
|
440
|
+
throw new Error(`Mistral conversational request failed (${response.status}): ${body}`);
|
|
441
|
+
}
|
|
442
|
+
const payload = await response.json();
|
|
443
|
+
return readAssistantText(payload);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function normalizeBaseUrl2(url) {
|
|
447
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
448
|
+
}
|
|
449
|
+
function readAssistantText(payload) {
|
|
450
|
+
const record = asRecord2(payload);
|
|
451
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
452
|
+
const firstChoice = asRecord2(choices[0]);
|
|
453
|
+
const message = asRecord2(firstChoice.message);
|
|
454
|
+
if (typeof message.content === "string") {
|
|
455
|
+
return message.content;
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(message.content)) {
|
|
458
|
+
const textParts = message.content.map((part) => {
|
|
459
|
+
const entry = asRecord2(part);
|
|
460
|
+
const text = entry.text;
|
|
461
|
+
return typeof text === "string" ? text : "";
|
|
462
|
+
}).filter((text) => text.length > 0);
|
|
463
|
+
return textParts.join("");
|
|
464
|
+
}
|
|
465
|
+
return "";
|
|
466
|
+
}
|
|
467
|
+
function asRecord2(value) {
|
|
468
|
+
if (value && typeof value === "object") {
|
|
469
|
+
return value;
|
|
470
|
+
}
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
export {
|
|
474
|
+
MistralConversationalProvider
|
|
475
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/impls/async-event-queue.ts
|
|
2
|
+
class AsyncEventQueue {
|
|
3
|
+
values = [];
|
|
4
|
+
waiters = [];
|
|
5
|
+
done = false;
|
|
6
|
+
push(value) {
|
|
7
|
+
if (this.done) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const waiter = this.waiters.shift();
|
|
11
|
+
if (waiter) {
|
|
12
|
+
waiter({ value, done: false });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.values.push(value);
|
|
16
|
+
}
|
|
17
|
+
close() {
|
|
18
|
+
if (this.done) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.done = true;
|
|
22
|
+
for (const waiter of this.waiters) {
|
|
23
|
+
waiter({ value: undefined, done: true });
|
|
24
|
+
}
|
|
25
|
+
this.waiters.length = 0;
|
|
26
|
+
}
|
|
27
|
+
[Symbol.asyncIterator]() {
|
|
28
|
+
return {
|
|
29
|
+
next: async () => {
|
|
30
|
+
const value = this.values.shift();
|
|
31
|
+
if (value != null) {
|
|
32
|
+
return { value, done: false };
|
|
33
|
+
}
|
|
34
|
+
if (this.done) {
|
|
35
|
+
return { value: undefined, done: true };
|
|
36
|
+
}
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
this.waiters.push(resolve);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/impls/mistral-conversational.session.ts
|
|
46
|
+
class MistralConversationSession {
|
|
47
|
+
events;
|
|
48
|
+
queue = new AsyncEventQueue;
|
|
49
|
+
turns = [];
|
|
50
|
+
history = [];
|
|
51
|
+
sessionId = crypto.randomUUID();
|
|
52
|
+
startedAt = Date.now();
|
|
53
|
+
sessionConfig;
|
|
54
|
+
defaultModel;
|
|
55
|
+
complete;
|
|
56
|
+
sttProvider;
|
|
57
|
+
pending = Promise.resolve();
|
|
58
|
+
closed = false;
|
|
59
|
+
closedSummary;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.sessionConfig = options.sessionConfig;
|
|
62
|
+
this.defaultModel = options.defaultModel;
|
|
63
|
+
this.complete = options.complete;
|
|
64
|
+
this.sttProvider = options.sttProvider;
|
|
65
|
+
this.events = this.queue;
|
|
66
|
+
this.queue.push({
|
|
67
|
+
type: "session_started",
|
|
68
|
+
sessionId: this.sessionId
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
sendAudio(chunk) {
|
|
72
|
+
if (this.closed) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.pending = this.pending.then(async () => {
|
|
76
|
+
const transcription = await this.sttProvider.transcribe({
|
|
77
|
+
audio: {
|
|
78
|
+
data: chunk,
|
|
79
|
+
format: this.sessionConfig.inputFormat ?? "pcm",
|
|
80
|
+
sampleRateHz: 16000
|
|
81
|
+
},
|
|
82
|
+
language: this.sessionConfig.language
|
|
83
|
+
});
|
|
84
|
+
const transcriptText = transcription.text.trim();
|
|
85
|
+
if (transcriptText.length > 0) {
|
|
86
|
+
await this.handleUserText(transcriptText);
|
|
87
|
+
}
|
|
88
|
+
}).catch((error) => {
|
|
89
|
+
this.emitError(error);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
sendText(text) {
|
|
93
|
+
if (this.closed) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const normalized = text.trim();
|
|
97
|
+
if (normalized.length === 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.pending = this.pending.then(() => this.handleUserText(normalized)).catch((error) => {
|
|
101
|
+
this.emitError(error);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
interrupt() {
|
|
105
|
+
if (this.closed) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.queue.push({
|
|
109
|
+
type: "error",
|
|
110
|
+
error: new Error("Interrupt is not supported for non-streaming sessions.")
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async close() {
|
|
114
|
+
if (this.closedSummary) {
|
|
115
|
+
return this.closedSummary;
|
|
116
|
+
}
|
|
117
|
+
this.closed = true;
|
|
118
|
+
await this.pending;
|
|
119
|
+
const durationMs = Date.now() - this.startedAt;
|
|
120
|
+
const summary = {
|
|
121
|
+
sessionId: this.sessionId,
|
|
122
|
+
durationMs,
|
|
123
|
+
turns: this.turns.map((turn) => ({
|
|
124
|
+
role: turn.role === "assistant" ? "agent" : turn.role,
|
|
125
|
+
text: turn.text,
|
|
126
|
+
startMs: turn.startMs,
|
|
127
|
+
endMs: turn.endMs
|
|
128
|
+
})),
|
|
129
|
+
transcript: this.turns.map((turn) => `${turn.role}: ${turn.text}`).join(`
|
|
130
|
+
`)
|
|
131
|
+
};
|
|
132
|
+
this.closedSummary = summary;
|
|
133
|
+
this.queue.push({
|
|
134
|
+
type: "session_ended",
|
|
135
|
+
reason: "closed_by_client",
|
|
136
|
+
durationMs
|
|
137
|
+
});
|
|
138
|
+
this.queue.close();
|
|
139
|
+
return summary;
|
|
140
|
+
}
|
|
141
|
+
async handleUserText(text) {
|
|
142
|
+
if (this.closed) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const userStart = Date.now();
|
|
146
|
+
this.queue.push({ type: "user_speech_started" });
|
|
147
|
+
this.queue.push({ type: "user_speech_ended", transcript: text });
|
|
148
|
+
this.queue.push({
|
|
149
|
+
type: "transcript",
|
|
150
|
+
role: "user",
|
|
151
|
+
text,
|
|
152
|
+
timestamp: userStart
|
|
153
|
+
});
|
|
154
|
+
this.turns.push({
|
|
155
|
+
role: "user",
|
|
156
|
+
text,
|
|
157
|
+
startMs: userStart,
|
|
158
|
+
endMs: Date.now()
|
|
159
|
+
});
|
|
160
|
+
this.history.push({ role: "user", content: text });
|
|
161
|
+
const assistantStart = Date.now();
|
|
162
|
+
const assistantText = await this.complete(this.history, {
|
|
163
|
+
...this.sessionConfig,
|
|
164
|
+
llmModel: this.sessionConfig.llmModel ?? this.defaultModel
|
|
165
|
+
});
|
|
166
|
+
if (this.closed) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const normalizedAssistantText = assistantText.trim();
|
|
170
|
+
const finalAssistantText = normalizedAssistantText.length > 0 ? normalizedAssistantText : "I was unable to produce a response.";
|
|
171
|
+
this.queue.push({
|
|
172
|
+
type: "agent_speech_started",
|
|
173
|
+
text: finalAssistantText
|
|
174
|
+
});
|
|
175
|
+
this.queue.push({
|
|
176
|
+
type: "transcript",
|
|
177
|
+
role: "agent",
|
|
178
|
+
text: finalAssistantText,
|
|
179
|
+
timestamp: assistantStart
|
|
180
|
+
});
|
|
181
|
+
this.queue.push({ type: "agent_speech_ended" });
|
|
182
|
+
this.turns.push({
|
|
183
|
+
role: "assistant",
|
|
184
|
+
text: finalAssistantText,
|
|
185
|
+
startMs: assistantStart,
|
|
186
|
+
endMs: Date.now()
|
|
187
|
+
});
|
|
188
|
+
this.history.push({ role: "assistant", content: finalAssistantText });
|
|
189
|
+
}
|
|
190
|
+
emitError(error) {
|
|
191
|
+
if (this.closed) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.queue.push({ type: "error", error: toError(error) });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function toError(error) {
|
|
198
|
+
if (error instanceof Error) {
|
|
199
|
+
return error;
|
|
200
|
+
}
|
|
201
|
+
return new Error(String(error));
|
|
202
|
+
}
|
|
203
|
+
export {
|
|
204
|
+
MistralConversationSession
|
|
205
|
+
};
|