@absolutejs/voice 0.0.22-beta.481 → 0.0.22-beta.483

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/index.d.ts CHANGED
@@ -71,10 +71,14 @@ export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, crea
71
71
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool, } from "./agent";
72
72
  export { createAIVoiceModel } from "./aiVoiceModel";
73
73
  export type { CreateAIVoiceModelOptions } from "./aiVoiceModel";
74
+ export { createVoiceAIJudgeCompletion, createVoiceLLMJudge, } from "./llmJudge";
75
+ export type { CreateVoiceAIJudgeCompletionOptions, CreateVoiceLLMJudgeOptions, VoiceLLMJudge, VoiceLLMJudgeCompletion, VoiceLLMJudgeCriterionVerdict, VoiceLLMJudgeInput, VoiceLLMJudgeRubric, VoiceLLMJudgeRubricCriterion, VoiceLLMJudgeVerdict, } from "./llmJudge";
74
76
  export { DEFAULT_VOICE_REDACTION_PATTERNS, createVoiceTranscriptRedactor, redactVoiceTranscript, } from "./redaction";
75
77
  export type { CreateVoiceTranscriptRedactorOptions, VoiceRedactionPattern, VoiceTranscriptRedactor, } from "./redaction";
76
78
  export { DEFAULT_VOICE_PRICE_BOOK, createVoiceCostAccountant, } from "./costAccounting";
77
79
  export type { CreateVoiceCostAccountantOptions, VoiceCostAccountant, VoiceCostBreakdown, VoiceCostLLMRecord, VoiceCostSTTRecord, VoiceCostTTSRecord, VoiceCostTelephonyRecord, VoicePriceBook, VoiceProviderRates, } from "./costAccounting";
80
+ export { createPunctuationSemanticTurnDetector, createRegexSemanticTurnDetector, } from "./semanticTurn";
81
+ export type { CreatePunctuationSemanticTurnDetectorOptions, CreateRegexSemanticTurnDetectorOptions, VoiceSemanticTurnDetector, VoiceSemanticTurnInput, VoiceSemanticTurnVerdict, } from "./semanticTurn";
78
82
  export { createMonologueAMDDetector } from "./amdDetector";
79
83
  export type { MonologueAMDDetectorOptions, VoiceAMDDetector, VoiceAMDDetectorInput, VoiceAMDVerdict, } from "./amdDetector";
80
84
  export { createVoiceRAGTool } from "./ragTool";
package/dist/index.js CHANGED
@@ -4786,6 +4786,18 @@ var createVoiceSession = (options) => {
4786
4786
  session,
4787
4787
  type: "turn.transcript"
4788
4788
  });
4789
+ if (options.semanticTurnDetector) {
4790
+ const verdict = await Promise.resolve(options.semanticTurnDetector.evaluate({
4791
+ lastFinalTranscript: transcript,
4792
+ partialText: session.currentTurn.partialText,
4793
+ silenceMs: session.currentTurn.silenceStartedAt !== undefined ? Date.now() - session.currentTurn.silenceStartedAt : 0,
4794
+ transcripts: session.currentTurn.transcripts
4795
+ }));
4796
+ if (verdict.endOfTurn) {
4797
+ clearSilenceTimer();
4798
+ await requestTurnCommit("vendor");
4799
+ }
4800
+ }
4789
4801
  };
