@vellumai/assistant 0.3.4 → 0.3.5
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/Dockerfile +2 -0
- package/README.md +37 -2
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +70 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -17
- package/src/__tests__/channel-approvals.test.ts +48 -1
- package/src/__tests__/channel-guardian.test.ts +74 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/handlers-twilio-config.test.ts +407 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +22 -11
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +21 -6
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/system-prompt.ts +24 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/daemon/handlers/config.ts +783 -9
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +108 -4
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +1 -1
- package/src/daemon/server.ts +6 -2
- package/src/daemon/session-agent-loop.ts +5 -1
- package/src/daemon/session-runtime-assembly.ts +55 -0
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +11 -1
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-init.ts +144 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/media-store.ts +759 -0
- package/src/memory/retriever.ts +6 -1
- package/src/memory/schema.ts +98 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +24 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +12 -4
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/http-server.ts +53 -27
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +67 -21
- package/src/runtime/run-orchestrator.ts +35 -2
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +35 -0
package/src/calls/twilio-rest.ts
CHANGED
|
@@ -155,6 +155,38 @@ export async function provisionPhoneNumber(
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/** Fetch the current status of a Twilio message by SID. */
|
|
159
|
+
export async function fetchMessageStatus(
|
|
160
|
+
accountSid: string,
|
|
161
|
+
authToken: string,
|
|
162
|
+
messageSid: string,
|
|
163
|
+
): Promise<{ status: string; errorCode?: string; errorMessage?: string }> {
|
|
164
|
+
const res = await fetch(
|
|
165
|
+
`${twilioBaseUrl(accountSid)}/Messages/${encodeURIComponent(messageSid)}.json`,
|
|
166
|
+
{
|
|
167
|
+
method: 'GET',
|
|
168
|
+
headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const text = await res.text();
|
|
174
|
+
throw new Error(`Twilio API error ${res.status}: ${text}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const data = (await res.json()) as {
|
|
178
|
+
status?: string;
|
|
179
|
+
error_code?: number | null;
|
|
180
|
+
error_message?: string | null;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
status: data.status ?? 'unknown',
|
|
185
|
+
errorCode: data.error_code != null ? String(data.error_code) : undefined,
|
|
186
|
+
errorMessage: data.error_message ?? undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
158
190
|
export interface WebhookUrls {
|
|
159
191
|
voiceUrl: string;
|
|
160
192
|
statusCallbackUrl: string;
|
|
@@ -224,3 +256,247 @@ export async function updatePhoneNumberWebhooks(
|
|
|
224
256
|
throw new Error(`Twilio API error ${updateRes.status} updating webhooks: ${text}`);
|
|
225
257
|
}
|
|
226
258
|
}
|
|
259
|
+
|
|
260
|
+
// ── Toll-Free Verification ──────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/** Twilio Messaging API base URL for toll-free verification endpoints. */
|
|
263
|
+
const TOLLFREE_VERIFICATION_BASE = 'https://messaging.twilio.com/v1/Tollfree/Verifications';
|
|
264
|
+
|
|
265
|
+
export interface TollFreeVerification {
|
|
266
|
+
sid: string;
|
|
267
|
+
status: string;
|
|
268
|
+
rejectionReason?: string;
|
|
269
|
+
rejectionReasons?: string[];
|
|
270
|
+
errorCode?: string;
|
|
271
|
+
editAllowed?: boolean;
|
|
272
|
+
editExpiration?: string;
|
|
273
|
+
regulationType?: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseTollFreeVerification(raw: Record<string, unknown>): TollFreeVerification {
|
|
277
|
+
return {
|
|
278
|
+
sid: raw.sid as string,
|
|
279
|
+
status: raw.status as string,
|
|
280
|
+
rejectionReason: (raw.rejection_reason as string) ?? undefined,
|
|
281
|
+
rejectionReasons: (raw.rejection_reasons as string[]) ?? undefined,
|
|
282
|
+
errorCode: (raw.error_code != null ? String(raw.error_code) : undefined),
|
|
283
|
+
editAllowed: (raw.edit_allowed as boolean) ?? undefined,
|
|
284
|
+
editExpiration: (raw.edit_expiration as string) ?? undefined,
|
|
285
|
+
regulationType: (raw.regulation_type as string) ?? undefined,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get toll-free verification status for a phone number.
|
|
291
|
+
* If `phoneNumberSid` is provided, filters by that SID; otherwise returns the
|
|
292
|
+
* first verification found.
|
|
293
|
+
*/
|
|
294
|
+
export async function getTollFreeVerificationStatus(
|
|
295
|
+
accountSid: string,
|
|
296
|
+
authToken: string,
|
|
297
|
+
phoneNumberSid?: string,
|
|
298
|
+
): Promise<TollFreeVerification | null> {
|
|
299
|
+
const params = new URLSearchParams();
|
|
300
|
+
if (phoneNumberSid) params.set('TollfreePhoneNumberSid', phoneNumberSid);
|
|
301
|
+
|
|
302
|
+
const url = params.toString()
|
|
303
|
+
? `${TOLLFREE_VERIFICATION_BASE}?${params.toString()}`
|
|
304
|
+
: TOLLFREE_VERIFICATION_BASE;
|
|
305
|
+
|
|
306
|
+
const res = await fetch(url, {
|
|
307
|
+
method: 'GET',
|
|
308
|
+
headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (!res.ok) {
|
|
312
|
+
const text = await res.text();
|
|
313
|
+
throw new Error(`Twilio Toll-Free Verification API error ${res.status}: ${text}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const data = (await res.json()) as { verifications?: Array<Record<string, unknown>> };
|
|
317
|
+
const verifications = data.verifications ?? [];
|
|
318
|
+
if (verifications.length === 0) return null;
|
|
319
|
+
|
|
320
|
+
return parseTollFreeVerification(verifications[0]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface TollFreeVerificationSubmitParams {
|
|
324
|
+
tollfreePhoneNumberSid: string;
|
|
325
|
+
businessName: string;
|
|
326
|
+
businessWebsite: string;
|
|
327
|
+
notificationEmail: string;
|
|
328
|
+
useCaseCategories: string[];
|
|
329
|
+
useCaseSummary: string;
|
|
330
|
+
productionMessageSample: string;
|
|
331
|
+
optInImageUrls: string[];
|
|
332
|
+
optInType: string;
|
|
333
|
+
messageVolume: string;
|
|
334
|
+
businessType?: string;
|
|
335
|
+
customerProfileSid?: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Submit a new toll-free verification request. */
|
|
339
|
+
export async function submitTollFreeVerification(
|
|
340
|
+
accountSid: string,
|
|
341
|
+
authToken: string,
|
|
342
|
+
params: TollFreeVerificationSubmitParams,
|
|
343
|
+
): Promise<TollFreeVerification> {
|
|
344
|
+
const body = new URLSearchParams();
|
|
345
|
+
body.set('TollfreePhoneNumberSid', params.tollfreePhoneNumberSid);
|
|
346
|
+
body.set('BusinessName', params.businessName);
|
|
347
|
+
body.set('BusinessWebsite', params.businessWebsite);
|
|
348
|
+
body.set('NotificationEmail', params.notificationEmail);
|
|
349
|
+
body.set('UseCaseSummary', params.useCaseSummary);
|
|
350
|
+
body.set('ProductionMessageSample', params.productionMessageSample);
|
|
351
|
+
body.set('OptInType', params.optInType);
|
|
352
|
+
body.set('MessageVolume', params.messageVolume);
|
|
353
|
+
body.set('BusinessType', params.businessType ?? 'SOLE_PROPRIETOR');
|
|
354
|
+
|
|
355
|
+
for (const cat of params.useCaseCategories) {
|
|
356
|
+
body.append('UseCaseCategories', cat);
|
|
357
|
+
}
|
|
358
|
+
for (const url of params.optInImageUrls) {
|
|
359
|
+
body.append('OptInImageUrls', url);
|
|
360
|
+
}
|
|
361
|
+
if (params.customerProfileSid) {
|
|
362
|
+
body.set('CustomerProfileSid', params.customerProfileSid);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const res = await fetch(TOLLFREE_VERIFICATION_BASE, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: {
|
|
368
|
+
Authorization: twilioAuthHeader(accountSid, authToken),
|
|
369
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
370
|
+
},
|
|
371
|
+
body: body.toString(),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
const text = await res.text();
|
|
376
|
+
throw new Error(`Twilio Toll-Free Verification submit error ${res.status}: ${text}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
380
|
+
return parseTollFreeVerification(data);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Update an existing toll-free verification. */
|
|
384
|
+
export async function updateTollFreeVerification(
|
|
385
|
+
accountSid: string,
|
|
386
|
+
authToken: string,
|
|
387
|
+
verificationSid: string,
|
|
388
|
+
params: Partial<TollFreeVerificationSubmitParams>,
|
|
389
|
+
): Promise<TollFreeVerification> {
|
|
390
|
+
const body = new URLSearchParams();
|
|
391
|
+
if (params.businessName) body.set('BusinessName', params.businessName);
|
|
392
|
+
if (params.businessWebsite) body.set('BusinessWebsite', params.businessWebsite);
|
|
393
|
+
if (params.notificationEmail) body.set('NotificationEmail', params.notificationEmail);
|
|
394
|
+
if (params.useCaseSummary) body.set('UseCaseSummary', params.useCaseSummary);
|
|
395
|
+
if (params.productionMessageSample) body.set('ProductionMessageSample', params.productionMessageSample);
|
|
396
|
+
if (params.optInType) body.set('OptInType', params.optInType);
|
|
397
|
+
if (params.messageVolume) body.set('MessageVolume', params.messageVolume);
|
|
398
|
+
if (params.businessType) body.set('BusinessType', params.businessType);
|
|
399
|
+
if (params.useCaseCategories) {
|
|
400
|
+
for (const cat of params.useCaseCategories) {
|
|
401
|
+
body.append('UseCaseCategories', cat);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (params.optInImageUrls) {
|
|
405
|
+
for (const url of params.optInImageUrls) {
|
|
406
|
+
body.append('OptInImageUrls', url);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (params.customerProfileSid) body.set('CustomerProfileSid', params.customerProfileSid);
|
|
410
|
+
|
|
411
|
+
const res = await fetch(`${TOLLFREE_VERIFICATION_BASE}/${encodeURIComponent(verificationSid)}`, {
|
|
412
|
+
method: 'POST',
|
|
413
|
+
headers: {
|
|
414
|
+
Authorization: twilioAuthHeader(accountSid, authToken),
|
|
415
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
416
|
+
},
|
|
417
|
+
body: body.toString(),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (!res.ok) {
|
|
421
|
+
const text = await res.text();
|
|
422
|
+
throw new Error(`Twilio Toll-Free Verification update error ${res.status}: ${text}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
426
|
+
return parseTollFreeVerification(data);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Delete a toll-free verification. */
|
|
430
|
+
export async function deleteTollFreeVerification(
|
|
431
|
+
accountSid: string,
|
|
432
|
+
authToken: string,
|
|
433
|
+
verificationSid: string,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const res = await fetch(`${TOLLFREE_VERIFICATION_BASE}/${encodeURIComponent(verificationSid)}`, {
|
|
436
|
+
method: 'DELETE',
|
|
437
|
+
headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!res.ok) {
|
|
441
|
+
const text = await res.text();
|
|
442
|
+
throw new Error(`Twilio Toll-Free Verification delete error ${res.status}: ${text}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get the SID for an incoming phone number.
|
|
448
|
+
* Looks up the number via `IncomingPhoneNumbers.json?PhoneNumber=...`.
|
|
449
|
+
*/
|
|
450
|
+
export async function getPhoneNumberSid(
|
|
451
|
+
accountSid: string,
|
|
452
|
+
authToken: string,
|
|
453
|
+
phoneNumber: string,
|
|
454
|
+
): Promise<string | null> {
|
|
455
|
+
const res = await fetch(
|
|
456
|
+
`${twilioBaseUrl(accountSid)}/IncomingPhoneNumbers.json?PhoneNumber=${encodeURIComponent(phoneNumber)}`,
|
|
457
|
+
{
|
|
458
|
+
method: 'GET',
|
|
459
|
+
headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
if (!res.ok) {
|
|
464
|
+
const text = await res.text();
|
|
465
|
+
throw new Error(`Twilio API error ${res.status} looking up phone number SID: ${text}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const data = (await res.json()) as {
|
|
469
|
+
incoming_phone_numbers: Array<{ sid: string; phone_number: string }>;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const match = data.incoming_phone_numbers.find((n) => n.phone_number === phoneNumber);
|
|
473
|
+
return match?.sid ?? null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Release (delete) an incoming phone number from the Twilio account.
|
|
478
|
+
* Looks up the SID by phone number then sends a DELETE request.
|
|
479
|
+
*/
|
|
480
|
+
export async function releasePhoneNumber(
|
|
481
|
+
accountSid: string,
|
|
482
|
+
authToken: string,
|
|
483
|
+
phoneNumber: string,
|
|
484
|
+
): Promise<void> {
|
|
485
|
+
const sid = await getPhoneNumberSid(accountSid, authToken, phoneNumber);
|
|
486
|
+
if (!sid) {
|
|
487
|
+
throw new Error(`Phone number ${phoneNumber} not found on Twilio account ${accountSid}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const res = await fetch(
|
|
491
|
+
`${twilioBaseUrl(accountSid)}/IncomingPhoneNumbers/${sid}.json`,
|
|
492
|
+
{
|
|
493
|
+
method: 'DELETE',
|
|
494
|
+
headers: { Authorization: twilioAuthHeader(accountSid, authToken) },
|
|
495
|
+
},
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
if (!res.ok) {
|
|
499
|
+
const text = await res.text();
|
|
500
|
+
throw new Error(`Twilio API error ${res.status} releasing phone number: ${text}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -29,6 +29,10 @@ import { resolveVoiceQualityProfile, isVoiceProfileValid } from './voice-quality
|
|
|
29
29
|
|
|
30
30
|
const log = getLogger('twilio-routes');
|
|
31
31
|
|
|
32
|
+
const CONTEXT_BLOCK_SPLIT_REGEX = /\n\s*\nContext:\s*/i;
|
|
33
|
+
const MAX_TASK_SUMMARY_CHARS = 120;
|
|
34
|
+
const DEFAULT_WELCOME_GREETING = 'Hello, this is an assistant calling. Is now a good time to talk?';
|
|
35
|
+
|
|
32
36
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
33
37
|
|
|
34
38
|
function escapeXml(str: string): string {
|
|
@@ -63,6 +67,40 @@ export function generateTwiML(
|
|
|
63
67
|
</Response>`;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
function summarizeTaskForGreeting(task: string | null): string | null {
|
|
71
|
+
if (!task) return null;
|
|
72
|
+
const primaryTask = task.split(CONTEXT_BLOCK_SPLIT_REGEX)[0]?.trim() ?? '';
|
|
73
|
+
if (!primaryTask) return null;
|
|
74
|
+
|
|
75
|
+
const compact = primaryTask.replace(/\s+/g, ' ').trim().replace(/[.!?]+$/, '');
|
|
76
|
+
if (!compact) return null;
|
|
77
|
+
if (compact.length <= MAX_TASK_SUMMARY_CHARS) return compact;
|
|
78
|
+
return `${compact.slice(0, MAX_TASK_SUMMARY_CHARS - 3).trimEnd()}...`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatTaskAsCallPurpose(taskSummary: string): string {
|
|
82
|
+
const lower = taskSummary.toLowerCase();
|
|
83
|
+
if (
|
|
84
|
+
lower.startsWith('about ') ||
|
|
85
|
+
lower.startsWith('to ') ||
|
|
86
|
+
lower.startsWith('for ') ||
|
|
87
|
+
lower.startsWith('regarding ')
|
|
88
|
+
) {
|
|
89
|
+
return taskSummary;
|
|
90
|
+
}
|
|
91
|
+
return `about ${taskSummary}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildWelcomeGreeting(task: string | null, configuredGreeting?: string): string {
|
|
95
|
+
const override = configuredGreeting?.trim();
|
|
96
|
+
if (override) return override;
|
|
97
|
+
|
|
98
|
+
const taskSummary = summarizeTaskForGreeting(task);
|
|
99
|
+
if (!taskSummary) return DEFAULT_WELCOME_GREETING;
|
|
100
|
+
|
|
101
|
+
return `Hello, I am calling ${formatTaskAsCallPurpose(taskSummary)}. Is now a good time to talk?`;
|
|
102
|
+
}
|
|
103
|
+
|
|
66
104
|
/**
|
|
67
105
|
* Resolve the WebSocket relay URL from Twilio config.
|
|
68
106
|
*
|
|
@@ -183,7 +221,7 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
183
221
|
// Fallback to legacy resolution when ingress is not configured
|
|
184
222
|
relayUrl = resolveRelayUrl(twilioConfig.wssBaseUrl, twilioConfig.webhookBaseUrl);
|
|
185
223
|
}
|
|
186
|
-
const welcomeGreeting = process.env.CALL_WELCOME_GREETING
|
|
224
|
+
const welcomeGreeting = buildWelcomeGreeting(session.task, process.env.CALL_WELCOME_GREETING);
|
|
187
225
|
|
|
188
226
|
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, profile);
|
|
189
227
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Knowledge Graph
|
|
2
|
+
|
|
3
|
+
Query the entity knowledge graph to explore relationships between people, projects, tools, and other entities tracked in memory.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- When the user asks about relationships between entities ("what tools does project X use?", "who works on project Y?")
|
|
8
|
+
- When the user wants to explore connected entities across the knowledge graph
|
|
9
|
+
- When automatic memory recall doesn't surface the right relationship-based information
|
|
10
|
+
|
|
11
|
+
## Capabilities
|
|
12
|
+
|
|
13
|
+
- **Neighbors**: Find entities directly connected to seed entities (optionally filtered by relation and entity type)
|
|
14
|
+
- **Typed traversal**: Multi-step traversal with type constraints at each step (e.g., "me -> works_on -> projects -> uses -> tools")
|
|
15
|
+
- **Intersection**: Find entities reachable from ALL given seeds (e.g., "projects both Alice and Bob work on")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"tools": [
|
|
4
|
+
{
|
|
5
|
+
"name": "knowledge_graph_query",
|
|
6
|
+
"description": "Query the entity knowledge graph to find related entities and their associated memory items. Supports three query types: 'neighbors' (direct connections), 'typed_traversal' (multi-step with type filters), and 'intersection' (entities reachable from ALL seeds).",
|
|
7
|
+
"category": "memory",
|
|
8
|
+
"risk": "low",
|
|
9
|
+
"input_schema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"query_type": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"enum": ["neighbors", "typed_traversal", "intersection"],
|
|
15
|
+
"description": "Type of graph query to execute"
|
|
16
|
+
},
|
|
17
|
+
"seeds": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "type": "string" },
|
|
20
|
+
"description": "Entity names or aliases to use as starting points"
|
|
21
|
+
},
|
|
22
|
+
"steps": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"relation_types": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": { "type": "string" },
|
|
30
|
+
"description": "Relation types to follow (e.g., 'uses', 'works_on', 'depends_on')"
|
|
31
|
+
},
|
|
32
|
+
"entity_types": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "type": "string" },
|
|
35
|
+
"description": "Entity types to include (e.g., 'person', 'project', 'tool')"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"description": "Traversal steps for typed_traversal and intersection queries. Each step defines type filters for one hop."
|
|
40
|
+
},
|
|
41
|
+
"max_results": {
|
|
42
|
+
"type": "number",
|
|
43
|
+
"description": "Maximum number of entities to return (default: 20)"
|
|
44
|
+
},
|
|
45
|
+
"include_items": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"description": "Include associated memory items for each found entity (default: true)"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": ["query_type", "seeds"]
|
|
51
|
+
},
|
|
52
|
+
"executor": "tools/graph-query.ts",
|
|
53
|
+
"execution_target": "host"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { initializeDb, getDb } from '../../../../memory/db.js';
|
|
2
|
+
import { findMatchedEntities, findNeighborEntities, getEntityLinkedItemCandidates, collectTypedNeighbors } from '../../../../memory/search/entity.js';
|
|
3
|
+
import { memoryEntities } from '../../../../memory/schema.js';
|
|
4
|
+
import { inArray } from 'drizzle-orm';
|
|
5
|
+
import type { TraversalStep } from '../../../../memory/search/types.js';
|
|
6
|
+
import type { EntityRelationType, EntityType } from '../../../../memory/entity-extractor.js';
|
|
7
|
+
import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
|
|
8
|
+
|
|
9
|
+
interface GraphQueryInput {
|
|
10
|
+
query_type: 'neighbors' | 'typed_traversal' | 'intersection';
|
|
11
|
+
seeds: string[];
|
|
12
|
+
steps?: Array<{
|
|
13
|
+
relation_types?: string[];
|
|
14
|
+
entity_types?: string[];
|
|
15
|
+
}>;
|
|
16
|
+
max_results?: number;
|
|
17
|
+
include_items?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface EntityResult {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
aliases: string[];
|
|
25
|
+
items?: Array<{ subject: string; statement: string }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function run(
|
|
29
|
+
input: Record<string, unknown>,
|
|
30
|
+
_context: ToolContext,
|
|
31
|
+
): Promise<ToolExecutionResult> {
|
|
32
|
+
const params = input as unknown as GraphQueryInput;
|
|
33
|
+
|
|
34
|
+
initializeDb();
|
|
35
|
+
|
|
36
|
+
const maxResults = params.max_results ?? 20;
|
|
37
|
+
const includeItems = params.include_items ?? true;
|
|
38
|
+
|
|
39
|
+
// Resolve seed entity names to IDs
|
|
40
|
+
const seedEntityIds: string[] = [];
|
|
41
|
+
const resolvedSeeds: Array<{ name: string; id: string }> = [];
|
|
42
|
+
for (const seedName of params.seeds) {
|
|
43
|
+
const matched = findMatchedEntities(seedName, 5);
|
|
44
|
+
if (matched.length > 0) {
|
|
45
|
+
seedEntityIds.push(matched[0].id);
|
|
46
|
+
resolvedSeeds.push({ name: seedName, id: matched[0].id });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (seedEntityIds.length === 0) {
|
|
51
|
+
return {
|
|
52
|
+
content: JSON.stringify({
|
|
53
|
+
error: 'No matching entities found for the provided seed names',
|
|
54
|
+
seeds: params.seeds,
|
|
55
|
+
}),
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// For intersection queries, all seeds must resolve — dropping any seed silently
|
|
61
|
+
// changes semantics from "reachable from ALL seeds" to "reachable from resolved seeds"
|
|
62
|
+
if (params.query_type === 'intersection' && seedEntityIds.length < params.seeds.length) {
|
|
63
|
+
const unresolvedSeeds = params.seeds.filter(
|
|
64
|
+
name => !resolvedSeeds.some(s => s.name === name),
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
content: JSON.stringify({
|
|
68
|
+
error: 'Some seed entities could not be resolved. Intersection requires all seeds to match.',
|
|
69
|
+
unresolved_seeds: unresolvedSeeds,
|
|
70
|
+
resolved_seeds: resolvedSeeds,
|
|
71
|
+
}),
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let resultEntityIds: string[];
|
|
77
|
+
|
|
78
|
+
switch (params.query_type) {
|
|
79
|
+
case 'neighbors': {
|
|
80
|
+
const steps = params.steps?.[0];
|
|
81
|
+
const result = findNeighborEntities(seedEntityIds, {
|
|
82
|
+
maxEdges: 40,
|
|
83
|
+
maxNeighborEntities: maxResults,
|
|
84
|
+
maxDepth: 1,
|
|
85
|
+
relationTypes: steps?.relation_types as EntityRelationType[] | undefined,
|
|
86
|
+
entityTypes: steps?.entity_types as EntityType[] | undefined,
|
|
87
|
+
});
|
|
88
|
+
resultEntityIds = result.neighborEntityIds;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'typed_traversal': {
|
|
93
|
+
const traversalSteps: TraversalStep[] = (params.steps ?? []).map(s => ({
|
|
94
|
+
relationTypes: s.relation_types as EntityRelationType[] | undefined,
|
|
95
|
+
entityTypes: s.entity_types as EntityType[] | undefined,
|
|
96
|
+
}));
|
|
97
|
+
resultEntityIds = collectTypedNeighbors(seedEntityIds, traversalSteps, {
|
|
98
|
+
maxResultsPerStep: maxResults,
|
|
99
|
+
maxEdgesPerStep: 40,
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'intersection': {
|
|
105
|
+
// Run typed traversal from each seed independently, then intersect
|
|
106
|
+
const traversalSteps: TraversalStep[] = (params.steps ?? []).map(s => ({
|
|
107
|
+
relationTypes: s.relation_types as EntityRelationType[] | undefined,
|
|
108
|
+
entityTypes: s.entity_types as EntityType[] | undefined,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const resultSets: Set<string>[] = [];
|
|
112
|
+
for (const seedId of seedEntityIds) {
|
|
113
|
+
const result = collectTypedNeighbors([seedId], traversalSteps, {
|
|
114
|
+
maxResultsPerStep: maxResults,
|
|
115
|
+
maxEdgesPerStep: 40,
|
|
116
|
+
});
|
|
117
|
+
resultSets.push(new Set(result));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (resultSets.length === 0) {
|
|
121
|
+
resultEntityIds = [];
|
|
122
|
+
} else {
|
|
123
|
+
// Intersect all sets
|
|
124
|
+
const intersection = [...resultSets[0]].filter(id =>
|
|
125
|
+
resultSets.every(set => set.has(id))
|
|
126
|
+
);
|
|
127
|
+
resultEntityIds = intersection;
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
return {
|
|
134
|
+
content: JSON.stringify({ error: `Unknown query_type: ${params.query_type}` }),
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Look up entity details
|
|
140
|
+
const db = getDb();
|
|
141
|
+
const entities: EntityResult[] = [];
|
|
142
|
+
|
|
143
|
+
if (resultEntityIds.length > 0) {
|
|
144
|
+
const entityRows = db
|
|
145
|
+
.select()
|
|
146
|
+
.from(memoryEntities)
|
|
147
|
+
.where(inArray(memoryEntities.id, resultEntityIds.slice(0, maxResults)))
|
|
148
|
+
.all();
|
|
149
|
+
|
|
150
|
+
for (const row of entityRows) {
|
|
151
|
+
const entity: EntityResult = {
|
|
152
|
+
id: row.id,
|
|
153
|
+
name: row.name,
|
|
154
|
+
type: row.type,
|
|
155
|
+
aliases: row.aliases ? JSON.parse(row.aliases) as string[] : [],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (includeItems) {
|
|
159
|
+
const candidates = getEntityLinkedItemCandidates([row.id], {
|
|
160
|
+
source: 'entity_direct',
|
|
161
|
+
scopeIds: _context.memoryScopeId ? [_context.memoryScopeId] : undefined,
|
|
162
|
+
});
|
|
163
|
+
entity.items = candidates.slice(0, 5).map(c => {
|
|
164
|
+
const parts = c.text.split(': ');
|
|
165
|
+
return {
|
|
166
|
+
subject: parts[0] ?? '',
|
|
167
|
+
statement: parts.slice(1).join(': ') || c.text,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
entities.push(entity);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
content: JSON.stringify({
|
|
178
|
+
query_type: params.query_type,
|
|
179
|
+
resolved_seeds: resolvedSeeds,
|
|
180
|
+
result_count: entities.length,
|
|
181
|
+
entities,
|
|
182
|
+
}, null, 2),
|
|
183
|
+
isError: false,
|
|
184
|
+
};
|
|
185
|
+
}
|