@stackbone/cli 0.9.1 → 0.9.3
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/CHANGELOG.md +56 -0
- package/main.js +1154 -233
- package/migrations/0122_model_routing_object.sql +16 -0
- package/package.json +1 -1
- package/stackbone-cli-0.9.3.tgz +0 -0
- package/voice-worker/main.js +955 -342
- package/voice-worker/main.js.map +4 -4
- package/voice-worker/package.json +1 -0
- package/stackbone-cli-0.9.1.tgz +0 -0
package/voice-worker/main.js
CHANGED
|
@@ -149,9 +149,10 @@ var loadVad = async () => {
|
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
// libs/runtime/voice-worker/src/worker.ts
|
|
152
|
-
import { log as
|
|
152
|
+
import { log as log5, voice as voice4 } from "@livekit/agents";
|
|
153
153
|
import {
|
|
154
154
|
BackgroundVoiceCancellation,
|
|
155
|
+
NoiseCancellation,
|
|
155
156
|
TelephonyBackgroundVoiceCancellation
|
|
156
157
|
} from "@livekit/noise-cancellation-node";
|
|
157
158
|
import { ParticipantKind } from "@livekit/rtc-node";
|
|
@@ -562,14 +563,255 @@ var schemaResponseSchema = z5.object({
|
|
|
562
563
|
handlers: jsonObject().nullable().optional()
|
|
563
564
|
});
|
|
564
565
|
|
|
566
|
+
// libs/shared/validators/src/lib/agents/queue-jobs.ts
|
|
567
|
+
import cronParser from "cron-parser";
|
|
568
|
+
import { z as z7 } from "zod";
|
|
569
|
+
|
|
570
|
+
// libs/shared/validators/src/lib/common/utf8-byte-length.ts
|
|
571
|
+
var ENCODER = new TextEncoder();
|
|
572
|
+
function utf8ByteLength(value) {
|
|
573
|
+
return ENCODER.encode(value).length;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// libs/shared/validators/src/lib/studio/studio-queues.ts
|
|
577
|
+
import { z as z6 } from "zod";
|
|
578
|
+
var QUEUE_NAME_REGEX = /^[A-Za-z0-9_.-]+$/;
|
|
579
|
+
var queueNameSchema = z6.string().min(1).max(128).regex(QUEUE_NAME_REGEX, "queue name must be a BullMQ queue identifier ([A-Za-z0-9_.-])");
|
|
580
|
+
var jobSourceSchema = z6.enum(["bullmq", "journal"]);
|
|
581
|
+
var bullmqJobStatusSchema = z6.enum(["pending", "active", "delayed", "failed"]);
|
|
582
|
+
var journalTerminalStatusSchema = z6.enum(["done", "failed"]);
|
|
583
|
+
var journalJobStatusSchema = z6.enum([
|
|
584
|
+
...journalTerminalStatusSchema.options,
|
|
585
|
+
"running",
|
|
586
|
+
"lost"
|
|
587
|
+
]);
|
|
588
|
+
var jobStatusSchema = z6.enum([
|
|
589
|
+
...bullmqJobStatusSchema.options,
|
|
590
|
+
...journalJobStatusSchema.options
|
|
591
|
+
]);
|
|
592
|
+
var jobStatusFilterSchema = z6.enum([
|
|
593
|
+
...bullmqJobStatusSchema.options,
|
|
594
|
+
...journalTerminalStatusSchema.options
|
|
595
|
+
]);
|
|
596
|
+
var queueSummarySchema = z6.object({
|
|
597
|
+
name: z6.string(),
|
|
598
|
+
// `is_dlq: true` flags queues whose name follows the `*-dlq` / `*_dlq`
|
|
599
|
+
// convention. Studio renders them under a "Dead-letter" subgroup; the
|
|
600
|
+
// server-side actions (retry/discard) are unchanged.
|
|
601
|
+
is_dlq: z6.boolean(),
|
|
602
|
+
// BullMQ live counts. `failed` is the dispatch-failure / exhausted-retry
|
|
603
|
+
// bucket the inspector surfaces for manual retry. Completed jobs are not
|
|
604
|
+
// counted here — their record lives in the agent journal.
|
|
605
|
+
pending: z6.number().int().nonnegative(),
|
|
606
|
+
active: z6.number().int().nonnegative(),
|
|
607
|
+
delayed: z6.number().int().nonnegative(),
|
|
608
|
+
failed: z6.number().int().nonnegative()
|
|
609
|
+
}).strict();
|
|
610
|
+
var queueListResponseSchema = z6.object({
|
|
611
|
+
items: z6.array(queueSummarySchema)
|
|
612
|
+
});
|
|
613
|
+
var jobCommonFields = {
|
|
614
|
+
id: z6.string(),
|
|
615
|
+
queue: z6.string(),
|
|
616
|
+
// Opaque job name the agent chose at publish time (e.g. `send-email`).
|
|
617
|
+
// Null for legacy/anonymous rows that predate named jobs.
|
|
618
|
+
name: z6.string().nullable(),
|
|
619
|
+
// BullMQ `attemptsMade` for in-flight rows; the journal's recorded attempt
|
|
620
|
+
// count for execution rows.
|
|
621
|
+
attempts: z6.number().int().nonnegative(),
|
|
622
|
+
enqueued_at: z6.iso.datetime(),
|
|
623
|
+
// Failure reason: the BullMQ `failedReason` (dispatch failure) or the
|
|
624
|
+
// application error the agent returned (also persisted in the journal).
|
|
625
|
+
error: z6.string().nullable(),
|
|
626
|
+
// Correlation key between a BullMQ job and its journal execution row.
|
|
627
|
+
// BullMQ rows carry their own id here; journal rows carry the originating
|
|
628
|
+
// BullMQ job id. Null when uncorrelated.
|
|
629
|
+
dispatch_id: z6.string().nullable(),
|
|
630
|
+
payload: jsonObject()
|
|
631
|
+
};
|
|
632
|
+
var bullmqJobSchema = z6.object({
|
|
633
|
+
...jobCommonFields,
|
|
634
|
+
source: z6.literal("bullmq"),
|
|
635
|
+
status: bullmqJobStatusSchema,
|
|
636
|
+
// When the job is next expected to run: BullMQ `delay`/next-retry timestamp
|
|
637
|
+
// for `delayed`/`pending` rows, null when it runs immediately.
|
|
638
|
+
next_run_at: z6.iso.datetime().nullable(),
|
|
639
|
+
// BullMQ does not own an execution window; null until/unless the journal
|
|
640
|
+
// correlates one. A dispatch failure has neither.
|
|
641
|
+
started_at: z6.iso.datetime().nullable(),
|
|
642
|
+
finished_at: z6.iso.datetime().nullable()
|
|
643
|
+
}).strict();
|
|
644
|
+
var journalJobSchema = z6.object({
|
|
645
|
+
...jobCommonFields,
|
|
646
|
+
source: z6.literal("journal"),
|
|
647
|
+
status: journalJobStatusSchema,
|
|
648
|
+
// A journal row has no live scheduling state, so `next_run_at` is always
|
|
649
|
+
// null. Modelled as a nullable date-time pinned to null (rather than a bare
|
|
650
|
+
// `z.null()`) so it emits the `anyOf:[date-time, null]` shape the OpenAPI
|
|
651
|
+
// 3.0 downgrader already collapses to `nullable: true` — a standalone
|
|
652
|
+
// `{ type: 'null' }` is not valid OpenAPI 3.0 and orval rejects it.
|
|
653
|
+
next_run_at: z6.iso.datetime().nullable().refine((value) => value === null, {
|
|
654
|
+
message: "a journal row never carries a live next_run_at"
|
|
655
|
+
}),
|
|
656
|
+
started_at: z6.iso.datetime(),
|
|
657
|
+
finished_at: z6.iso.datetime().nullable()
|
|
658
|
+
}).strict();
|
|
659
|
+
var jobSchema = z6.discriminatedUnion("source", [bullmqJobSchema, journalJobSchema]);
|
|
660
|
+
var listJobsQuerySchema = forwardPageQuerySchema.extend({
|
|
661
|
+
queue: queueNameSchema,
|
|
662
|
+
// Filter by the BullMQ-live or journal-terminal states only. The journal
|
|
663
|
+
// `running` row exists in the DB as a NULL status, so a `WHERE status =
|
|
664
|
+
// 'running'` filter could never match it — it is reachable only on an
|
|
665
|
+
// unfiltered page, never as a filter value.
|
|
666
|
+
status: jobStatusFilterSchema.optional(),
|
|
667
|
+
limit: limitParamSchema(200).default(50)
|
|
668
|
+
});
|
|
669
|
+
var jobListResponseSchema = forwardPageResponseSchema(jobSchema);
|
|
670
|
+
var jobActionParamsSchema = z6.object({
|
|
671
|
+
queue: queueNameSchema,
|
|
672
|
+
jobId: z6.string().min(1, "job id must be a non-empty BullMQ job id")
|
|
673
|
+
});
|
|
674
|
+
var jobActionResponseSchema = z6.object({
|
|
675
|
+
// Echoes the job id the action operated on. Retry re-enqueues the same job;
|
|
676
|
+
// discard only drops it.
|
|
677
|
+
id: z6.string(),
|
|
678
|
+
queue: z6.string(),
|
|
679
|
+
status: jobStatusSchema
|
|
680
|
+
});
|
|
681
|
+
var peekQueueResponseSchema = z6.object({
|
|
682
|
+
items: z6.array(bullmqJobSchema)
|
|
683
|
+
});
|
|
684
|
+
var purgeQueueResponseSchema = z6.object({
|
|
685
|
+
purged: z6.number().int().nonnegative()
|
|
686
|
+
});
|
|
687
|
+
var pushJobRequestSchema = z6.object({
|
|
688
|
+
payload: jsonObject(),
|
|
689
|
+
retries: z6.number().int().nonnegative().optional(),
|
|
690
|
+
delay: z6.number().int().nonnegative().optional()
|
|
691
|
+
});
|
|
692
|
+
var pushJobResponseSchema = z6.object({
|
|
693
|
+
message_id: z6.string(),
|
|
694
|
+
queue: queueNameSchema
|
|
695
|
+
});
|
|
696
|
+
var queueNameParamsSchema = z6.object({
|
|
697
|
+
name: queueNameSchema
|
|
698
|
+
});
|
|
699
|
+
var peekQueueQuerySchema = z6.object({
|
|
700
|
+
limit: limitParamSchema(50).default(10)
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// libs/shared/validators/src/lib/agents/queue-jobs.ts
|
|
704
|
+
var MAX_JOB_ATTEMPTS = 20;
|
|
705
|
+
var CRON_FIELD_COUNT = 5;
|
|
706
|
+
var MAX_DELAY_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
707
|
+
var MAX_JOB_PAYLOAD_BYTES = 100 * 1024;
|
|
708
|
+
var MAX_JOB_REQUEST_BYTES = MAX_JOB_PAYLOAD_BYTES + 16 * 1024;
|
|
709
|
+
var boundedJobPayload = jsonObject().refine(
|
|
710
|
+
(payload) => utf8ByteLength(JSON.stringify(payload)) <= MAX_JOB_PAYLOAD_BYTES,
|
|
711
|
+
{ message: `payload exceeds the ${MAX_JOB_PAYLOAD_BYTES}-byte limit` }
|
|
712
|
+
);
|
|
713
|
+
var baseJobFields = {
|
|
714
|
+
// The job name the agent chose. Surfaced in the Studio inspector and used as
|
|
715
|
+
// the BullMQ job name. Same character class as a queue name so it stays a
|
|
716
|
+
// clean Redis-keyed identifier.
|
|
717
|
+
name: queueNameSchema,
|
|
718
|
+
payload: boundedJobPayload,
|
|
719
|
+
// Per-job override of the platform retry default. Omitted → DEFAULT_JOB_ATTEMPTS.
|
|
720
|
+
retries: z7.number().int().min(0).max(MAX_JOB_ATTEMPTS).optional()
|
|
721
|
+
};
|
|
722
|
+
var publishJobRequestSchema = z7.object({
|
|
723
|
+
...baseJobFields,
|
|
724
|
+
// Deferred delivery in milliseconds. Omitted / 0 → immediate. Capped at
|
|
725
|
+
// MAX_DELAY_MS (30 days) so a unit slip can't pin a delayed job indefinitely.
|
|
726
|
+
delay: z7.number().int().min(0).max(MAX_DELAY_MS).optional(),
|
|
727
|
+
// Optional idempotency hint. When set the dispatcher uses it as the BullMQ
|
|
728
|
+
// job id so a duplicate publish with the same key collapses to one job.
|
|
729
|
+
deduplicationId: z7.string().min(1).max(255).optional()
|
|
730
|
+
}).strict();
|
|
731
|
+
var publishJobResponseSchema = z7.object({
|
|
732
|
+
// The BullMQ job id the dispatcher minted (or the dedupe id when supplied).
|
|
733
|
+
messageId: z7.string()
|
|
734
|
+
}).strict();
|
|
735
|
+
var cronExpressionSchema = z7.string().min(1).max(120);
|
|
736
|
+
function isKnownTimeZone(tz) {
|
|
737
|
+
try {
|
|
738
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
739
|
+
return true;
|
|
740
|
+
} catch {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function validateCronAndTz(cron, tz) {
|
|
745
|
+
const issues = [];
|
|
746
|
+
const trimmed = cron.trim();
|
|
747
|
+
const isMacro = trimmed.startsWith("@");
|
|
748
|
+
const fields = trimmed.split(/\s+/).filter(Boolean);
|
|
749
|
+
const shapeValid = isMacro || fields.length === CRON_FIELD_COUNT;
|
|
750
|
+
if (!shapeValid) {
|
|
751
|
+
issues.push({
|
|
752
|
+
path: "cron",
|
|
753
|
+
message: `cron must have exactly ${CRON_FIELD_COUNT} fields (minute hour day month weekday) or a macro (@hourly, @daily, @weekly, @monthly, @yearly); sub-minute schedules are not allowed`
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const tzValid = tz === void 0 || isKnownTimeZone(tz);
|
|
757
|
+
if (!tzValid) {
|
|
758
|
+
issues.push({ path: "tz", message: `unknown IANA timezone: ${tz}` });
|
|
759
|
+
}
|
|
760
|
+
if (shapeValid && tzValid) {
|
|
761
|
+
try {
|
|
762
|
+
cronParser.parseExpression(trimmed, tz === void 0 ? void 0 : { tz }).next();
|
|
763
|
+
} catch {
|
|
764
|
+
issues.push({ path: "cron", message: `invalid cron expression: ${cron}` });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return issues;
|
|
768
|
+
}
|
|
769
|
+
var scheduleJobRequestSchema = z7.object({
|
|
770
|
+
...baseJobFields,
|
|
771
|
+
cron: cronExpressionSchema,
|
|
772
|
+
// IANA timezone the cron pattern is evaluated in. Defaults to UTC.
|
|
773
|
+
tz: z7.string().min(1).max(64).optional()
|
|
774
|
+
}).strict().superRefine((value, ctx) => {
|
|
775
|
+
for (const issue of validateCronAndTz(value.cron, value.tz)) {
|
|
776
|
+
ctx.addIssue({ code: "custom", path: [issue.path], message: issue.message });
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
var scheduleJobResponseSchema = z7.object({
|
|
780
|
+
// The scheduler id (unique per installation) the dispatcher upserted.
|
|
781
|
+
scheduleId: z7.string(),
|
|
782
|
+
name: z7.string(),
|
|
783
|
+
cron: z7.string(),
|
|
784
|
+
tz: z7.string()
|
|
785
|
+
}).strict();
|
|
786
|
+
var unscheduleJobRequestSchema = z7.object({
|
|
787
|
+
name: queueNameSchema
|
|
788
|
+
}).strict();
|
|
789
|
+
var unscheduleJobResponseSchema = z7.object({
|
|
790
|
+
name: z7.string(),
|
|
791
|
+
// True when a schedule with that name existed and was removed; false when
|
|
792
|
+
// nothing matched (the call stays idempotent either way).
|
|
793
|
+
removed: z7.boolean()
|
|
794
|
+
}).strict();
|
|
795
|
+
var scheduleSummarySchema = z7.object({
|
|
796
|
+
scheduleId: z7.string(),
|
|
797
|
+
name: z7.string(),
|
|
798
|
+
cron: z7.string(),
|
|
799
|
+
tz: z7.string(),
|
|
800
|
+
// Next planned fire time (ISO), null when BullMQ has not computed one yet.
|
|
801
|
+
next_run_at: z7.iso.datetime().nullable()
|
|
802
|
+
}).strict();
|
|
803
|
+
var listSchedulesResponseSchema = z7.object({
|
|
804
|
+
items: z7.array(scheduleSummarySchema)
|
|
805
|
+
}).strict();
|
|
806
|
+
|
|
565
807
|
// libs/shared/validators/src/lib/studio/studio-voice.ts
|
|
566
|
-
import { z as
|
|
808
|
+
import { z as z11 } from "zod";
|
|
567
809
|
|
|
568
810
|
// libs/shared/validators/src/lib/studio/memory-identities.ts
|
|
569
|
-
import { z as
|
|
570
|
-
var identityKeySchema =
|
|
571
|
-
var identityValueSchema =
|
|
572
|
-
var integrationIdentitiesSchema =
|
|
811
|
+
import { z as z8 } from "zod";
|
|
812
|
+
var identityKeySchema = z8.string().min(1).max(100);
|
|
813
|
+
var identityValueSchema = z8.string().min(1).max(500);
|
|
814
|
+
var integrationIdentitiesSchema = z8.unknown().superRefine((input, ctx) => {
|
|
573
815
|
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
574
816
|
ctx.addIssue({ code: "custom", message: "Identities must be a string map." });
|
|
575
817
|
return;
|
|
@@ -586,18 +828,45 @@ var integrationIdentitiesSchema = z6.unknown().superRefine((input, ctx) => {
|
|
|
586
828
|
});
|
|
587
829
|
}
|
|
588
830
|
}).transform((input) => Object.fromEntries(Object.entries(input)));
|
|
589
|
-
var memoryIdentitySnapshotSchema =
|
|
590
|
-
|
|
591
|
-
|
|
831
|
+
var memoryIdentitySnapshotSchema = z8.union([
|
|
832
|
+
z8.strictObject({ key: z8.null(), value: z8.null() }),
|
|
833
|
+
z8.strictObject({ key: identityKeySchema, value: identityValueSchema.nullable() })
|
|
592
834
|
]);
|
|
593
|
-
var memoryIdentityBindingSchema =
|
|
835
|
+
var memoryIdentityBindingSchema = z8.strictObject({
|
|
594
836
|
identityKey: identityKeySchema,
|
|
595
|
-
source:
|
|
596
|
-
fixedAt:
|
|
837
|
+
source: z8.enum(["code", "studio", "legacy"]),
|
|
838
|
+
fixedAt: z8.iso.datetime()
|
|
597
839
|
});
|
|
598
840
|
|
|
841
|
+
// libs/shared/validators/src/lib/studio/voice-greeting.ts
|
|
842
|
+
import { z as z9 } from "zod";
|
|
843
|
+
var VOICE_GREETING_MAX = 500;
|
|
844
|
+
var VOICE_CALL_VARIABLES_MAX = 20;
|
|
845
|
+
var VOICE_CALL_VARIABLE_VALUE_MAX = 200;
|
|
846
|
+
var VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
847
|
+
var voiceCallVariablesSchema = z9.unknown().superRefine((input, ctx) => {
|
|
848
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
849
|
+
ctx.addIssue({ code: "custom", message: "Call variables must be a string map." });
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const entries = Object.entries(input);
|
|
853
|
+
if (entries.length > VOICE_CALL_VARIABLES_MAX)
|
|
854
|
+
ctx.addIssue({
|
|
855
|
+
code: "custom",
|
|
856
|
+
message: `At most ${VOICE_CALL_VARIABLES_MAX} call variables are allowed.`
|
|
857
|
+
});
|
|
858
|
+
for (const [key, value] of entries) {
|
|
859
|
+
if (!VARIABLE_NAME.test(key) || typeof value !== "string" || value.trim() === "" || value.length > VOICE_CALL_VARIABLE_VALUE_MAX)
|
|
860
|
+
ctx.addIssue({
|
|
861
|
+
code: "custom",
|
|
862
|
+
path: [key],
|
|
863
|
+
message: `Call variable names are letters, digits and _ (not starting with a digit); values are 1\u2013${VOICE_CALL_VARIABLE_VALUE_MAX} characters.`
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}).transform((input) => Object.fromEntries(Object.entries(input)));
|
|
867
|
+
|
|
599
868
|
// libs/shared/validators/src/lib/studio/studio-rag.ts
|
|
600
|
-
import { z as
|
|
869
|
+
import { z as z10 } from "zod";
|
|
601
870
|
var STUDIO_RAG_DOCS_LIMIT_DEFAULT = 50;
|
|
602
871
|
var STUDIO_RAG_DOCS_LIMIT_MAX = 200;
|
|
603
872
|
var STUDIO_RAG_TOPK_DEFAULT = 10;
|
|
@@ -605,63 +874,63 @@ var STUDIO_RAG_TOPK_MAX = 50;
|
|
|
605
874
|
var STUDIO_RAG_JOBS_LIMIT_DEFAULT = 50;
|
|
606
875
|
var STUDIO_RAG_JOBS_LIMIT_MAX = 200;
|
|
607
876
|
var STUDIO_RAG_COLLECTION_REGEX = /^[A-Za-z0-9][A-Za-z0-9._\-/:]{0,127}$/;
|
|
608
|
-
var ragCollectionNameParamSchema =
|
|
609
|
-
var ragDistanceSchema =
|
|
610
|
-
var ragIndexKindSchema =
|
|
611
|
-
var ragNumberFormatSchema =
|
|
612
|
-
var ragRangeFieldSchema =
|
|
877
|
+
var ragCollectionNameParamSchema = z10.string().min(1).max(128).regex(STUDIO_RAG_COLLECTION_REGEX, "collection must be 1..128 ASCII identifier-safe chars");
|
|
878
|
+
var ragDistanceSchema = z10.enum(["cosine", "l2", "ip"]);
|
|
879
|
+
var ragIndexKindSchema = z10.enum(["hnsw", "exact"]);
|
|
880
|
+
var ragNumberFormatSchema = z10.enum(["eu", "us"]);
|
|
881
|
+
var ragRangeFieldSchema = z10.string().min(1).refine((field) => !field.includes("."), {
|
|
613
882
|
message: "field must be a literal top-level metadata key without dots"
|
|
614
883
|
});
|
|
615
|
-
var ragExtractFieldSchema =
|
|
616
|
-
name:
|
|
617
|
-
type:
|
|
618
|
-
removed:
|
|
884
|
+
var ragExtractFieldSchema = z10.object({
|
|
885
|
+
name: z10.string().trim().pipe(ragRangeFieldSchema),
|
|
886
|
+
type: z10.enum(["number", "date", "text"]),
|
|
887
|
+
removed: z10.boolean().optional()
|
|
619
888
|
});
|
|
620
889
|
var uniqueFieldNames = (fields) => new Set(fields.map((field) => field.name)).size === fields.length;
|
|
621
|
-
var ragExtractFieldsRequestSchema =
|
|
890
|
+
var ragExtractFieldsRequestSchema = z10.array(ragExtractFieldSchema).refine(uniqueFieldNames, {
|
|
622
891
|
message: "Field names must be unique"
|
|
623
892
|
});
|
|
624
|
-
var ragCollectionSchema =
|
|
625
|
-
name:
|
|
626
|
-
dimensions:
|
|
627
|
-
embedding_state:
|
|
893
|
+
var ragCollectionSchema = z10.object({
|
|
894
|
+
name: z10.string(),
|
|
895
|
+
dimensions: z10.number().int().min(1),
|
|
896
|
+
embedding_state: z10.enum(["ready", "rebuilding", "failed"]).optional(),
|
|
628
897
|
// Optional for boxes that predate collection-owned embeddings.
|
|
629
|
-
embedding_provider:
|
|
630
|
-
embedding_model:
|
|
898
|
+
embedding_provider: z10.string().min(1).optional(),
|
|
899
|
+
embedding_model: z10.string().min(1).optional(),
|
|
631
900
|
distance: ragDistanceSchema,
|
|
632
901
|
index_kind: ragIndexKindSchema,
|
|
633
|
-
chunk_count:
|
|
634
|
-
doc_count:
|
|
902
|
+
chunk_count: z10.number().int().nonnegative(),
|
|
903
|
+
doc_count: z10.number().int().nonnegative(),
|
|
635
904
|
// Maximum `created_at` across the collection's documents. Null when the
|
|
636
905
|
// collection has zero documents (an empty collection row created before its
|
|
637
906
|
// first ingest).
|
|
638
|
-
last_ingested_at:
|
|
907
|
+
last_ingested_at: z10.iso.datetime().nullable(),
|
|
639
908
|
// STA3-57. Optional so a list from a box older than hybrid search still parses.
|
|
640
|
-
hybrid_enabled:
|
|
909
|
+
hybrid_enabled: z10.boolean().optional(),
|
|
641
910
|
number_format: ragNumberFormatSchema.optional(),
|
|
642
|
-
extract_enabled:
|
|
643
|
-
extract_fields:
|
|
911
|
+
extract_enabled: z10.boolean().optional(),
|
|
912
|
+
extract_fields: z10.array(ragExtractFieldSchema).optional()
|
|
644
913
|
});
|
|
645
|
-
var ragCollectionListResponseSchema =
|
|
646
|
-
items:
|
|
914
|
+
var ragCollectionListResponseSchema = z10.object({
|
|
915
|
+
items: z10.array(ragCollectionSchema)
|
|
647
916
|
});
|
|
648
|
-
var ragJobStatusSchema =
|
|
649
|
-
var ragDocumentSchema =
|
|
650
|
-
id:
|
|
651
|
-
namespace:
|
|
917
|
+
var ragJobStatusSchema = z10.enum(["queued", "running", "succeeded", "failed", "cancelled"]);
|
|
918
|
+
var ragDocumentSchema = z10.object({
|
|
919
|
+
id: z10.string(),
|
|
920
|
+
namespace: z10.string(),
|
|
652
921
|
// Zero while the document is still ingesting (staged row, no chunks yet) or
|
|
653
922
|
// when its ingest run failed before writing any chunk. A ready document has
|
|
654
923
|
// at least one chunk.
|
|
655
|
-
chunks_count:
|
|
924
|
+
chunks_count: z10.number().int().nonnegative(),
|
|
656
925
|
// Document-level metadata copied onto the chunks at ingest time. We surface
|
|
657
926
|
// the document's `metadata` so the UI can render filterable tags without
|
|
658
927
|
// paging through chunks.
|
|
659
928
|
metadata: jsonObject(),
|
|
660
929
|
// Truncated content of the first chunk. Full body is one click away via
|
|
661
930
|
// the docs drawer in the component.
|
|
662
|
-
preview:
|
|
663
|
-
first_seen_at:
|
|
664
|
-
last_seen_at:
|
|
931
|
+
preview: z10.string(),
|
|
932
|
+
first_seen_at: z10.iso.datetime(),
|
|
933
|
+
last_seen_at: z10.iso.datetime(),
|
|
665
934
|
// Ingest status of the run that produced this document, when the document
|
|
666
935
|
// was uploaded through Studio's managed ingest (which fires a durable
|
|
667
936
|
// `rag-ingest` run). `null` for documents the agent wrote directly via
|
|
@@ -672,22 +941,22 @@ var ragDocumentSchema = z7.object({
|
|
|
672
941
|
// Id of the ingest run that produced this document, for deep-linking to the
|
|
673
942
|
// run details screen (`studio/runs/:runId`). `null`/absent when there is no
|
|
674
943
|
// tracked run (direct SDK ingest, or the cloud surface).
|
|
675
|
-
run_id:
|
|
944
|
+
run_id: z10.string().nullable().optional(),
|
|
676
945
|
// Error message copied off the ingest run when it failed, so the docs table
|
|
677
946
|
// can surface it inline without opening the run. `null`/absent otherwise.
|
|
678
|
-
ingest_error:
|
|
947
|
+
ingest_error: z10.string().nullable().optional(),
|
|
679
948
|
// STA3-8. Every version of a document is its own row; the list shows the
|
|
680
949
|
// published one, so `id` is that version's id. `version_count` counts every
|
|
681
950
|
// version, published or not. `pending_version` is the newest version past the
|
|
682
951
|
// published one whose ingest finished, the "v2 ready to publish" on the row;
|
|
683
952
|
// `null` when there is none. Optional, like the ingest fields above, so a box
|
|
684
953
|
// from before versioning still parses.
|
|
685
|
-
version:
|
|
686
|
-
version_count:
|
|
687
|
-
pending_version:
|
|
954
|
+
version: z10.number().int().min(1).optional(),
|
|
955
|
+
version_count: z10.number().int().min(1).optional(),
|
|
956
|
+
pending_version: z10.number().int().min(1).nullable().optional()
|
|
688
957
|
});
|
|
689
958
|
var ragFilterSchema = jsonObject();
|
|
690
|
-
var ragDateValueSchema =
|
|
959
|
+
var ragDateValueSchema = z10.string().length(10).regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/).refine((value) => {
|
|
691
960
|
const year = Number(value.slice(0, 4));
|
|
692
961
|
const month = Number(value.slice(5, 7));
|
|
693
962
|
const day = Number(value.slice(8, 10));
|
|
@@ -695,18 +964,18 @@ var ragDateValueSchema = z7.string().length(10).regex(/^[0-9]{4}-[0-9]{2}-[0-9]{
|
|
|
695
964
|
date.setUTCFullYear(year, month - 1, day);
|
|
696
965
|
return year >= 1 && year <= 9999 && date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
697
966
|
}, "date must be a real YYYY-MM-DD day in years 0001\u20139999");
|
|
698
|
-
var ragRangeSchema =
|
|
699
|
-
|
|
967
|
+
var ragRangeSchema = z10.discriminatedUnion("type", [
|
|
968
|
+
z10.strictObject({
|
|
700
969
|
field: ragRangeFieldSchema,
|
|
701
|
-
type:
|
|
702
|
-
gt:
|
|
703
|
-
gte:
|
|
704
|
-
lt:
|
|
705
|
-
lte:
|
|
970
|
+
type: z10.literal("number"),
|
|
971
|
+
gt: z10.number().finite().optional(),
|
|
972
|
+
gte: z10.number().finite().optional(),
|
|
973
|
+
lt: z10.number().finite().optional(),
|
|
974
|
+
lte: z10.number().finite().optional()
|
|
706
975
|
}),
|
|
707
|
-
|
|
976
|
+
z10.strictObject({
|
|
708
977
|
field: ragRangeFieldSchema,
|
|
709
|
-
type:
|
|
978
|
+
type: z10.literal("date"),
|
|
710
979
|
gt: ragDateValueSchema.optional(),
|
|
711
980
|
gte: ragDateValueSchema.optional(),
|
|
712
981
|
lt: ragDateValueSchema.optional(),
|
|
@@ -728,121 +997,121 @@ var ragRangeSchema = z7.discriminatedUnion("type", [
|
|
|
728
997
|
ctx.addIssue({ code: "custom", message: "lower bound must not exceed upper bound" });
|
|
729
998
|
}
|
|
730
999
|
});
|
|
731
|
-
var ragRangesSchema =
|
|
1000
|
+
var ragRangesSchema = z10.array(ragRangeSchema).max(10);
|
|
732
1001
|
var listRagDocsQuerySchema = forwardPageQuerySchema.extend({
|
|
733
1002
|
limit: limitParamSchema(STUDIO_RAG_DOCS_LIMIT_MAX).default(STUDIO_RAG_DOCS_LIMIT_DEFAULT),
|
|
734
1003
|
// Filter is sent JSON-encoded inside a single query param so the UI can
|
|
735
1004
|
// pass an arbitrarily nested object without colliding with the URL syntax.
|
|
736
1005
|
// Empty/absent => no filter.
|
|
737
|
-
filter:
|
|
1006
|
+
filter: z10.string().optional()
|
|
738
1007
|
});
|
|
739
1008
|
var ragDocumentListResponseSchema = forwardPageResponseSchema(ragDocumentSchema);
|
|
740
|
-
var ragDocumentVersionSchema =
|
|
741
|
-
id:
|
|
742
|
-
version:
|
|
743
|
-
is_active:
|
|
744
|
-
ready:
|
|
745
|
-
chunks_count:
|
|
1009
|
+
var ragDocumentVersionSchema = z10.object({
|
|
1010
|
+
id: z10.string(),
|
|
1011
|
+
version: z10.number().int().min(1),
|
|
1012
|
+
is_active: z10.boolean(),
|
|
1013
|
+
ready: z10.boolean(),
|
|
1014
|
+
chunks_count: z10.number().int().nonnegative(),
|
|
746
1015
|
ingest_status: ragJobStatusSchema.nullable(),
|
|
747
|
-
run_id:
|
|
748
|
-
ingest_error:
|
|
749
|
-
filename:
|
|
750
|
-
created_at:
|
|
1016
|
+
run_id: z10.string().nullable(),
|
|
1017
|
+
ingest_error: z10.string().nullable(),
|
|
1018
|
+
filename: z10.string().nullable(),
|
|
1019
|
+
created_at: z10.iso.datetime()
|
|
751
1020
|
});
|
|
752
|
-
var ragDocumentVersionListResponseSchema =
|
|
753
|
-
items:
|
|
1021
|
+
var ragDocumentVersionListResponseSchema = z10.object({
|
|
1022
|
+
items: z10.array(ragDocumentVersionSchema)
|
|
754
1023
|
});
|
|
755
|
-
var ragDocumentVersionNumberSchema =
|
|
756
|
-
var updateRagDocumentValuesRequestSchema =
|
|
1024
|
+
var ragDocumentVersionNumberSchema = z10.number().int().min(1).max(2147483647);
|
|
1025
|
+
var updateRagDocumentValuesRequestSchema = z10.strictObject({
|
|
757
1026
|
version: ragDocumentVersionNumberSchema.optional(),
|
|
758
1027
|
values: jsonObject(),
|
|
759
|
-
add_fields:
|
|
1028
|
+
add_fields: z10.array(ragExtractFieldSchema.omit({ removed: true })).refine(uniqueFieldNames, {
|
|
760
1029
|
message: "Field names must be unique"
|
|
761
1030
|
}).optional()
|
|
762
1031
|
});
|
|
763
|
-
var updateRagDocumentValuesResponseSchema =
|
|
764
|
-
id:
|
|
765
|
-
collection:
|
|
1032
|
+
var updateRagDocumentValuesResponseSchema = z10.object({
|
|
1033
|
+
id: z10.string(),
|
|
1034
|
+
collection: z10.string(),
|
|
766
1035
|
version: ragDocumentVersionNumberSchema,
|
|
767
1036
|
metadata: jsonObject()
|
|
768
1037
|
});
|
|
769
|
-
var extractRagDocumentRequestSchema =
|
|
1038
|
+
var extractRagDocumentRequestSchema = z10.strictObject({
|
|
770
1039
|
version: ragDocumentVersionNumberSchema.optional()
|
|
771
1040
|
});
|
|
772
|
-
var extractRagDocumentResponseSchema =
|
|
773
|
-
proposal:
|
|
1041
|
+
var extractRagDocumentResponseSchema = z10.object({
|
|
1042
|
+
proposal: z10.array(
|
|
774
1043
|
ragExtractFieldSchema.omit({ removed: true }).extend({
|
|
775
|
-
value:
|
|
776
|
-
new:
|
|
1044
|
+
value: z10.union([z10.number().finite(), z10.string()]),
|
|
1045
|
+
new: z10.boolean()
|
|
777
1046
|
})
|
|
778
1047
|
),
|
|
779
|
-
failed:
|
|
780
|
-
truncated:
|
|
781
|
-
});
|
|
782
|
-
var ragDocumentVersionParamSchema =
|
|
783
|
-
var activateRagDocumentVersionResponseSchema =
|
|
784
|
-
id:
|
|
785
|
-
collection:
|
|
786
|
-
version:
|
|
787
|
-
previous_version:
|
|
788
|
-
changed:
|
|
789
|
-
});
|
|
790
|
-
var ragQueryRequestSchema =
|
|
791
|
-
text:
|
|
792
|
-
embedding:
|
|
793
|
-
model:
|
|
794
|
-
topK:
|
|
1048
|
+
failed: z10.boolean(),
|
|
1049
|
+
truncated: z10.boolean()
|
|
1050
|
+
});
|
|
1051
|
+
var ragDocumentVersionParamSchema = z10.string().regex(/^\d+$/, { message: "version must be a positive integer" }).transform((value) => Number.parseInt(value, 10)).pipe(ragDocumentVersionNumberSchema);
|
|
1052
|
+
var activateRagDocumentVersionResponseSchema = z10.object({
|
|
1053
|
+
id: z10.string(),
|
|
1054
|
+
collection: z10.string(),
|
|
1055
|
+
version: z10.number().int().min(1),
|
|
1056
|
+
previous_version: z10.number().int().min(1).nullable(),
|
|
1057
|
+
changed: z10.boolean()
|
|
1058
|
+
});
|
|
1059
|
+
var ragQueryRequestSchema = z10.object({
|
|
1060
|
+
text: z10.string().min(1).optional(),
|
|
1061
|
+
embedding: z10.array(z10.number().finite()).min(1).optional(),
|
|
1062
|
+
model: z10.string().min(1).optional(),
|
|
1063
|
+
topK: z10.coerce.number().int().min(1).max(STUDIO_RAG_TOPK_MAX).default(STUDIO_RAG_TOPK_DEFAULT),
|
|
795
1064
|
filter: ragFilterSchema.optional(),
|
|
796
1065
|
ranges: ragRangesSchema.optional(),
|
|
797
|
-
preview:
|
|
798
|
-
hybrid:
|
|
1066
|
+
preview: z10.object({ docId: z10.string().min(1), version: ragDocumentVersionNumberSchema }).optional(),
|
|
1067
|
+
hybrid: z10.boolean().optional()
|
|
799
1068
|
}).refine((value) => value.embedding !== void 0 || value.text !== void 0, {
|
|
800
1069
|
message: "either `embedding` or `text` must be provided",
|
|
801
1070
|
path: ["embedding"]
|
|
802
1071
|
});
|
|
803
|
-
var ragQueryHitSchema =
|
|
804
|
-
id:
|
|
805
|
-
chunk_idx:
|
|
1072
|
+
var ragQueryHitSchema = z10.object({
|
|
1073
|
+
id: z10.string(),
|
|
1074
|
+
chunk_idx: z10.number().int().nonnegative(),
|
|
806
1075
|
// Cosine similarity in [-1, 1]. The query handler emits `1 - (embedding <=>
|
|
807
1076
|
// query)` over `stackbone_rag_chunks`, so higher is closer. We surface the
|
|
808
1077
|
// raw value; the UI formats it.
|
|
809
|
-
score:
|
|
1078
|
+
score: z10.number(),
|
|
810
1079
|
// The reciprocal rank fusion score a hybrid search ordered by (STA3-57 D10).
|
|
811
1080
|
// Absent when only the search by meaning ran, and from a box older than it.
|
|
812
|
-
mixed_score:
|
|
813
|
-
dense_rank:
|
|
814
|
-
lexical_rank:
|
|
815
|
-
content:
|
|
1081
|
+
mixed_score: z10.number().optional(),
|
|
1082
|
+
dense_rank: z10.number().int().positive().optional(),
|
|
1083
|
+
lexical_rank: z10.number().int().positive().optional(),
|
|
1084
|
+
content: z10.string(),
|
|
816
1085
|
metadata: jsonObject()
|
|
817
1086
|
});
|
|
818
|
-
var ragQueryResponseSchema =
|
|
819
|
-
items:
|
|
1087
|
+
var ragQueryResponseSchema = z10.object({
|
|
1088
|
+
items: z10.array(ragQueryHitSchema),
|
|
820
1089
|
// Echoes the dimensions used at retrieval time so the UI can flag a
|
|
821
1090
|
// dim-mismatch before it round-trips to a 4xx.
|
|
822
|
-
dimensions:
|
|
1091
|
+
dimensions: z10.number().int().min(1),
|
|
823
1092
|
// Provider-pinned model (`provider::model`) used to embed `text`. Null when
|
|
824
1093
|
// the caller passed `embedding` directly.
|
|
825
|
-
embedded_with:
|
|
1094
|
+
embedded_with: z10.string().nullable()
|
|
826
1095
|
});
|
|
827
|
-
var ragJobSchema =
|
|
828
|
-
id:
|
|
829
|
-
collection:
|
|
1096
|
+
var ragJobSchema = z10.object({
|
|
1097
|
+
id: z10.string(),
|
|
1098
|
+
collection: z10.string(),
|
|
830
1099
|
status: ragJobStatusSchema,
|
|
831
|
-
attempts:
|
|
832
|
-
error:
|
|
833
|
-
created_at:
|
|
834
|
-
finished_at:
|
|
1100
|
+
attempts: z10.number().int().nonnegative(),
|
|
1101
|
+
error: z10.string().nullable(),
|
|
1102
|
+
created_at: z10.iso.datetime(),
|
|
1103
|
+
finished_at: z10.iso.datetime().nullable()
|
|
835
1104
|
});
|
|
836
1105
|
var listRagJobsQuerySchema = forwardPageQuerySchema.extend({
|
|
837
1106
|
collection: ragCollectionNameParamSchema.optional(),
|
|
838
1107
|
limit: limitParamSchema(STUDIO_RAG_JOBS_LIMIT_MAX).default(STUDIO_RAG_JOBS_LIMIT_DEFAULT)
|
|
839
1108
|
});
|
|
840
1109
|
var ragJobListResponseSchema = forwardPageResponseSchema(ragJobSchema);
|
|
841
|
-
var ragJobIdParamSchema =
|
|
842
|
-
jobId:
|
|
1110
|
+
var ragJobIdParamSchema = z10.object({
|
|
1111
|
+
jobId: z10.string().min(1).max(128)
|
|
843
1112
|
});
|
|
844
|
-
var ragJobActionResponseSchema =
|
|
845
|
-
id:
|
|
1113
|
+
var ragJobActionResponseSchema = z10.object({
|
|
1114
|
+
id: z10.string(),
|
|
846
1115
|
status: ragJobStatusSchema
|
|
847
1116
|
});
|
|
848
1117
|
var STUDIO_RAG_SUPPORTED_CONTENT_TYPES = [
|
|
@@ -850,16 +1119,16 @@ var STUDIO_RAG_SUPPORTED_CONTENT_TYPES = [
|
|
|
850
1119
|
"text/markdown",
|
|
851
1120
|
"application/pdf"
|
|
852
1121
|
];
|
|
853
|
-
var ragSupportedContentTypeSchema =
|
|
854
|
-
var createRagCollectionRequestSchema =
|
|
1122
|
+
var ragSupportedContentTypeSchema = z10.enum(STUDIO_RAG_SUPPORTED_CONTENT_TYPES);
|
|
1123
|
+
var createRagCollectionRequestSchema = z10.object({
|
|
855
1124
|
name: ragCollectionNameParamSchema,
|
|
856
1125
|
metadata: jsonObject().optional(),
|
|
857
1126
|
embedding_provider: pickedModelProviderIdSchema.optional(),
|
|
858
|
-
embedding_model:
|
|
1127
|
+
embedding_model: z10.string().trim().min(1).optional(),
|
|
859
1128
|
// Omitted, the box creates the collection with hybrid search off and `eu`.
|
|
860
|
-
hybrid_enabled:
|
|
1129
|
+
hybrid_enabled: z10.boolean().optional(),
|
|
861
1130
|
number_format: ragNumberFormatSchema.optional(),
|
|
862
|
-
extract_enabled:
|
|
1131
|
+
extract_enabled: z10.boolean().optional(),
|
|
863
1132
|
extract_fields: ragExtractFieldsRequestSchema.optional()
|
|
864
1133
|
}).refine(
|
|
865
1134
|
(value) => value.embedding_provider === void 0 === (value.embedding_model === void 0),
|
|
@@ -868,78 +1137,78 @@ var createRagCollectionRequestSchema = z7.object({
|
|
|
868
1137
|
path: ["embedding_model"]
|
|
869
1138
|
}
|
|
870
1139
|
);
|
|
871
|
-
var updateRagCollectionRequestSchema =
|
|
872
|
-
hybrid_enabled:
|
|
1140
|
+
var updateRagCollectionRequestSchema = z10.strictObject({
|
|
1141
|
+
hybrid_enabled: z10.boolean().optional(),
|
|
873
1142
|
number_format: ragNumberFormatSchema.optional(),
|
|
874
|
-
extract_enabled:
|
|
1143
|
+
extract_enabled: z10.boolean().optional(),
|
|
875
1144
|
extract_fields: ragExtractFieldsRequestSchema.optional()
|
|
876
1145
|
}).refine((value) => Object.values(value).some((setting) => setting !== void 0), {
|
|
877
1146
|
message: "Send at least one collection setting"
|
|
878
1147
|
});
|
|
879
|
-
var updateRagCollectionResponseSchema =
|
|
880
|
-
name:
|
|
881
|
-
hybrid_enabled:
|
|
1148
|
+
var updateRagCollectionResponseSchema = z10.object({
|
|
1149
|
+
name: z10.string(),
|
|
1150
|
+
hybrid_enabled: z10.boolean(),
|
|
882
1151
|
number_format: ragNumberFormatSchema,
|
|
883
|
-
extract_enabled:
|
|
884
|
-
extract_fields:
|
|
885
|
-
unconverted:
|
|
1152
|
+
extract_enabled: z10.boolean().optional(),
|
|
1153
|
+
extract_fields: z10.array(ragExtractFieldSchema).optional(),
|
|
1154
|
+
unconverted: z10.number().int().nonnegative().optional()
|
|
886
1155
|
});
|
|
887
|
-
var deleteRagCollectionResponseSchema =
|
|
888
|
-
name:
|
|
889
|
-
deleted:
|
|
1156
|
+
var deleteRagCollectionResponseSchema = z10.object({
|
|
1157
|
+
name: z10.string(),
|
|
1158
|
+
deleted: z10.literal(true)
|
|
890
1159
|
});
|
|
891
|
-
var uploadRagDocumentRequestSchema =
|
|
1160
|
+
var uploadRagDocumentRequestSchema = z10.object({
|
|
892
1161
|
collection: ragCollectionNameParamSchema,
|
|
893
|
-
filename:
|
|
1162
|
+
filename: z10.string().min(1).max(255),
|
|
894
1163
|
content_type: ragSupportedContentTypeSchema,
|
|
895
1164
|
metadata: jsonObject().optional(),
|
|
896
|
-
replaces:
|
|
1165
|
+
replaces: z10.string().min(1).optional()
|
|
897
1166
|
});
|
|
898
|
-
var uploadRagDocumentResponseSchema =
|
|
899
|
-
job_id:
|
|
1167
|
+
var uploadRagDocumentResponseSchema = z10.object({
|
|
1168
|
+
job_id: z10.string(),
|
|
900
1169
|
status: ragJobStatusSchema,
|
|
901
|
-
version:
|
|
1170
|
+
version: z10.number().int().min(1)
|
|
902
1171
|
});
|
|
903
|
-
var deleteRagDocumentResponseSchema =
|
|
904
|
-
id:
|
|
905
|
-
collection:
|
|
906
|
-
deleted:
|
|
1172
|
+
var deleteRagDocumentResponseSchema = z10.object({
|
|
1173
|
+
id: z10.string(),
|
|
1174
|
+
collection: z10.string(),
|
|
1175
|
+
deleted: z10.literal(true)
|
|
907
1176
|
});
|
|
908
|
-
var deleteRagDocumentVersionResponseSchema =
|
|
909
|
-
id:
|
|
910
|
-
collection:
|
|
1177
|
+
var deleteRagDocumentVersionResponseSchema = z10.object({
|
|
1178
|
+
id: z10.string(),
|
|
1179
|
+
collection: z10.string(),
|
|
911
1180
|
version: ragDocumentVersionNumberSchema,
|
|
912
|
-
deleted:
|
|
1181
|
+
deleted: z10.literal(true)
|
|
913
1182
|
});
|
|
914
|
-
var downloadRagDocumentResponseSchema =
|
|
915
|
-
url:
|
|
916
|
-
filename:
|
|
917
|
-
content_type:
|
|
1183
|
+
var downloadRagDocumentResponseSchema = z10.object({
|
|
1184
|
+
url: z10.string(),
|
|
1185
|
+
filename: z10.string(),
|
|
1186
|
+
content_type: z10.string()
|
|
918
1187
|
});
|
|
919
|
-
var changeRagEmbeddingRequestSchema =
|
|
1188
|
+
var changeRagEmbeddingRequestSchema = z10.object({
|
|
920
1189
|
provider: pickedModelProviderIdSchema,
|
|
921
|
-
model:
|
|
922
|
-
});
|
|
923
|
-
var retryRagRebuildRequestSchema =
|
|
924
|
-
var ragEmbeddingIdentitySchema =
|
|
925
|
-
provider:
|
|
926
|
-
model:
|
|
927
|
-
dimension:
|
|
928
|
-
});
|
|
929
|
-
var ragRebuildStatusSchema =
|
|
930
|
-
id:
|
|
931
|
-
collection:
|
|
932
|
-
attempt:
|
|
933
|
-
status:
|
|
1190
|
+
model: z10.string().trim().min(1)
|
|
1191
|
+
});
|
|
1192
|
+
var retryRagRebuildRequestSchema = z10.object({ id: z10.uuid() });
|
|
1193
|
+
var ragEmbeddingIdentitySchema = z10.object({
|
|
1194
|
+
provider: z10.string().min(1),
|
|
1195
|
+
model: z10.string().min(1),
|
|
1196
|
+
dimension: z10.number().int().positive()
|
|
1197
|
+
});
|
|
1198
|
+
var ragRebuildStatusSchema = z10.object({
|
|
1199
|
+
id: z10.uuid(),
|
|
1200
|
+
collection: z10.string(),
|
|
1201
|
+
attempt: z10.number().int().positive(),
|
|
1202
|
+
status: z10.enum(["queued", "running", "failed", "interrupted", "complete"]),
|
|
934
1203
|
current: ragEmbeddingIdentitySchema,
|
|
935
1204
|
target: ragEmbeddingIdentitySchema,
|
|
936
|
-
completed_versions:
|
|
937
|
-
total_versions:
|
|
938
|
-
run_id:
|
|
939
|
-
world_run_id:
|
|
940
|
-
error:
|
|
1205
|
+
completed_versions: z10.number().int().nonnegative(),
|
|
1206
|
+
total_versions: z10.number().int().nonnegative(),
|
|
1207
|
+
run_id: z10.string().nullable(),
|
|
1208
|
+
world_run_id: z10.string().nullable(),
|
|
1209
|
+
error: z10.string().nullable()
|
|
941
1210
|
});
|
|
942
|
-
var ragRebuildStatusResponseSchema =
|
|
1211
|
+
var ragRebuildStatusResponseSchema = z10.object({
|
|
943
1212
|
rebuild: ragRebuildStatusSchema.nullable()
|
|
944
1213
|
});
|
|
945
1214
|
|
|
@@ -963,87 +1232,140 @@ var VOICE_BACKGROUND_CLIPS = [
|
|
|
963
1232
|
];
|
|
964
1233
|
var VOICE_AMBIENT_SOUNDS = ["none", ...VOICE_BACKGROUND_CLIPS];
|
|
965
1234
|
var VOICE_THINKING_SOUNDS = ["none", "chime", ...VOICE_BACKGROUND_CLIPS];
|
|
966
|
-
var thresholdSchema =
|
|
967
|
-
var millisecondsSchema =
|
|
968
|
-
var keytermsSchema =
|
|
969
|
-
var vadSchema =
|
|
1235
|
+
var thresholdSchema = z11.number().min(0).max(1);
|
|
1236
|
+
var millisecondsSchema = z11.int().min(0).max(1e4);
|
|
1237
|
+
var keytermsSchema = z11.array(z11.string().trim().min(1).max(64)).max(50);
|
|
1238
|
+
var vadSchema = z11.strictObject({
|
|
970
1239
|
activationThreshold: thresholdSchema,
|
|
971
1240
|
minSpeechMs: millisecondsSchema,
|
|
972
1241
|
minSilenceMs: millisecondsSchema
|
|
973
1242
|
});
|
|
974
|
-
var turnSchema =
|
|
975
|
-
mode:
|
|
1243
|
+
var turnSchema = z11.strictObject({
|
|
1244
|
+
mode: z11.enum(["stt", "livekit"]),
|
|
976
1245
|
minEndpointingDelayMs: millisecondsSchema,
|
|
977
1246
|
maxEndpointingDelayMs: millisecondsSchema,
|
|
978
|
-
detectorThreshold: thresholdSchema
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1247
|
+
detectorThreshold: thresholdSchema,
|
|
1248
|
+
// Flux refuses under 0.3 and above its own end-of-turn threshold (0.7 by default,
|
|
1249
|
+
// not a setting), so the bound is the range Flux takes with that default.
|
|
1250
|
+
eagerThreshold: z11.number().min(0.3).max(0.7)
|
|
1251
|
+
});
|
|
1252
|
+
var VOICE_EAGER_THRESHOLD_DEFAULT = 0.3;
|
|
1253
|
+
var VOICE_SPEAK_FIRST_CLAUSE_DEFAULT = false;
|
|
1254
|
+
var timeZoneSchema = z11.string().max(64).refine(isKnownTimeZone, { message: "timeZone must be an IANA zone such as Europe/Madrid" }).nullable();
|
|
1255
|
+
var VOICE_TIME_ZONE_DEFAULT = null;
|
|
1256
|
+
var VOICE_NOISE_CANCELLATIONS = ["background", "standard", "off"];
|
|
1257
|
+
var noiseCancellationSchema = z11.enum(VOICE_NOISE_CANCELLATIONS);
|
|
1258
|
+
var VOICE_NOISE_CANCELLATION_DEFAULT = "background";
|
|
1259
|
+
var VOICE_INTERRUPTION_MODES = ["adaptive", "vad"];
|
|
1260
|
+
var VOICE_INTERRUPTION_MODE_DEFAULT = "adaptive";
|
|
1261
|
+
var interruptionsSchema = z11.strictObject({
|
|
1262
|
+
enabled: z11.boolean(),
|
|
1263
|
+
mode: z11.enum(VOICE_INTERRUPTION_MODES),
|
|
982
1264
|
minDurationMs: millisecondsSchema,
|
|
983
|
-
minWords:
|
|
1265
|
+
minWords: z11.int().min(0).max(20)
|
|
984
1266
|
});
|
|
985
|
-
var ragSchema =
|
|
1267
|
+
var ragSchema = z11.strictObject({
|
|
986
1268
|
collection: ragCollectionNameParamSchema.nullable(),
|
|
987
|
-
minScore:
|
|
988
|
-
minWords:
|
|
989
|
-
topK:
|
|
990
|
-
timeoutMs:
|
|
991
|
-
previousExchanges:
|
|
992
|
-
});
|
|
993
|
-
var volumeSchema =
|
|
994
|
-
var backgroundAudioSchema =
|
|
995
|
-
ambientSound:
|
|
1269
|
+
minScore: z11.number().min(0).max(1),
|
|
1270
|
+
minWords: z11.int().min(0).max(20),
|
|
1271
|
+
topK: z11.int().min(1).max(10),
|
|
1272
|
+
timeoutMs: z11.int().min(200).max(1e4),
|
|
1273
|
+
previousExchanges: z11.int().min(0).max(5)
|
|
1274
|
+
});
|
|
1275
|
+
var volumeSchema = z11.number().min(0).max(1);
|
|
1276
|
+
var backgroundAudioSchema = z11.strictObject({
|
|
1277
|
+
ambientSound: z11.enum(VOICE_AMBIENT_SOUNDS),
|
|
996
1278
|
ambientVolume: volumeSchema,
|
|
997
|
-
thinkingSound:
|
|
1279
|
+
thinkingSound: z11.enum(VOICE_THINKING_SOUNDS),
|
|
998
1280
|
thinkingVolume: volumeSchema,
|
|
999
1281
|
/**
|
|
1000
1282
|
* How long a reply's wait runs before the thinking sound starts, when no box tool
|
|
1001
|
-
* call is open
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1283
|
+
* call is open. A box tool starts it at once, or, after a holding line, 300 ms after
|
|
1284
|
+
* the line has played out (at once when its TTS is lost, at most
|
|
1285
|
+
* `holdingLineMaxWaitMs` after the tool start), so the caller hears the line first
|
|
1286
|
+
* (STA3-99). Every turn waits for the knowledge search and the model's first token,
|
|
1287
|
+
* about 1.4 s at the median on a voice box (2026-11-09), so a delay under that plays
|
|
1288
|
+
* the sound on nearly every reply.
|
|
1004
1289
|
*/
|
|
1005
|
-
thinkingDelayMs: millisecondsSchema
|
|
1290
|
+
thinkingDelayMs: millisecondsSchema,
|
|
1291
|
+
/**
|
|
1292
|
+
* The cap on the thinking sound's wait for a holding line to play out, from the tool
|
|
1293
|
+
* start (STA3-99): a long line, or a TTS that never reports its end, must not keep
|
|
1294
|
+
* the caller in silence while the tool runs.
|
|
1295
|
+
*/
|
|
1296
|
+
holdingLineMaxWaitMs: millisecondsSchema
|
|
1006
1297
|
});
|
|
1007
1298
|
var VOICE_BACKGROUND_AUDIO_DEFAULTS = Object.freeze({
|
|
1008
1299
|
ambientSound: "office-ambience",
|
|
1009
1300
|
ambientVolume: 0.7,
|
|
1010
1301
|
thinkingSound: "keyboard-typing",
|
|
1011
1302
|
thinkingVolume: 0.6,
|
|
1012
|
-
thinkingDelayMs: 2e3
|
|
1303
|
+
thinkingDelayMs: 2e3,
|
|
1304
|
+
holdingLineMaxWaitMs: 1e4
|
|
1013
1305
|
});
|
|
1014
|
-
var effectiveVoiceSettingsObjectSchema =
|
|
1306
|
+
var effectiveVoiceSettingsObjectSchema = z11.strictObject({
|
|
1015
1307
|
/** BCP-47-ish tag the STT is hinted with and the TTS speaks (`es`, `en-US`), or `multi`. */
|
|
1016
|
-
language:
|
|
1017
|
-
sttModel:
|
|
1308
|
+
language: z11.string().max(16).regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/),
|
|
1309
|
+
sttModel: z11.enum(VOICE_STT_MODELS),
|
|
1018
1310
|
keyterms: keytermsSchema.default(() => []),
|
|
1019
1311
|
vad: vadSchema,
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1312
|
+
// Defaulted here, on the read side only: a box older than STA3-94 answers, and
|
|
1313
|
+
// dispatches, a `turn` without it. Not on `turnSchema`, whose `.partial()` layers
|
|
1314
|
+
// would then all declare it.
|
|
1315
|
+
turn: turnSchema.extend({
|
|
1316
|
+
eagerThreshold: turnSchema.shape.eagerThreshold.default(VOICE_EAGER_THRESHOLD_DEFAULT)
|
|
1317
|
+
}),
|
|
1318
|
+
// Read side only, like `turn.eagerThreshold`: a box older than the setting answers,
|
|
1319
|
+
// and dispatches, a group without it.
|
|
1320
|
+
interruptions: interruptionsSchema.extend({
|
|
1321
|
+
mode: interruptionsSchema.shape.mode.default(VOICE_INTERRUPTION_MODE_DEFAULT)
|
|
1322
|
+
}),
|
|
1323
|
+
/** The filter on the caller's track (`VOICE_NOISE_CANCELLATIONS`). */
|
|
1324
|
+
// Read side only, like `speakFirstClause`.
|
|
1325
|
+
noiseCancellation: noiseCancellationSchema.default(VOICE_NOISE_CANCELLATION_DEFAULT),
|
|
1326
|
+
ttsModel: z11.enum(VOICE_TTS_MODELS),
|
|
1023
1327
|
/**
|
|
1024
1328
|
* The provider's own voice id (a Cartesia UUID, an ElevenLabs id, a Deepgram name),
|
|
1025
1329
|
* so only its outline is checked: no surrounding whitespace, no control characters.
|
|
1026
1330
|
*/
|
|
1027
|
-
ttsVoice:
|
|
1331
|
+
ttsVoice: z11.string().max(128).regex(/^[^\s\p{Cc}](?:[^\p{Cc}]*[^\s\p{Cc}])?$/u),
|
|
1028
1332
|
/** Cartesia only (D18): another model keeps its own pace, so it takes only 1. */
|
|
1029
|
-
ttsSpeed:
|
|
1333
|
+
ttsSpeed: z11.number().min(0.5).max(2),
|
|
1030
1334
|
/**
|
|
1031
1335
|
* What the agent says when the call connects. `null`: it waits for the caller. A
|
|
1032
1336
|
* blank string is refused rather than read as either: it would be an empty
|
|
1033
|
-
* utterance, not a wait.
|
|
1337
|
+
* utterance, not a wait. It may hold `{{name}}` placeholders, filled per call from
|
|
1338
|
+
* the call's variables (`renderVoiceGreeting`).
|
|
1034
1339
|
*/
|
|
1035
|
-
greeting:
|
|
1340
|
+
greeting: z11.string().trim().min(1).max(VOICE_GREETING_MAX).nullable(),
|
|
1036
1341
|
/** Band-pass the agent's voice to 300-3400 Hz so it sounds like a phone line (D5). */
|
|
1037
|
-
phoneLine:
|
|
1342
|
+
phoneLine: z11.boolean(),
|
|
1038
1343
|
/** LiveKit expressive mode: the turn gets Cartesia's markup guide (D5). Cartesia only. */
|
|
1039
|
-
expressive:
|
|
1040
|
-
|
|
1344
|
+
expressive: z11.boolean(),
|
|
1345
|
+
/**
|
|
1346
|
+
* The reply's first clause goes to the TTS at its first comma or sentence end,
|
|
1347
|
+
* instead of when the next sentence starts or the reply ends: LiveKit's sentence
|
|
1348
|
+
* tokenizer holds a one-sentence reply until the model has finished it, 550 to 790 ms
|
|
1349
|
+
* of silence on a model that writes 25 to 45 tokens/s (2026-11-11). The clause is its
|
|
1350
|
+
* own TTS segment, so the intonation can change at that pause.
|
|
1351
|
+
*/
|
|
1352
|
+
// Defaulted on the read side only, like `turn.eagerThreshold`: a box older than the
|
|
1353
|
+
// setting answers, and dispatches, settings without it. The layer schema drops the
|
|
1354
|
+
// default, or every layer would declare it.
|
|
1355
|
+
speakFirstClause: z11.boolean().default(VOICE_SPEAK_FIRST_CLAUSE_DEFAULT),
|
|
1356
|
+
// Read side only, like `turn.eagerThreshold`: a box older than STA3-99 answers a
|
|
1357
|
+
// group without it.
|
|
1358
|
+
backgroundAudio: backgroundAudioSchema.extend({
|
|
1359
|
+
holdingLineMaxWaitMs: backgroundAudioSchema.shape.holdingLineMaxWaitMs.default(
|
|
1360
|
+
VOICE_BACKGROUND_AUDIO_DEFAULTS.holdingLineMaxWaitMs
|
|
1361
|
+
)
|
|
1362
|
+
}),
|
|
1041
1363
|
/**
|
|
1042
1364
|
* Whether the box records the call to its bucket (D30). Off, no egress starts: a
|
|
1043
1365
|
* box may not be allowed to keep a caller's audio. The box alone reads it, so the
|
|
1044
1366
|
* dispatch to the worker leaves it out (`callVoiceSettingsSchema`).
|
|
1045
1367
|
*/
|
|
1046
|
-
recording:
|
|
1368
|
+
recording: z11.boolean(),
|
|
1047
1369
|
/** The box runs the search before the model's turn, so the worker never reads it. */
|
|
1048
1370
|
rag: ragSchema,
|
|
1049
1371
|
/**
|
|
@@ -1053,7 +1375,16 @@ var effectiveVoiceSettingsObjectSchema = z8.strictObject({
|
|
|
1053
1375
|
* The box answers the turn itself, so the worker never reads it. Not nullable: a
|
|
1054
1376
|
* blocked turn always says something.
|
|
1055
1377
|
*/
|
|
1056
|
-
refusalPhrase:
|
|
1378
|
+
refusalPhrase: z11.string().trim().min(1).max(300),
|
|
1379
|
+
/**
|
|
1380
|
+
* The zone of the date every model call of a call is told as today. The agent's
|
|
1381
|
+
* prompt is rendered once when its graph is built and kept for days, so a date
|
|
1382
|
+
* written into it goes stale, and a `today` tool costs the caller a second model
|
|
1383
|
+
* call. The box writes the date into the turn itself, so the worker never reads it.
|
|
1384
|
+
*/
|
|
1385
|
+
// Defaulted on the read side only, like `speakFirstClause`: a box older than the
|
|
1386
|
+
// setting answers settings without it.
|
|
1387
|
+
timeZone: timeZoneSchema.default(VOICE_TIME_ZONE_DEFAULT)
|
|
1057
1388
|
});
|
|
1058
1389
|
var checkCrossFieldRules = (value, ctx) => {
|
|
1059
1390
|
const min = value.turn?.minEndpointingDelayMs;
|
|
@@ -1084,10 +1415,12 @@ var effectiveVoiceSettingsSchema = effectiveVoiceSettingsObjectSchema.superRefin
|
|
|
1084
1415
|
var callVoiceSettingsObjectSchema = effectiveVoiceSettingsObjectSchema.omit({
|
|
1085
1416
|
recording: true,
|
|
1086
1417
|
rag: true,
|
|
1087
|
-
refusalPhrase: true
|
|
1418
|
+
refusalPhrase: true,
|
|
1419
|
+
timeZone: true
|
|
1088
1420
|
}).extend({
|
|
1089
|
-
// A box older than `thinkingDelayMs` dispatches the group
|
|
1090
|
-
|
|
1421
|
+
// A box older than `thinkingDelayMs` or `holdingLineMaxWaitMs` dispatches the group
|
|
1422
|
+
// without it; the effective schema already defaults the second.
|
|
1423
|
+
backgroundAudio: effectiveVoiceSettingsObjectSchema.shape.backgroundAudio.extend({
|
|
1091
1424
|
thinkingDelayMs: millisecondsSchema.default(
|
|
1092
1425
|
VOICE_BACKGROUND_AUDIO_DEFAULTS.thinkingDelayMs
|
|
1093
1426
|
)
|
|
@@ -1105,20 +1438,25 @@ var VOICE_SETTINGS_LEAF_PATHS = [
|
|
|
1105
1438
|
"turn.minEndpointingDelayMs",
|
|
1106
1439
|
"turn.maxEndpointingDelayMs",
|
|
1107
1440
|
"turn.detectorThreshold",
|
|
1441
|
+
"turn.eagerThreshold",
|
|
1108
1442
|
"interruptions.enabled",
|
|
1443
|
+
"interruptions.mode",
|
|
1109
1444
|
"interruptions.minDurationMs",
|
|
1110
1445
|
"interruptions.minWords",
|
|
1446
|
+
"noiseCancellation",
|
|
1111
1447
|
"ttsModel",
|
|
1112
1448
|
"ttsVoice",
|
|
1113
1449
|
"ttsSpeed",
|
|
1114
1450
|
"greeting",
|
|
1115
1451
|
"phoneLine",
|
|
1116
1452
|
"expressive",
|
|
1453
|
+
"speakFirstClause",
|
|
1117
1454
|
"backgroundAudio.ambientSound",
|
|
1118
1455
|
"backgroundAudio.ambientVolume",
|
|
1119
1456
|
"backgroundAudio.thinkingSound",
|
|
1120
1457
|
"backgroundAudio.thinkingVolume",
|
|
1121
1458
|
"backgroundAudio.thinkingDelayMs",
|
|
1459
|
+
"backgroundAudio.holdingLineMaxWaitMs",
|
|
1122
1460
|
"recording",
|
|
1123
1461
|
"rag.collection",
|
|
1124
1462
|
"rag.minScore",
|
|
@@ -1126,14 +1464,28 @@ var VOICE_SETTINGS_LEAF_PATHS = [
|
|
|
1126
1464
|
"rag.topK",
|
|
1127
1465
|
"rag.timeoutMs",
|
|
1128
1466
|
"rag.previousExchanges",
|
|
1129
|
-
"refusalPhrase"
|
|
1467
|
+
"refusalPhrase",
|
|
1468
|
+
"timeZone"
|
|
1130
1469
|
];
|
|
1131
|
-
var voiceSettingOriginSchema =
|
|
1132
|
-
var
|
|
1470
|
+
var voiceSettingOriginSchema = z11.enum(["studio", "code", "default"]);
|
|
1471
|
+
var LEAVES_NEWER_THAN_SOME_BOXES = /* @__PURE__ */ new Set([
|
|
1472
|
+
"turn.eagerThreshold",
|
|
1473
|
+
"interruptions.mode",
|
|
1474
|
+
"noiseCancellation",
|
|
1475
|
+
"speakFirstClause",
|
|
1476
|
+
"backgroundAudio.holdingLineMaxWaitMs",
|
|
1477
|
+
"timeZone"
|
|
1478
|
+
]);
|
|
1479
|
+
var effectiveVoiceSettingsViewSchema = z11.strictObject({
|
|
1133
1480
|
value: effectiveVoiceSettingsObjectSchema,
|
|
1134
|
-
origins:
|
|
1481
|
+
origins: z11.strictObject(
|
|
1135
1482
|
Object.fromEntries(
|
|
1136
|
-
|
|
1483
|
+
// A leaf newer than the box answering does not come back: that box runs its default.
|
|
1484
|
+
// Every other leaf stays required, so a box that drops one is still caught.
|
|
1485
|
+
VOICE_SETTINGS_LEAF_PATHS.map((path) => [
|
|
1486
|
+
path,
|
|
1487
|
+
LEAVES_NEWER_THAN_SOME_BOXES.has(path) ? voiceSettingOriginSchema.default("default") : voiceSettingOriginSchema
|
|
1488
|
+
])
|
|
1137
1489
|
)
|
|
1138
1490
|
)
|
|
1139
1491
|
});
|
|
@@ -1148,6 +1500,9 @@ var checkTtsPair = (value, ctx) => {
|
|
|
1148
1500
|
};
|
|
1149
1501
|
var voiceSettingsSchema = effectiveVoiceSettingsObjectSchema.extend({
|
|
1150
1502
|
keyterms: keytermsSchema,
|
|
1503
|
+
noiseCancellation: noiseCancellationSchema,
|
|
1504
|
+
speakFirstClause: z11.boolean(),
|
|
1505
|
+
timeZone: timeZoneSchema,
|
|
1151
1506
|
vad: vadSchema.partial(),
|
|
1152
1507
|
turn: turnSchema.partial(),
|
|
1153
1508
|
interruptions: interruptionsSchema.partial(),
|
|
@@ -1165,11 +1520,11 @@ var checkStudioTtsPair = (value, ctx) => {
|
|
|
1165
1520
|
};
|
|
1166
1521
|
var studioVoiceSettingsPatchSchema = voiceSettingsSchema.superRefine(checkStudioTtsPair);
|
|
1167
1522
|
var voiceSettingsResponseSchema = effectiveVoiceSettingsViewSchema.extend({
|
|
1168
|
-
issue:
|
|
1523
|
+
issue: z11.string().nullable()
|
|
1169
1524
|
});
|
|
1170
|
-
var deepAgentNameSchema =
|
|
1171
|
-
var voiceCallIdSchema =
|
|
1172
|
-
var voiceTagsSchema =
|
|
1525
|
+
var deepAgentNameSchema = z11.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/);
|
|
1526
|
+
var voiceCallIdSchema = z11.string().min(1).max(RUN_TAG_VALUE_MAX);
|
|
1527
|
+
var voiceTagsSchema = z11.record(runTagKeySchema, runTagValueSchema).refine((tags) => Object.keys(tags).length <= RUN_TAGS_MAX, {
|
|
1173
1528
|
message: `A call carries at most ${RUN_TAGS_MAX} tags.`
|
|
1174
1529
|
});
|
|
1175
1530
|
var VOICE_TTS_MARKUP_CONTEXT = "voice-tts-markup";
|
|
@@ -1183,7 +1538,7 @@ var isVoiceBoxUrl = (raw) => {
|
|
|
1183
1538
|
}
|
|
1184
1539
|
};
|
|
1185
1540
|
var VOICE_BOX_URL_MAX = 2048;
|
|
1186
|
-
var voiceBoxUrlSchema =
|
|
1541
|
+
var voiceBoxUrlSchema = z11.string().max(VOICE_BOX_URL_MAX).refine(isVoiceBoxUrl, {
|
|
1187
1542
|
message: "boxUrl must be an absolute http(s) URL with no user part, query or fragment."
|
|
1188
1543
|
});
|
|
1189
1544
|
var VOICE_TRACES_PATH = "/voice/v1/traces";
|
|
@@ -1192,7 +1547,7 @@ var VOICE_WORKER_AGENT_NAME_ENV = "STACKBONE_VOICE_AGENT_NAME";
|
|
|
1192
1547
|
var VOICE_WORKER_PORT_ENV = "STACKBONE_VOICE_WORKER_PORT";
|
|
1193
1548
|
var VOICE_TURN_METRICS_MESSAGE = "voice turn metrics";
|
|
1194
1549
|
var VOICE_CALL_TOKEN_MAX = 2048;
|
|
1195
|
-
var voiceDispatchSchema =
|
|
1550
|
+
var voiceDispatchSchema = z11.strictObject({
|
|
1196
1551
|
agent: deepAgentNameSchema,
|
|
1197
1552
|
/**
|
|
1198
1553
|
* The merged value (Studio > code > box default), cross-field rules included,
|
|
@@ -1203,54 +1558,59 @@ var voiceDispatchSchema = z8.strictObject({
|
|
|
1203
1558
|
tags: voiceTagsSchema,
|
|
1204
1559
|
boxUrl: voiceBoxUrlSchema,
|
|
1205
1560
|
/** The call token: the bearer on every AG-UI request of this call, for this agent and session only. */
|
|
1206
|
-
callToken:
|
|
1561
|
+
callToken: z11.string().max(VOICE_CALL_TOKEN_MAX).regex(/^\S+$/, { message: "callToken must be a non-empty token with no whitespace." }),
|
|
1207
1562
|
/**
|
|
1208
1563
|
* Set only when the box exports traces to an OTLP endpoint of its own (FDE-62 S11):
|
|
1209
1564
|
* the worker then sends each call's spans to `<boxUrl>` + {@link VOICE_TRACES_PATH} with the
|
|
1210
1565
|
* call token. Off is the absent key, never `false`.
|
|
1211
1566
|
*/
|
|
1212
|
-
traces:
|
|
1567
|
+
traces: z11.literal(true).optional()
|
|
1213
1568
|
});
|
|
1214
|
-
var voiceRoomRequestSchema =
|
|
1569
|
+
var voiceRoomRequestSchema = z11.strictObject({
|
|
1215
1570
|
agent: deepAgentNameSchema,
|
|
1216
1571
|
origin: runTagKeySchema.default("studio"),
|
|
1217
|
-
identities: integrationIdentitiesSchema.optional()
|
|
1572
|
+
identities: integrationIdentitiesSchema.optional(),
|
|
1573
|
+
/** Fill the greeting's placeholders and reach the agent as data (PLA-2409). */
|
|
1574
|
+
variables: voiceCallVariablesSchema.optional()
|
|
1218
1575
|
});
|
|
1219
|
-
var voiceRoomResponseSchema =
|
|
1220
|
-
url:
|
|
1221
|
-
token:
|
|
1576
|
+
var voiceRoomResponseSchema = z11.strictObject({
|
|
1577
|
+
url: z11.string().min(1),
|
|
1578
|
+
token: z11.string().min(1),
|
|
1222
1579
|
room: voiceCallIdSchema,
|
|
1223
1580
|
agent: deepAgentNameSchema,
|
|
1224
1581
|
sessionKey: voiceCallIdSchema,
|
|
1225
1582
|
voiceSettings: callVoiceSettingsObjectSchema
|
|
1226
1583
|
});
|
|
1227
1584
|
var VOICE_RECORDING_STATUSES = ["recording", "ready", "failed"];
|
|
1228
|
-
var voiceRecordingQuerySchema =
|
|
1229
|
-
var voiceRecordingResponseSchema =
|
|
1585
|
+
var voiceRecordingQuerySchema = z11.object({ session_id: z11.uuid() });
|
|
1586
|
+
var voiceRecordingResponseSchema = z11.strictObject({
|
|
1230
1587
|
room: voiceCallIdSchema,
|
|
1231
|
-
status:
|
|
1232
|
-
url:
|
|
1233
|
-
expires_at:
|
|
1234
|
-
started_at:
|
|
1235
|
-
ended_at:
|
|
1588
|
+
status: z11.enum(VOICE_RECORDING_STATUSES),
|
|
1589
|
+
url: z11.string().nullable(),
|
|
1590
|
+
expires_at: z11.iso.datetime().nullable(),
|
|
1591
|
+
started_at: z11.iso.datetime(),
|
|
1592
|
+
ended_at: z11.iso.datetime().nullable()
|
|
1236
1593
|
});
|
|
1237
|
-
var voiceStatusResponseSchema =
|
|
1238
|
-
configured:
|
|
1239
|
-
reachable:
|
|
1240
|
-
checkedAt:
|
|
1594
|
+
var voiceStatusResponseSchema = z11.strictObject({
|
|
1595
|
+
configured: z11.literal(true),
|
|
1596
|
+
reachable: z11.boolean(),
|
|
1597
|
+
checkedAt: z11.iso.datetime()
|
|
1241
1598
|
});
|
|
1242
1599
|
var HEARD_TEXT_MAX = 4e3;
|
|
1243
|
-
var aguiHeardSchema =
|
|
1244
|
-
text:
|
|
1600
|
+
var aguiHeardSchema = z11.strictObject({
|
|
1601
|
+
text: z11.string().min(1).max(HEARD_TEXT_MAX)
|
|
1245
1602
|
});
|
|
1246
1603
|
var VOICE_CALLS_PATH = "/voice/v1/calls";
|
|
1247
|
-
var voiceCallBootstrapSchema =
|
|
1604
|
+
var voiceCallBootstrapSchema = z11.strictObject({
|
|
1248
1605
|
identities: integrationIdentitiesSchema.optional()
|
|
1249
1606
|
});
|
|
1250
|
-
var
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1607
|
+
var voiceCallKnowledgeSchema = z11.strictObject({
|
|
1608
|
+
text: z11.string().min(1).max(HEARD_TEXT_MAX)
|
|
1609
|
+
});
|
|
1610
|
+
var voiceCallTranscriptSchema = z11.array(
|
|
1611
|
+
z11.strictObject({
|
|
1612
|
+
role: z11.enum(["user", "assistant"]),
|
|
1613
|
+
text: z11.string().trim().min(1).max(16e3)
|
|
1254
1614
|
})
|
|
1255
1615
|
).max(1e3).refine((items) => items.reduce((size, item) => size + item.text.length, 0) <= 2e5, {
|
|
1256
1616
|
message: "A call transcript may contain at most 200000 characters."
|
|
@@ -1258,31 +1618,38 @@ var voiceCallTranscriptSchema = z8.array(
|
|
|
1258
1618
|
var VOICE_CALL_TURNS_MAX = 1e3;
|
|
1259
1619
|
var AGUI_RUN_ID_MAX = 200;
|
|
1260
1620
|
var VOICE_TURN_LATENCY_MAX_MS = 36e5;
|
|
1261
|
-
var turnLatencyMs =
|
|
1262
|
-
var voiceCallTurnMetricsSchema =
|
|
1621
|
+
var turnLatencyMs = z11.number().int().min(0).max(VOICE_TURN_LATENCY_MAX_MS).optional();
|
|
1622
|
+
var voiceCallTurnMetricsSchema = z11.strictObject({
|
|
1263
1623
|
/** The AG-UI `runId` of the box run that answered; absent on a reply the box did not run. */
|
|
1264
|
-
runId:
|
|
1624
|
+
runId: z11.string().min(1).max(AGUI_RUN_ID_MAX).optional(),
|
|
1265
1625
|
endOfTurnMs: turnLatencyMs,
|
|
1266
1626
|
transcriptionMs: turnLatencyMs,
|
|
1267
1627
|
llmTtftMs: turnLatencyMs,
|
|
1268
1628
|
ttsTtfbMs: turnLatencyMs,
|
|
1269
|
-
e2eMs: turnLatencyMs
|
|
1629
|
+
e2eMs: turnLatencyMs,
|
|
1630
|
+
/** How long before the final transcript the turn's last eager end of turn left for the box (STA3-94). */
|
|
1631
|
+
preflightLeadMs: turnLatencyMs
|
|
1270
1632
|
});
|
|
1271
|
-
var voiceCallTurnsSchema =
|
|
1272
|
-
var voiceCallCompleteSchema =
|
|
1633
|
+
var voiceCallTurnsSchema = z11.array(voiceCallTurnMetricsSchema).max(VOICE_CALL_TURNS_MAX);
|
|
1634
|
+
var voiceCallCompleteSchema = z11.strictObject({
|
|
1273
1635
|
transcript: voiceCallTranscriptSchema,
|
|
1274
|
-
outcome:
|
|
1636
|
+
outcome: z11.enum(["completed", "disconnected", "failed"]),
|
|
1275
1637
|
// The transcript, the memory and the `onEnd` start this body carries must not be
|
|
1276
1638
|
// refused over a bad latency, so `turns` is any value here and read apart through
|
|
1277
1639
|
// `readVoiceCallTurns`. Not `.catch()` on the array: this schema is a documented
|
|
1278
1640
|
// route's body, and the OpenAPI generator cannot describe a catch (STA3-56).
|
|
1279
|
-
turns:
|
|
1641
|
+
turns: z11.unknown().optional()
|
|
1280
1642
|
});
|
|
1281
|
-
var voiceCallLifecycleResponseSchema =
|
|
1643
|
+
var voiceCallLifecycleResponseSchema = z11.strictObject({ ok: z11.literal(true) });
|
|
1282
1644
|
|
|
1283
1645
|
// libs/runtime/voice-worker/src/box-llm.ts
|
|
1284
1646
|
import { randomUUID } from "node:crypto";
|
|
1285
|
-
import {
|
|
1647
|
+
import {
|
|
1648
|
+
DEFAULT_API_CONNECT_OPTIONS,
|
|
1649
|
+
FlushSentinel,
|
|
1650
|
+
llm as llm2,
|
|
1651
|
+
log
|
|
1652
|
+
} from "@livekit/agents";
|
|
1286
1653
|
|
|
1287
1654
|
// libs/shared/llm-wire/src/lib/sse/read-sse-data.ts
|
|
1288
1655
|
async function* readSseData(body) {
|
|
@@ -1316,6 +1683,7 @@ async function* readSseData(body) {
|
|
|
1316
1683
|
// libs/runtime/voice-worker/src/box-wire.ts
|
|
1317
1684
|
var BODY_EXCERPT_CHARS = 300;
|
|
1318
1685
|
var TURN_IDLE_TIMEOUT_MS = 3e4;
|
|
1686
|
+
var KNOWLEDGE_TIMEOUT_MS = 2e3;
|
|
1319
1687
|
var NON_ASCII = /[\u007f-\uffff]/g;
|
|
1320
1688
|
var runErrorOf = ({ message, code }) => new Error(`box run failed${code ? ` (${code})` : ""}: ${message}`);
|
|
1321
1689
|
var BoxTurnError = class extends Error {
|
|
@@ -1424,29 +1792,46 @@ var toWireEvent = (ev) => {
|
|
|
1424
1792
|
return { kind: "other", type };
|
|
1425
1793
|
}
|
|
1426
1794
|
};
|
|
1795
|
+
var postCallOperation = (wire, operation, payload, timeoutMs) => fetch(
|
|
1796
|
+
boxEndpoint(
|
|
1797
|
+
wire.boxUrl,
|
|
1798
|
+
`${VOICE_CALLS_PATH}/${encodeURIComponent(wire.sessionKey)}/${operation}`
|
|
1799
|
+
),
|
|
1800
|
+
{
|
|
1801
|
+
method: "POST",
|
|
1802
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${wire.token}` },
|
|
1803
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1804
|
+
body: JSON.stringify(payload)
|
|
1805
|
+
}
|
|
1806
|
+
);
|
|
1427
1807
|
var voiceCallRequest = async (wire, operation, payload) => {
|
|
1428
|
-
const response = await
|
|
1429
|
-
boxEndpoint(
|
|
1430
|
-
wire.boxUrl,
|
|
1431
|
-
`${VOICE_CALLS_PATH}/${encodeURIComponent(wire.sessionKey)}/${operation}`
|
|
1432
|
-
),
|
|
1433
|
-
{
|
|
1434
|
-
method: "POST",
|
|
1435
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${wire.token}` },
|
|
1436
|
-
signal: AbortSignal.timeout(TURN_IDLE_TIMEOUT_MS),
|
|
1437
|
-
body: JSON.stringify(payload)
|
|
1438
|
-
}
|
|
1439
|
-
);
|
|
1808
|
+
const response = await postCallOperation(wire, operation, payload, TURN_IDLE_TIMEOUT_MS);
|
|
1440
1809
|
if (!response.ok)
|
|
1441
1810
|
throw new BoxTurnError(wire.agent, response.status, await readExcerpt(response));
|
|
1442
1811
|
voiceCallLifecycleResponseSchema.parse(await response.json());
|
|
1443
1812
|
};
|
|
1813
|
+
var voiceCallKnowledge = async (wire, text) => {
|
|
1814
|
+
try {
|
|
1815
|
+
const response = await postCallOperation(
|
|
1816
|
+
wire,
|
|
1817
|
+
"knowledge",
|
|
1818
|
+
{ text },
|
|
1819
|
+
KNOWLEDGE_TIMEOUT_MS
|
|
1820
|
+
);
|
|
1821
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1822
|
+
if (response.ok) return "sent";
|
|
1823
|
+
return response.status === 404 ? "gone" : "failed";
|
|
1824
|
+
} catch {
|
|
1825
|
+
return "failed";
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1444
1828
|
|
|
1445
1829
|
// libs/runtime/voice-worker/src/heard.ts
|
|
1446
1830
|
var VOICE_RESPONSE_MARKER = "stackboneVoiceResponse";
|
|
1447
1831
|
var WORD_CHAR = /[\p{L}\p{N}]/u;
|
|
1448
1832
|
var SPACE = /\s/u;
|
|
1449
|
-
var
|
|
1833
|
+
var TAG_NAME_START = /[\p{L}/]/u;
|
|
1834
|
+
var TAG = new RegExp(`^<${TAG_NAME_START.source}[^>]*>`, "u");
|
|
1450
1835
|
var charAt = (text, index) => String.fromCodePoint(text.codePointAt(index) ?? 0);
|
|
1451
1836
|
var heardPrefixOf = (boxText, heardText) => {
|
|
1452
1837
|
let b = 0;
|
|
@@ -1609,6 +1994,60 @@ var lastNewAssistantMessage = (chatCtx, seen) => {
|
|
|
1609
1994
|
return void 0;
|
|
1610
1995
|
};
|
|
1611
1996
|
|
|
1997
|
+
// libs/runtime/voice-worker/src/first-clause.ts
|
|
1998
|
+
var FIRST_CLAUSE_MIN_CHARS = 25;
|
|
1999
|
+
var CLAUSE_END = /* @__PURE__ */ new Set([",", ";", ":", ".", "?", "!", "\u2026"]);
|
|
2000
|
+
var FirstClause = class {
|
|
2001
|
+
#done = false;
|
|
2002
|
+
#spoken = 0;
|
|
2003
|
+
/** The text of the tag being read, from its `<`. */
|
|
2004
|
+
#tag;
|
|
2005
|
+
#openTags = 0;
|
|
2006
|
+
/** The last character was a mark that can end the clause: the next one decides. */
|
|
2007
|
+
#markPending = false;
|
|
2008
|
+
push(text) {
|
|
2009
|
+
if (!this.#done) {
|
|
2010
|
+
let at = 0;
|
|
2011
|
+
for (const char of text) {
|
|
2012
|
+
if (this.#markPending && SPACE.test(char)) {
|
|
2013
|
+
this.#done = true;
|
|
2014
|
+
return { head: text.slice(0, at), cut: true, tail: text.slice(at) };
|
|
2015
|
+
}
|
|
2016
|
+
this.#read(char);
|
|
2017
|
+
at += char.length;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return { head: text, cut: false, tail: "" };
|
|
2021
|
+
}
|
|
2022
|
+
#read(char) {
|
|
2023
|
+
this.#markPending = false;
|
|
2024
|
+
if (this.#tag !== void 0) {
|
|
2025
|
+
if (this.#tag !== "<" || TAG_NAME_START.test(char)) {
|
|
2026
|
+
this.#tag += char;
|
|
2027
|
+
if (char === ">") this.#closeTag(this.#tag);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
this.#tag = void 0;
|
|
2031
|
+
this.#spoken += 1;
|
|
2032
|
+
}
|
|
2033
|
+
if (char === "<") {
|
|
2034
|
+
this.#tag = "<";
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
if (SPACE.test(char)) {
|
|
2038
|
+
if (this.#spoken > 0) this.#spoken += 1;
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
this.#spoken += 1;
|
|
2042
|
+
this.#markPending = CLAUSE_END.has(char) && this.#openTags === 0 && this.#spoken >= FIRST_CLAUSE_MIN_CHARS;
|
|
2043
|
+
}
|
|
2044
|
+
#closeTag(tag) {
|
|
2045
|
+
this.#tag = void 0;
|
|
2046
|
+
if (tag.startsWith("</")) this.#openTags = Math.max(0, this.#openTags - 1);
|
|
2047
|
+
else if (!tag.endsWith("/>")) this.#openTags += 1;
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
|
|
1612
2051
|
// libs/runtime/voice-worker/src/turn-metrics.ts
|
|
1613
2052
|
import { llm, voice as voice2 } from "@livekit/agents";
|
|
1614
2053
|
var AGUI_RUN_ID_EXTRA = "stackboneAguiRunId";
|
|
@@ -1621,11 +2060,13 @@ var defined = (metrics) => Object.fromEntries(Object.entries(metrics).filter(([,
|
|
|
1621
2060
|
var watchTurnMetrics = (session, write, options = {}) => {
|
|
1622
2061
|
let caller;
|
|
1623
2062
|
let delivered;
|
|
2063
|
+
let lead;
|
|
1624
2064
|
session.on(voice2.AgentSessionEventTypes.ConversationItemAdded, ({ item }) => {
|
|
1625
2065
|
if (!(item instanceof llm.ChatMessage)) return;
|
|
1626
2066
|
if (item.role === "user") {
|
|
1627
2067
|
caller = item.metrics;
|
|
1628
2068
|
delivered = options.endOfTurnMs?.();
|
|
2069
|
+
lead = options.preflightLeadMs?.();
|
|
1629
2070
|
return;
|
|
1630
2071
|
}
|
|
1631
2072
|
if (item.role !== "assistant") return;
|
|
@@ -1637,10 +2078,12 @@ var watchTurnMetrics = (session, write, options = {}) => {
|
|
|
1637
2078
|
transcriptionMs: options.endOfTurnMs ? void 0 : ms(caller?.transcriptionDelay),
|
|
1638
2079
|
llmTtftMs: ms(reply.llmNodeTtft),
|
|
1639
2080
|
ttsTtfbMs: ms(reply.ttsNodeTtfb),
|
|
1640
|
-
e2eMs: e2eMs === void 0 || delivered === void 0 ? e2eMs : e2eMs + delivered
|
|
2081
|
+
e2eMs: e2eMs === void 0 || delivered === void 0 ? e2eMs : e2eMs + delivered,
|
|
2082
|
+
preflightLeadMs: lead === void 0 ? void 0 : Math.round(lead)
|
|
1641
2083
|
});
|
|
1642
2084
|
caller = void 0;
|
|
1643
2085
|
delivered = void 0;
|
|
2086
|
+
lead = void 0;
|
|
1644
2087
|
if (Object.keys(metrics).length === 0) return;
|
|
1645
2088
|
write(typeof runId === "string" ? { ...metrics, runId } : metrics);
|
|
1646
2089
|
});
|
|
@@ -1764,6 +2207,8 @@ var BoxLLMStream = class extends llm2.LLMStream {
|
|
|
1764
2207
|
async runTurn() {
|
|
1765
2208
|
const {
|
|
1766
2209
|
onToolCall,
|
|
2210
|
+
speakFirstClause = false,
|
|
2211
|
+
onSpeechSegments,
|
|
1767
2212
|
onTurnError,
|
|
1768
2213
|
onRunFinished,
|
|
1769
2214
|
onClientToolCall,
|
|
@@ -1789,6 +2234,28 @@ var BoxLLMStream = class extends llm2.LLMStream {
|
|
|
1789
2234
|
timer = setTimeout(() => idle.abort(), turnIdleTimeoutMs);
|
|
1790
2235
|
};
|
|
1791
2236
|
const openToolCalls = /* @__PURE__ */ new Set();
|
|
2237
|
+
let textSinceFlush = false;
|
|
2238
|
+
let speechSinceTool = false;
|
|
2239
|
+
let lastChar = "";
|
|
2240
|
+
let flushed = false;
|
|
2241
|
+
let segments = 0;
|
|
2242
|
+
const newClause = () => speakFirstClause ? new FirstClause() : void 0;
|
|
2243
|
+
let clause = newClause();
|
|
2244
|
+
const put = (content) => this.queue.put({
|
|
2245
|
+
id,
|
|
2246
|
+
delta: {
|
|
2247
|
+
role: "assistant",
|
|
2248
|
+
content,
|
|
2249
|
+
extra: { [VOICE_RESPONSE_MARKER]: id, [AGUI_RUN_ID_EXTRA]: runId }
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
const flushSegment = () => {
|
|
2253
|
+
this.queue.put(FlushSentinel);
|
|
2254
|
+
textSinceFlush = false;
|
|
2255
|
+
segments += 1;
|
|
2256
|
+
onSpeechSegments?.(segments);
|
|
2257
|
+
};
|
|
2258
|
+
onSpeechSegments?.(0);
|
|
1792
2259
|
let finished;
|
|
1793
2260
|
let failure;
|
|
1794
2261
|
try {
|
|
@@ -1805,22 +2272,38 @@ var BoxLLMStream = class extends llm2.LLMStream {
|
|
|
1805
2272
|
restartIdle();
|
|
1806
2273
|
reply.observe(ev);
|
|
1807
2274
|
switch (ev.kind) {
|
|
1808
|
-
case "text":
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2275
|
+
case "text": {
|
|
2276
|
+
const content = flushed && !/^\s/.test(ev.text) && !/\s$/.test(lastChar) ? ` ${ev.text}` : ev.text;
|
|
2277
|
+
flushed = false;
|
|
2278
|
+
if (/\S/.test(ev.text)) {
|
|
2279
|
+
textSinceFlush = true;
|
|
2280
|
+
speechSinceTool = true;
|
|
2281
|
+
}
|
|
2282
|
+
lastChar = content.slice(-1);
|
|
2283
|
+
const split = clause?.push(content);
|
|
2284
|
+
if (!split?.cut) {
|
|
2285
|
+
put(content);
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2288
|
+
if (split.head) put(split.head);
|
|
2289
|
+
flushSegment();
|
|
2290
|
+
textSinceFlush = /\S/.test(split.tail);
|
|
2291
|
+
put(split.tail);
|
|
1817
2292
|
break;
|
|
2293
|
+
}
|
|
1818
2294
|
case "tool_call_start":
|
|
1819
2295
|
if (clientToolNames.has(ev.name)) {
|
|
1820
2296
|
clientToolCalls.set(ev.id, { id: ev.id, name: ev.name });
|
|
1821
2297
|
} else if (!openToolCalls.has(ev.id)) {
|
|
2298
|
+
const afterSpeech = speechSinceTool;
|
|
2299
|
+
if (textSinceFlush) {
|
|
2300
|
+
flushSegment();
|
|
2301
|
+
flushed = true;
|
|
2302
|
+
}
|
|
2303
|
+
speechSinceTool = false;
|
|
2304
|
+
clause = newClause();
|
|
1822
2305
|
openToolCalls.add(ev.id);
|
|
1823
|
-
onToolCall?.("start", ev.id);
|
|
2306
|
+
onToolCall?.("start", ev.id, afterSpeech);
|
|
1824
2307
|
}
|
|
1825
2308
|
break;
|
|
1826
2309
|
case "tool_call_result":
|
|
@@ -1979,15 +2462,56 @@ var createHangUp = ({
|
|
|
1979
2462
|
// libs/runtime/voice-worker/src/session-options.ts
|
|
1980
2463
|
import { tts, voice as voice3 } from "@livekit/agents";
|
|
1981
2464
|
|
|
1982
|
-
// libs/runtime/voice-worker/src/
|
|
2465
|
+
// libs/runtime/voice-worker/src/knowledge-prefetch.ts
|
|
2466
|
+
import { log as log3, stt as stt2 } from "@livekit/agents";
|
|
1983
2467
|
import { TransformStream as TransformStream2 } from "node:stream/web";
|
|
2468
|
+
var knowledgeTap = (send, { onLead, now = Date.now } = {}) => {
|
|
2469
|
+
let sentThisTurn;
|
|
2470
|
+
let sentAt;
|
|
2471
|
+
let gone = false;
|
|
2472
|
+
let failureLogged = false;
|
|
2473
|
+
const tap = (event) => {
|
|
2474
|
+
if (gone || typeof event === "string") return;
|
|
2475
|
+
if (event.type === stt2.SpeechEventType.FINAL_TRANSCRIPT) {
|
|
2476
|
+
if (sentAt !== void 0) onLead?.(now() - sentAt);
|
|
2477
|
+
sentThisTurn = void 0;
|
|
2478
|
+
sentAt = void 0;
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
if (event.type !== stt2.SpeechEventType.PREFLIGHT_TRANSCRIPT) return;
|
|
2482
|
+
const text = event.alternatives?.[0]?.text.trim();
|
|
2483
|
+
if (!text || text === sentThisTurn) return;
|
|
2484
|
+
sentThisTurn = text;
|
|
2485
|
+
sentAt = now();
|
|
2486
|
+
void send(text).then((answer) => {
|
|
2487
|
+
if (answer === "gone") gone = true;
|
|
2488
|
+
if (answer === "failed" && !failureLogged) {
|
|
2489
|
+
failureLogged = true;
|
|
2490
|
+
log3().warn(
|
|
2491
|
+
"voice worker: the box refused a knowledge prefetch; turns search on their own"
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
}).catch(() => void 0);
|
|
2495
|
+
};
|
|
2496
|
+
return (events) => events.pipeThrough(
|
|
2497
|
+
new TransformStream2({
|
|
2498
|
+
transform(event, controller) {
|
|
2499
|
+
tap(event);
|
|
2500
|
+
controller.enqueue(event);
|
|
2501
|
+
}
|
|
2502
|
+
})
|
|
2503
|
+
);
|
|
2504
|
+
};
|
|
2505
|
+
|
|
2506
|
+
// libs/runtime/voice-worker/src/number-text.ts
|
|
2507
|
+
import { TransformStream as TransformStream3 } from "node:stream/web";
|
|
1984
2508
|
var THOUSANDS_COMMA = /(\d),(?=\d{3}(?!\d))/g;
|
|
1985
2509
|
var TRAILING_NUMBER = /[\d.,]+$/;
|
|
1986
2510
|
var rewrite = (text) => text.replace(THOUSANDS_COMMA, "$1");
|
|
1987
2511
|
var dropThousandsComma = (input) => {
|
|
1988
2512
|
let pending = "";
|
|
1989
2513
|
return input.pipeThrough(
|
|
1990
|
-
new
|
|
2514
|
+
new TransformStream3({
|
|
1991
2515
|
transform(chunk, controller) {
|
|
1992
2516
|
const text = pending + chunk;
|
|
1993
2517
|
const tail = TRAILING_NUMBER.exec(text)?.[0] ?? "";
|
|
@@ -2004,7 +2528,7 @@ var dropThousandsComma = (input) => {
|
|
|
2004
2528
|
|
|
2005
2529
|
// libs/runtime/voice-worker/src/phone-voice.ts
|
|
2006
2530
|
import { AudioFrame as AudioFrame2 } from "@livekit/rtc-node";
|
|
2007
|
-
import { TransformStream as
|
|
2531
|
+
import { TransformStream as TransformStream4 } from "node:stream/web";
|
|
2008
2532
|
var LOW_CUT_HZ = 300;
|
|
2009
2533
|
var HIGH_CUT_HZ = 3400;
|
|
2010
2534
|
var Q = Math.SQRT1_2;
|
|
@@ -2040,7 +2564,7 @@ var phoneLine = (audio) => {
|
|
|
2040
2564
|
let chainRate = 0;
|
|
2041
2565
|
const norm = Math.tanh(DRIVE);
|
|
2042
2566
|
return audio.pipeThrough(
|
|
2043
|
-
new
|
|
2567
|
+
new TransformStream4({
|
|
2044
2568
|
transform(frame, controller) {
|
|
2045
2569
|
if (!chain || chainRate !== frame.sampleRate) {
|
|
2046
2570
|
chainRate = frame.sampleRate;
|
|
@@ -2066,11 +2590,18 @@ var phoneLine = (audio) => {
|
|
|
2066
2590
|
};
|
|
2067
2591
|
|
|
2068
2592
|
// libs/runtime/voice-worker/src/session-options.ts
|
|
2069
|
-
var sttOptions = ({ sttModel, language, keyterms }) => {
|
|
2593
|
+
var sttOptions = ({ sttModel, language, keyterms, turn }) => {
|
|
2070
2594
|
const terms = keyterms.length > 0 ? [...keyterms] : void 0;
|
|
2071
2595
|
const keyterm = terms ? { keyterm: terms } : void 0;
|
|
2072
2596
|
if (sttModel.startsWith("deepgram/flux")) {
|
|
2073
|
-
return {
|
|
2597
|
+
return {
|
|
2598
|
+
model: sttModel,
|
|
2599
|
+
modelOptions: {
|
|
2600
|
+
language_hint: language,
|
|
2601
|
+
...keyterm,
|
|
2602
|
+
eager_eot_threshold: turn.eagerThreshold
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2074
2605
|
}
|
|
2075
2606
|
if (sttModel.startsWith("assemblyai/")) {
|
|
2076
2607
|
return {
|
|
@@ -2094,12 +2625,28 @@ var markupContext = (expressive, speech) => {
|
|
|
2094
2625
|
return guide === void 0 ? [] : [{ description: VOICE_TTS_MARKUP_CONTEXT, value: guide }];
|
|
2095
2626
|
};
|
|
2096
2627
|
var VoiceCallAgent = class extends voice3.Agent {
|
|
2097
|
-
constructor() {
|
|
2098
|
-
super(
|
|
2628
|
+
constructor({ sendKnowledge, ...options }) {
|
|
2629
|
+
super(options);
|
|
2099
2630
|
this.sttDelivery = createSttDeliveryMeter();
|
|
2631
|
+
this.#knowledge = sendKnowledge && knowledgeTap(sendKnowledge, {
|
|
2632
|
+
onLead: (ms2) => {
|
|
2633
|
+
this.#preflightLead = ms2;
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
#knowledge;
|
|
2638
|
+
#preflightLead;
|
|
2639
|
+
/** The last turn's preflight head start in ms, once: a turn never reuses another's. */
|
|
2640
|
+
takePreflightLead() {
|
|
2641
|
+
const lead = this.#preflightLead;
|
|
2642
|
+
this.#preflightLead = void 0;
|
|
2643
|
+
return lead;
|
|
2100
2644
|
}
|
|
2101
2645
|
async sttNode(audio, modelSettings) {
|
|
2102
|
-
return this.sttDelivery.wrap(audio, (counted) =>
|
|
2646
|
+
return this.sttDelivery.wrap(audio, async (counted) => {
|
|
2647
|
+
const events = await super.sttNode(counted, modelSettings);
|
|
2648
|
+
return events && this.#knowledge ? this.#knowledge(events) : events;
|
|
2649
|
+
});
|
|
2103
2650
|
}
|
|
2104
2651
|
};
|
|
2105
2652
|
var PhoneLineAgent = class extends VoiceCallAgent {
|
|
@@ -2116,6 +2663,11 @@ var voiceSessionOptions = (settings, pieces) => {
|
|
|
2116
2663
|
minSilenceDuration: vad.minSilenceMs
|
|
2117
2664
|
});
|
|
2118
2665
|
const speech = pieces.createTts(ttsOptions(settings));
|
|
2666
|
+
const agentOptions = {
|
|
2667
|
+
// The box holds the prompt: LiveKit's own chat context stays prompt-less.
|
|
2668
|
+
instructions: "",
|
|
2669
|
+
sendKnowledge: pieces.sendKnowledge
|
|
2670
|
+
};
|
|
2119
2671
|
return {
|
|
2120
2672
|
session: {
|
|
2121
2673
|
vad: pieces.vad,
|
|
@@ -2139,13 +2691,18 @@ var voiceSessionOptions = (settings, pieces) => {
|
|
|
2139
2691
|
endpointing: { minDelay: turn.minEndpointingDelayMs, maxDelay: turn.maxEndpointingDelayMs },
|
|
2140
2692
|
// LiveKit 1.9.0 reads `enabled` as the session's `allowInterruptions`, the
|
|
2141
2693
|
// default of every speech; the old top-level option is deprecated.
|
|
2142
|
-
//
|
|
2143
|
-
// turns adaptive interruption off unless the worker runs on LiveKit Cloud or in
|
|
2144
|
-
// deprecated `dev` mode, and `stackbone dev` runs `start` since S15
|
|
2145
|
-
//
|
|
2146
|
-
//
|
|
2694
|
+
// The mode is always sent: with none, LiveKit 1.9.0 (`resolveInterruptionDetector`)
|
|
2695
|
+
// turns adaptive interruption off unless the worker runs on LiveKit Cloud or in
|
|
2696
|
+
// its deprecated `dev` mode, and `stackbone dev` runs `start` since S15.
|
|
2697
|
+
// `adaptive`: shortly after the agent starts speaking (LiveKit's backchannel
|
|
2698
|
+
// boundary) LiveKit stops cutting on voice activity and cuts only when its model
|
|
2699
|
+
// calls the overlap a barge-in, so a "sí", a "vale" or a cough do not cut the
|
|
2700
|
+
// agent off, and a real barge-in can be missed. LiveKit still skips adaptive, with
|
|
2701
|
+
// a warning, when the STT gives no word-aligned transcript, and when interruptions
|
|
2702
|
+
// are off. `vad`: any speech of `minDuration` (and `minWords`, when set) cuts, so
|
|
2703
|
+
// a short acknowledgement or a cough can cut too.
|
|
2147
2704
|
interruption: {
|
|
2148
|
-
mode:
|
|
2705
|
+
mode: interruptions.mode,
|
|
2149
2706
|
enabled: interruptions.enabled,
|
|
2150
2707
|
minDuration: interruptions.minDurationMs,
|
|
2151
2708
|
minWords: interruptions.minWords
|
|
@@ -2155,14 +2712,13 @@ var voiceSessionOptions = (settings, pieces) => {
|
|
|
2155
2712
|
preemptiveGeneration: { enabled: false, preemptiveTts: false }
|
|
2156
2713
|
}
|
|
2157
2714
|
},
|
|
2158
|
-
|
|
2159
|
-
agent: settings.phoneLine ? new PhoneLineAgent({ instructions: "" }) : new VoiceCallAgent({ instructions: "" }),
|
|
2715
|
+
agent: settings.phoneLine ? new PhoneLineAgent(agentOptions) : new VoiceCallAgent(agentOptions),
|
|
2160
2716
|
boxContext: markupContext(settings.expressive, speech)
|
|
2161
2717
|
};
|
|
2162
2718
|
};
|
|
2163
2719
|
|
|
2164
2720
|
// libs/runtime/voice-worker/src/telemetry/call-traces.ts
|
|
2165
|
-
import { log as
|
|
2721
|
+
import { log as log4, telemetry } from "@livekit/agents";
|
|
2166
2722
|
import { ExportResultCode } from "@opentelemetry/core";
|
|
2167
2723
|
import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
|
|
2168
2724
|
import {
|
|
@@ -2174,7 +2730,7 @@ var warnOnRefusedBatch = (exporter, sessionKey) => ({
|
|
|
2174
2730
|
export: (spans, done) => exporter.export(spans, (result) => {
|
|
2175
2731
|
try {
|
|
2176
2732
|
if (result.code !== ExportResultCode.SUCCESS) {
|
|
2177
|
-
|
|
2733
|
+
log4().warn(
|
|
2178
2734
|
{ sessionKey, error: result.error?.message ?? "unknown" },
|
|
2179
2735
|
"voice worker: a batch of call spans failed to export to the box"
|
|
2180
2736
|
);
|
|
@@ -2234,19 +2790,35 @@ var CALL_CLOSING_PHRASE = Object.freeze({
|
|
|
2234
2790
|
});
|
|
2235
2791
|
var MAX_FAILED_TURNS_IN_A_ROW = 3;
|
|
2236
2792
|
var inCallLanguage = (phrase, language) => language.split("-")[0]?.toLowerCase() === "es" ? phrase.es : phrase.en;
|
|
2237
|
-
var
|
|
2793
|
+
var CUE_AFTER_HOLDING_LINE_MS = 300;
|
|
2794
|
+
var thinkingCue = (delayMs, holdingLineMaxWaitMs) => {
|
|
2238
2795
|
let cue;
|
|
2239
2796
|
let open = 0;
|
|
2240
2797
|
let waitedLong = false;
|
|
2241
2798
|
let wait;
|
|
2242
2799
|
let playing = false;
|
|
2800
|
+
let holding = false;
|
|
2801
|
+
let flushedSegments = 0;
|
|
2802
|
+
let playedSegments = 0;
|
|
2803
|
+
let beat;
|
|
2804
|
+
let cap;
|
|
2243
2805
|
const sync = () => {
|
|
2244
|
-
const play = waitedLong || open > 0;
|
|
2806
|
+
const play = waitedLong || open > 0 && !holding;
|
|
2245
2807
|
if (play === playing || !cue) return;
|
|
2246
2808
|
playing = play;
|
|
2247
2809
|
if (play) cue.start();
|
|
2248
2810
|
else cue.stop();
|
|
2249
2811
|
};
|
|
2812
|
+
const clearHold = () => {
|
|
2813
|
+
clearTimeout(beat);
|
|
2814
|
+
clearTimeout(cap);
|
|
2815
|
+
beat = cap = void 0;
|
|
2816
|
+
holding = false;
|
|
2817
|
+
};
|
|
2818
|
+
const releaseHold = () => {
|
|
2819
|
+
clearHold();
|
|
2820
|
+
sync();
|
|
2821
|
+
};
|
|
2250
2822
|
return {
|
|
2251
2823
|
// A turn can start before the track is up.
|
|
2252
2824
|
attach: (ready) => {
|
|
@@ -2268,34 +2840,58 @@ var thinkingCue = (delayMs) => {
|
|
|
2268
2840
|
sync();
|
|
2269
2841
|
}, delayMs);
|
|
2270
2842
|
},
|
|
2271
|
-
onToolCall: (phase) => {
|
|
2272
|
-
if (phase === "start")
|
|
2273
|
-
|
|
2843
|
+
onToolCall: (phase, _id, afterSpeech = false) => {
|
|
2844
|
+
if (phase === "start") {
|
|
2845
|
+
open += 1;
|
|
2846
|
+
if (afterSpeech && holdingLineMaxWaitMs > 0 && playedSegments < flushedSegments) {
|
|
2847
|
+
clearHold();
|
|
2848
|
+
holding = true;
|
|
2849
|
+
cap = setTimeout(releaseHold, holdingLineMaxWaitMs);
|
|
2850
|
+
}
|
|
2851
|
+
} else if (open > 0) open -= 1;
|
|
2852
|
+
if (open === 0) clearHold();
|
|
2274
2853
|
sync();
|
|
2275
2854
|
},
|
|
2855
|
+
/** BoxLLM's count of the turn's flushed segments; a turn starts at 0 (`onSpeechSegments`). */
|
|
2856
|
+
onSpeechSegments: (count) => {
|
|
2857
|
+
if (count === 0) playedSegments = 0;
|
|
2858
|
+
flushedSegments = count;
|
|
2859
|
+
},
|
|
2860
|
+
/** A segment of a reply finished playing out, or was cut. */
|
|
2861
|
+
onPlaybackFinished: () => {
|
|
2862
|
+
if (playedSegments < flushedSegments) playedSegments += 1;
|
|
2863
|
+
if (!holding || beat !== void 0 || playedSegments < flushedSegments) return;
|
|
2864
|
+
beat = setTimeout(releaseHold, CUE_AFTER_HOLDING_LINE_MS);
|
|
2865
|
+
},
|
|
2866
|
+
onSessionError: ({ error }) => {
|
|
2867
|
+
if (holding && error.type === "tts_error" && !error.recoverable) releaseHold();
|
|
2868
|
+
},
|
|
2276
2869
|
/** The call is ending: a pending delay must not start the sound in a closed room. */
|
|
2277
2870
|
close: () => {
|
|
2278
2871
|
clearTimeout(wait);
|
|
2279
2872
|
wait = void 0;
|
|
2280
2873
|
waitedLong = false;
|
|
2281
2874
|
open = 0;
|
|
2875
|
+
clearHold();
|
|
2282
2876
|
sync();
|
|
2283
2877
|
}
|
|
2284
2878
|
};
|
|
2285
2879
|
};
|
|
2286
2880
|
var messageOf = (err) => err instanceof Error ? err.message : String(err);
|
|
2287
|
-
var callerNoiseCancellation = (caller) => {
|
|
2881
|
+
var callerNoiseCancellation = (caller, mode) => {
|
|
2882
|
+
if (mode === "off") return void 0;
|
|
2288
2883
|
try {
|
|
2289
|
-
|
|
2884
|
+
if (caller.kind === ParticipantKind.SIP) return TelephonyBackgroundVoiceCancellation();
|
|
2885
|
+
return mode === "standard" ? NoiseCancellation() : BackgroundVoiceCancellation();
|
|
2290
2886
|
} catch (err) {
|
|
2291
|
-
|
|
2887
|
+
log5().warn({ error: messageOf(err) }, "voice worker: no noise cancellation on this call");
|
|
2292
2888
|
return void 0;
|
|
2293
2889
|
}
|
|
2294
2890
|
};
|
|
2295
2891
|
var runVoiceJob = async (ctx, deps) => {
|
|
2296
2892
|
const dispatch = readDispatch(ctx.job.metadata);
|
|
2297
2893
|
if (!dispatch) {
|
|
2298
|
-
|
|
2894
|
+
log5().warn(
|
|
2299
2895
|
{ metadataChars: ctx.job.metadata?.length ?? 0 },
|
|
2300
2896
|
"voice worker: refused a job without a valid dispatch"
|
|
2301
2897
|
);
|
|
@@ -2304,7 +2900,7 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2304
2900
|
}
|
|
2305
2901
|
await ctx.connect();
|
|
2306
2902
|
const caller = await ctx.waitForParticipant().catch((err) => {
|
|
2307
|
-
|
|
2903
|
+
log5().info(
|
|
2308
2904
|
{ reason: messageOf(err) },
|
|
2309
2905
|
"voice worker: the room closed before the caller joined"
|
|
2310
2906
|
);
|
|
@@ -2324,7 +2920,10 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2324
2920
|
deps
|
|
2325
2921
|
);
|
|
2326
2922
|
const settings = dispatch.voiceSettings;
|
|
2327
|
-
const cue = thinkingCue(
|
|
2923
|
+
const cue = thinkingCue(
|
|
2924
|
+
settings.backgroundAudio.thinkingDelayMs,
|
|
2925
|
+
settings.backgroundAudio.holdingLineMaxWaitMs
|
|
2926
|
+
);
|
|
2328
2927
|
const fallback = inCallLanguage(TURN_FALLBACK_PHRASE, settings.language);
|
|
2329
2928
|
let failedInARow = 0;
|
|
2330
2929
|
let ending = false;
|
|
@@ -2332,22 +2931,22 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2332
2931
|
try {
|
|
2333
2932
|
session.say(text);
|
|
2334
2933
|
} catch (err) {
|
|
2335
|
-
|
|
2934
|
+
log5().info({ error: messageOf(err) }, `voice worker: the ${what} was not spoken`);
|
|
2336
2935
|
}
|
|
2337
2936
|
};
|
|
2338
2937
|
const hangUpAfter = (played, reason) => {
|
|
2339
2938
|
ending = true;
|
|
2340
2939
|
session.input.setAudioEnabled(false);
|
|
2341
2940
|
void Promise.resolve().then(played).catch((err) => {
|
|
2342
|
-
|
|
2941
|
+
log5().info({ error: messageOf(err) }, "voice worker: the closing phrase was not spoken");
|
|
2343
2942
|
}).then(() => ctx.deleteRoom()).catch((err) => {
|
|
2344
|
-
|
|
2943
|
+
log5().warn({ error: messageOf(err) }, "voice worker: the room was not deleted");
|
|
2345
2944
|
}).then(
|
|
2346
2945
|
() => finishCall(
|
|
2347
2946
|
reason === "turns kept failing" || reason === "box refused the call token" ? "failed" : "completed"
|
|
2348
2947
|
)
|
|
2349
2948
|
).catch(() => {
|
|
2350
|
-
|
|
2949
|
+
log5().warn(
|
|
2351
2950
|
{ agent: dispatch.agent, sessionKey: dispatch.sessionKey, reason: "completion_failed" },
|
|
2352
2951
|
"voice worker: call finalization failed"
|
|
2353
2952
|
);
|
|
@@ -2367,12 +2966,12 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2367
2966
|
});
|
|
2368
2967
|
const onTurnError = (err) => {
|
|
2369
2968
|
if (err instanceof BoxTurnError && err.status === 401) {
|
|
2370
|
-
|
|
2969
|
+
log5().error(boxFailure(err), "voice worker: the box refused the call token (401)");
|
|
2371
2970
|
if (!ending) endCall(CALL_CLOSING_PHRASE, "box refused the call token");
|
|
2372
2971
|
return;
|
|
2373
2972
|
}
|
|
2374
2973
|
failedInARow += 1;
|
|
2375
|
-
|
|
2974
|
+
log5().warn({ ...boxFailure(err), failedInARow }, "voice worker: a turn failed");
|
|
2376
2975
|
if (ending) return;
|
|
2377
2976
|
if (failedInARow >= MAX_FAILED_TURNS_IN_A_ROW) {
|
|
2378
2977
|
endCall(CALL_CLOSING_PHRASE, "turns kept failing");
|
|
@@ -2384,13 +2983,20 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2384
2983
|
failedInARow = 0;
|
|
2385
2984
|
if (outcome === "interrupt" && !ending) endCall(PAUSED_RUN_PHRASE, "run paused for review");
|
|
2386
2985
|
};
|
|
2387
|
-
const
|
|
2388
|
-
const wire = {
|
|
2986
|
+
const call = {
|
|
2389
2987
|
boxUrl: dispatch.boxUrl,
|
|
2390
2988
|
token: dispatch.callToken,
|
|
2391
2989
|
agent: dispatch.agent,
|
|
2392
2990
|
sessionKey: dispatch.sessionKey,
|
|
2393
|
-
tags: dispatch.tags
|
|
2991
|
+
tags: dispatch.tags
|
|
2992
|
+
};
|
|
2993
|
+
const plan = voiceSessionOptions(settings, {
|
|
2994
|
+
...deps,
|
|
2995
|
+
vad: ctx.proc.userData.vad,
|
|
2996
|
+
sendKnowledge: (text) => voiceCallKnowledge(call, text)
|
|
2997
|
+
});
|
|
2998
|
+
const wire = {
|
|
2999
|
+
...call,
|
|
2394
3000
|
// With `expressive`, the TTS markup guide: LiveKit gives it only to its own chat context.
|
|
2395
3001
|
context: plan.boxContext
|
|
2396
3002
|
};
|
|
@@ -2406,7 +3012,7 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2406
3012
|
hangUpAfter,
|
|
2407
3013
|
isEnding: () => ending,
|
|
2408
3014
|
onResumeFailed: (err) => {
|
|
2409
|
-
|
|
3015
|
+
log5().warn(boxFailure(err), "voice worker: the hang_up resume failed");
|
|
2410
3016
|
}
|
|
2411
3017
|
});
|
|
2412
3018
|
const brain = new BoxLLM({
|
|
@@ -2416,6 +3022,8 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2416
3022
|
pendingResume: hangUp.pendingResume,
|
|
2417
3023
|
isEnding: () => ending,
|
|
2418
3024
|
onToolCall: cue.onToolCall,
|
|
3025
|
+
speakFirstClause: settings.speakFirstClause,
|
|
3026
|
+
onSpeechSegments: cue.onSpeechSegments,
|
|
2419
3027
|
onTurnError,
|
|
2420
3028
|
onRunFinished
|
|
2421
3029
|
});
|
|
@@ -2438,14 +3046,14 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2438
3046
|
if (!(turns.length > 0 && error instanceof BoxTurnError && error.status === 400)) {
|
|
2439
3047
|
throw error;
|
|
2440
3048
|
}
|
|
2441
|
-
|
|
3049
|
+
log5().warn(
|
|
2442
3050
|
{ agent: dispatch.agent, sessionKey: dispatch.sessionKey, status: error.status },
|
|
2443
3051
|
"voice worker: the box refused the turn metrics, completing without them"
|
|
2444
3052
|
);
|
|
2445
3053
|
await complete({ transcript, outcome });
|
|
2446
3054
|
}
|
|
2447
3055
|
}).catch((error) => {
|
|
2448
|
-
|
|
3056
|
+
log5().warn(
|
|
2449
3057
|
{ agent: dispatch.agent, sessionKey: dispatch.sessionKey, reason: "completion_failed" },
|
|
2450
3058
|
"voice worker: call finalization failed"
|
|
2451
3059
|
);
|
|
@@ -2458,6 +3066,7 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2458
3066
|
voice4.AgentSessionEventTypes.AgentStateChanged,
|
|
2459
3067
|
({ newState }) => cue.onAgentState(newState)
|
|
2460
3068
|
);
|
|
3069
|
+
session.on(voice4.AgentSessionEventTypes.Error, cue.onSessionError);
|
|
2461
3070
|
session.on(voice4.AgentSessionEventTypes.Close, () => {
|
|
2462
3071
|
if (completion || ending) return;
|
|
2463
3072
|
void finishCall().catch(() => void 0).finally(() => ctx.shutdown("caller left"));
|
|
@@ -2466,22 +3075,26 @@ var runVoiceJob = async (ctx, deps) => {
|
|
|
2466
3075
|
session,
|
|
2467
3076
|
(metrics) => {
|
|
2468
3077
|
if (turns.length < VOICE_CALL_TURNS_MAX) turns.push(metrics);
|
|
2469
|
-
|
|
3078
|
+
log5().info(
|
|
2470
3079
|
{ agent: dispatch.agent, sessionKey: dispatch.sessionKey, room: ctx.room.name, ...metrics },
|
|
2471
3080
|
VOICE_TURN_METRICS_MESSAGE
|
|
2472
3081
|
);
|
|
2473
3082
|
},
|
|
2474
|
-
|
|
3083
|
+
{
|
|
3084
|
+
...deliversItsOwnEndOfTurn(settings) ? { endOfTurnMs: () => plan.agent.sttDelivery.take() } : {},
|
|
3085
|
+
preflightLeadMs: () => plan.agent.takePreflightLead()
|
|
3086
|
+
}
|
|
2475
3087
|
);
|
|
2476
|
-
const noiseCancellation = callerNoiseCancellation(participant);
|
|
3088
|
+
const noiseCancellation = callerNoiseCancellation(participant, settings.noiseCancellation);
|
|
2477
3089
|
await session.start({
|
|
2478
3090
|
agent: plan.agent,
|
|
2479
3091
|
room: ctx.room,
|
|
2480
3092
|
inputOptions: noiseCancellation ? { noiseCancellation } : {}
|
|
2481
3093
|
});
|
|
3094
|
+
session.output?.audio?.on("playbackFinished", cue.onPlaybackFinished);
|
|
2482
3095
|
if (settings.greeting !== null) sayIfOpen(settings.greeting, "greeting");
|
|
2483
3096
|
await deps.createCue({ room: ctx.room, session, audio: settings.backgroundAudio }).then(cue.attach, (err) => {
|
|
2484
|
-
|
|
3097
|
+
log5().warn({ error: messageOf(err) }, "voice worker: the background audio did not start");
|
|
2485
3098
|
});
|
|
2486
3099
|
};
|
|
2487
3100
|
|