@fonoster/autopilot 0.27.0 → 0.30.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.
Files changed (138) hide show
  1. package/README.md +14 -51
  2. package/dist/index.d.ts +0 -5
  3. package/dist/index.js +0 -5
  4. package/dist/knowledge/AbstractKnowledgeBase.d.ts +3 -2
  5. package/dist/knowledge/AbstractKnowledgeBase.js +2 -7
  6. package/dist/knowledge/FilesKnowledgeBase.d.ts +1 -1
  7. package/dist/knowledge/types.d.ts +1 -1
  8. package/dist/models/LanguageModelFactory.d.ts +5 -24
  9. package/dist/models/LanguageModelFactory.js +0 -1
  10. package/dist/models/createLanguageModel.d.ts +2 -2
  11. package/dist/models/evaluations/createEvalEffectiveConfig.d.ts +10 -106
  12. package/dist/models/evaluations/createEvalEffectiveConfig.js +47 -7
  13. package/dist/models/evaluations/createEvaluateIntelligence.d.ts +10 -20
  14. package/dist/models/evaluations/createEvaluateIntelligence.js +32 -23
  15. package/dist/models/evaluations/evalTestCases.d.ts +2 -2
  16. package/dist/models/evaluations/extractAiResponse.d.ts +2 -3
  17. package/dist/models/evaluations/index.d.ts +1 -0
  18. package/dist/models/evaluations/index.js +1 -0
  19. package/dist/models/evaluations/runEval.d.ts +2 -20
  20. package/dist/models/evaluations/session/EvalSession.d.ts +73 -0
  21. package/dist/models/evaluations/session/EvalSession.js +134 -0
  22. package/dist/models/evaluations/session/evaluateSessionScenario.d.ts +58 -0
  23. package/dist/models/evaluations/session/evaluateSessionScenario.js +182 -0
  24. package/dist/{vad → models/evaluations/session}/index.d.ts +3 -2
  25. package/dist/{vad → models/evaluations/session}/index.js +3 -2
  26. package/dist/models/evaluations/session/runSessionEval.d.ts +17 -0
  27. package/dist/models/evaluations/session/runSessionEval.js +59 -0
  28. package/dist/models/evaluations/types.d.ts +7 -6
  29. package/dist/models/types.d.ts +19 -1
  30. package/dist/session/Agent.d.ts +138 -0
  31. package/dist/session/Agent.js +474 -0
  32. package/dist/{loadAssistantFromAPI.d.ts → session/AsyncQueue.d.ts} +12 -7
  33. package/dist/session/AsyncQueue.js +67 -0
  34. package/dist/{machine/guards/index.d.ts → session/config.d.ts} +2 -2
  35. package/dist/session/config.js +27 -0
  36. package/dist/session/convertLegacyAssistant.d.ts +77 -0
  37. package/dist/session/convertLegacyAssistant.js +118 -0
  38. package/dist/session/createAutopilot.d.ts +47 -0
  39. package/dist/session/createAutopilot.js +119 -0
  40. package/dist/session/hosted.d.ts +456 -0
  41. package/dist/session/hosted.js +506 -0
  42. package/dist/{assistants → session}/index.d.ts +11 -2
  43. package/dist/{machine/actors → session}/index.js +11 -3
  44. package/dist/{machine/actions/appendSpeech.js → session/knowledge.d.ts} +14 -17
  45. package/dist/session/knowledge.js +10 -0
  46. package/dist/session/llm.d.ts +86 -0
  47. package/dist/session/llm.js +83 -0
  48. package/dist/session/machine.d.ts +206 -0
  49. package/dist/session/machine.js +126 -0
  50. package/dist/session/main.js +44 -0
  51. package/dist/session/sentences.d.ts +5 -0
  52. package/dist/session/sentences.js +53 -0
  53. package/dist/{machine/actors/index.d.ts → session/think.d.ts} +2 -3
  54. package/dist/{machine/actions/announceSystemError.js → session/think.js} +8 -10
  55. package/dist/session/tools.d.ts +22 -0
  56. package/dist/session/tools.js +83 -0
  57. package/dist/tools/builtin/transferToolDefinition.js +1 -1
  58. package/dist/voice/index.d.ts +0 -1
  59. package/dist/voice/index.js +0 -1
  60. package/dist/voice/types.d.ts +5 -18
  61. package/package.json +13 -14
  62. package/dist/Autopilot.d.ts +0 -14
  63. package/dist/Autopilot.js +0 -133
  64. package/dist/assistants/index.js +0 -36
  65. package/dist/assistants/loadAndValidateAssistant.d.ts +0 -3
  66. package/dist/assistants/loadAndValidateAssistant.js +0 -49
  67. package/dist/assistants/types.d.ts +0 -24
  68. package/dist/assistants/types.js +0 -2
  69. package/dist/envs.d.ts +0 -15
  70. package/dist/envs.js +0 -78
  71. package/dist/handleVoiceRequest.d.ts +0 -3
  72. package/dist/handleVoiceRequest.js +0 -167
  73. package/dist/loadAssistantConfigFromFile.d.ts +0 -3
  74. package/dist/loadAssistantConfigFromFile.js +0 -68
  75. package/dist/loadAssistantFromAPI.js +0 -98
  76. package/dist/machine/actions/announceSystemError.d.ts +0 -4
  77. package/dist/machine/actions/appendSpeech.d.ts +0 -1
  78. package/dist/machine/actions/cleanSpeech.d.ts +0 -1
  79. package/dist/machine/actions/cleanSpeech.js +0 -23
  80. package/dist/machine/actions/goodbye.d.ts +0 -4
  81. package/dist/machine/actions/goodbye.js +0 -31
  82. package/dist/machine/actions/increaseIdleTimeoutCount.d.ts +0 -1
  83. package/dist/machine/actions/increaseIdleTimeoutCount.js +0 -31
  84. package/dist/machine/actions/index.d.ts +0 -26
  85. package/dist/machine/actions/index.js +0 -37
  86. package/dist/machine/actions/interruptPlayback.d.ts +0 -4
  87. package/dist/machine/actions/interruptPlayback.js +0 -43
  88. package/dist/machine/actions/resetIdleTimeoutCount.d.ts +0 -1
  89. package/dist/machine/actions/resetIdleTimeoutCount.js +0 -31
  90. package/dist/machine/actions/resetState.d.ts +0 -1
  91. package/dist/machine/actions/resetState.js +0 -32
  92. package/dist/machine/actors/doAnnounceIdleTimeout.d.ts +0 -4
  93. package/dist/machine/actors/doAnnounceIdleTimeout.js +0 -41
  94. package/dist/machine/actors/doGreetUser.d.ts +0 -4
  95. package/dist/machine/actors/doGreetUser.js +0 -45
  96. package/dist/machine/actors/doProcessUserRequest.d.ts +0 -4
  97. package/dist/machine/actors/doProcessUserRequest.js +0 -75
  98. package/dist/machine/context.d.ts +0 -52
  99. package/dist/machine/context.js +0 -28
  100. package/dist/machine/delays.d.ts +0 -30
  101. package/dist/machine/delays.js +0 -37
  102. package/dist/machine/guards/hasSpeechResult.d.ts +0 -4
  103. package/dist/machine/guards/hasSpeechResult.js +0 -30
  104. package/dist/machine/guards/idleTimeoutCountExceedsMax.d.ts +0 -4
  105. package/dist/machine/guards/idleTimeoutCountExceedsMax.js +0 -31
  106. package/dist/machine/guards/index.js +0 -25
  107. package/dist/machine/index.d.ts +0 -19
  108. package/dist/machine/index.js +0 -23
  109. package/dist/machine/machine.d.ts +0 -408
  110. package/dist/machine/machine.js +0 -206
  111. package/dist/machine/setup.d.ts +0 -328
  112. package/dist/machine/setup.js +0 -116
  113. package/dist/machine/types.d.ts +0 -54
  114. package/dist/machine/types.js +0 -2
  115. package/dist/sendConversationEndedEvent.d.ts +0 -32
  116. package/dist/sendConversationEndedEvent.js +0 -66
  117. package/dist/sendConversationStartedEvent.d.ts +0 -6
  118. package/dist/sendConversationStartedEvent.js +0 -63
  119. package/dist/server.d.ts +0 -2
  120. package/dist/server.js +0 -25
  121. package/dist/types.d.ts +0 -40
  122. package/dist/types.js +0 -8
  123. package/dist/vad/SileroVad.d.ts +0 -18
  124. package/dist/vad/SileroVad.js +0 -40
  125. package/dist/vad/SileroVadModel.d.ts +0 -14
  126. package/dist/vad/SileroVadModel.js +0 -83
  127. package/dist/vad/chunkToFloat32Array.d.ts +0 -20
  128. package/dist/vad/chunkToFloat32Array.js +0 -34
  129. package/dist/vad/createVad.d.ts +0 -3
  130. package/dist/vad/createVad.js +0 -126
  131. package/dist/vad/types.d.ts +0 -60
  132. package/dist/vad/types.js +0 -2
  133. package/dist/vadWorker.js +0 -36
  134. package/dist/voice/Voice.d.ts +0 -61
  135. package/dist/voice/Voice.js +0 -96
  136. package/dist/voiceServerSetup.d.ts +0 -2
  137. package/dist/voiceServerSetup.js +0 -29
  138. /package/dist/{vadWorker.d.ts → session/main.d.ts} +0 -0