4790
4802
  const resumePendingTurnCommit = (session) => {
4791
4803
  const pendingText = buildTurnText(session.currentTurn.transcripts, session.currentTurn.partialText, {
@@ -34981,6 +34993,134 @@ var createAIVoiceModel = (options) => ({
34981
34993
  return output;
34982
34994
  }
34983
34995
  });
34996
+ // src/llmJudge.ts
34997
+ var DEFAULT_SYSTEM_PROMPT = "You are an impartial evaluator scoring a voice-agent transcript against a rubric. " + "For each criterion, decide pass/fail and give a one-sentence rationale grounded in the transcript. " + 'Respond with strict JSON: {"criteria":[{"criterionId":"\u2026","passed":true,"rationale":"\u2026"}],"summary":"\u2026"}.';
34998
+ var buildPrompt = (rubric, input) => {
34999
+ const criteriaBlock = rubric.criteria.map((criterion) => `- ${criterion.id}${criterion.required ? " (required)" : ""}: ${criterion.description}`).join(`
35000
+ `);
35001
+ const metadataBlock = input.metadata ? `
35002
+ Metadata:
35003
+ ${JSON.stringify(input.metadata, null, 2)}
35004
+ ` : "";
35005
+ return `Rubric criteria:
35006
+ ${criteriaBlock}
35007
+ ${metadataBlock}
35008
+ Transcript:
35009
+ ${input.transcript}
35010
+
35011
+ Return JSON only.`;
35012
+ };
35013
+ var extractJson = (raw) => {
35014
+ const trimmed = raw.trim();
35015
+ if (!trimmed) {
35016
+ throw new Error("LLM judge returned an empty response");
35017
+ }
35018
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
35019
+ const candidate = fenced ? fenced[1].trim() : trimmed;
35020
+ try {
35021
+ return JSON.parse(candidate);
35022
+ } catch {
35023
+ const start = candidate.indexOf("{");
35024
+ const end = candidate.lastIndexOf("}");
35025
+ if (start >= 0 && end > start) {
35026
+ return JSON.parse(candidate.slice(start, end + 1));
35027
+ }
35028
+ throw new Error(`LLM judge response was not valid JSON: ${raw.slice(0, 200)}`);
35029
+ }
35030
+ };
35031
+ var parseCriteria = (payload, rubric) => {
35032
+ if (!payload || typeof payload !== "object") {
35033
+ throw new Error("LLM judge response is not a JSON object");
35034
+ }
35035
+ const root = payload;
35036
+ const criteriaRaw = root.criteria;
35037
+ if (!Array.isArray(criteriaRaw)) {
35038
+ throw new Error("LLM judge response is missing the 'criteria' array");
35039
+ }
35040
+ const verdictById = new Map;
35041
+ for (const entry of criteriaRaw) {
35042
+ if (!entry || typeof entry !== "object") {
35043
+ continue;
35044
+ }
35045
+ const record = entry;
35046
+ const criterionId = typeof record.criterionId === "string" ? record.criterionId : typeof record.id === "string" ? record.id : undefined;
35047
+ if (!criterionId) {
35048
+ continue;
35049
+ }
35050
+ verdictById.set(criterionId, {
35051
+ criterionId,
35052
+ passed: record.passed === true,
35053
+ rationale: typeof record.rationale === "string" ? record.rationale : ""
35054
+ });
35055
+ }
35056
+ const criteria = rubric.criteria.map((criterion) => verdictById.get(criterion.id) ?? {
35057
+ criterionId: criterion.id,
35058
+ passed: false,
35059
+ rationale: "Judge did not return a verdict for this criterion."
35060
+ });
35061
+ return {
35062
+ criteria,
35063
+ summary: typeof root.summary === "string" ? root.summary : undefined
35064
+ };
35065
+ };
35066
+ var scoreVerdict = (rubric, criteria) => {
35067
+ const totalWeight = rubric.criteria.reduce((sum, criterion) => sum + (criterion.weight ?? 1), 0);
35068
+ if (totalWeight === 0) {
35069
+ return { passed: false, score: 0 };
35070
+ }
35071
+ const weightById = new Map(rubric.criteria.map((criterion) => [criterion.id, criterion.weight ?? 1]));
35072
+ const requiredIds = new Set(rubric.criteria.filter((criterion) => criterion.required).map((criterion) => criterion.id));
35073
+ let earned = 0;
35074
+ let allRequiredPassed = true;
35075
+ for (const verdict of criteria) {
35076
+ if (verdict.passed) {
35077
+ earned += weightById.get(verdict.criterionId) ?? 1;
35078
+ } else if (requiredIds.has(verdict.criterionId)) {
35079
+ allRequiredPassed = false;
35080
+ }
35081
+ }
35082
+ const score = earned / totalWeight;
35083
+ const minPassScore = rubric.minPassScore ?? 1;
35084
+ return {
35085
+ passed: allRequiredPassed && score >= minPassScore,
35086
+ score
35087
+ };
35088
+ };
35089
+ var createVoiceLLMJudge = (options) => ({
35090
+ evaluate: async (input) => {
35091
+ const prompt = buildPrompt(options.rubric, input);
35092
+ const raw = await options.completion({
35093
+ prompt,
35094
+ systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT
35095
+ });
35096
+ const parsed = parseCriteria(extractJson(raw), options.rubric);
35097
+ const { passed, score } = scoreVerdict(options.rubric, parsed.criteria);
35098
+ return {
35099
+ criteria: parsed.criteria,
35100
+ passed,
35101
+ score,
35102
+ summary: parsed.summary
35103
+ };
35104
+ },
35105
+ rubric: options.rubric
35106
+ });
35107
+ var createVoiceAIJudgeCompletion = (options) => async ({ prompt, systemPrompt }) => {
35108
+ const messages = [
35109
+ { content: prompt, role: "user" }
35110
+ ];
35111
+ const stream = options.provider.stream({
35112
+ messages,
35113
+ model: options.model,
35114
+ systemPrompt
35115
+ });
35116
+ let buffered = "";
35117
+ for await (const chunk of stream) {
35118
+ if (chunk.type === "text") {
35119
+ buffered += chunk.content;
35120
+ }
35121
+ }
35122
+ return buffered;
35123
+ };
34984
35124
  // src/redaction.ts
34985
35125
  var DEFAULT_VOICE_REDACTION_PATTERNS = [
34986
35126
  {
@@ -35164,6 +35304,76 @@ var createVoiceCostAccountant = (options = {}) => {
35164
35304
  })
35165
35305
  };
35166
35306
  };
35307
+ // src/semanticTurn.ts
35308
+ var DEFAULT_END_PUNCTUATION = [".", "?", "!"];
35309
+ var DEFAULT_FILLER_WORDS = [
35310
+ "uh",
35311
+ "um",
35312
+ "er",
35313
+ "ah",
35314
+ "like",
35315
+ "you know",
35316
+ "i mean",
35317
+ "well",
35318
+ "so"
35319
+ ];
35320
+ var stripTerminalPunctuation = (text) => text.replace(/[\s.?!]+$/u, "").trim();
35321
+ var createPunctuationSemanticTurnDetector = (options = {}) => {
35322
+ const endPunctuation = options.endPunctuation ?? DEFAULT_END_PUNCTUATION;
35323
+ const fillerWords = (options.fillerWords ?? DEFAULT_FILLER_WORDS).map((word) => word.toLowerCase());
35324
+ const minPartialWords = options.minPartialWords ?? 2;
35325
+ return {
35326
+ evaluate: ({ lastFinalTranscript, partialText }) => {
35327
+ const candidate = partialText.trim().length > 0 ? partialText : lastFinalTranscript?.text ?? "";
35328
+ const trimmed = candidate.trim();
35329
+ if (!trimmed) {
35330
+ return { endOfTurn: false, reason: "empty" };
35331
+ }
35332
+ const wordCount = trimmed.split(/\s+/u).filter(Boolean).length;
35333
+ if (wordCount < minPartialWords) {
35334
+ return { endOfTurn: false, reason: "below-min-words" };
35335
+ }
35336
+ const lastChar = trimmed.at(-1);
35337
+ const endsWithTerminal = typeof lastChar === "string" && endPunctuation.includes(lastChar);
35338
+ if (!endsWithTerminal) {
35339
+ return { endOfTurn: false, reason: "no-terminal-punctuation" };
35340
+ }
35341
+ const lastWord = stripTerminalPunctuation(trimmed).split(/\s+/u).at(-1)?.toLowerCase();
35342
+ if (lastWord && fillerWords.includes(lastWord)) {
35343
+ return { endOfTurn: false, reason: "trailing-filler" };
35344
+ }
35345
+ return {
35346
+ confidence: 0.9,
35347
+ endOfTurn: true,
35348
+ reason: "terminal-punctuation"
35349
+ };
35350
+ }
35351
+ };
35352
+ };
35353
+ var createRegexSemanticTurnDetector = (options) => {
35354
+ const minPartialWords = options.minPartialWords ?? 2;
35355
+ return {
35356
+ evaluate: ({ lastFinalTranscript, partialText }) => {
35357
+ const candidate = partialText.trim().length > 0 ? partialText : lastFinalTranscript?.text ?? "";
35358
+ const trimmed = candidate.trim();
35359
+ if (!trimmed) {
35360
+ return { endOfTurn: false, reason: "empty" };
35361
+ }
35362
+ const wordCount = trimmed.split(/\s+/u).filter(Boolean).length;
35363
+ if (wordCount < minPartialWords) {
35364
+ return { endOfTurn: false, reason: "below-min-words" };
35365
+ }
35366
+ const match = options.endPattern.exec(trimmed);
35367
+ if (!match) {
35368
+ return { endOfTurn: false, reason: "pattern-miss" };
35369
+ }
35370
+ return {
35371
+ endOfTurn: true,
35372
+ reason: "pattern-match"
35373
+ };
35374
+ }
35375
+ };
35376
+ };
35167
35377
  // src/amdDetector.ts
35168
35378
  var createMonologueAMDDetector = (options = {}) => {
35169
35379
  const minMonologueMs = options.minMonologueMs ?? 8000;
@@ -46252,6 +46462,7 @@ export {
46252
46462
  createVoiceLinearIssueUpdateSink,
46253
46463
  createVoiceLinearIssueSyncSinks,
46254
46464
  createVoiceLinearIssueSink,
46465
+ createVoiceLLMJudge,
46255
46466
  createVoiceIntegrationSinkWorkerLoop,
46256
46467
  createVoiceIntegrationSinkWorker,
46257
46468
  createVoiceIntegrationHTTPSink,
@@ -46347,6 +46558,7 @@ export {
46347
46558
  createVoiceAgentTool,
46348
46559
  createVoiceAgentSquad,
46349
46560
  createVoiceAgent,
46561
+ createVoiceAIJudgeCompletion,
46350
46562
  createTwilioVoiceRoutes,
46351
46563
  createTwilioVoiceResponse,
46352
46564
  createTwilioMediaStreamBridge,
@@ -46359,6 +46571,8 @@ export {
46359
46571
  createStoredVoiceExternalObjectMap,
46360
46572
  createStoredVoiceCallReviewArtifact,
46361
46573
  createRiskyTurnCorrectionHandler,
46574
+ createRegexSemanticTurnDetector,
46575
+ createPunctuationSemanticTurnDetector,
46362
46576
  createPlivoVoiceRoutes,
46363
46577
  createPlivoVoiceResponse,
46364
46578
  createPlivoMediaStreamBridge,
@@ -0,0 +1,45 @@
1
+ import type { AIProviderConfig } from "@absolutejs/ai";
2
+ export type VoiceLLMJudgeRubricCriterion = {
3
+ description: string;
4
+ id: string;
5
+ required?: boolean;
6
+ weight?: number;
7
+ };
8
+ export type VoiceLLMJudgeRubric = {
9
+ criteria: VoiceLLMJudgeRubricCriterion[];
10
+ minPassScore?: number;
11
+ };
12
+ export type VoiceLLMJudgeCriterionVerdict = {
13
+ criterionId: string;
14
+ passed: boolean;
15
+ rationale: string;
16
+ };
17
+ export type VoiceLLMJudgeVerdict = {
18
+ criteria: VoiceLLMJudgeCriterionVerdict[];
19
+ passed: boolean;
20
+ score: number;
21
+ summary?: string;
22
+ };
23
+ export type VoiceLLMJudgeInput = {
24
+ metadata?: Record<string, unknown>;
25
+ transcript: string;
26
+ };
27
+ export type VoiceLLMJudgeCompletion = (input: {
28
+ prompt: string;
29
+ systemPrompt?: string;
30
+ }) => Promise<string>;
31
+ export type CreateVoiceLLMJudgeOptions = {
32
+ completion: VoiceLLMJudgeCompletion;
33
+ rubric: VoiceLLMJudgeRubric;
34
+ systemPrompt?: string;
35
+ };
36
+ export type VoiceLLMJudge = {
37
+ evaluate: (input: VoiceLLMJudgeInput) => Promise<VoiceLLMJudgeVerdict>;
38
+ rubric: VoiceLLMJudgeRubric;
39
+ };
40
+ export declare const createVoiceLLMJudge: (options: CreateVoiceLLMJudgeOptions) => VoiceLLMJudge;
41
+ export type CreateVoiceAIJudgeCompletionOptions = {
42
+ model: string;
43
+ provider: AIProviderConfig;
44
+ };
45
+ export declare const createVoiceAIJudgeCompletion: (options: CreateVoiceAIJudgeCompletionOptions) => VoiceLLMJudgeCompletion;
@@ -0,0 +1,27 @@
1
+ import type { Transcript } from "./types";
2
+ export type VoiceSemanticTurnInput = {
3
+ audioLevel?: number;
4
+ lastFinalTranscript?: Transcript;
5
+ partialText: string;
6
+ silenceMs: number;
7
+ transcripts: Transcript[];
8
+ };
9
+ export type VoiceSemanticTurnVerdict = {
10
+ confidence?: number;
11
+ endOfTurn: boolean;
12
+ reason?: string;
13
+ };
14
+ export type VoiceSemanticTurnDetector = {
15
+ evaluate: (input: VoiceSemanticTurnInput) => Promise<VoiceSemanticTurnVerdict> | VoiceSemanticTurnVerdict;
16
+ };
17
+ export type CreatePunctuationSemanticTurnDetectorOptions = {
18
+ endPunctuation?: ReadonlyArray<string>;
19
+ fillerWords?: ReadonlyArray<string>;
20
+ minPartialWords?: number;
21
+ };
22
+ export declare const createPunctuationSemanticTurnDetector: (options?: CreatePunctuationSemanticTurnDetectorOptions) => VoiceSemanticTurnDetector;
23
+ export type CreateRegexSemanticTurnDetectorOptions = {
24
+ endPattern: RegExp;
25
+ minPartialWords?: number;
26
+ };
27
+ export declare const createRegexSemanticTurnDetector: (options: CreateRegexSemanticTurnDetectorOptions) => VoiceSemanticTurnDetector;
@@ -6754,6 +6754,18 @@ var createVoiceSession = (options) => {
6754
6754
  session,
6755
6755
  type: "turn.transcript"
6756
6756
  });
6757
+ if (options.semanticTurnDetector) {
6758
+ const verdict = await Promise.resolve(options.semanticTurnDetector.evaluate({
6759
+ lastFinalTranscript: transcript,
6760
+ partialText: session.currentTurn.partialText,
6761
+ silenceMs: session.currentTurn.silenceStartedAt !== undefined ? Date.now() - session.currentTurn.silenceStartedAt : 0,
6762
+ transcripts: session.currentTurn.transcripts
6763
+ }));
6764
+ if (verdict.endOfTurn) {
6765
+ clearSilenceTimer();
6766
+ await requestTurnCommit("vendor");
6767
+ }
6768
+ }
6757
6769
  };
6758
6770
  const resumePendingTurnCommit = (session) => {
6759
6771
  const pendingText = buildTurnText(session.currentTurn.transcripts, session.currentTurn.partialText, {
package/dist/types.d.ts CHANGED
@@ -731,6 +731,7 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
731
731
  provider?: string;
732
732
  };
733
733
  redact?: import("./redaction").VoiceTranscriptRedactor;
734
+ semanticTurnDetector?: import("./semanticTurn").VoiceSemanticTurnDetector;
734
735
  reconnect: Required<VoiceReconnectConfig>;
735
736
  phraseHints?: VoicePhraseHint[];
736
737
  sessionMetadata?: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.481",
3
+ "version": "0.0.22-beta.483",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",