@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.
- package/README.md +14 -51
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -5
- package/dist/knowledge/AbstractKnowledgeBase.d.ts +3 -2
- package/dist/knowledge/AbstractKnowledgeBase.js +2 -7
- package/dist/knowledge/FilesKnowledgeBase.d.ts +1 -1
- package/dist/knowledge/types.d.ts +1 -1
- package/dist/models/LanguageModelFactory.d.ts +5 -24
- package/dist/models/LanguageModelFactory.js +0 -1
- package/dist/models/createLanguageModel.d.ts +2 -2
- package/dist/models/evaluations/createEvalEffectiveConfig.d.ts +10 -106
- package/dist/models/evaluations/createEvalEffectiveConfig.js +47 -7
- package/dist/models/evaluations/createEvaluateIntelligence.d.ts +10 -20
- package/dist/models/evaluations/createEvaluateIntelligence.js +32 -23
- package/dist/models/evaluations/evalTestCases.d.ts +2 -2
- package/dist/models/evaluations/extractAiResponse.d.ts +2 -3
- package/dist/models/evaluations/index.d.ts +1 -0
- package/dist/models/evaluations/index.js +1 -0
- package/dist/models/evaluations/runEval.d.ts +2 -20
- package/dist/models/evaluations/session/EvalSession.d.ts +73 -0
- package/dist/models/evaluations/session/EvalSession.js +134 -0
- package/dist/models/evaluations/session/evaluateSessionScenario.d.ts +58 -0
- package/dist/models/evaluations/session/evaluateSessionScenario.js +182 -0
- package/dist/{vad → models/evaluations/session}/index.d.ts +3 -2
- package/dist/{vad → models/evaluations/session}/index.js +3 -2
- package/dist/models/evaluations/session/runSessionEval.d.ts +17 -0
- package/dist/models/evaluations/session/runSessionEval.js +59 -0
- package/dist/models/evaluations/types.d.ts +7 -6
- package/dist/models/types.d.ts +19 -1
- package/dist/session/Agent.d.ts +138 -0
- package/dist/session/Agent.js +474 -0
- package/dist/{loadAssistantFromAPI.d.ts → session/AsyncQueue.d.ts} +12 -7
- package/dist/session/AsyncQueue.js +67 -0
- package/dist/{machine/guards/index.d.ts → session/config.d.ts} +2 -2
- package/dist/session/config.js +27 -0
- package/dist/session/convertLegacyAssistant.d.ts +77 -0
- package/dist/session/convertLegacyAssistant.js +118 -0
- package/dist/session/createAutopilot.d.ts +47 -0
- package/dist/session/createAutopilot.js +119 -0
- package/dist/session/hosted.d.ts +456 -0
- package/dist/session/hosted.js +506 -0
- package/dist/{assistants → session}/index.d.ts +11 -2
- package/dist/{machine/actors → session}/index.js +11 -3
- package/dist/{machine/actions/appendSpeech.js → session/knowledge.d.ts} +14 -17
- package/dist/session/knowledge.js +10 -0
- package/dist/session/llm.d.ts +86 -0
- package/dist/session/llm.js +83 -0
- package/dist/session/machine.d.ts +206 -0
- package/dist/session/machine.js +126 -0
- package/dist/session/main.js +44 -0
- package/dist/session/sentences.d.ts +5 -0
- package/dist/session/sentences.js +53 -0
- package/dist/{machine/actors/index.d.ts → session/think.d.ts} +2 -3
- package/dist/{machine/actions/announceSystemError.js → session/think.js} +8 -10
- package/dist/session/tools.d.ts +22 -0
- package/dist/session/tools.js +83 -0
- package/dist/tools/builtin/transferToolDefinition.js +1 -1
- package/dist/voice/index.d.ts +0 -1
- package/dist/voice/index.js +0 -1
- package/dist/voice/types.d.ts +5 -18
- package/package.json +13 -14
- package/dist/Autopilot.d.ts +0 -14
- package/dist/Autopilot.js +0 -133
- package/dist/assistants/index.js +0 -36
- package/dist/assistants/loadAndValidateAssistant.d.ts +0 -3
- package/dist/assistants/loadAndValidateAssistant.js +0 -49
- package/dist/assistants/types.d.ts +0 -24
- package/dist/assistants/types.js +0 -2
- package/dist/envs.d.ts +0 -15
- package/dist/envs.js +0 -78
- package/dist/handleVoiceRequest.d.ts +0 -3
- package/dist/handleVoiceRequest.js +0 -167
- package/dist/loadAssistantConfigFromFile.d.ts +0 -3
- package/dist/loadAssistantConfigFromFile.js +0 -68
- package/dist/loadAssistantFromAPI.js +0 -98
- package/dist/machine/actions/announceSystemError.d.ts +0 -4
- package/dist/machine/actions/appendSpeech.d.ts +0 -1
- package/dist/machine/actions/cleanSpeech.d.ts +0 -1
- package/dist/machine/actions/cleanSpeech.js +0 -23
- package/dist/machine/actions/goodbye.d.ts +0 -4
- package/dist/machine/actions/goodbye.js +0 -31
- package/dist/machine/actions/increaseIdleTimeoutCount.d.ts +0 -1
- package/dist/machine/actions/increaseIdleTimeoutCount.js +0 -31
- package/dist/machine/actions/index.d.ts +0 -26
- package/dist/machine/actions/index.js +0 -37
- package/dist/machine/actions/interruptPlayback.d.ts +0 -4
- package/dist/machine/actions/interruptPlayback.js +0 -43
- package/dist/machine/actions/resetIdleTimeoutCount.d.ts +0 -1
- package/dist/machine/actions/resetIdleTimeoutCount.js +0 -31
- package/dist/machine/actions/resetState.d.ts +0 -1
- package/dist/machine/actions/resetState.js +0 -32
- package/dist/machine/actors/doAnnounceIdleTimeout.d.ts +0 -4
- package/dist/machine/actors/doAnnounceIdleTimeout.js +0 -41
- package/dist/machine/actors/doGreetUser.d.ts +0 -4
- package/dist/machine/actors/doGreetUser.js +0 -45
- package/dist/machine/actors/doProcessUserRequest.d.ts +0 -4
- package/dist/machine/actors/doProcessUserRequest.js +0 -75
- package/dist/machine/context.d.ts +0 -52
- package/dist/machine/context.js +0 -28
- package/dist/machine/delays.d.ts +0 -30
- package/dist/machine/delays.js +0 -37
- package/dist/machine/guards/hasSpeechResult.d.ts +0 -4
- package/dist/machine/guards/hasSpeechResult.js +0 -30
- package/dist/machine/guards/idleTimeoutCountExceedsMax.d.ts +0 -4
- package/dist/machine/guards/idleTimeoutCountExceedsMax.js +0 -31
- package/dist/machine/guards/index.js +0 -25
- package/dist/machine/index.d.ts +0 -19
- package/dist/machine/index.js +0 -23
- package/dist/machine/machine.d.ts +0 -408
- package/dist/machine/machine.js +0 -206
- package/dist/machine/setup.d.ts +0 -328
- package/dist/machine/setup.js +0 -116
- package/dist/machine/types.d.ts +0 -54
- package/dist/machine/types.js +0 -2
- package/dist/sendConversationEndedEvent.d.ts +0 -32
- package/dist/sendConversationEndedEvent.js +0 -66
- package/dist/sendConversationStartedEvent.d.ts +0 -6
- package/dist/sendConversationStartedEvent.js +0 -63
- package/dist/server.d.ts +0 -2
- package/dist/server.js +0 -25
- package/dist/types.d.ts +0 -40
- package/dist/types.js +0 -8
- package/dist/vad/SileroVad.d.ts +0 -18
- package/dist/vad/SileroVad.js +0 -40
- package/dist/vad/SileroVadModel.d.ts +0 -14
- package/dist/vad/SileroVadModel.js +0 -83
- package/dist/vad/chunkToFloat32Array.d.ts +0 -20
- package/dist/vad/chunkToFloat32Array.js +0 -34
- package/dist/vad/createVad.d.ts +0 -3
- package/dist/vad/createVad.js +0 -126
- package/dist/vad/types.d.ts +0 -60
- package/dist/vad/types.js +0 -2
- package/dist/vadWorker.js +0 -36
- package/dist/voice/Voice.d.ts +0 -61
- package/dist/voice/Voice.js +0 -96
- package/dist/voiceServerSetup.d.ts +0 -2
- package/dist/voiceServerSetup.js +0 -29
- /package/dist/{vadWorker.d.ts → session/main.d.ts} +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { SessionAssistant } from "./config";
|
|
2
|
+
import { KnowledgePort } from "./knowledge";
|
|
3
|
+
import { ChatMessage, Think } from "./think";
|
|
4
|
+
import { ToolPort } from "./tools";
|
|
5
|
+
import type { Session } from "@fonoster/voice";
|
|
6
|
+
export type SpokenSource = "greeting" | "reply" | "idle" | "error";
|
|
7
|
+
export type AgentEvent = {
|
|
8
|
+
type: "TURN_USER_STARTED";
|
|
9
|
+
} | {
|
|
10
|
+
type: "TURN_LIKELY_ENDED";
|
|
11
|
+
text: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "TURN_RESUMED";
|
|
14
|
+
} | {
|
|
15
|
+
type: "TURN_ENDED";
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "TURN_BACKCHANNEL";
|
|
19
|
+
} | {
|
|
20
|
+
type: "REPLY_SPEAKING";
|
|
21
|
+
}
|
|
22
|
+
/** An output finished; `source` says which, so stale events from an earlier phase are ignored. */
|
|
23
|
+
| {
|
|
24
|
+
type: "SPOKEN";
|
|
25
|
+
source: SpokenSource;
|
|
26
|
+
} | {
|
|
27
|
+
type: "IDLE";
|
|
28
|
+
} | {
|
|
29
|
+
type: "MAX_DURATION";
|
|
30
|
+
} | {
|
|
31
|
+
type: "HANGUP_REQUESTED";
|
|
32
|
+
} | {
|
|
33
|
+
type: "TRANSFER_REQUESTED";
|
|
34
|
+
} | {
|
|
35
|
+
type: "SESSION_END";
|
|
36
|
+
};
|
|
37
|
+
/** One turn's trace for the call record (rework-autopilot A10). */
|
|
38
|
+
export type TurnTrace = {
|
|
39
|
+
userText: string;
|
|
40
|
+
replyText: string;
|
|
41
|
+
heardText: string;
|
|
42
|
+
interrupted: boolean;
|
|
43
|
+
toolCalls: {
|
|
44
|
+
name: string;
|
|
45
|
+
ok: boolean;
|
|
46
|
+
}[];
|
|
47
|
+
/** ms from turn.ended to the first token, and to the first speak sent. */
|
|
48
|
+
firstTokenMs?: number;
|
|
49
|
+
firstSpeakMs?: number;
|
|
50
|
+
};
|
|
51
|
+
export interface Recorder {
|
|
52
|
+
turn(trace: TurnTrace): void;
|
|
53
|
+
note(event: Record<string, unknown>): void;
|
|
54
|
+
}
|
|
55
|
+
/** Writes turn traces and notes to the call record through the session (record.note). */
|
|
56
|
+
export declare function sessionRecorder(session: Session): Recorder;
|
|
57
|
+
export declare const logRecorder: Recorder;
|
|
58
|
+
/** The side effects of Autopilot: everything the state machine asks it to do on the session. */
|
|
59
|
+
export declare class Agent {
|
|
60
|
+
private readonly session;
|
|
61
|
+
private readonly assistant;
|
|
62
|
+
private readonly think;
|
|
63
|
+
private readonly tools;
|
|
64
|
+
private readonly recorder;
|
|
65
|
+
private readonly knowledge?;
|
|
66
|
+
readonly history: ChatMessage[];
|
|
67
|
+
private reply?;
|
|
68
|
+
private inflight?;
|
|
69
|
+
private prepared?;
|
|
70
|
+
private idleTimer?;
|
|
71
|
+
private maxTimer?;
|
|
72
|
+
private idleCount;
|
|
73
|
+
private resumes;
|
|
74
|
+
/** Whether a reply is prepared on turn.likely_ended (off at high patience or once adapted). */
|
|
75
|
+
private prepareEarly;
|
|
76
|
+
private stopped;
|
|
77
|
+
send: (event: AgentEvent) => void;
|
|
78
|
+
constructor(session: Session, assistant: SessionAssistant, think: Think, tools: ToolPort, recorder?: Recorder, knowledge?: KnowledgePort);
|
|
79
|
+
get settings(): {
|
|
80
|
+
firstMessage?: string;
|
|
81
|
+
systemPrompt?: string;
|
|
82
|
+
goodbyeMessage?: string;
|
|
83
|
+
systemErrorMessage?: string;
|
|
84
|
+
initialDtmf?: string;
|
|
85
|
+
maxSessionDuration?: number;
|
|
86
|
+
transferOptions?: {
|
|
87
|
+
message?: string;
|
|
88
|
+
phoneNumber?: string;
|
|
89
|
+
};
|
|
90
|
+
idleOptions?: {
|
|
91
|
+
message?: string;
|
|
92
|
+
timeout?: number;
|
|
93
|
+
maxTimeoutCount?: number;
|
|
94
|
+
};
|
|
95
|
+
allowUserBargeIn?: boolean;
|
|
96
|
+
patience?: "low" | "normal" | "high";
|
|
97
|
+
hints?: string[];
|
|
98
|
+
filler?: {
|
|
99
|
+
url?: string;
|
|
100
|
+
};
|
|
101
|
+
voice?: string;
|
|
102
|
+
language?: string;
|
|
103
|
+
vad?: any;
|
|
104
|
+
maxSpeechWaitTimeout?: any;
|
|
105
|
+
};
|
|
106
|
+
/** Greeting: subscribe to the estimate, send initial DTMF, speak the first message. */
|
|
107
|
+
greet(): Promise<void>;
|
|
108
|
+
stop(): void;
|
|
109
|
+
armIdle(): void;
|
|
110
|
+
disarmIdle(): void;
|
|
111
|
+
/** The next idle timeout ends the call (the goodbye counts as the last announcement). */
|
|
112
|
+
idleExhausted(): boolean;
|
|
113
|
+
sayIdle(): Promise<void>;
|
|
114
|
+
/** turn.likely_ended: start generating silently (when preparing early). */
|
|
115
|
+
prepare(text: string): void;
|
|
116
|
+
/** turn.resumed: the caller kept talking; the prepared reply is dropped. */
|
|
117
|
+
resumed(): void;
|
|
118
|
+
clear(): Promise<void>;
|
|
119
|
+
abortReply(): void;
|
|
120
|
+
/** turn.ended: answer the caller, streaming sentences into one speak id. */
|
|
121
|
+
answer(text: string): Promise<void>;
|
|
122
|
+
private respond;
|
|
123
|
+
private speakRound;
|
|
124
|
+
private runTools;
|
|
125
|
+
goodbye(): Promise<void>;
|
|
126
|
+
transfer(): Promise<void>;
|
|
127
|
+
private messages;
|
|
128
|
+
/** Knowledge for this turn, as a system message for the request only (never in history). */
|
|
129
|
+
private contextFor;
|
|
130
|
+
private toolSpecs;
|
|
131
|
+
private startStream;
|
|
132
|
+
private takePrepared;
|
|
133
|
+
private discardPrepared;
|
|
134
|
+
private pump;
|
|
135
|
+
private speechOptions;
|
|
136
|
+
/** Runs session work; a session that has ended stops everything quietly. */
|
|
137
|
+
private safely;
|
|
138
|
+
}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Agent = exports.logRecorder = void 0;
|
|
4
|
+
exports.sessionRecorder = sessionRecorder;
|
|
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 logger_1 = require("@fonoster/logger");
|
|
24
|
+
const voice_1 = require("@fonoster/voice");
|
|
25
|
+
const AsyncQueue_1 = require("./AsyncQueue");
|
|
26
|
+
const knowledge_1 = require("./knowledge");
|
|
27
|
+
const sentences_1 = require("./sentences");
|
|
28
|
+
const logger = (0, logger_1.getLogger)({ service: "autopilot", filePath: __filename });
|
|
29
|
+
const MAX_TOOL_ROUNDS = 4;
|
|
30
|
+
const BUILT_IN_TOOLS = [
|
|
31
|
+
{
|
|
32
|
+
name: "hangup",
|
|
33
|
+
description: "End the call when the conversation is over.",
|
|
34
|
+
parameters: { type: "object", properties: {} }
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "transfer",
|
|
38
|
+
description: "Transfer the caller to an operator (a person).",
|
|
39
|
+
parameters: { type: "object", properties: {} }
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
/** Writes turn traces and notes to the call record through the session (record.note). */
|
|
43
|
+
function sessionRecorder(session) {
|
|
44
|
+
const write = (data) => void session.note(data).catch(() => undefined);
|
|
45
|
+
return {
|
|
46
|
+
turn: (trace) => write({ kind: "autopilot.turn", ...trace }),
|
|
47
|
+
note: (event) => write({ kind: "autopilot.note", ...event })
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
exports.logRecorder = {
|
|
51
|
+
turn: (trace) => logger.verbose("autopilot turn", trace),
|
|
52
|
+
note: (event) => logger.verbose("autopilot note", event)
|
|
53
|
+
};
|
|
54
|
+
/** The side effects of Autopilot: everything the state machine asks it to do on the session. */
|
|
55
|
+
class Agent {
|
|
56
|
+
constructor(session, assistant, think, tools, recorder = exports.logRecorder, knowledge) {
|
|
57
|
+
this.session = session;
|
|
58
|
+
this.assistant = assistant;
|
|
59
|
+
this.think = think;
|
|
60
|
+
this.tools = tools;
|
|
61
|
+
this.recorder = recorder;
|
|
62
|
+
this.knowledge = knowledge;
|
|
63
|
+
this.history = [];
|
|
64
|
+
this.idleCount = 0;
|
|
65
|
+
this.resumes = 0;
|
|
66
|
+
this.stopped = false;
|
|
67
|
+
this.send = () => undefined;
|
|
68
|
+
this.prepareEarly = assistant.conversationSettings.patience !== "high";
|
|
69
|
+
}
|
|
70
|
+
get settings() {
|
|
71
|
+
return this.assistant.conversationSettings;
|
|
72
|
+
}
|
|
73
|
+
// ---- lifecycle ----------------------------------------------------------------------
|
|
74
|
+
/** Greeting: subscribe to the estimate, send initial DTMF, speak the first message. */
|
|
75
|
+
async greet() {
|
|
76
|
+
this.maxTimer = setTimeout(() => this.send({ type: "MAX_DURATION" }), this.settings.maxSessionDuration);
|
|
77
|
+
await this.safely(async () => {
|
|
78
|
+
await this.session.subscribe(["turn.estimate"]);
|
|
79
|
+
// The gateway's Pacing policy owns turn timing; Autopilot only tells it how patient to be.
|
|
80
|
+
// Recognition hints (names, brands) add to the app's defaults at the gateway (VO-B3).
|
|
81
|
+
const hints = this.settings.hints?.length
|
|
82
|
+
? this.settings.hints
|
|
83
|
+
: undefined;
|
|
84
|
+
if (this.settings.patience !== "normal" || hints)
|
|
85
|
+
await this.session.listen({
|
|
86
|
+
...(this.settings.patience !== "normal"
|
|
87
|
+
? { patience: this.settings.patience }
|
|
88
|
+
: {}),
|
|
89
|
+
...(hints ? { hints } : {})
|
|
90
|
+
});
|
|
91
|
+
if (this.settings.initialDtmf)
|
|
92
|
+
await this.session.playDtmf(this.settings.initialDtmf);
|
|
93
|
+
if (this.settings.firstMessage) {
|
|
94
|
+
const r = await this.session.say(this.settings.firstMessage, this.speechOptions());
|
|
95
|
+
this.history.push({ role: "assistant", content: r.heardText });
|
|
96
|
+
}
|
|
97
|
+
this.send({ type: "SPOKEN", source: "greeting" });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
stop() {
|
|
101
|
+
this.stopped = true;
|
|
102
|
+
this.abortReply();
|
|
103
|
+
this.discardPrepared();
|
|
104
|
+
clearTimeout(this.idleTimer);
|
|
105
|
+
clearTimeout(this.maxTimer);
|
|
106
|
+
}
|
|
107
|
+
// ---- idle -------------------------------------------------------------------------
|
|
108
|
+
armIdle() {
|
|
109
|
+
clearTimeout(this.idleTimer);
|
|
110
|
+
const idle = this.settings.idleOptions;
|
|
111
|
+
if (idle)
|
|
112
|
+
this.idleTimer = setTimeout(() => this.send({ type: "IDLE" }), idle.timeout);
|
|
113
|
+
}
|
|
114
|
+
disarmIdle() {
|
|
115
|
+
clearTimeout(this.idleTimer);
|
|
116
|
+
}
|
|
117
|
+
/** The next idle timeout ends the call (the goodbye counts as the last announcement). */
|
|
118
|
+
idleExhausted() {
|
|
119
|
+
const max = this.settings.idleOptions?.maxTimeoutCount ?? 2;
|
|
120
|
+
return this.idleCount + 1 >= max;
|
|
121
|
+
}
|
|
122
|
+
async sayIdle() {
|
|
123
|
+
this.idleCount++;
|
|
124
|
+
await this.safely(async () => {
|
|
125
|
+
await this.session.say(this.settings.idleOptions.message, this.speechOptions());
|
|
126
|
+
this.send({ type: "SPOKEN", source: "idle" });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ---- pacing -----------------------------------------------------------------------
|
|
130
|
+
/** turn.likely_ended: start generating silently (when preparing early). */
|
|
131
|
+
prepare(text) {
|
|
132
|
+
if (!this.prepareEarly || !text.trim())
|
|
133
|
+
return;
|
|
134
|
+
this.discardPrepared();
|
|
135
|
+
const abort = new AbortController();
|
|
136
|
+
const chunks = new AsyncQueue_1.AsyncQueue();
|
|
137
|
+
const prepared = { text, abort, chunks, context: [] };
|
|
138
|
+
this.prepared = prepared;
|
|
139
|
+
void (async () => {
|
|
140
|
+
prepared.context = await this.contextFor(text, abort.signal);
|
|
141
|
+
if (abort.signal.aborted)
|
|
142
|
+
return chunks.end();
|
|
143
|
+
const messages = [
|
|
144
|
+
...this.messages(prepared.context),
|
|
145
|
+
{ role: "user", content: text }
|
|
146
|
+
];
|
|
147
|
+
await this.pump(this.think.stream(messages, this.toolSpecs(), abort.signal), chunks);
|
|
148
|
+
})();
|
|
149
|
+
}
|
|
150
|
+
/** turn.resumed: the caller kept talking; the prepared reply is dropped. */
|
|
151
|
+
resumed() {
|
|
152
|
+
this.discardPrepared();
|
|
153
|
+
this.resumes++;
|
|
154
|
+
if (this.resumes >= 3 && this.prepareEarly) {
|
|
155
|
+
// A caller who pauses often: stop preparing early for the rest of this call. The gateway
|
|
156
|
+
// adapts its own turn timing within the call (TT-B6), so Autopilot does not also change
|
|
157
|
+
// the patience it asked for (AP-B2).
|
|
158
|
+
this.prepareEarly = false;
|
|
159
|
+
this.recorder.note({
|
|
160
|
+
type: "pacing.adapted",
|
|
161
|
+
prepare: "on turn.ended",
|
|
162
|
+
resumes: this.resumes
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ---- replies ----------------------------------------------------------------------
|
|
167
|
+
async clear() {
|
|
168
|
+
await this.safely(() => this.session.stopSay());
|
|
169
|
+
}
|
|
170
|
+
abortReply() {
|
|
171
|
+
this.reply?.abort();
|
|
172
|
+
this.reply = undefined;
|
|
173
|
+
}
|
|
174
|
+
/** turn.ended: answer the caller, streaming sentences into one speak id. */
|
|
175
|
+
answer(text) {
|
|
176
|
+
const previous = this.inflight;
|
|
177
|
+
this.abortReply();
|
|
178
|
+
const abort = new AbortController();
|
|
179
|
+
this.reply = abort;
|
|
180
|
+
const run = this.respond(text, abort, previous);
|
|
181
|
+
this.inflight = run;
|
|
182
|
+
return run;
|
|
183
|
+
}
|
|
184
|
+
async respond(text, abort, previous) {
|
|
185
|
+
// Let a cancelled reply settle first, so what the caller heard of it is recorded before
|
|
186
|
+
// this turn (honest history, A6).
|
|
187
|
+
await previous?.catch(() => undefined);
|
|
188
|
+
this.idleCount = 0;
|
|
189
|
+
const turnEnded = Date.now();
|
|
190
|
+
this.history.push({ role: "user", content: text });
|
|
191
|
+
if (abort.signal.aborted)
|
|
192
|
+
return;
|
|
193
|
+
const trace = {
|
|
194
|
+
userText: text,
|
|
195
|
+
replyText: "",
|
|
196
|
+
heardText: "",
|
|
197
|
+
interrupted: false,
|
|
198
|
+
toolCalls: []
|
|
199
|
+
};
|
|
200
|
+
let preempt = false;
|
|
201
|
+
if (this.settings.filler) {
|
|
202
|
+
preempt = true;
|
|
203
|
+
void this.session
|
|
204
|
+
.play(this.settings.filler.url, { interruptible: true })
|
|
205
|
+
.catch(() => undefined);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const prepared = this.takePrepared(text);
|
|
209
|
+
const context = prepared
|
|
210
|
+
? prepared.context
|
|
211
|
+
: await this.contextFor(text, abort.signal);
|
|
212
|
+
let source = prepared?.chunks ?? this.startStream(abort.signal, context);
|
|
213
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
214
|
+
const outcome = await this.speakRound(source, abort, preempt, turnEnded, trace);
|
|
215
|
+
preempt = false;
|
|
216
|
+
if (abort.signal.aborted &&
|
|
217
|
+
!outcome.interrupted &&
|
|
218
|
+
!outcome.completed) {
|
|
219
|
+
// Cancelled by a new turn: only what the caller already heard is kept.
|
|
220
|
+
if (outcome.heard)
|
|
221
|
+
this.history.push({ role: "assistant", content: outcome.heard });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.history.push({
|
|
225
|
+
role: "assistant",
|
|
226
|
+
content: outcome.heard,
|
|
227
|
+
...(outcome.toolCalls.length ? { tool_calls: outcome.toolCalls } : {})
|
|
228
|
+
});
|
|
229
|
+
if (outcome.interrupted ||
|
|
230
|
+
!outcome.toolCalls.length ||
|
|
231
|
+
abort.signal.aborted)
|
|
232
|
+
break;
|
|
233
|
+
const builtIn = outcome.toolCalls.find((c) => c.name === "hangup" || c.name === "transfer");
|
|
234
|
+
if (builtIn) {
|
|
235
|
+
this.recorder.turn({
|
|
236
|
+
...trace,
|
|
237
|
+
toolCalls: [...trace.toolCalls, { name: builtIn.name, ok: true }]
|
|
238
|
+
});
|
|
239
|
+
this.send({
|
|
240
|
+
type: builtIn.name === "hangup"
|
|
241
|
+
? "HANGUP_REQUESTED"
|
|
242
|
+
: "TRANSFER_REQUESTED"
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await this.runTools(outcome.toolCalls, abort, trace);
|
|
247
|
+
if (abort.signal.aborted)
|
|
248
|
+
return;
|
|
249
|
+
source = this.startStream(abort.signal, context);
|
|
250
|
+
}
|
|
251
|
+
this.recorder.turn(trace);
|
|
252
|
+
if (this.reply === abort)
|
|
253
|
+
this.send({ type: "SPOKEN", source: "reply" });
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
if (abort.signal.aborted ||
|
|
257
|
+
e instanceof voice_1.SessionEndedError ||
|
|
258
|
+
this.stopped)
|
|
259
|
+
return;
|
|
260
|
+
logger.error(`autopilot reply failed: ${e.message}`);
|
|
261
|
+
await this.safely(async () => {
|
|
262
|
+
await this.session.say(this.settings.systemErrorMessage, this.speechOptions());
|
|
263
|
+
this.send({ type: "SPOKEN", source: "error" });
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async speakRound(source, abort, preempt, turnEnded, trace) {
|
|
268
|
+
const text = new AsyncQueue_1.AsyncQueue();
|
|
269
|
+
let toolCalls = [];
|
|
270
|
+
let full = "";
|
|
271
|
+
const pump = (async () => {
|
|
272
|
+
try {
|
|
273
|
+
for await (const chunk of source) {
|
|
274
|
+
if (abort.signal.aborted)
|
|
275
|
+
break;
|
|
276
|
+
if (chunk.type === "text") {
|
|
277
|
+
if (trace.firstTokenMs === undefined)
|
|
278
|
+
trace.firstTokenMs = Date.now() - turnEnded;
|
|
279
|
+
full += chunk.text;
|
|
280
|
+
text.push(chunk.text);
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
toolCalls = chunk.calls;
|
|
284
|
+
}
|
|
285
|
+
text.end();
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
text.fail(e);
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
const iterator = (0, sentences_1.sentences)(text)[Symbol.asyncIterator]();
|
|
292
|
+
const first = await iterator.next();
|
|
293
|
+
let heard = "";
|
|
294
|
+
let interrupted = false;
|
|
295
|
+
let completed = false;
|
|
296
|
+
if (!first.done && !abort.signal.aborted) {
|
|
297
|
+
this.send({ type: "REPLY_SPEAKING" });
|
|
298
|
+
trace.firstSpeakMs = Date.now() - turnEnded;
|
|
299
|
+
const pieces = (async function* () {
|
|
300
|
+
yield first.value;
|
|
301
|
+
for (;;) {
|
|
302
|
+
const next = await iterator.next();
|
|
303
|
+
if (next.done)
|
|
304
|
+
return;
|
|
305
|
+
yield next.value;
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
const result = await this.session.say(pieces, {
|
|
309
|
+
...this.speechOptions(),
|
|
310
|
+
preempt: preempt || undefined
|
|
311
|
+
});
|
|
312
|
+
heard = result.heardText;
|
|
313
|
+
interrupted = result.interrupted;
|
|
314
|
+
completed = !interrupted;
|
|
315
|
+
if (interrupted)
|
|
316
|
+
abort.abort();
|
|
317
|
+
}
|
|
318
|
+
await pump;
|
|
319
|
+
trace.replyText += full;
|
|
320
|
+
trace.heardText += heard;
|
|
321
|
+
trace.interrupted || (trace.interrupted = interrupted);
|
|
322
|
+
return { heard, interrupted, completed, toolCalls };
|
|
323
|
+
}
|
|
324
|
+
async runTools(calls, abort, trace) {
|
|
325
|
+
for (const call of calls) {
|
|
326
|
+
const tool = this.assistant.languageModel.tools.find((t) => t.name === call.name);
|
|
327
|
+
// The start message is spoken while the tool runs; the gateway queues the reply after it.
|
|
328
|
+
if (tool?.requestStartMessage) {
|
|
329
|
+
void this.session
|
|
330
|
+
.say(tool.requestStartMessage, this.speechOptions())
|
|
331
|
+
.catch(() => undefined);
|
|
332
|
+
}
|
|
333
|
+
let args = {};
|
|
334
|
+
try {
|
|
335
|
+
args = call.arguments ? JSON.parse(call.arguments) : {};
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
args = {};
|
|
339
|
+
}
|
|
340
|
+
const outcome = await this.tools.run(call.name, args, abort.signal);
|
|
341
|
+
trace.toolCalls.push({ name: call.name, ok: outcome.ok });
|
|
342
|
+
this.history.push({
|
|
343
|
+
role: "tool",
|
|
344
|
+
tool_call_id: call.id,
|
|
345
|
+
content: JSON.stringify(outcome.ok ? outcome.result : { error: outcome.error })
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// ---- endings ----------------------------------------------------------------------
|
|
350
|
+
async goodbye() {
|
|
351
|
+
this.abortReply();
|
|
352
|
+
this.discardPrepared();
|
|
353
|
+
clearTimeout(this.idleTimer);
|
|
354
|
+
await this.safely(async () => {
|
|
355
|
+
await this.session.say(this.settings.goodbyeMessage, {
|
|
356
|
+
...this.speechOptions(),
|
|
357
|
+
interruptible: false
|
|
358
|
+
});
|
|
359
|
+
await this.session.hangup();
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async transfer() {
|
|
363
|
+
this.abortReply();
|
|
364
|
+
clearTimeout(this.idleTimer);
|
|
365
|
+
const options = this.settings.transferOptions;
|
|
366
|
+
await this.safely(async () => {
|
|
367
|
+
if (!options) {
|
|
368
|
+
await this.session.say(this.settings.systemErrorMessage, this.speechOptions());
|
|
369
|
+
this.send({ type: "SPOKEN", source: "error" });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
await this.session.say(options.message, {
|
|
373
|
+
...this.speechOptions(),
|
|
374
|
+
interruptible: false
|
|
375
|
+
});
|
|
376
|
+
await this.session.transfer(options.phoneNumber);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// ---- internals --------------------------------------------------------------------
|
|
380
|
+
messages(context = []) {
|
|
381
|
+
return [
|
|
382
|
+
{ role: "system", content: this.settings.systemPrompt },
|
|
383
|
+
...context,
|
|
384
|
+
...this.history
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
/** Knowledge for this turn, as a system message for the request only (never in history). */
|
|
388
|
+
async contextFor(question, signal) {
|
|
389
|
+
if (!this.knowledge)
|
|
390
|
+
return [];
|
|
391
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve(""), knowledge_1.KNOWLEDGE_TIMEOUT_MS));
|
|
392
|
+
try {
|
|
393
|
+
const found = await Promise.race([
|
|
394
|
+
this.knowledge.find(question, signal),
|
|
395
|
+
timeout
|
|
396
|
+
]);
|
|
397
|
+
return found?.trim()
|
|
398
|
+
? [
|
|
399
|
+
{
|
|
400
|
+
role: "system",
|
|
401
|
+
content: `Relevant information:\n${found.trim()}`
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
: [];
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
toolSpecs() {
|
|
411
|
+
const own = this.assistant.languageModel.tools.map((t) => ({
|
|
412
|
+
name: t.name,
|
|
413
|
+
description: t.description,
|
|
414
|
+
parameters: t.parameters
|
|
415
|
+
}));
|
|
416
|
+
const builtIn = BUILT_IN_TOOLS.filter((b) => b.name !== "transfer" || this.settings.transferOptions);
|
|
417
|
+
return [...own, ...builtIn];
|
|
418
|
+
}
|
|
419
|
+
startStream(signal, context = []) {
|
|
420
|
+
const chunks = new AsyncQueue_1.AsyncQueue();
|
|
421
|
+
void this.pump(this.think.stream(this.messages(context), this.toolSpecs(), signal), chunks);
|
|
422
|
+
return chunks;
|
|
423
|
+
}
|
|
424
|
+
takePrepared(text) {
|
|
425
|
+
const p = this.prepared;
|
|
426
|
+
this.prepared = undefined;
|
|
427
|
+
if (!p)
|
|
428
|
+
return undefined;
|
|
429
|
+
if (p.text === text && !p.abort.signal.aborted) {
|
|
430
|
+
// The reply prepared on turn.likely_ended is still valid; adopt its abort controller.
|
|
431
|
+
this.reply?.signal.addEventListener("abort", () => p.abort.abort(), {
|
|
432
|
+
once: true
|
|
433
|
+
});
|
|
434
|
+
return p;
|
|
435
|
+
}
|
|
436
|
+
p.abort.abort();
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
discardPrepared() {
|
|
440
|
+
this.prepared?.abort.abort();
|
|
441
|
+
this.prepared = undefined;
|
|
442
|
+
}
|
|
443
|
+
async pump(stream, into) {
|
|
444
|
+
try {
|
|
445
|
+
for await (const chunk of stream)
|
|
446
|
+
into.push(chunk);
|
|
447
|
+
into.end();
|
|
448
|
+
}
|
|
449
|
+
catch (e) {
|
|
450
|
+
into.fail(e);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
speechOptions() {
|
|
454
|
+
return {
|
|
455
|
+
voice: this.settings.voice,
|
|
456
|
+
language: this.settings.language,
|
|
457
|
+
interruptible: this.settings.allowUserBargeIn
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/** Runs session work; a session that has ended stops everything quietly. */
|
|
461
|
+
async safely(fn) {
|
|
462
|
+
if (this.stopped)
|
|
463
|
+
return;
|
|
464
|
+
try {
|
|
465
|
+
await fn();
|
|
466
|
+
}
|
|
467
|
+
catch (e) {
|
|
468
|
+
if (e instanceof voice_1.SessionEndedError || this.stopped)
|
|
469
|
+
return;
|
|
470
|
+
logger.error(`autopilot session error: ${e.message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
exports.Agent = Agent;
|
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
* See the License for the specific language governing permissions and
|
|
17
17
|
* limitations under the License.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
/** A push-based async iterable: producers push, one consumer iterates. */
|
|
20
|
+
export declare class AsyncQueue<T> implements AsyncIterable<T> {
|
|
21
|
+
private items;
|
|
22
|
+
private waiting?;
|
|
23
|
+
private failWaiting?;
|
|
24
|
+
private ended;
|
|
25
|
+
private error?;
|
|
26
|
+
push(item: T): void;
|
|
27
|
+
end(): void;
|
|
28
|
+
fail(error: Error): void;
|
|
29
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AsyncQueue = 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
|
+
/** A push-based async iterable: producers push, one consumer iterates. */
|
|
23
|
+
class AsyncQueue {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.items = [];
|
|
26
|
+
this.ended = false;
|
|
27
|
+
}
|
|
28
|
+
push(item) {
|
|
29
|
+
if (this.ended)
|
|
30
|
+
return;
|
|
31
|
+
if (this.waiting) {
|
|
32
|
+
const resolve = this.waiting;
|
|
33
|
+
this.waiting = undefined;
|
|
34
|
+
resolve({ value: item, done: false });
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
this.items.push(item);
|
|
38
|
+
}
|
|
39
|
+
end() {
|
|
40
|
+
this.ended = true;
|
|
41
|
+
this.waiting?.({ value: undefined, done: true });
|
|
42
|
+
this.waiting = undefined;
|
|
43
|
+
}
|
|
44
|
+
fail(error) {
|
|
45
|
+
this.error = error;
|
|
46
|
+
this.ended = true;
|
|
47
|
+
this.failWaiting?.(error);
|
|
48
|
+
this.waiting = undefined;
|
|
49
|
+
}
|
|
50
|
+
[Symbol.asyncIterator]() {
|
|
51
|
+
return {
|
|
52
|
+
next: () => {
|
|
53
|
+
if (this.items.length)
|
|
54
|
+
return Promise.resolve({ value: this.items.shift(), done: false });
|
|
55
|
+
if (this.error)
|
|
56
|
+
return Promise.reject(this.error);
|
|
57
|
+
if (this.ended)
|
|
58
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
this.waiting = resolve;
|
|
61
|
+
this.failWaiting = reject;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.AsyncQueue = AsyncQueue;
|