nexus-ai-pro 0.6.0 → 0.7.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/EXPLANATION.md +13 -1
- package/NEXUS.md +21 -2
- package/README.md +139 -2
- package/dist/core/nexus.d.ts +16 -1
- package/dist/core/nexus.js +31 -0
- package/dist/evals/index.d.ts +3 -0
- package/dist/evals/index.js +3 -0
- package/dist/evals/judge.d.ts +50 -0
- package/dist/evals/judge.js +154 -0
- package/dist/evals/runner.d.ts +13 -1
- package/dist/evals/runner.js +18 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/telephony/errors.d.ts +8 -0
- package/dist/telephony/errors.js +16 -0
- package/dist/telephony/index.d.ts +4 -0
- package/dist/telephony/index.js +3 -0
- package/dist/telephony/manager.d.ts +18 -0
- package/dist/telephony/manager.js +78 -0
- package/dist/telephony/providers/twilio.d.ts +24 -0
- package/dist/telephony/providers/twilio.js +274 -0
- package/dist/telephony/twiml.d.ts +2 -0
- package/dist/telephony/twiml.js +87 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/telephony.d.ts +171 -0
- package/dist/types/telephony.js +1 -0
- package/dist/types/voice.d.ts +71 -1
- package/dist/voice/index.d.ts +2 -1
- package/dist/voice/index.js +1 -0
- package/dist/voice/manager.d.ts +3 -1
- package/dist/voice/manager.js +4 -0
- package/dist/voice/session.d.ts +33 -0
- package/dist/voice/session.js +234 -0
- package/package.json +19 -3
package/EXPLANATION.md
CHANGED
|
@@ -269,6 +269,7 @@ Evals:
|
|
|
269
269
|
- operational metrics
|
|
270
270
|
- RAG metrics
|
|
271
271
|
- safety heuristics
|
|
272
|
+
- optional LLM-as-judge scoring with rubrics and thresholds
|
|
272
273
|
|
|
273
274
|
## Voice Integrations
|
|
274
275
|
|
|
@@ -285,6 +286,7 @@ The basic operations are:
|
|
|
285
286
|
- `ai.transcribe(...)` for speech-to-text
|
|
286
287
|
- `ai.speak(...)` for text-to-speech
|
|
287
288
|
- `ai.voice(...)` for the full voice pipeline
|
|
289
|
+
- `ai.createVoiceSession(...)` for multi-turn call sessions with tools
|
|
288
290
|
- `registerVoiceProvider(...)` for private, local, or hosted services
|
|
289
291
|
|
|
290
292
|
The full voice turn is:
|
|
@@ -299,7 +301,17 @@ audio/transcript
|
|
|
299
301
|
|
|
300
302
|
Because the completion step calls `ai.complete(...)`, it reuses routing, security, context-window compaction, token optimization, cache, retries, metrics, and tracing.
|
|
301
303
|
|
|
302
|
-
|
|
304
|
+
`VoiceSession` adds state across caller turns. It accepts transcript or audio, builds prompts from any combination of `prompt`, `instructions`, and conditional `taskPrompts`, executes registered tools when the model requests app data, and can synthesize the final answer.
|
|
305
|
+
|
|
306
|
+
Low-level realtime transport is still future work. That should be added as a bridge from telephony/WebSocket/SIP media events into `VoiceSession` instead of forcing socket state into normal completion streams.
|
|
307
|
+
|
|
308
|
+
Phone-number telephony is a separate optional layer:
|
|
309
|
+
|
|
310
|
+
- `nexus-ai-pro/telephony` provides provider-neutral call, webhook, and media-stream types.
|
|
311
|
+
- `nexus-ai-pro/telephony/twilio` provides a Twilio adapter without adding the Twilio SDK as a required dependency.
|
|
312
|
+
- `NexusAI` exposes helper methods for outbound calls, TwiML webhook responses, webhook validation, and media event normalization.
|
|
313
|
+
|
|
314
|
+
This keeps a small transcription/TTS app small, while larger phone-agent apps can add Twilio or another carrier as needed.
|
|
303
315
|
|
|
304
316
|
## Choosing a Setup
|
|
305
317
|
|
package/NEXUS.md
CHANGED
|
@@ -83,6 +83,7 @@ Supporting modules:
|
|
|
83
83
|
- Chain of Verification
|
|
84
84
|
- self-consistency sampling
|
|
85
85
|
- eval runner and metrics
|
|
86
|
+
- optional LLM-as-judge eval scoring
|
|
86
87
|
- workflow templates
|
|
87
88
|
- web/search connector tools
|
|
88
89
|
- provider conformance fixtures
|
|
@@ -95,6 +96,8 @@ Voice is implemented as an optional layer. Import size is controlled through sub
|
|
|
95
96
|
|
|
96
97
|
- `nexus-ai-pro/voice` for provider-neutral types, errors, and `VoiceManager`
|
|
97
98
|
- `nexus-ai-pro/voice/openai` for the OpenAI voice adapter
|
|
99
|
+
- `nexus-ai-pro/telephony` for provider-neutral phone-call types and `TelephonyManager`
|
|
100
|
+
- `nexus-ai-pro/telephony/twilio` for the Twilio phone-number adapter
|
|
98
101
|
|
|
99
102
|
Current support:
|
|
100
103
|
|
|
@@ -102,21 +105,30 @@ Current support:
|
|
|
102
105
|
- `ai.speak(...)`
|
|
103
106
|
- `ai.voice(...)`
|
|
104
107
|
- `ai.voiceTurn(...)`
|
|
108
|
+
- `ai.createVoiceSession(...)`
|
|
105
109
|
- `registerVoiceProvider(...)`
|
|
106
110
|
- configurable `voice.providers`
|
|
107
111
|
- custom voice providers
|
|
108
112
|
- OpenAI voice provider adapter
|
|
109
113
|
- full completion pipeline reuse after transcription
|
|
114
|
+
- stateful `VoiceSession` turns with prompts, task prompts, tool calls, history, and optional speech
|
|
115
|
+
- optional telephony providers for calls, webhook responses, webhook validation, and media-stream event parsing
|
|
110
116
|
|
|
111
117
|
Useful test cases:
|
|
112
118
|
|
|
113
119
|
- registered voice provider can transcribe
|
|
114
120
|
- registered voice provider can synthesize speech
|
|
115
121
|
- `ai.voice(...)` transcribes, appends transcript, calls `ai.complete(...)`, and optionally speaks
|
|
122
|
+
- `VoiceSession` can accept transcript or audio, select matching task prompts, execute tools, and speak
|
|
116
123
|
- `nexus-ai-pro/voice` imports without provider adapters
|
|
117
124
|
- `nexus-ai-pro/voice/openai` imports separately
|
|
125
|
+
- `nexus-ai-pro/voice/session` imports separately
|
|
118
126
|
- type consumer can configure `VoiceConfig`
|
|
119
127
|
- missing voice provider errors clearly
|
|
128
|
+
- registered telephony provider can create calls
|
|
129
|
+
- Twilio adapter can create inline-TwiML/media-stream calls
|
|
130
|
+
- Twilio webhook signatures can be validated
|
|
131
|
+
- Twilio media-stream messages can be normalized
|
|
120
132
|
|
|
121
133
|
Example:
|
|
122
134
|
|
|
@@ -144,9 +156,16 @@ const result = await ai.voice({
|
|
|
144
156
|
|
|
145
157
|
Still future work:
|
|
146
158
|
|
|
147
|
-
- realtime `VoiceSession`
|
|
148
159
|
- audio chunks in `NexusStream`
|
|
149
|
-
- WebRTC/
|
|
160
|
+
- WebRTC/SIP session helpers
|
|
161
|
+
- fully managed Twilio/OpenAI realtime bridge
|
|
162
|
+
|
|
163
|
+
Telephony is intentionally separate from voice:
|
|
164
|
+
|
|
165
|
+
- `voice` handles file/buffer/stream audio turns.
|
|
166
|
+
- `VoiceSession` handles multi-turn call state and app/tool requests during a conversation.
|
|
167
|
+
- `telephony` handles phone numbers, TwiML, provider webhooks, and media-stream protocol messages.
|
|
168
|
+
- apps can use one, both, or neither.
|
|
150
169
|
|
|
151
170
|
## Context Window Testing
|
|
152
171
|
|
package/README.md
CHANGED
|
@@ -343,6 +343,62 @@ console.log(result.response.content);
|
|
|
343
343
|
console.log(result.speech?.audio.data);
|
|
344
344
|
```
|
|
345
345
|
|
|
346
|
+
### Voice Session With App Requests
|
|
347
|
+
|
|
348
|
+
Use `VoiceSession` when a call has multiple turns, should keep history, and may need to call your app while talking.
|
|
349
|
+
|
|
350
|
+
The prompt setup is flexible:
|
|
351
|
+
|
|
352
|
+
- `prompt` describes the assistant or business role.
|
|
353
|
+
- `instructions` describe behavior.
|
|
354
|
+
- `taskPrompts` add conditional instructions when the caller asks about something specific.
|
|
355
|
+
- `tools` let the model call your app, such as booking, CRM, billing, or inventory APIs.
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { tool } from 'nexus-ai-pro';
|
|
359
|
+
|
|
360
|
+
const session = ai.createVoiceSession({
|
|
361
|
+
model: 'auto',
|
|
362
|
+
prompt: 'You are a phone booking assistant for a small clinic.',
|
|
363
|
+
instructions: [
|
|
364
|
+
'Keep answers short and natural for a phone call.',
|
|
365
|
+
'Ask one follow-up question at a time.',
|
|
366
|
+
],
|
|
367
|
+
taskPrompts: [
|
|
368
|
+
{
|
|
369
|
+
name: 'booking',
|
|
370
|
+
when: ['book', 'appointment', 'free slot'],
|
|
371
|
+
instructions: 'When the caller asks about availability, call check_free_slots before offering times.',
|
|
372
|
+
tools: ['check_free_slots'],
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
tools: [
|
|
376
|
+
tool({
|
|
377
|
+
name: 'check_free_slots',
|
|
378
|
+
description: 'Check available booking slots for a date.',
|
|
379
|
+
parameters: {
|
|
380
|
+
type: 'object',
|
|
381
|
+
properties: { date: { type: 'string' } },
|
|
382
|
+
required: ['date'],
|
|
383
|
+
},
|
|
384
|
+
execute: async ({ date }) => bookingApi.freeSlots(String(date)),
|
|
385
|
+
}),
|
|
386
|
+
],
|
|
387
|
+
toolSelection: 'task',
|
|
388
|
+
speech: { provider: 'openai', voice: 'alloy', format: 'mp3' },
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const turn = await session.handleTurn({
|
|
392
|
+
transcript: 'Do you have any free slots tomorrow?',
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
console.log(turn.response.content);
|
|
396
|
+
console.log(turn.toolSteps);
|
|
397
|
+
console.log(turn.speech?.audio.data);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
You can use only `prompt`, only `instructions`, only `taskPrompts`, or all of them together. For phone calls through Twilio media streams, parse incoming audio/media events in `telephony`, feed complete caller turns into `session.handleTurn(...)`, then send the returned speech audio back through your call transport.
|
|
401
|
+
|
|
346
402
|
### Custom Voice Provider
|
|
347
403
|
|
|
348
404
|
Bring any private, local, or hosted voice service:
|
|
@@ -369,7 +425,62 @@ ai.registerVoiceProvider('private-voice', {
|
|
|
369
425
|
});
|
|
370
426
|
```
|
|
371
427
|
|
|
372
|
-
|
|
428
|
+
Low-level realtime transport adapters are still future work; the current voice layer covers transcription, speech generation, full request/response voice turns, and stateful voice sessions with tool calls.
|
|
429
|
+
|
|
430
|
+
## Phone Number Telephony
|
|
431
|
+
|
|
432
|
+
Phone-number support is optional and separate from the voice layer. Use `voice` for audio transcription/TTS, and use `telephony` when calls, webhooks, TwiML, or media-stream events are involved.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import { TelephonyManager } from 'nexus-ai-pro/telephony';
|
|
436
|
+
import { TwilioTelephonyProvider } from 'nexus-ai-pro/telephony/twilio';
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Configure only the provider you need:
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
const ai = new NexusAI({
|
|
443
|
+
providers: { openai: { apiKey: process.env.OPENAI_API_KEY! } },
|
|
444
|
+
telephony: {
|
|
445
|
+
defaultProvider: 'twilio',
|
|
446
|
+
providers: {
|
|
447
|
+
twilio: new TwilioTelephonyProvider({
|
|
448
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID!,
|
|
449
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
450
|
+
}),
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Generate a Twilio webhook response for an inbound phone number:
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
const response = await ai.createTelephonyResponse({
|
|
460
|
+
say: 'Thanks for calling. Connecting you now.',
|
|
461
|
+
stream: {
|
|
462
|
+
url: 'wss://voice.example.com/twilio',
|
|
463
|
+
mode: 'bidirectional',
|
|
464
|
+
parameters: { tenant: 'acme' },
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return new Response(response.body, {
|
|
469
|
+
headers: { 'content-type': response.contentType },
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Start an outbound call with either a webhook, inline TwiML, an application SID, or a media-stream URL:
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
const call = await ai.createCall({
|
|
477
|
+
to: '+15551230000',
|
|
478
|
+
from: '+15557650000',
|
|
479
|
+
mediaStreamUrl: 'wss://voice.example.com/twilio',
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
For smaller apps, skip Twilio entirely and register your own `TelephonyProvider`.
|
|
373
484
|
|
|
374
485
|
## Security
|
|
375
486
|
|
|
@@ -435,9 +546,35 @@ Available runtime helpers:
|
|
|
435
546
|
- batch completion with concurrency control
|
|
436
547
|
- in-memory job queue
|
|
437
548
|
- Redis and BullMQ queue adapters
|
|
438
|
-
- eval runner with quality, operations, RAG, and
|
|
549
|
+
- eval runner with quality, operations, RAG, safety metrics, and optional LLM-as-judge scoring
|
|
550
|
+
- voice sessions for multi-turn calls with conditional task prompts and app tools
|
|
439
551
|
- workflow templates for RAG answers, extraction, classification, comparison, support, sales, legal review, and code review
|
|
440
552
|
|
|
553
|
+
Use a plain assertion for small evals, or add an LLM judge when a rubric is more useful than exact matching:
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
import { LLMJudge } from 'nexus-ai-pro/evals';
|
|
557
|
+
|
|
558
|
+
const judge = new LLMJudge({
|
|
559
|
+
client: ai,
|
|
560
|
+
model: 'openai/gpt-4.1-mini',
|
|
561
|
+
rubric: 'Score whether the answer is correct, concise, and grounded in the supplied context.',
|
|
562
|
+
passThreshold: 0.7,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const run = await ai.runEvals([
|
|
566
|
+
{
|
|
567
|
+
name: 'support answer quality',
|
|
568
|
+
request: {
|
|
569
|
+
model: 'auto',
|
|
570
|
+
messages: [{ role: 'user', content: 'Explain our refund policy.' }],
|
|
571
|
+
},
|
|
572
|
+
expected: 'Refunds are available within 30 days.',
|
|
573
|
+
judge: judge.asEvalJudge(),
|
|
574
|
+
},
|
|
575
|
+
]);
|
|
576
|
+
```
|
|
577
|
+
|
|
441
578
|
## Observability and Reliability
|
|
442
579
|
|
|
443
580
|
```ts
|
package/dist/core/nexus.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type { CompletionRequest } from '../types/messages.js';
|
|
|
3
3
|
import type { NexusPlan } from '../types/planning.js';
|
|
4
4
|
import type { NexusResponse, NexusStream } from '../types/response.js';
|
|
5
5
|
import type { AgentConfig, AgentResult } from '../types/agent.js';
|
|
6
|
-
import type { SpeechRequest, SpeechResponse, TranscriptionRequest, TranscriptionResponse, VoiceProvider, VoiceTurnRequest, VoiceTurnResponse } from '../types/voice.js';
|
|
6
|
+
import type { SpeechRequest, SpeechResponse, TranscriptionRequest, TranscriptionResponse, VoiceProvider, VoiceSessionConfig, VoiceTurnRequest, VoiceTurnResponse } from '../types/voice.js';
|
|
7
|
+
import type { CreateCallRequest, CreateCallResponse, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyProvider, TelephonyResponseRequest, TelephonyWebhookResponse, TelephonyWebhookValidationRequest } from '../types/telephony.js';
|
|
7
8
|
import type { PipelineStep } from '../pipeline/types.js';
|
|
8
9
|
import { BaseProvider } from '../providers/base.js';
|
|
10
|
+
import { VoiceSession } from '../voice/index.js';
|
|
9
11
|
import { type VerificationOptions } from '../hallucination/verification.js';
|
|
10
12
|
import { type SelfConsistencyOptions } from '../hallucination/consistency.js';
|
|
11
13
|
import { type BatchOptions, type BatchItemResult } from '../jobs/batch.js';
|
|
@@ -21,6 +23,7 @@ export declare class NexusAI {
|
|
|
21
23
|
private security;
|
|
22
24
|
private contextWindow;
|
|
23
25
|
private voiceManager;
|
|
26
|
+
private telephonyManager;
|
|
24
27
|
private optimizer;
|
|
25
28
|
private cache;
|
|
26
29
|
private auditLogger;
|
|
@@ -40,12 +43,24 @@ export declare class NexusAI {
|
|
|
40
43
|
speak(request: SpeechRequest): Promise<SpeechResponse>;
|
|
41
44
|
voice(request: VoiceTurnRequest): Promise<VoiceTurnResponse>;
|
|
42
45
|
voiceTurn(request: VoiceTurnRequest): Promise<VoiceTurnResponse>;
|
|
46
|
+
createVoiceSession(config: VoiceSessionConfig): VoiceSession;
|
|
47
|
+
createCall(request: CreateCallRequest): Promise<CreateCallResponse>;
|
|
48
|
+
createTelephonyResponse(request: TelephonyResponseRequest): Promise<TelephonyWebhookResponse>;
|
|
49
|
+
validateTelephonyWebhook(request: TelephonyWebhookValidationRequest): Promise<boolean>;
|
|
50
|
+
parseTelephonyMediaEvent(providerName: string, message: string | Record<string, unknown>): TelephonyMediaStreamEvent | undefined;
|
|
51
|
+
formatTelephonyAudioMessage(providerName: string, streamId: string, payload: string, options?: {
|
|
52
|
+
event?: 'media' | 'mark' | 'clear';
|
|
53
|
+
markName?: string;
|
|
54
|
+
}): TelephonyOutboundAudioMessage;
|
|
43
55
|
registerProvider(name: string, provider: BaseProvider): this;
|
|
44
56
|
registerVoiceProvider(name: string, provider: VoiceProvider): this;
|
|
57
|
+
registerTelephonyProvider(name: string, provider: TelephonyProvider): this;
|
|
45
58
|
hasProvider(name: string): boolean;
|
|
46
59
|
hasVoiceProvider(name: string): boolean;
|
|
60
|
+
hasTelephonyProvider(name: string): boolean;
|
|
47
61
|
listProviders(): string[];
|
|
48
62
|
listVoiceProviders(): string[];
|
|
63
|
+
listTelephonyProviders(): string[];
|
|
49
64
|
use(step: PipelineStep): this;
|
|
50
65
|
batchComplete(requests: CompletionRequest[], options?: BatchOptions): Promise<Array<BatchItemResult<NexusResponse>>>;
|
|
51
66
|
createQueue(options?: QueueOptions): JobQueue<CompletionRequest, NexusResponse>;
|
package/dist/core/nexus.js
CHANGED
|
@@ -11,6 +11,7 @@ import { Logger } from '../utils/logger.js';
|
|
|
11
11
|
import { SecurityPipeline } from '../security/index.js';
|
|
12
12
|
import { ContextWindowManager } from '../context/index.js';
|
|
13
13
|
import { VoiceManager } from '../voice/index.js';
|
|
14
|
+
import { TelephonyManager } from '../telephony/index.js';
|
|
14
15
|
import { TokenOptimizer } from '../optimizer/index.js';
|
|
15
16
|
import { AgentLoop } from '../agent/loop.js';
|
|
16
17
|
import { resolveModel } from '../models/registry.js';
|
|
@@ -39,6 +40,7 @@ export class NexusAI {
|
|
|
39
40
|
security;
|
|
40
41
|
contextWindow;
|
|
41
42
|
voiceManager;
|
|
43
|
+
telephonyManager;
|
|
42
44
|
optimizer;
|
|
43
45
|
cache;
|
|
44
46
|
auditLogger;
|
|
@@ -57,6 +59,7 @@ export class NexusAI {
|
|
|
57
59
|
this.security = new SecurityPipeline(this.config.security || 'standard');
|
|
58
60
|
this.contextWindow = new ContextWindowManager(this.config.contextWindow || {});
|
|
59
61
|
this.voiceManager = new VoiceManager(this.config.voice || {});
|
|
62
|
+
this.telephonyManager = new TelephonyManager(this.config.telephony || {});
|
|
60
63
|
this.optimizer = new TokenOptimizer(this.config.tokenOptimizer || {});
|
|
61
64
|
this.cache = new MemoryCache(this.config.cache?.maxEntries || 500);
|
|
62
65
|
this.auditLogger = new AuditLogger(this.config.auditLog);
|
|
@@ -320,6 +323,24 @@ export class NexusAI {
|
|
|
320
323
|
async voiceTurn(request) {
|
|
321
324
|
return this.voiceManager.runTurn(request, this);
|
|
322
325
|
}
|
|
326
|
+
createVoiceSession(config) {
|
|
327
|
+
return this.voiceManager.createSession(config, this);
|
|
328
|
+
}
|
|
329
|
+
async createCall(request) {
|
|
330
|
+
return this.telephonyManager.createCall(request);
|
|
331
|
+
}
|
|
332
|
+
async createTelephonyResponse(request) {
|
|
333
|
+
return this.telephonyManager.createWebhookResponse(request);
|
|
334
|
+
}
|
|
335
|
+
async validateTelephonyWebhook(request) {
|
|
336
|
+
return this.telephonyManager.validateWebhook(request);
|
|
337
|
+
}
|
|
338
|
+
parseTelephonyMediaEvent(providerName, message) {
|
|
339
|
+
return this.telephonyManager.parseMediaStreamEvent(providerName, message);
|
|
340
|
+
}
|
|
341
|
+
formatTelephonyAudioMessage(providerName, streamId, payload, options) {
|
|
342
|
+
return this.telephonyManager.formatAudioMessage(providerName, streamId, payload, options);
|
|
343
|
+
}
|
|
323
344
|
registerProvider(name, provider) {
|
|
324
345
|
this.providers.set(name, provider);
|
|
325
346
|
return this;
|
|
@@ -328,18 +349,28 @@ export class NexusAI {
|
|
|
328
349
|
this.voiceManager.registerProvider(name, provider);
|
|
329
350
|
return this;
|
|
330
351
|
}
|
|
352
|
+
registerTelephonyProvider(name, provider) {
|
|
353
|
+
this.telephonyManager.registerProvider(name, provider);
|
|
354
|
+
return this;
|
|
355
|
+
}
|
|
331
356
|
hasProvider(name) {
|
|
332
357
|
return this.providers.has(name);
|
|
333
358
|
}
|
|
334
359
|
hasVoiceProvider(name) {
|
|
335
360
|
return this.voiceManager.hasProvider(name);
|
|
336
361
|
}
|
|
362
|
+
hasTelephonyProvider(name) {
|
|
363
|
+
return this.telephonyManager.hasProvider(name);
|
|
364
|
+
}
|
|
337
365
|
listProviders() {
|
|
338
366
|
return [...this.providers.keys()];
|
|
339
367
|
}
|
|
340
368
|
listVoiceProviders() {
|
|
341
369
|
return this.voiceManager.listProviders();
|
|
342
370
|
}
|
|
371
|
+
listTelephonyProviders() {
|
|
372
|
+
return this.telephonyManager.listProviders();
|
|
373
|
+
}
|
|
343
374
|
use(step) {
|
|
344
375
|
this.pipeline.use(step);
|
|
345
376
|
return this;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { EvalRunner, type EvalCase, type EvalClient, type EvalJudge, type EvalJudgment, type EvalResult, type EvalRunResult, } from './runner.js';
|
|
2
|
+
export { LLMJudge, createLLMJudgeEval, parseJudgeResponse, type JudgeClient, type LLMJudgeInput, type LLMJudgeInputMapper, type LLMJudgeOptions, type LLMJudgeResult, } from './judge.js';
|
|
3
|
+
export { calculateEvalMetrics, exactMatch, f1Score, semanticSimilarity, passAtK, perplexity, tokensPerSecond, faithfulness, contextualPrecision, contextualRecall, toxicityScore, biasScore, policyAdherence, refusalRate, type EvalMetrics, type MetricInputs, type OperationalMetrics, type QualityMetrics, type RagMetrics, type SafetyMetrics, } from './metrics.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { EvalRunner, } from './runner.js';
|
|
2
|
+
export { LLMJudge, createLLMJudgeEval, parseJudgeResponse, } from './judge.js';
|
|
3
|
+
export { calculateEvalMetrics, exactMatch, f1Score, semanticSimilarity, passAtK, perplexity, tokensPerSecond, faithfulness, contextualPrecision, contextualRecall, toxicityScore, biasScore, policyAdherence, refusalRate, } from './metrics.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CompletionRequest } from '../types/messages.js';
|
|
2
|
+
import type { NexusResponse } from '../types/response.js';
|
|
3
|
+
import type { EvalCase, EvalJudge } from './runner.js';
|
|
4
|
+
export interface JudgeClient<Response = NexusResponse> {
|
|
5
|
+
complete(request: CompletionRequest): Promise<Response>;
|
|
6
|
+
}
|
|
7
|
+
export interface LLMJudgeInput {
|
|
8
|
+
actual: string;
|
|
9
|
+
expected?: string;
|
|
10
|
+
query?: string;
|
|
11
|
+
rubric?: string;
|
|
12
|
+
contexts?: string[];
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface LLMJudgeOptions<Response = NexusResponse> {
|
|
16
|
+
client: JudgeClient<Response>;
|
|
17
|
+
model: string;
|
|
18
|
+
rubric?: string;
|
|
19
|
+
systemPrompt?: string;
|
|
20
|
+
passThreshold?: number;
|
|
21
|
+
range?: [number, number];
|
|
22
|
+
temperature?: number;
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
export interface LLMJudgeResult {
|
|
27
|
+
score: number;
|
|
28
|
+
passed: boolean;
|
|
29
|
+
rationale?: string;
|
|
30
|
+
labels?: string[];
|
|
31
|
+
raw: unknown;
|
|
32
|
+
providerUsed?: string;
|
|
33
|
+
modelUsed?: string;
|
|
34
|
+
}
|
|
35
|
+
export type LLMJudgeInputMapper<Response> = (response: Response, testCase: EvalCase<Response>) => LLMJudgeInput | Promise<LLMJudgeInput>;
|
|
36
|
+
export declare class LLMJudge<Response = NexusResponse> {
|
|
37
|
+
private options;
|
|
38
|
+
constructor(options: LLMJudgeOptions<Response>);
|
|
39
|
+
evaluate(input: LLMJudgeInput): Promise<LLMJudgeResult>;
|
|
40
|
+
asEvalJudge(mapper?: LLMJudgeInputMapper<Response>): EvalJudge<Response>;
|
|
41
|
+
private messages;
|
|
42
|
+
}
|
|
43
|
+
export declare function createLLMJudgeEval<Response = NexusResponse>(judge: LLMJudge<Response>, mapper?: LLMJudgeInputMapper<Response>): EvalJudge<Response>;
|
|
44
|
+
export declare function parseJudgeResponse(content: string, options?: {
|
|
45
|
+
threshold?: number;
|
|
46
|
+
range?: [number, number];
|
|
47
|
+
raw?: unknown;
|
|
48
|
+
providerUsed?: string;
|
|
49
|
+
modelUsed?: string;
|
|
50
|
+
}): LLMJudgeResult;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const DEFAULT_SYSTEM_PROMPT = [
|
|
2
|
+
'You are a careful evaluation judge.',
|
|
3
|
+
'Return only JSON with these fields: score, passed, rationale, labels.',
|
|
4
|
+
'Score must be numeric within the configured range.',
|
|
5
|
+
].join(' ');
|
|
6
|
+
export class LLMJudge {
|
|
7
|
+
options;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
async evaluate(input) {
|
|
12
|
+
const range = this.options.range || [0, 1];
|
|
13
|
+
const threshold = this.options.passThreshold ?? midpoint(range);
|
|
14
|
+
const response = await this.options.client.complete({
|
|
15
|
+
model: this.options.model,
|
|
16
|
+
messages: this.messages(input, range, threshold),
|
|
17
|
+
temperature: this.options.temperature ?? 0,
|
|
18
|
+
maxTokens: this.options.maxTokens ?? 512,
|
|
19
|
+
responseFormat: {
|
|
20
|
+
type: 'json_schema',
|
|
21
|
+
schema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
properties: {
|
|
25
|
+
score: { type: 'number' },
|
|
26
|
+
passed: { type: 'boolean' },
|
|
27
|
+
rationale: { type: 'string' },
|
|
28
|
+
labels: { type: 'array', items: { type: 'string' } },
|
|
29
|
+
},
|
|
30
|
+
required: ['score', 'passed'],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
metadata: {
|
|
34
|
+
...this.options.metadata,
|
|
35
|
+
llmJudge: true,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return parseJudgeResponse(contentOf(response), {
|
|
39
|
+
threshold,
|
|
40
|
+
range,
|
|
41
|
+
raw: response,
|
|
42
|
+
providerUsed: providerOf(response),
|
|
43
|
+
modelUsed: modelOf(response),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
asEvalJudge(mapper) {
|
|
47
|
+
return async (response, testCase) => {
|
|
48
|
+
const input = mapper
|
|
49
|
+
? await mapper(response, testCase)
|
|
50
|
+
: {
|
|
51
|
+
actual: contentOf(response),
|
|
52
|
+
expected: testCase.expected,
|
|
53
|
+
rubric: this.options.rubric,
|
|
54
|
+
};
|
|
55
|
+
return this.evaluate(input);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
messages(input, range, threshold) {
|
|
59
|
+
const rubric = input.rubric || this.options.rubric || 'Judge whether the answer satisfies the request.';
|
|
60
|
+
const sections = [
|
|
61
|
+
`Rubric:\n${rubric}`,
|
|
62
|
+
`Score range: ${range[0]} to ${range[1]}`,
|
|
63
|
+
`Pass threshold: ${threshold}`,
|
|
64
|
+
input.query ? `User query:\n${input.query}` : undefined,
|
|
65
|
+
input.expected ? `Reference answer:\n${input.expected}` : undefined,
|
|
66
|
+
input.contexts?.length ? `Context:\n${input.contexts.join('\n\n')}` : undefined,
|
|
67
|
+
`Model answer:\n${input.actual}`,
|
|
68
|
+
].filter(Boolean);
|
|
69
|
+
return [
|
|
70
|
+
{ role: 'system', content: this.options.systemPrompt || DEFAULT_SYSTEM_PROMPT },
|
|
71
|
+
{ role: 'user', content: sections.join('\n\n') },
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function createLLMJudgeEval(judge, mapper) {
|
|
76
|
+
return judge.asEvalJudge(mapper);
|
|
77
|
+
}
|
|
78
|
+
export function parseJudgeResponse(content, options = {}) {
|
|
79
|
+
const range = options.range || [0, 1];
|
|
80
|
+
const threshold = options.threshold ?? midpoint(range);
|
|
81
|
+
const parsed = parseJsonObject(content);
|
|
82
|
+
const score = clamp(numberField(parsed.score) ?? numberFromText(content) ?? range[0], range);
|
|
83
|
+
const passed = typeof parsed.passed === 'boolean' ? parsed.passed : score >= threshold;
|
|
84
|
+
const labels = Array.isArray(parsed.labels)
|
|
85
|
+
? parsed.labels.filter((label) => typeof label === 'string')
|
|
86
|
+
: undefined;
|
|
87
|
+
return {
|
|
88
|
+
score,
|
|
89
|
+
passed,
|
|
90
|
+
rationale: typeof parsed.rationale === 'string' ? parsed.rationale : undefined,
|
|
91
|
+
labels,
|
|
92
|
+
raw: options.raw ?? parsed,
|
|
93
|
+
providerUsed: options.providerUsed,
|
|
94
|
+
modelUsed: options.modelUsed,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function contentOf(response) {
|
|
98
|
+
if (typeof response === 'string')
|
|
99
|
+
return response;
|
|
100
|
+
if (response && typeof response === 'object' && 'content' in response) {
|
|
101
|
+
const content = response.content;
|
|
102
|
+
if (typeof content === 'string')
|
|
103
|
+
return content;
|
|
104
|
+
}
|
|
105
|
+
return JSON.stringify(response);
|
|
106
|
+
}
|
|
107
|
+
function providerOf(response) {
|
|
108
|
+
return response && typeof response === 'object'
|
|
109
|
+
? response.meta?.providerUsed
|
|
110
|
+
: undefined;
|
|
111
|
+
}
|
|
112
|
+
function modelOf(response) {
|
|
113
|
+
return response && typeof response === 'object'
|
|
114
|
+
? response.meta?.modelUsed
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
function parseJsonObject(content) {
|
|
118
|
+
const trimmed = content.trim();
|
|
119
|
+
const unfenced = trimmed
|
|
120
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
121
|
+
.replace(/\s*```$/, '')
|
|
122
|
+
.trim();
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(unfenced);
|
|
125
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
126
|
+
? parsed
|
|
127
|
+
: {};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function numberField(value) {
|
|
134
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
135
|
+
return value;
|
|
136
|
+
if (typeof value === 'string') {
|
|
137
|
+
const parsed = Number(value);
|
|
138
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
function numberFromText(value) {
|
|
143
|
+
const match = value.match(/-?\d+(?:\.\d+)?/);
|
|
144
|
+
if (!match)
|
|
145
|
+
return undefined;
|
|
146
|
+
const parsed = Number(match[0]);
|
|
147
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
148
|
+
}
|
|
149
|
+
function clamp(value, [min, max]) {
|
|
150
|
+
return Math.min(Math.max(value, min), max);
|
|
151
|
+
}
|
|
152
|
+
function midpoint([min, max]) {
|
|
153
|
+
return min + (max - min) / 2;
|
|
154
|
+
}
|
package/dist/evals/runner.d.ts
CHANGED
|
@@ -3,10 +3,21 @@ import type { EvalMetrics, MetricInputs } from './metrics.js';
|
|
|
3
3
|
export interface EvalClient<Response = unknown> {
|
|
4
4
|
complete(request: CompletionRequest): Promise<Response>;
|
|
5
5
|
}
|
|
6
|
+
export interface EvalJudgment {
|
|
7
|
+
score: number;
|
|
8
|
+
passed: boolean;
|
|
9
|
+
rationale?: string;
|
|
10
|
+
labels?: string[];
|
|
11
|
+
raw?: unknown;
|
|
12
|
+
providerUsed?: string;
|
|
13
|
+
modelUsed?: string;
|
|
14
|
+
}
|
|
15
|
+
export type EvalJudge<Response = unknown> = (response: Response, testCase: EvalCase<Response>) => boolean | EvalJudgment | Promise<boolean | EvalJudgment>;
|
|
6
16
|
export interface EvalCase<Response = unknown> {
|
|
7
17
|
name: string;
|
|
8
18
|
request: CompletionRequest;
|
|
9
|
-
assert
|
|
19
|
+
assert?: (response: Response) => boolean | Promise<boolean>;
|
|
20
|
+
judge?: EvalJudge<Response>;
|
|
10
21
|
expected?: string;
|
|
11
22
|
metrics?: (response: Response) => MetricInputs | Promise<MetricInputs>;
|
|
12
23
|
tags?: string[];
|
|
@@ -17,6 +28,7 @@ export interface EvalResult<Response = unknown> {
|
|
|
17
28
|
durationMs: number;
|
|
18
29
|
response?: Response;
|
|
19
30
|
metrics?: EvalMetrics;
|
|
31
|
+
judgment?: EvalJudgment;
|
|
20
32
|
error?: string;
|
|
21
33
|
tags?: string[];
|
|
22
34
|
}
|
package/dist/evals/runner.js
CHANGED
|
@@ -11,7 +11,14 @@ export class EvalRunner {
|
|
|
11
11
|
const caseStarted = Date.now();
|
|
12
12
|
try {
|
|
13
13
|
const response = await this.client.complete(testCase.request);
|
|
14
|
-
|
|
14
|
+
if (!testCase.assert && !testCase.judge) {
|
|
15
|
+
throw new Error(`Eval case "${testCase.name}" requires assert or judge`);
|
|
16
|
+
}
|
|
17
|
+
const assertPassed = testCase.assert ? await testCase.assert(response) : true;
|
|
18
|
+
const judgment = testCase.judge
|
|
19
|
+
? normalizeJudgment(await testCase.judge(response, testCase))
|
|
20
|
+
: undefined;
|
|
21
|
+
const passed = assertPassed && (judgment?.passed ?? true);
|
|
15
22
|
const metrics = testCase.metrics
|
|
16
23
|
? await calculateEvalMetrics(await testCase.metrics(response))
|
|
17
24
|
: undefined;
|
|
@@ -21,6 +28,7 @@ export class EvalRunner {
|
|
|
21
28
|
durationMs: Date.now() - caseStarted,
|
|
22
29
|
response,
|
|
23
30
|
metrics,
|
|
31
|
+
judgment,
|
|
24
32
|
tags: testCase.tags,
|
|
25
33
|
});
|
|
26
34
|
}
|
|
@@ -45,3 +53,12 @@ export class EvalRunner {
|
|
|
45
53
|
};
|
|
46
54
|
}
|
|
47
55
|
}
|
|
56
|
+
function normalizeJudgment(value) {
|
|
57
|
+
if (typeof value === 'boolean') {
|
|
58
|
+
return {
|
|
59
|
+
score: value ? 1 : 0,
|
|
60
|
+
passed: value,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|