signalwire-agents 0.1.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/CLAUDE.md +112 -0
- package/README.md +417 -0
- package/dist/AgentBase.d.ts +481 -0
- package/dist/AgentBase.js +1155 -0
- package/dist/AgentBase.js.map +1 -0
- package/dist/AgentServer.d.ts +60 -0
- package/dist/AgentServer.js +128 -0
- package/dist/AgentServer.js.map +1 -0
- package/dist/AuthHandler.d.ts +56 -0
- package/dist/AuthHandler.js +119 -0
- package/dist/AuthHandler.js.map +1 -0
- package/dist/ConfigLoader.d.ts +68 -0
- package/dist/ConfigLoader.js +150 -0
- package/dist/ConfigLoader.js.map +1 -0
- package/dist/ContextBuilder.d.ts +431 -0
- package/dist/ContextBuilder.js +751 -0
- package/dist/ContextBuilder.js.map +1 -0
- package/dist/DataMap.d.ts +179 -0
- package/dist/DataMap.js +321 -0
- package/dist/DataMap.js.map +1 -0
- package/dist/Logger.d.ts +91 -0
- package/dist/Logger.js +191 -0
- package/dist/Logger.js.map +1 -0
- package/dist/PomBuilder.d.ts +144 -0
- package/dist/PomBuilder.js +249 -0
- package/dist/PomBuilder.js.map +1 -0
- package/dist/PromptManager.d.ts +85 -0
- package/dist/PromptManager.js +108 -0
- package/dist/PromptManager.js.map +1 -0
- package/dist/SchemaUtils.d.ts +42 -0
- package/dist/SchemaUtils.js +148 -0
- package/dist/SchemaUtils.js.map +1 -0
- package/dist/ServerlessAdapter.d.ts +100 -0
- package/dist/ServerlessAdapter.js +166 -0
- package/dist/ServerlessAdapter.js.map +1 -0
- package/dist/SessionManager.d.ts +86 -0
- package/dist/SessionManager.js +155 -0
- package/dist/SessionManager.js.map +1 -0
- package/dist/SslConfig.d.ts +77 -0
- package/dist/SslConfig.js +100 -0
- package/dist/SslConfig.js.map +1 -0
- package/dist/SwaigFunction.d.ts +88 -0
- package/dist/SwaigFunction.js +125 -0
- package/dist/SwaigFunction.js.map +1 -0
- package/dist/SwaigFunctionResult.d.ts +442 -0
- package/dist/SwaigFunctionResult.js +735 -0
- package/dist/SwaigFunctionResult.js.map +1 -0
- package/dist/SwmlBuilder.d.ts +37 -0
- package/dist/SwmlBuilder.js +55 -0
- package/dist/SwmlBuilder.js.map +1 -0
- package/dist/cli/agent-loader.d.ts +22 -0
- package/dist/cli/agent-loader.js +106 -0
- package/dist/cli/agent-loader.js.map +1 -0
- package/dist/cli/mock-data.d.ts +37 -0
- package/dist/cli/mock-data.js +79 -0
- package/dist/cli/mock-data.js.map +1 -0
- package/dist/cli/swaig-test.d.ts +32 -0
- package/dist/cli/swaig-test.js +405 -0
- package/dist/cli/swaig-test.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/prefabs/ConciergeAgent.d.ts +64 -0
- package/dist/prefabs/ConciergeAgent.js +188 -0
- package/dist/prefabs/ConciergeAgent.js.map +1 -0
- package/dist/prefabs/FAQBotAgent.d.ts +68 -0
- package/dist/prefabs/FAQBotAgent.js +206 -0
- package/dist/prefabs/FAQBotAgent.js.map +1 -0
- package/dist/prefabs/InfoGathererAgent.d.ts +73 -0
- package/dist/prefabs/InfoGathererAgent.js +202 -0
- package/dist/prefabs/InfoGathererAgent.js.map +1 -0
- package/dist/prefabs/ReceptionistAgent.d.ts +65 -0
- package/dist/prefabs/ReceptionistAgent.js +205 -0
- package/dist/prefabs/ReceptionistAgent.js.map +1 -0
- package/dist/prefabs/SurveyAgent.d.ts +84 -0
- package/dist/prefabs/SurveyAgent.js +324 -0
- package/dist/prefabs/SurveyAgent.js.map +1 -0
- package/dist/prefabs/index.d.ts +13 -0
- package/dist/prefabs/index.js +14 -0
- package/dist/prefabs/index.js.map +1 -0
- package/dist/skills/SkillBase.d.ts +134 -0
- package/dist/skills/SkillBase.js +105 -0
- package/dist/skills/SkillBase.js.map +1 -0
- package/dist/skills/SkillManager.d.ts +83 -0
- package/dist/skills/SkillManager.js +159 -0
- package/dist/skills/SkillManager.js.map +1 -0
- package/dist/skills/SkillRegistry.d.ts +105 -0
- package/dist/skills/SkillRegistry.js +195 -0
- package/dist/skills/SkillRegistry.js.map +1 -0
- package/dist/skills/builtin/api_ninjas_trivia.d.ts +35 -0
- package/dist/skills/builtin/api_ninjas_trivia.js +161 -0
- package/dist/skills/builtin/api_ninjas_trivia.js.map +1 -0
- package/dist/skills/builtin/claude_skills.d.ts +34 -0
- package/dist/skills/builtin/claude_skills.js +156 -0
- package/dist/skills/builtin/claude_skills.js.map +1 -0
- package/dist/skills/builtin/custom_skills.d.ts +59 -0
- package/dist/skills/builtin/custom_skills.js +231 -0
- package/dist/skills/builtin/custom_skills.js.map +1 -0
- package/dist/skills/builtin/datasphere.d.ts +34 -0
- package/dist/skills/builtin/datasphere.js +163 -0
- package/dist/skills/builtin/datasphere.js.map +1 -0
- package/dist/skills/builtin/datasphere_serverless.d.ts +54 -0
- package/dist/skills/builtin/datasphere_serverless.js +155 -0
- package/dist/skills/builtin/datasphere_serverless.js.map +1 -0
- package/dist/skills/builtin/datetime.d.ts +31 -0
- package/dist/skills/builtin/datetime.js +95 -0
- package/dist/skills/builtin/datetime.js.map +1 -0
- package/dist/skills/builtin/google_maps.d.ts +33 -0
- package/dist/skills/builtin/google_maps.js +233 -0
- package/dist/skills/builtin/google_maps.js.map +1 -0
- package/dist/skills/builtin/index.d.ts +28 -0
- package/dist/skills/builtin/index.js +75 -0
- package/dist/skills/builtin/index.js.map +1 -0
- package/dist/skills/builtin/info_gatherer.d.ts +52 -0
- package/dist/skills/builtin/info_gatherer.js +237 -0
- package/dist/skills/builtin/info_gatherer.js.map +1 -0
- package/dist/skills/builtin/joke.d.ts +32 -0
- package/dist/skills/builtin/joke.js +142 -0
- package/dist/skills/builtin/joke.js.map +1 -0
- package/dist/skills/builtin/math.d.ts +33 -0
- package/dist/skills/builtin/math.js +120 -0
- package/dist/skills/builtin/math.js.map +1 -0
- package/dist/skills/builtin/mcp_gateway.d.ts +35 -0
- package/dist/skills/builtin/mcp_gateway.js +85 -0
- package/dist/skills/builtin/mcp_gateway.js.map +1 -0
- package/dist/skills/builtin/native_vector_search.d.ts +44 -0
- package/dist/skills/builtin/native_vector_search.js +248 -0
- package/dist/skills/builtin/native_vector_search.js.map +1 -0
- package/dist/skills/builtin/play_background_file.d.ts +34 -0
- package/dist/skills/builtin/play_background_file.js +135 -0
- package/dist/skills/builtin/play_background_file.js.map +1 -0
- package/dist/skills/builtin/spider.d.ts +34 -0
- package/dist/skills/builtin/spider.js +164 -0
- package/dist/skills/builtin/spider.js.map +1 -0
- package/dist/skills/builtin/swml_transfer.d.ts +44 -0
- package/dist/skills/builtin/swml_transfer.js +204 -0
- package/dist/skills/builtin/swml_transfer.js.map +1 -0
- package/dist/skills/builtin/weather_api.d.ts +34 -0
- package/dist/skills/builtin/weather_api.js +136 -0
- package/dist/skills/builtin/weather_api.js.map +1 -0
- package/dist/skills/builtin/web_search.d.ts +34 -0
- package/dist/skills/builtin/web_search.js +141 -0
- package/dist/skills/builtin/web_search.js.map +1 -0
- package/dist/skills/builtin/wikipedia_search.d.ts +34 -0
- package/dist/skills/builtin/wikipedia_search.js +166 -0
- package/dist/skills/builtin/wikipedia_search.js.map +1 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +10 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/docs/agent-guide.md +869 -0
- package/docs/api-reference.md +2419 -0
- package/docs/architecture.md +843 -0
- package/docs/cli-guide.md +579 -0
- package/docs/configuration.md +422 -0
- package/docs/contexts-guide.md +787 -0
- package/docs/datamap-guide.md +1141 -0
- package/docs/prefabs-guide.md +830 -0
- package/docs/security.md +436 -0
- package/docs/serverless-guide.md +513 -0
- package/docs/skills-guide.md +845 -0
- package/docs/swaig-reference.md +1627 -0
- package/examples/call-flow.ts +60 -0
- package/examples/contexts-steps.ts +62 -0
- package/examples/datamap-tools.ts +52 -0
- package/examples/dynamic-config.ts +51 -0
- package/examples/multi-agent.ts +67 -0
- package/examples/pom-prompt.ts +68 -0
- package/examples/simple-agent.ts +33 -0
- package/package.json +35 -0
- package/src/AgentBase.ts +1279 -0
- package/src/AgentServer.ts +149 -0
- package/src/AuthHandler.ts +141 -0
- package/src/ConfigLoader.ts +165 -0
- package/src/ContextBuilder.ts +796 -0
- package/src/DataMap.ts +365 -0
- package/src/Logger.ts +197 -0
- package/src/PomBuilder.ts +298 -0
- package/src/PromptManager.ts +133 -0
- package/src/SchemaUtils.ts +170 -0
- package/src/ServerlessAdapter.ts +218 -0
- package/src/SessionManager.ts +161 -0
- package/src/SslConfig.ts +118 -0
- package/src/SwaigFunction.ts +168 -0
- package/src/SwaigFunctionResult.ts +864 -0
- package/src/SwmlBuilder.ts +62 -0
- package/src/cli/agent-loader.ts +122 -0
- package/src/cli/mock-data.ts +110 -0
- package/src/cli/swaig-test.ts +440 -0
- package/src/index.ts +99 -0
- package/src/prefabs/ConciergeAgent.ts +251 -0
- package/src/prefabs/FAQBotAgent.ts +263 -0
- package/src/prefabs/InfoGathererAgent.ts +276 -0
- package/src/prefabs/ReceptionistAgent.ts +277 -0
- package/src/prefabs/SurveyAgent.ts +413 -0
- package/src/prefabs/index.ts +23 -0
- package/src/skills/SkillBase.ts +184 -0
- package/src/skills/SkillManager.ts +177 -0
- package/src/skills/SkillRegistry.ts +231 -0
- package/src/skills/builtin/api_ninjas_trivia.ts +230 -0
- package/src/skills/builtin/claude_skills.ts +239 -0
- package/src/skills/builtin/custom_skills.ts +326 -0
- package/src/skills/builtin/datasphere.ts +225 -0
- package/src/skills/builtin/datasphere_serverless.ts +194 -0
- package/src/skills/builtin/datetime.ts +115 -0
- package/src/skills/builtin/google_maps.ts +347 -0
- package/src/skills/builtin/index.ts +79 -0
- package/src/skills/builtin/info_gatherer.ts +331 -0
- package/src/skills/builtin/joke.ts +180 -0
- package/src/skills/builtin/math.ts +150 -0
- package/src/skills/builtin/mcp_gateway.ts +99 -0
- package/src/skills/builtin/native_vector_search.ts +327 -0
- package/src/skills/builtin/play_background_file.ts +189 -0
- package/src/skills/builtin/spider.ts +232 -0
- package/src/skills/builtin/swml_transfer.ts +285 -0
- package/src/skills/builtin/weather_api.ts +193 -0
- package/src/skills/builtin/web_search.ts +212 -0
- package/src/skills/builtin/wikipedia_search.ts +248 -0
- package/src/skills/index.ts +12 -0
- package/src/types.ts +99 -0
- package/tests/AgentBase.test.ts +693 -0
- package/tests/AgentServer.test.ts +102 -0
- package/tests/AuthHandler.test.ts +121 -0
- package/tests/ConfigLoader.test.ts +121 -0
- package/tests/DataMap.test.ts +286 -0
- package/tests/Logger.test.ts +218 -0
- package/tests/PomBuilder.test.ts +164 -0
- package/tests/ProxyDetection.test.ts +185 -0
- package/tests/SchemaUtils.test.ts +106 -0
- package/tests/ServerlessAdapter.test.ts +200 -0
- package/tests/SessionManager.test.ts +139 -0
- package/tests/SslConfig.test.ts +97 -0
- package/tests/SwaigFunctionResult.test.ts +298 -0
- package/tests/cli/mock-data.test.ts +94 -0
- package/tests/cli/swaig-test.test.ts +96 -0
- package/tests/prefabs/prefabs.test.ts +854 -0
- package/tests/setup.ts +6 -0
- package/tests/skills/SkillBase.test.ts +118 -0
- package/tests/skills/SkillManager.test.ts +164 -0
- package/tests/skills/SkillRegistry.test.ts +124 -0
- package/tests/skills/builtin.test.ts +924 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
package/src/AgentBase.ts
ADDED
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentBase - Core agent class with Hono HTTP server and 5-phase SWML rendering.
|
|
3
|
+
*
|
|
4
|
+
* Composes PromptManager, SessionManager, SwmlBuilder, and a tool registry
|
|
5
|
+
* into a single HTTP-servable agent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import { basicAuth } from 'hono/basic-auth';
|
|
10
|
+
import { cors } from 'hono/cors';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { PromptManager } from './PromptManager.js';
|
|
13
|
+
import { SessionManager } from './SessionManager.js';
|
|
14
|
+
import { SwmlBuilder } from './SwmlBuilder.js';
|
|
15
|
+
import { SwaigFunction, type SwaigHandler, type SwaigFunctionOptions } from './SwaigFunction.js';
|
|
16
|
+
import { SwaigFunctionResult } from './SwaigFunctionResult.js';
|
|
17
|
+
import { ContextBuilder } from './ContextBuilder.js';
|
|
18
|
+
import { getLogger, suppressAllLogs } from './Logger.js';
|
|
19
|
+
import { SkillManager } from './skills/SkillManager.js';
|
|
20
|
+
import type { SkillBase } from './skills/SkillBase.js';
|
|
21
|
+
import type {
|
|
22
|
+
AgentOptions,
|
|
23
|
+
LanguageConfig,
|
|
24
|
+
PronunciationRule,
|
|
25
|
+
FunctionInclude,
|
|
26
|
+
DynamicConfigCallback,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Core agent class that composes an HTTP server, prompt management, session handling,
|
|
31
|
+
* SWAIG tool registry, and 5-phase SWML rendering into a single deployable unit.
|
|
32
|
+
*/
|
|
33
|
+
export class AgentBase {
|
|
34
|
+
/** Display name of this agent. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** HTTP route path this agent listens on. */
|
|
37
|
+
route: string;
|
|
38
|
+
/** Hostname the HTTP server binds to. */
|
|
39
|
+
host: string;
|
|
40
|
+
/** Port number the HTTP server listens on. */
|
|
41
|
+
port: number;
|
|
42
|
+
/** Unique identifier for this agent instance. */
|
|
43
|
+
agentId: string;
|
|
44
|
+
|
|
45
|
+
// Internal managers
|
|
46
|
+
private promptManager: PromptManager;
|
|
47
|
+
private sessionManager: SessionManager;
|
|
48
|
+
private swmlBuilder: SwmlBuilder;
|
|
49
|
+
private toolRegistry: Map<string, SwaigFunction | Record<string, unknown>> = new Map();
|
|
50
|
+
|
|
51
|
+
// Auth
|
|
52
|
+
private basicAuthCreds: [string, string];
|
|
53
|
+
private basicAuthSource: 'provided' | 'environment' | 'generated' = 'generated';
|
|
54
|
+
|
|
55
|
+
// Call settings
|
|
56
|
+
private autoAnswer: boolean;
|
|
57
|
+
private _recordCall: boolean;
|
|
58
|
+
private recordFormat: string;
|
|
59
|
+
private recordStereo: boolean;
|
|
60
|
+
private defaultWebhookUrl: string | null;
|
|
61
|
+
private nativeFunctions: string[];
|
|
62
|
+
|
|
63
|
+
// AI config
|
|
64
|
+
private hints: string[] = [];
|
|
65
|
+
private languages: LanguageConfig[] = [];
|
|
66
|
+
private pronounce: PronunciationRule[] = [];
|
|
67
|
+
private params: Record<string, unknown> = {};
|
|
68
|
+
private globalData: Record<string, unknown> = {};
|
|
69
|
+
private functionIncludes: FunctionInclude[] = [];
|
|
70
|
+
private promptLlmParams: Record<string, unknown> = {};
|
|
71
|
+
private postPromptLlmParams: Record<string, unknown> = {};
|
|
72
|
+
private internalFillers: Record<string, Record<string, string[]>> = {};
|
|
73
|
+
|
|
74
|
+
// Call flow verbs
|
|
75
|
+
private preAnswerVerbs: [string, Record<string, unknown>][] = [];
|
|
76
|
+
private answerConfig: Record<string, unknown> = {};
|
|
77
|
+
private postAnswerVerbs: [string, Record<string, unknown>][] = [];
|
|
78
|
+
private postAiVerbs: [string, Record<string, unknown>][] = [];
|
|
79
|
+
|
|
80
|
+
// Dynamic config
|
|
81
|
+
private dynamicConfigCallback: DynamicConfigCallback | null = null;
|
|
82
|
+
private swaigQueryParams: Record<string, string> = {};
|
|
83
|
+
|
|
84
|
+
// Webhook URL overrides
|
|
85
|
+
private webHookUrlOverride: string | null = null;
|
|
86
|
+
private postPromptUrlOverride: string | null = null;
|
|
87
|
+
|
|
88
|
+
// Contexts
|
|
89
|
+
private contextsBuilder: ContextBuilder | null = null;
|
|
90
|
+
|
|
91
|
+
// Debug
|
|
92
|
+
private debugEventsEnabled = false;
|
|
93
|
+
private debugEventsLevel = 1;
|
|
94
|
+
|
|
95
|
+
// Proxy detection
|
|
96
|
+
private _proxyUrlBase: string | null = process.env['SWML_PROXY_URL_BASE'] ?? null;
|
|
97
|
+
private _proxyUrlBaseFromEnv = !!process.env['SWML_PROXY_URL_BASE'];
|
|
98
|
+
private _proxyDebug = process.env['SWML_PROXY_DEBUG'] === 'true';
|
|
99
|
+
|
|
100
|
+
/** Structured logger instance for this agent, configurable via SIGNALWIRE_LOG_LEVEL. */
|
|
101
|
+
protected log = getLogger('AgentBase');
|
|
102
|
+
|
|
103
|
+
// Skills
|
|
104
|
+
private skillManager = new SkillManager();
|
|
105
|
+
|
|
106
|
+
// Hono app
|
|
107
|
+
private _app: Hono | null = null;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create a new AgentBase instance.
|
|
111
|
+
* @param opts - Agent configuration options including name, route, auth, and call settings.
|
|
112
|
+
*/
|
|
113
|
+
constructor(opts: AgentOptions) {
|
|
114
|
+
this.name = opts.name;
|
|
115
|
+
this.route = (opts.route ?? '/').replace(/\/+$/, '') || '/';
|
|
116
|
+
this.host = opts.host ?? '0.0.0.0';
|
|
117
|
+
this.port = opts.port ?? parseInt(process.env['PORT'] ?? '3000', 10);
|
|
118
|
+
this.agentId = opts.agentId ?? randomBytes(8).toString('hex');
|
|
119
|
+
this.autoAnswer = opts.autoAnswer ?? true;
|
|
120
|
+
this._recordCall = opts.recordCall ?? false;
|
|
121
|
+
this.recordFormat = opts.recordFormat ?? 'mp4';
|
|
122
|
+
this.recordStereo = opts.recordStereo ?? true;
|
|
123
|
+
this.defaultWebhookUrl = opts.defaultWebhookUrl ?? null;
|
|
124
|
+
this.nativeFunctions = opts.nativeFunctions ?? [];
|
|
125
|
+
|
|
126
|
+
if (opts.suppressLogs) {
|
|
127
|
+
suppressAllLogs(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.promptManager = new PromptManager(opts.usePom ?? true);
|
|
131
|
+
this.sessionManager = new SessionManager(opts.tokenExpirySecs ?? 3600);
|
|
132
|
+
this.swmlBuilder = new SwmlBuilder();
|
|
133
|
+
|
|
134
|
+
// Setup auth
|
|
135
|
+
if (opts.basicAuth) {
|
|
136
|
+
this.basicAuthCreds = opts.basicAuth;
|
|
137
|
+
this.basicAuthSource = 'provided';
|
|
138
|
+
} else {
|
|
139
|
+
const envUser = process.env['SWML_BASIC_AUTH_USER'];
|
|
140
|
+
const envPass = process.env['SWML_BASIC_AUTH_PASSWORD'];
|
|
141
|
+
if (envUser && envPass) {
|
|
142
|
+
this.basicAuthCreds = [envUser, envPass];
|
|
143
|
+
this.basicAuthSource = 'environment';
|
|
144
|
+
} else {
|
|
145
|
+
this.basicAuthCreds = [this.name, randomBytes(8).toString('hex')];
|
|
146
|
+
this.basicAuthSource = 'generated';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Apply static PROMPT_SECTIONS from the class if defined
|
|
151
|
+
const ctor = this.constructor as typeof AgentBase;
|
|
152
|
+
if (ctor.PROMPT_SECTIONS) {
|
|
153
|
+
for (const section of ctor.PROMPT_SECTIONS) {
|
|
154
|
+
this.promptAddSection(section.title, section);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Static prompt sections: subclasses can define these declaratively.
|
|
161
|
+
* Each entry is applied via promptAddSection() in the constructor.
|
|
162
|
+
*/
|
|
163
|
+
static PROMPT_SECTIONS?: { title: string; body?: string; bullets?: string[]; numbered?: boolean }[];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Lifecycle method to register tools. Subclasses should call this at the
|
|
167
|
+
* end of their own constructor (after all fields are initialized).
|
|
168
|
+
* Not called automatically — call `this.defineTools()` explicitly.
|
|
169
|
+
*/
|
|
170
|
+
protected defineTools(): void {
|
|
171
|
+
// Default no-op — subclasses override
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Prompt methods ──────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Set the main system prompt text for the AI.
|
|
178
|
+
* @param text - The prompt text to use.
|
|
179
|
+
* @returns This agent instance for chaining.
|
|
180
|
+
*/
|
|
181
|
+
setPromptText(text: string): this {
|
|
182
|
+
this.promptManager.setPromptText(text);
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Set the post-prompt text evaluated after the call ends.
|
|
188
|
+
* @param text - The post-prompt text to use.
|
|
189
|
+
* @returns This agent instance for chaining.
|
|
190
|
+
*/
|
|
191
|
+
setPostPrompt(text: string): this {
|
|
192
|
+
this.promptManager.setPostPrompt(text);
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Add a new section to the prompt with optional body, bullets, and subsections.
|
|
198
|
+
* @param title - Section heading.
|
|
199
|
+
* @param opts - Optional section content including body text, bullet points, and subsections.
|
|
200
|
+
* @returns This agent instance for chaining.
|
|
201
|
+
*/
|
|
202
|
+
promptAddSection(
|
|
203
|
+
title: string,
|
|
204
|
+
opts?: {
|
|
205
|
+
body?: string;
|
|
206
|
+
bullets?: string[];
|
|
207
|
+
numbered?: boolean;
|
|
208
|
+
numberedBullets?: boolean;
|
|
209
|
+
subsections?: { title: string; body?: string; bullets?: string[] }[];
|
|
210
|
+
},
|
|
211
|
+
): this {
|
|
212
|
+
this.promptManager.addSection(title, opts);
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Append content to an existing prompt section.
|
|
218
|
+
* @param title - Title of the section to append to.
|
|
219
|
+
* @param opts - Content to add: body text, a single bullet, or multiple bullets.
|
|
220
|
+
* @returns This agent instance for chaining.
|
|
221
|
+
*/
|
|
222
|
+
promptAddToSection(
|
|
223
|
+
title: string,
|
|
224
|
+
opts?: { body?: string; bullet?: string; bullets?: string[] },
|
|
225
|
+
): this {
|
|
226
|
+
this.promptManager.addToSection(title, opts);
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Add a subsection under an existing prompt section.
|
|
232
|
+
* @param parentTitle - Title of the parent section.
|
|
233
|
+
* @param title - Title of the new subsection.
|
|
234
|
+
* @param opts - Optional body text and bullet points for the subsection.
|
|
235
|
+
* @returns This agent instance for chaining.
|
|
236
|
+
*/
|
|
237
|
+
promptAddSubsection(parentTitle: string, title: string, opts?: { body?: string; bullets?: string[] }): this {
|
|
238
|
+
this.promptManager.addSubsection(parentTitle, title, opts);
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check whether a prompt section with the given title exists.
|
|
244
|
+
* @param title - Section title to look for.
|
|
245
|
+
* @returns True if the section exists.
|
|
246
|
+
*/
|
|
247
|
+
promptHasSection(title: string): boolean {
|
|
248
|
+
return this.promptManager.hasSection(title);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get the fully rendered main prompt text.
|
|
253
|
+
* @returns The assembled prompt string.
|
|
254
|
+
*/
|
|
255
|
+
getPrompt(): string {
|
|
256
|
+
return this.promptManager.getPrompt();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get the post-prompt text, if one has been set.
|
|
261
|
+
* @returns The post-prompt string, or null if not configured.
|
|
262
|
+
*/
|
|
263
|
+
getPostPrompt(): string | null {
|
|
264
|
+
return this.promptManager.getPostPrompt();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Contexts ────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Define or replace the contexts configuration for the AI verb.
|
|
271
|
+
* @param contexts - An existing ContextBuilder instance or a plain object; a new ContextBuilder is created if omitted.
|
|
272
|
+
* @returns The active ContextBuilder for further configuration.
|
|
273
|
+
*/
|
|
274
|
+
defineContexts(contexts?: ContextBuilder | Record<string, unknown>): ContextBuilder {
|
|
275
|
+
if (contexts instanceof ContextBuilder) {
|
|
276
|
+
this.contextsBuilder = contexts;
|
|
277
|
+
} else {
|
|
278
|
+
this.contextsBuilder = new ContextBuilder();
|
|
279
|
+
}
|
|
280
|
+
return this.contextsBuilder;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── AI config methods ───────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Add a single speech-recognition hint.
|
|
287
|
+
* @param hint - Word or phrase to boost in speech recognition.
|
|
288
|
+
* @returns This agent instance for chaining.
|
|
289
|
+
*/
|
|
290
|
+
addHint(hint: string): this {
|
|
291
|
+
this.hints.push(hint);
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Add multiple speech-recognition hints at once.
|
|
297
|
+
* @param hints - Array of words or phrases to boost.
|
|
298
|
+
* @returns This agent instance for chaining.
|
|
299
|
+
*/
|
|
300
|
+
addHints(hints: string[]): this {
|
|
301
|
+
this.hints.push(...hints);
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Add a pattern-based speech-recognition hint with find-and-replace behavior.
|
|
307
|
+
* @param opts - Pattern hint configuration with regex pattern, replacement, and optional case flag.
|
|
308
|
+
* @returns This agent instance for chaining.
|
|
309
|
+
*/
|
|
310
|
+
addPatternHint(opts: { pattern: string; replace: string; ignoreCase?: boolean }): this {
|
|
311
|
+
this.hints.push(opts as unknown as string);
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Add a supported language to the AI configuration.
|
|
317
|
+
* @param config - Language configuration including name, code, voice, and optional fillers.
|
|
318
|
+
* @returns This agent instance for chaining.
|
|
319
|
+
*/
|
|
320
|
+
addLanguage(config: LanguageConfig): this {
|
|
321
|
+
const lang: Record<string, unknown> = {
|
|
322
|
+
name: config.name,
|
|
323
|
+
code: config.code,
|
|
324
|
+
};
|
|
325
|
+
if (config.voice) lang['voice'] = config.voice;
|
|
326
|
+
if (config.engine) lang['engine'] = config.engine;
|
|
327
|
+
if (config.fillers) lang['fillers'] = config.fillers;
|
|
328
|
+
if (config.speechModel) lang['speech_model'] = config.speechModel;
|
|
329
|
+
if (config.functionFillers) lang['function_fillers'] = config.functionFillers;
|
|
330
|
+
this.languages.push(lang as unknown as LanguageConfig);
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Replace all configured languages with a new list.
|
|
336
|
+
* @param languages - Array of language configurations.
|
|
337
|
+
* @returns This agent instance for chaining.
|
|
338
|
+
*/
|
|
339
|
+
setLanguages(languages: LanguageConfig[]): this {
|
|
340
|
+
this.languages = [];
|
|
341
|
+
for (const l of languages) this.addLanguage(l);
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Add a pronunciation override rule for the TTS engine.
|
|
347
|
+
* @param rule - Pronunciation rule specifying the text to replace and its substitute.
|
|
348
|
+
* @returns This agent instance for chaining.
|
|
349
|
+
*/
|
|
350
|
+
addPronunciation(rule: PronunciationRule): this {
|
|
351
|
+
const r: Record<string, unknown> = { replace: rule.replace, with: rule.with };
|
|
352
|
+
if (rule.ignoreCase) r['ignore_case'] = rule.ignoreCase;
|
|
353
|
+
this.pronounce.push(r as unknown as PronunciationRule);
|
|
354
|
+
return this;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Set a single AI parameter (e.g. "temperature", "top_p").
|
|
359
|
+
* @param key - Parameter name.
|
|
360
|
+
* @param value - Parameter value.
|
|
361
|
+
* @returns This agent instance for chaining.
|
|
362
|
+
*/
|
|
363
|
+
setParam(key: string, value: unknown): this {
|
|
364
|
+
this.params[key] = value;
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Merge multiple AI parameters into the existing params object.
|
|
370
|
+
* @param params - Key-value pairs to merge.
|
|
371
|
+
* @returns This agent instance for chaining.
|
|
372
|
+
*/
|
|
373
|
+
setParams(params: Record<string, unknown>): this {
|
|
374
|
+
Object.assign(this.params, params);
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Replace the entire global_data object passed into the AI configuration.
|
|
380
|
+
* @param data - New global data object.
|
|
381
|
+
* @returns This agent instance for chaining.
|
|
382
|
+
*/
|
|
383
|
+
setGlobalData(data: Record<string, unknown>): this {
|
|
384
|
+
this.globalData = data;
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Merge additional entries into the existing global_data object.
|
|
390
|
+
* @param data - Key-value pairs to merge into global data.
|
|
391
|
+
* @returns This agent instance for chaining.
|
|
392
|
+
*/
|
|
393
|
+
updateGlobalData(data: Record<string, unknown>): this {
|
|
394
|
+
Object.assign(this.globalData, data);
|
|
395
|
+
return this;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Set the list of native SWAIG function names (built-in platform functions).
|
|
400
|
+
* @param funcs - Array of native function names.
|
|
401
|
+
* @returns This agent instance for chaining.
|
|
402
|
+
*/
|
|
403
|
+
setNativeFunctions(funcs: string[]): this {
|
|
404
|
+
this.nativeFunctions = funcs;
|
|
405
|
+
return this;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Add internal filler phrases spoken while a specific function is executing.
|
|
410
|
+
* @param functionName - Name of the SWAIG function these fillers apply to.
|
|
411
|
+
* @param languageCode - BCP-47 language code for the fillers.
|
|
412
|
+
* @param fillers - Array of filler phrases.
|
|
413
|
+
* @returns This agent instance for chaining.
|
|
414
|
+
*/
|
|
415
|
+
addInternalFiller(functionName: string, languageCode: string, fillers: string[]): this {
|
|
416
|
+
if (!this.internalFillers[functionName]) {
|
|
417
|
+
this.internalFillers[functionName] = {};
|
|
418
|
+
}
|
|
419
|
+
this.internalFillers[functionName][languageCode] = fillers;
|
|
420
|
+
return this;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Add a remote SWAIG function include reference.
|
|
425
|
+
* @param url - URL of the remote SWAIG endpoint.
|
|
426
|
+
* @param functions - Function names available at that endpoint.
|
|
427
|
+
* @param metaData - Optional metadata to attach to the include.
|
|
428
|
+
* @returns This agent instance for chaining.
|
|
429
|
+
*/
|
|
430
|
+
addFunctionInclude(url: string, functions: string[], metaData?: Record<string, unknown>): this {
|
|
431
|
+
const inc: FunctionInclude = { url, functions };
|
|
432
|
+
if (metaData) inc.meta_data = metaData;
|
|
433
|
+
this.functionIncludes.push(inc);
|
|
434
|
+
return this;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Merge LLM-specific parameters into the main prompt configuration (e.g. model, temperature).
|
|
439
|
+
* @param params - Key-value LLM parameters to merge.
|
|
440
|
+
* @returns This agent instance for chaining.
|
|
441
|
+
*/
|
|
442
|
+
setPromptLlmParams(params: Record<string, unknown>): this {
|
|
443
|
+
Object.assign(this.promptLlmParams, params);
|
|
444
|
+
return this;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Merge LLM-specific parameters into the post-prompt configuration.
|
|
449
|
+
* @param params - Key-value LLM parameters to merge.
|
|
450
|
+
* @returns This agent instance for chaining.
|
|
451
|
+
*/
|
|
452
|
+
setPostPromptLlmParams(params: Record<string, unknown>): this {
|
|
453
|
+
Object.assign(this.postPromptLlmParams, params);
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Enable debug event webhooks for this agent.
|
|
459
|
+
* @param level - Debug verbosity level (defaults to 1).
|
|
460
|
+
* @returns This agent instance for chaining.
|
|
461
|
+
*/
|
|
462
|
+
enableDebugEvents(level = 1): this {
|
|
463
|
+
this.debugEventsEnabled = true;
|
|
464
|
+
this.debugEventsLevel = level;
|
|
465
|
+
return this;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Tools ───────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Register a SWAIG tool (function) that the AI can invoke during a call.
|
|
472
|
+
* @param opts - Tool definition including name, description, parameter schema, and handler callback.
|
|
473
|
+
* @returns This agent instance for chaining.
|
|
474
|
+
*/
|
|
475
|
+
defineTool(opts: {
|
|
476
|
+
name: string;
|
|
477
|
+
description: string;
|
|
478
|
+
parameters?: Record<string, unknown>;
|
|
479
|
+
handler: SwaigHandler;
|
|
480
|
+
secure?: boolean;
|
|
481
|
+
fillers?: Record<string, string[]>;
|
|
482
|
+
waitFile?: string;
|
|
483
|
+
waitFileLoops?: number;
|
|
484
|
+
required?: string[];
|
|
485
|
+
}): this {
|
|
486
|
+
const fn = new SwaigFunction({
|
|
487
|
+
name: opts.name,
|
|
488
|
+
description: opts.description,
|
|
489
|
+
parameters: opts.parameters,
|
|
490
|
+
handler: opts.handler,
|
|
491
|
+
secure: opts.secure,
|
|
492
|
+
fillers: opts.fillers,
|
|
493
|
+
waitFile: opts.waitFile,
|
|
494
|
+
waitFileLoops: opts.waitFileLoops,
|
|
495
|
+
required: opts.required,
|
|
496
|
+
});
|
|
497
|
+
this.toolRegistry.set(opts.name, fn);
|
|
498
|
+
return this;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Get a summary of all registered tools with their names, descriptions, and parameter schemas.
|
|
503
|
+
* @returns Array of tool descriptors.
|
|
504
|
+
*/
|
|
505
|
+
getRegisteredTools(): { name: string; description: string; parameters: Record<string, unknown> }[] {
|
|
506
|
+
const tools: { name: string; description: string; parameters: Record<string, unknown> }[] = [];
|
|
507
|
+
for (const [name, fn] of this.toolRegistry) {
|
|
508
|
+
if (fn instanceof SwaigFunction) {
|
|
509
|
+
tools.push({ name, description: fn.description, parameters: fn.parameters });
|
|
510
|
+
} else {
|
|
511
|
+
tools.push({
|
|
512
|
+
name,
|
|
513
|
+
description: (fn['purpose'] as string) ?? '',
|
|
514
|
+
parameters: (fn['argument'] as Record<string, unknown>) ?? {},
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return tools;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Look up a registered SwaigFunction by name.
|
|
523
|
+
* @param name - The tool name to search for.
|
|
524
|
+
* @returns The SwaigFunction instance, or undefined if not found or not a SwaigFunction.
|
|
525
|
+
*/
|
|
526
|
+
getTool(name: string): SwaigFunction | undefined {
|
|
527
|
+
const fn = this.toolRegistry.get(name);
|
|
528
|
+
return fn instanceof SwaigFunction ? fn : undefined;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Register a pre-built SwaigFunction instance or a raw function descriptor (e.g. DataMap).
|
|
533
|
+
* @param fn - A SwaigFunction instance or a plain object with a "function" key.
|
|
534
|
+
* @returns This agent instance for chaining.
|
|
535
|
+
*/
|
|
536
|
+
registerSwaigFunction(fn: SwaigFunction | Record<string, unknown>): this {
|
|
537
|
+
if (fn instanceof SwaigFunction) {
|
|
538
|
+
this.toolRegistry.set(fn.name, fn);
|
|
539
|
+
} else {
|
|
540
|
+
const name = fn['function'] as string;
|
|
541
|
+
this.toolRegistry.set(name, fn);
|
|
542
|
+
}
|
|
543
|
+
return this;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── Call flow ───────────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Add a SWML verb to execute before the answer phase (phase 1).
|
|
550
|
+
* @param verbName - Name of the SWML verb (e.g. "play", "record").
|
|
551
|
+
* @param config - Verb configuration object.
|
|
552
|
+
* @returns This agent instance for chaining.
|
|
553
|
+
*/
|
|
554
|
+
addPreAnswerVerb(verbName: string, config: Record<string, unknown>): this {
|
|
555
|
+
this.preAnswerVerbs.push([verbName, config]);
|
|
556
|
+
return this;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Configure the answer verb (phase 2) with optional settings.
|
|
561
|
+
* @param config - Optional answer verb configuration.
|
|
562
|
+
* @returns This agent instance for chaining.
|
|
563
|
+
*/
|
|
564
|
+
addAnswerVerb(config?: Record<string, unknown>): this {
|
|
565
|
+
this.answerConfig = config ?? {};
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Add a SWML verb to execute after the answer phase but before the AI verb (phase 3).
|
|
571
|
+
* @param verbName - Name of the SWML verb.
|
|
572
|
+
* @param config - Verb configuration object.
|
|
573
|
+
* @returns This agent instance for chaining.
|
|
574
|
+
*/
|
|
575
|
+
addPostAnswerVerb(verbName: string, config: Record<string, unknown>): this {
|
|
576
|
+
this.postAnswerVerbs.push([verbName, config]);
|
|
577
|
+
return this;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Add a SWML verb to execute after the AI verb (phase 5).
|
|
582
|
+
* @param verbName - Name of the SWML verb.
|
|
583
|
+
* @param config - Verb configuration object.
|
|
584
|
+
* @returns This agent instance for chaining.
|
|
585
|
+
*/
|
|
586
|
+
addPostAiVerb(verbName: string, config: Record<string, unknown>): this {
|
|
587
|
+
this.postAiVerbs.push([verbName, config]);
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Remove all pre-answer verbs.
|
|
593
|
+
* @returns This agent instance for chaining.
|
|
594
|
+
*/
|
|
595
|
+
clearPreAnswerVerbs(): this {
|
|
596
|
+
this.preAnswerVerbs = [];
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Remove all post-answer verbs.
|
|
602
|
+
* @returns This agent instance for chaining.
|
|
603
|
+
*/
|
|
604
|
+
clearPostAnswerVerbs(): this {
|
|
605
|
+
this.postAnswerVerbs = [];
|
|
606
|
+
return this;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Remove all post-AI verbs.
|
|
611
|
+
* @returns This agent instance for chaining.
|
|
612
|
+
*/
|
|
613
|
+
clearPostAiVerbs(): this {
|
|
614
|
+
this.postAiVerbs = [];
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Get the agent's display name.
|
|
620
|
+
* @returns The agent name string.
|
|
621
|
+
*/
|
|
622
|
+
getName(): string {
|
|
623
|
+
return this.name;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── Skills ─────────────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Add a skill to this agent, registering its tools, prompt sections, hints, and global data.
|
|
630
|
+
* @param skill - The skill instance to add.
|
|
631
|
+
* @returns This agent instance for chaining.
|
|
632
|
+
*/
|
|
633
|
+
async addSkill(skill: SkillBase): Promise<this> {
|
|
634
|
+
await this.skillManager.addSkill(skill);
|
|
635
|
+
|
|
636
|
+
// Register skill tools
|
|
637
|
+
for (const toolDef of skill.getTools()) {
|
|
638
|
+
this.defineTool(toolDef);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Inject prompt sections
|
|
642
|
+
for (const section of skill.getPromptSections()) {
|
|
643
|
+
this.promptAddSection(section.title, section);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Inject hints
|
|
647
|
+
const hints = skill.getHints();
|
|
648
|
+
if (hints.length) this.addHints(hints);
|
|
649
|
+
|
|
650
|
+
// Merge global data
|
|
651
|
+
const globalData = skill.getGlobalData();
|
|
652
|
+
if (Object.keys(globalData).length) this.updateGlobalData(globalData);
|
|
653
|
+
|
|
654
|
+
return this;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Remove a previously added skill by its instance ID.
|
|
659
|
+
* @param instanceId - The unique instance ID of the skill to remove.
|
|
660
|
+
* @returns True if the skill was found and removed.
|
|
661
|
+
*/
|
|
662
|
+
async removeSkill(instanceId: string): Promise<boolean> {
|
|
663
|
+
return this.skillManager.removeSkill(instanceId);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* List all registered skills with their names, instance IDs, and initialization status.
|
|
668
|
+
* @returns Array of skill descriptors.
|
|
669
|
+
*/
|
|
670
|
+
listSkills(): { name: string; instanceId: string; initialized: boolean }[] {
|
|
671
|
+
return this.skillManager.listSkills();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Check whether a skill with the given name is registered.
|
|
676
|
+
* @param skillName - The skill name to check.
|
|
677
|
+
* @returns True if a skill with that name exists.
|
|
678
|
+
*/
|
|
679
|
+
hasSkill(skillName: string): boolean {
|
|
680
|
+
return this.skillManager.hasSkill(skillName);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── Dynamic config ──────────────────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Set a callback invoked on each SWML request to dynamically modify an ephemeral agent copy.
|
|
687
|
+
* @param cb - The dynamic configuration callback.
|
|
688
|
+
* @returns This agent instance for chaining.
|
|
689
|
+
*/
|
|
690
|
+
setDynamicConfigCallback(cb: DynamicConfigCallback): this {
|
|
691
|
+
this.dynamicConfigCallback = cb;
|
|
692
|
+
return this;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Add extra query parameters appended to all SWAIG webhook URLs.
|
|
697
|
+
* @param params - Key-value pairs to append as query parameters.
|
|
698
|
+
* @returns This agent instance for chaining.
|
|
699
|
+
*/
|
|
700
|
+
addSwaigQueryParams(params: Record<string, string>): this {
|
|
701
|
+
Object.assign(this.swaigQueryParams, params);
|
|
702
|
+
return this;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Proxy detection ────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Manually set the proxy base URL used for webhook URL generation.
|
|
709
|
+
* @param url - The external-facing base URL (trailing slashes are stripped).
|
|
710
|
+
* @returns This agent instance for chaining.
|
|
711
|
+
*/
|
|
712
|
+
manualSetProxyUrl(url: string): this {
|
|
713
|
+
this._proxyUrlBase = url.replace(/\/+$/, '');
|
|
714
|
+
this._proxyUrlBaseFromEnv = false;
|
|
715
|
+
return this;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private detectProxyFromRequest(c: any): void {
|
|
719
|
+
// Never override env var setting
|
|
720
|
+
if (this._proxyUrlBaseFromEnv) return;
|
|
721
|
+
|
|
722
|
+
const get = (name: string): string | undefined =>
|
|
723
|
+
c.req.header(name) ?? undefined;
|
|
724
|
+
|
|
725
|
+
// 1. X-Forwarded-Host + X-Forwarded-Proto
|
|
726
|
+
const xfHost = get('x-forwarded-host');
|
|
727
|
+
if (xfHost) {
|
|
728
|
+
const proto = get('x-forwarded-proto') ?? 'https';
|
|
729
|
+
const url = `${proto}://${xfHost}`;
|
|
730
|
+
if (this._proxyDebug) this.log.debug(`Proxy detected from X-Forwarded-Host: ${url}`);
|
|
731
|
+
this._proxyUrlBase = url;
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// 2. Forwarded header (RFC 7239): Forwarded: host=example.com;proto=https
|
|
736
|
+
const forwarded = get('forwarded');
|
|
737
|
+
if (forwarded) {
|
|
738
|
+
const hostMatch = forwarded.match(/host=([^;,\s]+)/i);
|
|
739
|
+
const protoMatch = forwarded.match(/proto=([^;,\s]+)/i);
|
|
740
|
+
if (hostMatch) {
|
|
741
|
+
const proto = protoMatch ? protoMatch[1] : 'https';
|
|
742
|
+
const url = `${proto}://${hostMatch[1]}`;
|
|
743
|
+
if (this._proxyDebug) this.log.debug(`Proxy detected from Forwarded header: ${url}`);
|
|
744
|
+
this._proxyUrlBase = url;
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 3. X-Original-Host
|
|
750
|
+
const xOrigHost = get('x-original-host');
|
|
751
|
+
if (xOrigHost) {
|
|
752
|
+
const proto = get('x-forwarded-proto') ?? 'https';
|
|
753
|
+
const url = `${proto}://${xOrigHost}`;
|
|
754
|
+
if (this._proxyDebug) this.log.debug(`Proxy detected from X-Original-Host: ${url}`);
|
|
755
|
+
this._proxyUrlBase = url;
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// 4. X-Forwarded-For (warn only - no host info)
|
|
760
|
+
const xff = get('x-forwarded-for');
|
|
761
|
+
if (xff && this._proxyDebug) {
|
|
762
|
+
this.log.debug(`X-Forwarded-For detected (${xff}) but cannot determine host - set SWML_PROXY_URL_BASE manually`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ── URL ─────────────────────────────────────────────────────────────
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Override the default SWAIG webhook URL with a custom one.
|
|
770
|
+
* @param url - The custom webhook URL.
|
|
771
|
+
* @returns This agent instance for chaining.
|
|
772
|
+
*/
|
|
773
|
+
setWebHookUrl(url: string): this {
|
|
774
|
+
this.webHookUrlOverride = url;
|
|
775
|
+
return this;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Override the default post-prompt webhook URL with a custom one.
|
|
780
|
+
* @param url - The custom post-prompt URL.
|
|
781
|
+
* @returns This agent instance for chaining.
|
|
782
|
+
*/
|
|
783
|
+
setPostPromptUrl(url: string): this {
|
|
784
|
+
this.postPromptUrlOverride = url;
|
|
785
|
+
return this;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Get the full external URL of this agent, using the proxy base URL if available.
|
|
790
|
+
* @param includeAuth - Whether to embed basic-auth credentials in the URL (defaults to false).
|
|
791
|
+
* @returns The fully-qualified URL string.
|
|
792
|
+
*/
|
|
793
|
+
getFullUrl(includeAuth = false): string {
|
|
794
|
+
if (this._proxyUrlBase) {
|
|
795
|
+
let base = this._proxyUrlBase.replace(/\/+$/, '');
|
|
796
|
+
if (includeAuth) base = this.insertAuth(base);
|
|
797
|
+
return base;
|
|
798
|
+
}
|
|
799
|
+
const protocol = 'http';
|
|
800
|
+
const hostPart = this.host === '0.0.0.0' ? 'localhost' : this.host;
|
|
801
|
+
let base = `${protocol}://${hostPart}:${this.port}`;
|
|
802
|
+
if (includeAuth) base = this.insertAuth(base);
|
|
803
|
+
if (this.route && this.route !== '/') base += this.route;
|
|
804
|
+
return base;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private insertAuth(baseUrl: string): string {
|
|
808
|
+
const [user, pass] = this.basicAuthCreds;
|
|
809
|
+
if (!user || !pass) return baseUrl;
|
|
810
|
+
return baseUrl.replace('://', `://${user}:${pass}@`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private buildWebhookUrl(endpoint: string, extraParams?: Record<string, string>): string {
|
|
814
|
+
let url = `${this.getFullUrl(true)}/${endpoint}`;
|
|
815
|
+
const params = { ...extraParams };
|
|
816
|
+
const entries = Object.entries(params).filter(([, v]) => v);
|
|
817
|
+
if (entries.length) {
|
|
818
|
+
url += '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
|
|
819
|
+
}
|
|
820
|
+
return url;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ── Lifecycle hooks (override in subclass) ──────────────────────────
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Lifecycle hook called when a post-prompt summary is received. Override in subclasses.
|
|
827
|
+
* @param _summary - Parsed summary object, or null if extraction failed.
|
|
828
|
+
* @param _rawData - The full raw post-prompt payload.
|
|
829
|
+
*/
|
|
830
|
+
onSummary(_summary: Record<string, unknown> | null, _rawData: Record<string, unknown>): void | Promise<void> {
|
|
831
|
+
// Default no-op
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Lifecycle hook called on every SWML request before rendering. Override in subclasses.
|
|
836
|
+
* @param _rawData - The parsed request body.
|
|
837
|
+
*/
|
|
838
|
+
onSwmlRequest(_rawData: Record<string, unknown>): void | Promise<void> {
|
|
839
|
+
// Default no-op
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Lifecycle hook called when a debug event webhook is received. Override in subclasses.
|
|
844
|
+
* @param _event - The debug event payload.
|
|
845
|
+
*/
|
|
846
|
+
onDebugEvent(_event: Record<string, unknown>): void | Promise<void> {
|
|
847
|
+
// Default no-op
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Override to add custom basic-auth validation logic beyond credential matching.
|
|
852
|
+
* @param _username - The username from the request.
|
|
853
|
+
* @param _password - The password from the request.
|
|
854
|
+
* @returns True if the credentials are valid; false to reject the request.
|
|
855
|
+
*/
|
|
856
|
+
validateBasicAuth(_username: string, _password: string): boolean | Promise<boolean> {
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Pre-execution hook called before each SWAIG function. Override in subclasses.
|
|
862
|
+
* @param _name - Name of the function about to execute.
|
|
863
|
+
* @param _args - Parsed arguments for the function.
|
|
864
|
+
* @param _rawData - The full raw SWAIG request payload.
|
|
865
|
+
*/
|
|
866
|
+
onFunctionCall(_name: string, _args: Record<string, unknown>, _rawData: Record<string, unknown>): void | Promise<void> {
|
|
867
|
+
// Default no-op
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// ── 5-phase SWML rendering ─────────────────────────────────────────
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Render the complete SWML document by assembling all 5 phases: pre-answer, answer,
|
|
874
|
+
* post-answer, AI, and post-AI verbs.
|
|
875
|
+
* @param callId - Optional call ID to use for session tokens; auto-generated if omitted.
|
|
876
|
+
* @returns The rendered SWML document as a JSON string.
|
|
877
|
+
*/
|
|
878
|
+
renderSwml(callId?: string): string {
|
|
879
|
+
this.swmlBuilder.reset();
|
|
880
|
+
|
|
881
|
+
const prompt = this.getPrompt();
|
|
882
|
+
const postPrompt = this.getPostPrompt();
|
|
883
|
+
if (!callId) callId = this.sessionManager.createSession();
|
|
884
|
+
|
|
885
|
+
// Build webhook URLs
|
|
886
|
+
const qp = { ...this.swaigQueryParams };
|
|
887
|
+
let defaultWebhookUrl = this.buildWebhookUrl('swaig', qp);
|
|
888
|
+
if (this.webHookUrlOverride) defaultWebhookUrl = this.webHookUrlOverride;
|
|
889
|
+
|
|
890
|
+
// Build SWAIG object
|
|
891
|
+
const swaigObj: Record<string, unknown> = {};
|
|
892
|
+
if (this.nativeFunctions.length) swaigObj['native_functions'] = this.nativeFunctions;
|
|
893
|
+
if (this.functionIncludes.length) swaigObj['includes'] = this.functionIncludes;
|
|
894
|
+
if (Object.keys(this.internalFillers).length) swaigObj['internal_fillers'] = this.internalFillers;
|
|
895
|
+
|
|
896
|
+
// Build functions array
|
|
897
|
+
const functions: Record<string, unknown>[] = [];
|
|
898
|
+
for (const [name, fn] of this.toolRegistry) {
|
|
899
|
+
if (fn instanceof SwaigFunction) {
|
|
900
|
+
let token: string | undefined;
|
|
901
|
+
if (fn.secure && callId) {
|
|
902
|
+
token = this.sessionManager.createToolToken(name, callId);
|
|
903
|
+
}
|
|
904
|
+
const entry: Record<string, unknown> = {
|
|
905
|
+
function: name,
|
|
906
|
+
description: fn.description,
|
|
907
|
+
parameters: fn.parameters && Object.keys(fn.parameters).length
|
|
908
|
+
? ('type' in fn.parameters && 'properties' in fn.parameters
|
|
909
|
+
? fn.parameters
|
|
910
|
+
: { type: 'object', properties: fn.parameters, ...(fn.required.length ? { required: fn.required } : {}) })
|
|
911
|
+
: { type: 'object', properties: {} },
|
|
912
|
+
};
|
|
913
|
+
if (fn.fillers && Object.keys(fn.fillers).length) entry['fillers'] = fn.fillers;
|
|
914
|
+
if (fn.waitFile) entry['wait_file'] = fn.waitFile;
|
|
915
|
+
if (fn.waitFileLoops !== undefined) entry['wait_file_loops'] = fn.waitFileLoops;
|
|
916
|
+
if (fn.webhookUrl) {
|
|
917
|
+
entry['web_hook_url'] = fn.webhookUrl;
|
|
918
|
+
} else if (token || Object.keys(this.swaigQueryParams).length) {
|
|
919
|
+
const urlParams = { ...this.swaigQueryParams };
|
|
920
|
+
if (token) urlParams['__token'] = token;
|
|
921
|
+
entry['web_hook_url'] = this.buildWebhookUrl('swaig', urlParams);
|
|
922
|
+
}
|
|
923
|
+
Object.assign(entry, fn.extraFields);
|
|
924
|
+
functions.push(entry);
|
|
925
|
+
} else {
|
|
926
|
+
// Raw dict (DataMap) - use as-is
|
|
927
|
+
const entry = { ...fn, function: name };
|
|
928
|
+
functions.push(entry);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (functions.length) {
|
|
933
|
+
swaigObj['functions'] = functions;
|
|
934
|
+
swaigObj['defaults'] = { web_hook_url: defaultWebhookUrl };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Build post-prompt URL
|
|
938
|
+
let postPromptUrl: string | undefined;
|
|
939
|
+
if (postPrompt) {
|
|
940
|
+
const ppParams = { ...this.swaigQueryParams };
|
|
941
|
+
if (callId) {
|
|
942
|
+
const ppToken = this.sessionManager.createToolToken('post_prompt', callId);
|
|
943
|
+
if (ppToken) ppParams['__token'] = ppToken;
|
|
944
|
+
}
|
|
945
|
+
postPromptUrl = this.buildWebhookUrl('post_prompt', ppParams);
|
|
946
|
+
if (this.postPromptUrlOverride) postPromptUrl = this.postPromptUrlOverride;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ── PHASE 1: Pre-answer verbs ──
|
|
950
|
+
for (const [verb, config] of this.preAnswerVerbs) {
|
|
951
|
+
this.swmlBuilder.addVerb(verb, config);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ── PHASE 2: Answer verb ──
|
|
955
|
+
if (this.autoAnswer) {
|
|
956
|
+
this.swmlBuilder.addVerb('answer', this.answerConfig);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ── PHASE 3: Post-answer verbs ──
|
|
960
|
+
if (this._recordCall) {
|
|
961
|
+
this.swmlBuilder.addVerb('record_call', {
|
|
962
|
+
format: this.recordFormat,
|
|
963
|
+
stereo: this.recordStereo,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
for (const [verb, config] of this.postAnswerVerbs) {
|
|
967
|
+
this.swmlBuilder.addVerb(verb, config);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// ── PHASE 4: AI verb ──
|
|
971
|
+
const aiConfig: Record<string, unknown> = {};
|
|
972
|
+
|
|
973
|
+
// Prompt
|
|
974
|
+
if (this.contextsBuilder) {
|
|
975
|
+
const contextsDict = this.contextsBuilder.toDict();
|
|
976
|
+
const promptObj: Record<string, unknown> = { text: prompt || `You are ${this.name}, a helpful AI assistant.` };
|
|
977
|
+
if (Object.keys(this.promptLlmParams).length) Object.assign(promptObj, this.promptLlmParams);
|
|
978
|
+
aiConfig['prompt'] = promptObj;
|
|
979
|
+
aiConfig['contexts'] = contextsDict;
|
|
980
|
+
} else {
|
|
981
|
+
const promptObj: Record<string, unknown> = { text: prompt };
|
|
982
|
+
if (Object.keys(this.promptLlmParams).length) Object.assign(promptObj, this.promptLlmParams);
|
|
983
|
+
aiConfig['prompt'] = promptObj;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Post-prompt
|
|
987
|
+
if (postPrompt) {
|
|
988
|
+
const ppObj: Record<string, unknown> = { text: postPrompt };
|
|
989
|
+
if (Object.keys(this.postPromptLlmParams).length) Object.assign(ppObj, this.postPromptLlmParams);
|
|
990
|
+
aiConfig['post_prompt'] = ppObj;
|
|
991
|
+
if (postPromptUrl) aiConfig['post_prompt_url'] = postPromptUrl;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// SWAIG
|
|
995
|
+
if (Object.keys(swaigObj).length) aiConfig['SWAIG'] = swaigObj;
|
|
996
|
+
|
|
997
|
+
// Additional config
|
|
998
|
+
if (this.hints.length) aiConfig['hints'] = this.hints;
|
|
999
|
+
if (this.languages.length) aiConfig['languages'] = this.languages;
|
|
1000
|
+
if (this.pronounce.length) aiConfig['pronounce'] = this.pronounce;
|
|
1001
|
+
if (Object.keys(this.params).length) aiConfig['params'] = this.params;
|
|
1002
|
+
if (Object.keys(this.globalData).length) aiConfig['global_data'] = this.globalData;
|
|
1003
|
+
|
|
1004
|
+
// Debug events
|
|
1005
|
+
if (this.debugEventsEnabled) {
|
|
1006
|
+
aiConfig['debug_webhook_url'] = this.buildWebhookUrl('debug_events');
|
|
1007
|
+
aiConfig['debug_webhook_level'] = this.debugEventsLevel;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
this.swmlBuilder.addVerb('ai', aiConfig);
|
|
1011
|
+
|
|
1012
|
+
// ── PHASE 5: Post-AI verbs ──
|
|
1013
|
+
for (const [verb, config] of this.postAiVerbs) {
|
|
1014
|
+
this.swmlBuilder.addVerb(verb, config);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return this.swmlBuilder.renderDocument();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// ── Ephemeral copy for dynamic config ───────────────────────────────
|
|
1021
|
+
|
|
1022
|
+
private createEphemeralCopy(): AgentBase {
|
|
1023
|
+
const copy = Object.create(Object.getPrototypeOf(this)) as AgentBase;
|
|
1024
|
+
Object.assign(copy, this);
|
|
1025
|
+
// Deep-copy mutable state
|
|
1026
|
+
copy.promptManager = new PromptManager(true);
|
|
1027
|
+
// Carry over the current prompt
|
|
1028
|
+
const p = this.getPrompt();
|
|
1029
|
+
if (p) copy.promptManager.setPromptText(p);
|
|
1030
|
+
const pp = this.getPostPrompt();
|
|
1031
|
+
if (pp) copy.promptManager.setPostPrompt(pp);
|
|
1032
|
+
copy.toolRegistry = new Map(this.toolRegistry);
|
|
1033
|
+
copy.hints = [...this.hints];
|
|
1034
|
+
copy.languages = [...this.languages];
|
|
1035
|
+
copy.pronounce = [...this.pronounce];
|
|
1036
|
+
copy.params = { ...this.params };
|
|
1037
|
+
copy.globalData = { ...this.globalData };
|
|
1038
|
+
copy.preAnswerVerbs = [...this.preAnswerVerbs];
|
|
1039
|
+
copy.postAnswerVerbs = [...this.postAnswerVerbs];
|
|
1040
|
+
copy.postAiVerbs = [...this.postAiVerbs];
|
|
1041
|
+
copy.swmlBuilder = new SwmlBuilder();
|
|
1042
|
+
return copy;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ── Hono HTTP app ───────────────────────────────────────────────────
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Get or lazily create the Hono HTTP application with all routes, middleware, auth, and CORS.
|
|
1049
|
+
* @returns The configured Hono application instance.
|
|
1050
|
+
*/
|
|
1051
|
+
getApp(): Hono {
|
|
1052
|
+
if (this._app) return this._app;
|
|
1053
|
+
|
|
1054
|
+
const app = new Hono();
|
|
1055
|
+
|
|
1056
|
+
// Security headers
|
|
1057
|
+
const requestTimeout = parseInt(process.env['SWML_REQUEST_TIMEOUT'] ?? '30000', 10);
|
|
1058
|
+
const maxRequestSize = parseInt(process.env['SWML_MAX_REQUEST_SIZE'] ?? '1048576', 10);
|
|
1059
|
+
app.use('*', async (c, next) => {
|
|
1060
|
+
await next();
|
|
1061
|
+
c.res.headers.set('X-Content-Type-Options', 'nosniff');
|
|
1062
|
+
c.res.headers.set('X-Frame-Options', 'DENY');
|
|
1063
|
+
c.res.headers.set('X-XSS-Protection', '1; mode=block');
|
|
1064
|
+
c.res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// Request size limit
|
|
1068
|
+
app.use('*', async (c, next) => {
|
|
1069
|
+
const contentLength = c.req.header('content-length');
|
|
1070
|
+
if (contentLength && parseInt(contentLength, 10) > maxRequestSize) {
|
|
1071
|
+
return c.json({ error: 'Request too large' }, 413);
|
|
1072
|
+
}
|
|
1073
|
+
await next();
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Allowed hosts (configurable via env)
|
|
1077
|
+
const allowedHosts = process.env['SWML_ALLOWED_HOSTS'];
|
|
1078
|
+
if (allowedHosts) {
|
|
1079
|
+
const hostSet = new Set(allowedHosts.split(',').map(h => h.trim().toLowerCase()));
|
|
1080
|
+
app.use('*', async (c, next) => {
|
|
1081
|
+
const host = (c.req.header('host') ?? '').split(':')[0].toLowerCase();
|
|
1082
|
+
if (!hostSet.has(host)) {
|
|
1083
|
+
return c.json({ error: 'Forbidden: host not allowed' }, 403);
|
|
1084
|
+
}
|
|
1085
|
+
await next();
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Rate limiting (configurable via env, requests per minute per IP)
|
|
1090
|
+
const rateLimitStr = process.env['SWML_RATE_LIMIT'];
|
|
1091
|
+
if (rateLimitStr) {
|
|
1092
|
+
const maxPerMinute = parseInt(rateLimitStr, 10);
|
|
1093
|
+
if (maxPerMinute > 0) {
|
|
1094
|
+
const hits = new Map<string, { count: number; resetAt: number }>();
|
|
1095
|
+
app.use('*', async (c, next) => {
|
|
1096
|
+
const ip = c.req.header('x-forwarded-for')?.split(',')[0].trim()
|
|
1097
|
+
?? c.req.header('x-real-ip')
|
|
1098
|
+
?? 'unknown';
|
|
1099
|
+
const now = Date.now();
|
|
1100
|
+
let entry = hits.get(ip);
|
|
1101
|
+
if (!entry || now >= entry.resetAt) {
|
|
1102
|
+
entry = { count: 0, resetAt: now + 60_000 };
|
|
1103
|
+
hits.set(ip, entry);
|
|
1104
|
+
}
|
|
1105
|
+
entry.count++;
|
|
1106
|
+
if (entry.count > maxPerMinute) {
|
|
1107
|
+
return c.json({ error: 'Rate limit exceeded' }, 429);
|
|
1108
|
+
}
|
|
1109
|
+
await next();
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// CORS (configurable via env)
|
|
1115
|
+
const corsOrigins = process.env['SWML_CORS_ORIGINS'];
|
|
1116
|
+
const corsOrigin = corsOrigins ? corsOrigins.split(',').map(o => o.trim()) : '*';
|
|
1117
|
+
app.use('*', cors({ origin: corsOrigin, credentials: true }));
|
|
1118
|
+
|
|
1119
|
+
// Auth middleware
|
|
1120
|
+
const [user, pass] = this.basicAuthCreds;
|
|
1121
|
+
const authMw = basicAuth({ username: user, password: pass });
|
|
1122
|
+
|
|
1123
|
+
const basePath = this.route === '/' ? '' : this.route;
|
|
1124
|
+
|
|
1125
|
+
// Root - returns SWML
|
|
1126
|
+
const handleSwml = async (c: any) => {
|
|
1127
|
+
let body: Record<string, unknown> = {};
|
|
1128
|
+
try { body = await c.req.json(); } catch { /* GET or no body */ }
|
|
1129
|
+
|
|
1130
|
+
this.detectProxyFromRequest(c);
|
|
1131
|
+
await this.onSwmlRequest(body);
|
|
1132
|
+
|
|
1133
|
+
let agentToUse: AgentBase = this;
|
|
1134
|
+
if (this.dynamicConfigCallback) {
|
|
1135
|
+
agentToUse = this.createEphemeralCopy();
|
|
1136
|
+
const queryParams: Record<string, string> = {};
|
|
1137
|
+
const url = new URL(c.req.url);
|
|
1138
|
+
url.searchParams.forEach((v: string, k: string) => { queryParams[k] = v; });
|
|
1139
|
+
const headers: Record<string, string> = {};
|
|
1140
|
+
c.req.raw.headers.forEach((v: string, k: string) => { headers[k] = v; });
|
|
1141
|
+
await this.dynamicConfigCallback(queryParams, body, headers, agentToUse);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const callId = (body['call_id'] as string) || undefined;
|
|
1145
|
+
const swml = agentToUse.renderSwml(callId);
|
|
1146
|
+
return c.json(JSON.parse(swml));
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
app.get(`${basePath}`, authMw, handleSwml);
|
|
1150
|
+
app.post(`${basePath}`, authMw, handleSwml);
|
|
1151
|
+
if (basePath) {
|
|
1152
|
+
app.get(`${basePath}/`, authMw, handleSwml);
|
|
1153
|
+
app.post(`${basePath}/`, authMw, handleSwml);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// SWAIG function dispatcher
|
|
1157
|
+
const handleSwaig = async (c: any) => {
|
|
1158
|
+
let body: Record<string, unknown> = {};
|
|
1159
|
+
try { body = await c.req.json(); } catch { /* empty */ }
|
|
1160
|
+
|
|
1161
|
+
const fnName = body['function'] as string;
|
|
1162
|
+
if (!fnName) return c.json({ error: 'Missing function name' }, 400);
|
|
1163
|
+
|
|
1164
|
+
const fn = this.toolRegistry.get(fnName);
|
|
1165
|
+
if (!fn || !(fn instanceof SwaigFunction)) {
|
|
1166
|
+
return c.json({ error: `Unknown function: ${fnName}` }, 404);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Token validation for secure functions
|
|
1170
|
+
if (fn.secure) {
|
|
1171
|
+
const url = new URL(c.req.url);
|
|
1172
|
+
const token = url.searchParams.get('__token') ?? url.searchParams.get('token');
|
|
1173
|
+
const callId = (body['call_id'] as string) ?? '';
|
|
1174
|
+
if (!token || !this.sessionManager.validateToken(callId, fnName, token)) {
|
|
1175
|
+
return c.json({ error: 'Invalid or expired token' }, 403);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const args = (body['argument'] as Record<string, unknown>) ?? {};
|
|
1180
|
+
await this.onFunctionCall(fnName, args, body);
|
|
1181
|
+
const result = await fn.execute(args, body);
|
|
1182
|
+
return c.json(result);
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
app.get(`${basePath}/swaig`, authMw, handleSwaig);
|
|
1186
|
+
app.post(`${basePath}/swaig`, authMw, handleSwaig);
|
|
1187
|
+
|
|
1188
|
+
// Post-prompt handler
|
|
1189
|
+
const handlePostPrompt = async (c: any) => {
|
|
1190
|
+
let body: Record<string, unknown> = {};
|
|
1191
|
+
try { body = await c.req.json(); } catch { /* empty */ }
|
|
1192
|
+
|
|
1193
|
+
const summary = this.findSummary(body);
|
|
1194
|
+
await this.onSummary(summary, body);
|
|
1195
|
+
return c.json({ ok: true });
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
app.get(`${basePath}/post_prompt`, authMw, handlePostPrompt);
|
|
1199
|
+
app.post(`${basePath}/post_prompt`, authMw, handlePostPrompt);
|
|
1200
|
+
|
|
1201
|
+
// Debug events handler
|
|
1202
|
+
const handleDebugEvents = async (c: any) => {
|
|
1203
|
+
let body: Record<string, unknown> = {};
|
|
1204
|
+
try { body = await c.req.json(); } catch { /* empty */ }
|
|
1205
|
+
|
|
1206
|
+
this.log.debug('Debug event received', body);
|
|
1207
|
+
await this.onDebugEvent(body);
|
|
1208
|
+
return c.json({ ok: true });
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
app.post(`${basePath}/debug_events`, authMw, handleDebugEvents);
|
|
1212
|
+
|
|
1213
|
+
// Health / Ready
|
|
1214
|
+
app.get(`${basePath}/health`, (c: any) => c.json({ status: 'ok' }));
|
|
1215
|
+
app.get(`${basePath}/ready`, (c: any) => c.json({ status: 'ready' }));
|
|
1216
|
+
|
|
1217
|
+
this._app = app;
|
|
1218
|
+
return app;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Return this agent's Hono app for mounting as a sub-router in an AgentServer.
|
|
1223
|
+
* @returns The Hono application instance.
|
|
1224
|
+
*/
|
|
1225
|
+
asRouter(): Hono {
|
|
1226
|
+
return this.getApp();
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Start the HTTP server and begin listening for requests.
|
|
1231
|
+
* @returns A promise that resolves once the server is running.
|
|
1232
|
+
*/
|
|
1233
|
+
async serve(): Promise<void> {
|
|
1234
|
+
const { serve: honoServe } = await import('@hono/node-server');
|
|
1235
|
+
const app = this.getApp();
|
|
1236
|
+
this.log.info(`Agent '${this.name}' running at http://${this.host}:${this.port}${this.route}`);
|
|
1237
|
+
this.log.info(`Auth: ${this.basicAuthCreds[0]}:**** (source: ${this.basicAuthSource})`);
|
|
1238
|
+
honoServe({ fetch: app.fetch, port: this.port, hostname: this.host });
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Alias for {@link serve}. Starts the HTTP server.
|
|
1243
|
+
* @returns A promise that resolves once the server is running.
|
|
1244
|
+
*/
|
|
1245
|
+
async run(): Promise<void> {
|
|
1246
|
+
return this.serve();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
1250
|
+
|
|
1251
|
+
private findSummary(body: Record<string, unknown>): Record<string, unknown> | null {
|
|
1252
|
+
if (!body) return null;
|
|
1253
|
+
if (body['summary']) return body['summary'] as Record<string, unknown>;
|
|
1254
|
+
const ppd = body['post_prompt_data'] as Record<string, unknown> | undefined;
|
|
1255
|
+
if (ppd) {
|
|
1256
|
+
if (Array.isArray(ppd['parsed']) && ppd['parsed'].length) return ppd['parsed'][0];
|
|
1257
|
+
if (ppd['raw']) {
|
|
1258
|
+
try {
|
|
1259
|
+
return JSON.parse(ppd['raw'] as string);
|
|
1260
|
+
} catch {
|
|
1261
|
+
return ppd['raw'] as Record<string, unknown>;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Get the basic-auth credentials used by this agent.
|
|
1270
|
+
* @param includeSource - When true, a third element indicating the credential source is appended.
|
|
1271
|
+
* @returns A tuple of [username, password] or [username, password, source].
|
|
1272
|
+
*/
|
|
1273
|
+
getBasicAuthCredentials(includeSource?: false): [string, string];
|
|
1274
|
+
getBasicAuthCredentials(includeSource: true): [string, string, 'provided' | 'environment' | 'generated'];
|
|
1275
|
+
getBasicAuthCredentials(includeSource?: boolean): [string, string] | [string, string, 'provided' | 'environment' | 'generated'] {
|
|
1276
|
+
if (includeSource) return [...this.basicAuthCreds, this.basicAuthSource];
|
|
1277
|
+
return this.basicAuthCreds;
|
|
1278
|
+
}
|
|
1279
|
+
}
|