@@ -0,0 +1,73 @@
1
+ /** What the engine did on the session, in order. */
2
+ export type EvalOutput = {
3
+ kind: "say";
4
+ text: string;
5
+ /** True when the engine streamed the reply (say with an async iterable). */
6
+ streamed: boolean;
7
+ /** The pieces as they arrived (sentences, for a streamed reply). */
8
+ pieces: string[];
9
+ } | {
10
+ kind: "play";
11
+ url: string;
12
+ } | {
13
+ kind: "dtmf";
14
+ digits: string;
15
+ } | {
16
+ kind: "hangup";
17
+ } | {
18
+ kind: "transfer";
19
+ to: string;
20
+ };
21
+ export type EvalTelephony = {
22
+ callDirection?: string;
23
+ ingressNumber?: string;
24
+ callerNumber?: string;
25
+ metadata?: Record<string, string>;
26
+ };
27
+ /**
28
+ * An in-memory session for conversation evals (rework-autopilot 1.4): the session engine runs on
29
+ * it exactly as on a call, but there is no gateway, socket or audio. Turns go in as `turn.ended`;
30
+ * every reply is "heard" in full (no barge-in in an eval).
31
+ */
32
+ export declare class EvalSession {
33
+ readonly outputs: EvalOutput[];
34
+ readonly sessionRef = "eval-session";
35
+ readonly callRef = "eval-call";
36
+ readonly appRef = "eval-app";
37
+ readonly direction: string;
38
+ readonly from: string;
39
+ readonly to: string;
40
+ readonly metadata: Record<string, string>;
41
+ private readonly events;
42
+ private pending;
43
+ private endedReason?;
44
+ constructor(telephony?: EvalTelephony);
45
+ get ended(): boolean;
46
+ /** Says in flight (a streamed reply is in flight until its last piece). */
47
+ get busy(): boolean;
48
+ on(event: string, listener: (payload: never) => void): this;
49
+ /** Observe outputs as they are recorded (the evaluator waits on these). */
50
+ onOutput(listener: (output: EvalOutput) => void): void;
51
+ /** The caller finished a turn. */
52
+ turn(text: string): void;
53
+ /** The call is over (end of scenario, or the engine hung up or transferred). */
54
+ end(reason?: string): void;
55
+ say(input: string | AsyncIterable<string>, _options?: unknown): Promise<{
56
+ interrupted: boolean;
57
+ heardText: string;
58
+ heardMs: number;
59
+ }>;
60
+ stopSay(): Promise<void>;
61
+ play(url: string, _options?: unknown): Promise<{
62
+ interrupted: boolean;
63
+ heardMs: number;
64
+ }>;
65
+ playDtmf(digits: string): Promise<void>;
66
+ subscribe(_events: string[]): Promise<void>;
67
+ listen(_options: unknown): Promise<void>;
68
+ note(_data: Record<string, unknown>): Promise<void>;
69
+ hangup(): Promise<void>;
70
+ transfer(to: string): Promise<void>;
71
+ private record;
72
+ private assertOpen;
73
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EvalSession = void 0;
4
+ /**
5
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const events_1 = require("events");
23
+ const voice_1 = require("@fonoster/voice");
24
+ /**
25
+ * An in-memory session for conversation evals (rework-autopilot 1.4): the session engine runs on
26
+ * it exactly as on a call, but there is no gateway, socket or audio. Turns go in as `turn.ended`;
27
+ * every reply is "heard" in full (no barge-in in an eval).
28
+ */
29
+ class EvalSession {
30
+ constructor(telephony = {}) {
31
+ this.outputs = [];
32
+ this.sessionRef = "eval-session";
33
+ this.callRef = "eval-call";
34
+ this.appRef = "eval-app";
35
+ this.events = new events_1.EventEmitter();
36
+ this.pending = 0;
37
+ this.direction = (telephony.callDirection ?? "FROM_PSTN").startsWith("TO")
38
+ ? "outbound"
39
+ : "inbound";
40
+ this.from = telephony.callerNumber ?? "";
41
+ this.to = telephony.ingressNumber ?? "";
42
+ this.metadata = telephony.metadata ?? {};
43
+ this.events.setMaxListeners(50);
44
+ }
45
+ get ended() {
46
+ return this.endedReason !== undefined;
47
+ }
48
+ /** Says in flight (a streamed reply is in flight until its last piece). */
49
+ get busy() {
50
+ return this.pending > 0;
51
+ }
52
+ on(event, listener) {
53
+ this.events.on(event, listener);
54
+ return this;
55
+ }
56
+ /** Observe outputs as they are recorded (the evaluator waits on these). */
57
+ onOutput(listener) {
58
+ this.events.on("__output", listener);
59
+ }
60
+ /** The caller finished a turn. */
61
+ turn(text) {
62
+ this.events.emit("turn.ended", {
63
+ type: "turn.ended",
64
+ text,
65
+ language: "en",
66
+ confidence: 1,
67
+ durationMs: 0
68
+ });
69
+ }
70
+ /** The call is over (end of scenario, or the engine hung up or transferred). */
71
+ end(reason = "eval_done") {
72
+ if (this.ended)
73
+ return;
74
+ this.endedReason = reason;
75
+ this.events.emit("end", { type: "session.end", reason });
76
+ }
77
+ // ---- the Session surface the engine uses ---------------------------------------------
78
+ async say(input, _options = {}) {
79
+ this.assertOpen();
80
+ this.pending++;
81
+ try {
82
+ const pieces = [];
83
+ if (typeof input === "string")
84
+ pieces.push(input);
85
+ else
86
+ for await (const piece of input)
87
+ pieces.push(piece);
88
+ const text = pieces.join("");
89
+ this.record({
90
+ kind: "say",
91
+ text,
92
+ streamed: typeof input !== "string",
93
+ pieces
94
+ });
95
+ return { interrupted: false, heardText: text, heardMs: 0 };
96
+ }
97
+ finally {
98
+ this.pending--;
99
+ }
100
+ }
101
+ async stopSay() { }
102
+ async play(url, _options = {}) {
103
+ this.assertOpen();
104
+ this.record({ kind: "play", url });
105
+ return { interrupted: false, heardMs: 0 };
106
+ }
107
+ async playDtmf(digits) {
108
+ this.assertOpen();
109
+ this.record({ kind: "dtmf", digits });
110
+ }
111
+ async subscribe(_events) { }
112
+ async listen(_options) { }
113
+ async note(_data) { }
114
+ async hangup() {
115
+ if (this.ended)
116
+ return;
117
+ this.record({ kind: "hangup" });
118
+ this.end("hangup");
119
+ }
120
+ async transfer(to) {
121
+ this.assertOpen();
122
+ this.record({ kind: "transfer", to });
123
+ this.end("transferred");
124
+ }
125
+ record(output) {
126
+ this.outputs.push(output);
127
+ this.events.emit("__output", output);
128
+ }
129
+ assertOpen() {
130
+ if (this.endedReason)
131
+ throw new voice_1.SessionEndedError(this.endedReason);
132
+ }
133
+ }
134
+ exports.EvalSession = EvalSession;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import { ScenarioEvaluationReport, StepEvaluationReport } from "@fonoster/types";
20
+ import { SessionAssistant } from "../../../session/config";
21
+ import { ChatMessage, Think } from "../../../session/think";
22
+ import { ToolPort } from "../../../session/tools";
23
+ import { EvalScenario } from "../types";
24
+ /** A step report with what the session engine streamed (extra fields, ignored on the wire). */
25
+ export type SessionStepReport = StepEvaluationReport & {
26
+ streaming: {
27
+ /** The pieces of the reply as the engine sent them to `say` (sentences). */
28
+ pieces: string[];
29
+ firstTokenMs?: number;
30
+ firstSpeakMs?: number;
31
+ };
32
+ /** Tool calls and results as stored in the history (structured tool messages). */
33
+ toolMessages: ChatMessage[];
34
+ };
35
+ export type SessionScenarioReport = ScenarioEvaluationReport & {
36
+ steps: SessionStepReport[];
37
+ };
38
+ export type SessionEvalOptions = {
39
+ assistant: SessionAssistant;
40
+ /** The language model the engine streams from (a registry adapter, or the fake LLM). */
41
+ think: Think;
42
+ testTextSimilarity: (text1: string, text2: string) => Promise<boolean>;
43
+ /**
44
+ * Runs the assistant's own tools. Defaults to `evalToolPort`: evals never call tool endpoints
45
+ * (an eval must not book an appointment), every call succeeds with `{ status: "ok" }`.
46
+ */
47
+ tools?: ToolPort;
48
+ /** Upper bound for one step (default 60 s). */
49
+ stepTimeoutMs?: number;
50
+ };
51
+ /** The default eval tool port: records nothing, calls nothing, every tool succeeds. */
52
+ export declare const evalToolPort: ToolPort;
53
+ /**
54
+ * Runs one test-case scenario through the session engine (rework-autopilot 1.4): the real state
55
+ * machine and Agent on an in-memory session, replies streamed sentence by sentence, tool calls
56
+ * read back from the structured tool messages in the history.
57
+ */
58
+ export declare function evaluateSessionScenario(options: SessionEvalOptions, scenario: EvalScenario, onStepResult?: (scenarioRef: string, report: StepEvaluationReport) => void | Promise<void>): Promise<SessionScenarioReport>;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evalToolPort = void 0;
4
+ exports.evaluateSessionScenario = evaluateSessionScenario;
5
+ const createAutopilot_1 = require("../../../session/createAutopilot");
6
+ const evaluateTextResponse_1 = require("../evaluateTextResponse");
7
+ const evaluateToolCalls_1 = require("../evaluateToolCalls");
8
+ const EvalSession_1 = require("./EvalSession");
9
+ /** The default eval tool port: records nothing, calls nothing, every tool succeeds. */
10
+ exports.evalToolPort = {
11
+ run: async () => ({
12
+ ok: true,
13
+ result: { status: "ok" }
14
+ })
15
+ };
16
+ const SETTLE_POLL_MS = 10;
17
+ /** Resolves once the session has had no new output and no say in flight for two polls. */
18
+ async function settle(session) {
19
+ let seen = -1;
20
+ let quiet = 0;
21
+ while (quiet < 2) {
22
+ await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
23
+ if (!session.busy && session.outputs.length === seen)
24
+ quiet++;
25
+ else
26
+ quiet = 0;
27
+ seen = session.outputs.length;
28
+ }
29
+ }
30
+ function parseArgs(call) {
31
+ try {
32
+ return call.arguments ? JSON.parse(call.arguments) : {};
33
+ }
34
+ catch {
35
+ return {};
36
+ }
37
+ }
38
+ /**
39
+ * Runs one test-case scenario through the session engine (rework-autopilot 1.4): the real state
40
+ * machine and Agent on an in-memory session, replies streamed sentence by sentence, tool calls
41
+ * read back from the structured tool messages in the history.
42
+ */
43
+ async function evaluateSessionScenario(options, scenario, onStepResult) {
44
+ const { assistant, testTextSimilarity } = options;
45
+ const stepTimeoutMs = options.stepTimeoutMs ?? 60000;
46
+ const session = new EvalSession_1.EvalSession(scenario.telephonyContext);
47
+ let agent;
48
+ let signal;
49
+ let thinkError;
50
+ const traces = [];
51
+ // Think errors are swallowed by the engine (it apologises); keep them for the report.
52
+ const think = {
53
+ stream: (messages, tools, abort) => (async function* () {
54
+ try {
55
+ yield* options.think.stream(messages, tools, abort);
56
+ }
57
+ catch (e) {
58
+ if (!abort.aborted) {
59
+ thinkError = e;
60
+ signal?.();
61
+ }
62
+ throw e;
63
+ }
64
+ })()
65
+ };
66
+ const handler = (0, createAutopilot_1.createAutopilotHandler)({
67
+ assistant: { ...assistant, testCases: undefined },
68
+ think,
69
+ tools: options.tools ?? exports.evalToolPort,
70
+ recorder: {
71
+ turn: (t) => {
72
+ traces.push(t);
73
+ signal?.();
74
+ },
75
+ note: () => undefined
76
+ },
77
+ onAgent: (a) => (agent = a)
78
+ });
79
+ session.onOutput((o) => {
80
+ if (o.kind === "hangup" || o.kind === "transfer")
81
+ signal?.();
82
+ });
83
+ const running = handler(session);
84
+ const results = [];
85
+ try {
86
+ await settle(session); // greeting
87
+ for (const step of scenario.conversation) {
88
+ const type = step.expected.text.type;
89
+ const report = {
90
+ humanInput: step.userInput,
91
+ expectedResponse: step.expected.text.response,
92
+ aiResponse: "",
93
+ evaluationType: type,
94
+ passed: true,
95
+ streaming: { pieces: [] },
96
+ toolMessages: []
97
+ };
98
+ results.push(report);
99
+ if (session.ended) {
100
+ report.passed = false;
101
+ report.errorMessage = `The call had already ended before "${step.userInput}".`;
102
+ if (onStepResult)
103
+ await onStepResult(scenario.ref, report);
104
+ continue;
105
+ }
106
+ const outputsFrom = session.outputs.length;
107
+ const historyFrom = agent?.history.length ?? 0;
108
+ const tracesFrom = traces.length;
109
+ thinkError = undefined;
110
+ let timer;
111
+ const done = new Promise((resolve) => {
112
+ signal = () => resolve(true);
113
+ timer = setTimeout(() => resolve(false), stepTimeoutMs);
114
+ });
115
+ session.turn(step.userInput);
116
+ const finished = await done;
117
+ clearTimeout(timer);
118
+ signal = undefined;
119
+ await settle(session);
120
+ const outputs = session.outputs.slice(outputsFrom);
121
+ const says = outputs.filter((o) => o.kind === "say");
122
+ const reply = says.filter((s) => s.streamed);
123
+ const history = agent?.history.slice(historyFrom) ?? [];
124
+ const toolCalls = history.flatMap((m) => m.role === "assistant" ? (m.tool_calls ?? []) : []);
125
+ const builtIn = toolCalls.find((c) => c.name === "hangup" || c.name === "transfer");
126
+ const trace = traces[tracesFrom];
127
+ report.streaming = {
128
+ pieces: reply.flatMap((s) => s.pieces),
129
+ firstTokenMs: trace?.firstTokenMs,
130
+ firstSpeakMs: trace?.firstSpeakMs
131
+ };
132
+ report.toolMessages = history.filter((m) => m.role === "tool" || (m.role === "assistant" && m.tool_calls));
133
+ // What the caller hears: the streamed reply; for hangup or transfer, the message the
134
+ // engine says for it (as the legacy evals compared).
135
+ report.aiResponse = builtIn
136
+ ? builtIn.name === "hangup"
137
+ ? assistant.conversationSettings.goodbyeMessage
138
+ : (assistant.conversationSettings.transferOptions?.message ?? "")
139
+ : reply
140
+ .map((s) => s.text)
141
+ .join("")
142
+ .trim();
143
+ if (thinkError) {
144
+ report.passed = false;
145
+ report.aiResponse = says.map((s) => s.text).join(" ");
146
+ report.errorMessage = `Language model error for input "${step.userInput}": ${thinkError.message}`;
147
+ }
148
+ else if (!finished) {
149
+ report.passed = false;
150
+ report.errorMessage = `No reply within ${stepTimeoutMs} ms for "${step.userInput}".`;
151
+ }
152
+ else {
153
+ const text = await (0, evaluateTextResponse_1.evaluateTextResponse)({ type, response: step.expected.text.response }, report.aiResponse, testTextSimilarity);
154
+ if (!text.passed) {
155
+ report.passed = false;
156
+ report.errorMessage = text.errorMessage;
157
+ }
158
+ if (step.expected.tools && step.expected.tools.length > 0) {
159
+ const tools = (0, evaluateToolCalls_1.evaluateToolCalls)(step.expected.tools, toolCalls.map((c) => ({ name: c.name, args: parseArgs(c) })));
160
+ report.toolEvaluations = tools.evaluations;
161
+ if (!tools.passed) {
162
+ report.passed = false;
163
+ report.errorMessage = report.errorMessage
164
+ ? `${report.errorMessage} ${tools.errorMessage}`
165
+ : tools.errorMessage;
166
+ }
167
+ }
168
+ }
169
+ if (onStepResult)
170
+ await onStepResult(scenario.ref, report);
171
+ }
172
+ }
173
+ finally {
174
+ session.end();
175
+ await running.catch(() => undefined);
176
+ }
177
+ return {
178
+ scenarioRef: scenario.ref,
179
+ overallPassed: results.every((s) => s.passed),
180
+ steps: results
181
+ };
182
+ }
@@ -16,5 +16,6 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
- export * from "./SileroVad";
20
- export * from "./types";
19
+ export * from "./EvalSession";
20
+ export * from "./evaluateSessionScenario";
21
+ export * from "./runSessionEval";
@@ -32,5 +32,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
32
32
  * See the License for the specific language governing permissions and
33
33
  * limitations under the License.
34
34
  */
35
- __exportStar(require("./SileroVad"), exports);
36
- __exportStar(require("./types"), exports);
35
+ __exportStar(require("./EvalSession"), exports);
36
+ __exportStar(require("./evaluateSessionScenario"), exports);
37
+ __exportStar(require("./runSessionEval"), exports);
@@ -0,0 +1,17 @@
1
+ import { SessionAssistant } from "../../../session/config";
2
+ import { Think } from "../../../session/think";
3
+ import { RunEvalCallbacks } from "../types";
4
+ import { SessionEvalOptions, SessionScenarioReport } from "./evaluateSessionScenario";
5
+ export type RunSessionEvalOptions = Omit<SessionEvalOptions, "testTextSimilarity"> & {
6
+ /** Defaults to the OpenAI similarity judge from the assistant's testCases. */
7
+ testTextSimilarity?: SessionEvalOptions["testTextSimilarity"];
8
+ /** The judge's key (the testCases model is always OpenAI). */
9
+ evaluationApiKey?: string;
10
+ };
11
+ /**
12
+ * The session-engine Think for an assistant whose catalog provider speaks OpenAI-compatible Chat
13
+ * Completions with a known endpoint (openai, groq, ollama); undefined otherwise.
14
+ */
15
+ export declare function sessionThinkFor(assistant: SessionAssistant, apiKey: string | undefined): Think | undefined;
16
+ /** Runs every test-case scenario through the session engine, streaming results. */
17
+ export declare function runSessionEval(options: RunSessionEvalOptions, callbacks?: Partial<RunEvalCallbacks>): Promise<SessionScenarioReport[]>;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sessionThinkFor = sessionThinkFor;
4
+ exports.runSessionEval = runSessionEval;
5
+ /**
6
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
7
+ * http://github.com/fonoster/fonoster
8
+ *
9
+ * This file is part of Fonoster
10
+ *
11
+ * Licensed under the MIT License (the "License");
12
+ * you may not use this file except in compliance with
13
+ * the License. You may obtain a copy of the License at
14
+ *
15
+ * https://opensource.org/licenses/MIT
16
+ *
17
+ * Unless required by applicable law or agreed to in writing, software
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ * See the License for the specific language governing permissions and
21
+ * limitations under the License.
22
+ */
23
+ const common_1 = require("@fonoster/common");
24
+ const openai_1 = require("@fonoster/community/openai");
25
+ const createTestTextSimilarity_1 = require("../createTestTextSimilarity");
26
+ const textSimilaryPrompt_1 = require("../textSimilaryPrompt");
27
+ const evaluateSessionScenario_1 = require("./evaluateSessionScenario");
28
+ /**
29
+ * The session-engine Think for an assistant whose catalog provider speaks OpenAI-compatible Chat
30
+ * Completions with a known endpoint (openai, groq, ollama); undefined otherwise.
31
+ */
32
+ function sessionThinkFor(assistant, apiKey) {
33
+ const id = assistant.languageModel.model;
34
+ const slash = id.indexOf("/");
35
+ const baseUrl = openai_1.OPENAI_COMPATIBLE[id.slice(0, slash)];
36
+ if (!baseUrl)
37
+ return undefined;
38
+ return new openai_1.OpenAiCompatibleThink({ baseUrl, apiKey }, id.slice(slash + 1), {
39
+ temperature: assistant.languageModel.temperature,
40
+ maxTokens: assistant.languageModel.maxTokens
41
+ });
42
+ }
43
+ /** Runs every test-case scenario through the session engine, streaming results. */
44
+ async function runSessionEval(options, callbacks) {
45
+ const testCases = common_1.testCasesSchema.parse(options.assistant.testCases);
46
+ const testTextSimilarity = options.testTextSimilarity ??
47
+ (0, createTestTextSimilarity_1.createTestTextSimilarity)({
48
+ provider: testCases.evalsLanguageModel.provider,
49
+ model: testCases.evalsLanguageModel.model,
50
+ apiKey: options.evaluationApiKey ?? testCases.evalsLanguageModel.apiKey
51
+ }, testCases.evalsSystemPrompt || textSimilaryPrompt_1.textSimilaryPrompt);
52
+ const reports = [];
53
+ for (const scenario of testCases.scenarios) {
54
+ const report = await (0, evaluateSessionScenario_1.evaluateSessionScenario)({ ...options, testTextSimilarity }, scenario, callbacks?.onStepResult);
55
+ reports.push(report);
56
+ await callbacks?.onScenarioComplete?.(report.scenarioRef, report.overallPassed);
57
+ }
58
+ return reports;
59
+ }
@@ -17,8 +17,7 @@
17
17
  * limitations under the License.
18
18
  */
19
19
  import { StepEvaluationReport } from "@fonoster/types";
20
- import { AssistantConfig } from "../../assistants";
21
- import { LanguageModel } from "../types";
20
+ import { EvalAssistant, LanguageModel } from "../types";
22
21
  type EvalExpectedTool = {
23
22
  tool: string;
24
23
  parameters?: Record<string, unknown>;
@@ -46,18 +45,20 @@ type EvalScenario = {
46
45
  };
47
46
  type EvaluateIntelligenceRequest = {
48
47
  intelligence: {
49
- productRef: string;
50
- config: AssistantConfig;
48
+ /** Ignored: the assistant's catalog id names the provider. */
49
+ productRef?: string;
50
+ /** A session assistant (with testCases), as a protobuf Struct. */
51
+ config: unknown;
51
52
  };
52
53
  };
53
54
  type EvaluateStepParams = {
54
55
  step: EvalConversationStep;
55
56
  languageModel: LanguageModel;
56
57
  testTextSimilarity: (text1: string, text2: string) => Promise<boolean>;
57
- assistantConfig: AssistantConfig;
58
+ assistantConfig: EvalAssistant;
58
59
  };
59
60
  type ScenarioEvaluationRequest = {
60
- assistantConfig: AssistantConfig;
61
+ assistantConfig: EvalAssistant;
61
62
  scenario: EvalScenario;
62
63
  languageModel: LanguageModel;
63
64
  testTextSimilarity: (text1: string, text2: string) => Promise<boolean>;
@@ -16,11 +16,29 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
+ import { SessionConversationSettings, SessionLanguageModel, testCasesSchema } from "@fonoster/common";
19
20
  import { CallDirection } from "@fonoster/types";
20
21
  import { BaseChatModel } from "@langchain/core/language_models/chat_models";
21
22
  import { ToolCall } from "@langchain/core/messages/tool";
23
+ import { z } from "zod";
22
24
  import { KnowledgeBase } from "../knowledge";
23
25
  import { Tool } from "../tools/types";
26
+ /** The language model vendors the LangChain models (used by the evaluations only) implement. */
27
+ type LlmProvider = "openai" | "groq" | "ollama" | "google" | "anthropic";
28
+ /**
29
+ * A session assistant as the evaluations run it (rework-autopilot 8.2): the catalog id split into
30
+ * provider and model, and the keys the control plane resolved for this run. Never stored.
31
+ */
32
+ type EvalAssistant = {
33
+ conversationSettings: SessionConversationSettings;
34
+ languageModel: Omit<SessionLanguageModel, "model" | "provider" | "apiKey" | "baseUrl"> & {
35
+ provider: LlmProvider;
36
+ model: string;
37
+ apiKey?: string;
38
+ baseUrl?: string;
39
+ };
40
+ testCases?: z.infer<typeof testCasesSchema>;
41
+ };
24
42
  type LanguageModel = {
25
43
  invoke: (text: string, isReentry?: boolean) => Promise<InvocationResult>;
26
44
  };
@@ -49,4 +67,4 @@ type TelephonyContext = {
49
67
  callerNumber: string;
50
68
  metadata?: Record<string, string>;
51
69
  };
52
- export { BaseModelParams, InvocationResult, LanguageModel, LanguageModelParams, TelephonyContext };
70
+ export { BaseModelParams, EvalAssistant, LlmProvider, InvocationResult, LanguageModel, LanguageModelParams, TelephonyContext };