@alexkroman1/aai-cli 6.11.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* The first three are ordinary step work. The fourth was impossible until two
|
|
20
20
|
* things existed, and they are what this template is the reference use of:
|
|
21
21
|
*
|
|
22
|
-
* - **`stepSpeak`** (`@alexkroman1/aai/
|
|
22
|
+
* - **`stepSpeak`** (`@alexkroman1/aai/step`) synthesizes from inside a step.
|
|
23
23
|
* The session TTS surface cannot be used here at all: a `TtsSession` is an
|
|
24
24
|
* event stream wired into a live pipeline's playback, with a turn tracker and
|
|
25
25
|
* barge-in behind it, and a step has no turn to be part of and has to return
|
|
@@ -76,8 +76,9 @@
|
|
|
76
76
|
|
|
77
77
|
import { workflow, workflowApp } from "@alexkroman1/aai";
|
|
78
78
|
import { ASSEMBLYAI_TTS_DEFAULT_VOICE, ASSEMBLYAI_TTS_VOICES } from "@alexkroman1/aai/tts";
|
|
79
|
+
import type { WorkflowDef } from "@alexkroman1/aai/workflow-api";
|
|
79
80
|
import { z } from "zod";
|
|
80
|
-
import { spokenSummaryFlow } from "./workflows/summarize.ts";
|
|
81
|
+
import { type SpokenSummary, spokenSummaryFlow } from "./workflows/summarize.ts";
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
84
|
* The voices the form offers.
|
|
@@ -102,28 +103,40 @@ const VOICES = Object.entries(ASSEMBLYAI_TTS_VOICES)
|
|
|
102
103
|
*/
|
|
103
104
|
const [FIRST_VOICE = ASSEMBLYAI_TTS_DEFAULT_VOICE, ...OTHER_VOICES] = VOICES;
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The run input, as its own const.
|
|
108
|
+
*
|
|
109
|
+
* Named rather than inline because {@link spokenSummary} carries an explicit
|
|
110
|
+
* type, and that annotation is what lets `workflows/summarize.ts` name
|
|
111
|
+
* `WorkflowInputOf<typeof spokenSummary>` for its body's parameter: the body's
|
|
112
|
+
* own signature would otherwise be part of what infers this declaration's type,
|
|
113
|
+
* and TypeScript refuses the cycle (`TS7022`).
|
|
114
|
+
*/
|
|
115
|
+
const spokenSummaryInput = z.object({
|
|
116
|
+
// A plain string, because an upload id is what the run really receives.
|
|
117
|
+
// What makes it a file picker rather than a text box is the `uploads` line
|
|
118
|
+
// below.
|
|
119
|
+
recording: z.string().describe("A recording to summarize — WAV, MP3 or M4A"),
|
|
120
|
+
// An enum, so the form renders a SELECT rather than a text box — which is
|
|
121
|
+
// the whole reason the list is derived above rather than left free-form.
|
|
122
|
+
// Optional, so the SDK's own default voice applies when nobody chooses.
|
|
123
|
+
voice: z
|
|
124
|
+
.enum([FIRST_VOICE, ...OTHER_VOICES])
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("Voice to read the summary in"),
|
|
127
|
+
});
|
|
128
|
+
|
|
105
129
|
/**
|
|
106
130
|
* The declaration: schema, description, and the directive body.
|
|
107
131
|
*
|
|
108
132
|
* Exported so `WorkflowOutputOf<typeof spokenSummary>` names the output type in
|
|
109
133
|
* one place — including from `client.tsx`, where `import type` is erased and so
|
|
110
|
-
* bundles nothing server-side.
|
|
134
|
+
* bundles nothing server-side — and so `workflows/summarize.ts` can name
|
|
135
|
+
* `WorkflowInputOf<typeof spokenSummary>` for the body's parameter.
|
|
111
136
|
*/
|
|
112
|
-
export const spokenSummary = workflow({
|
|
137
|
+
export const spokenSummary: WorkflowDef<typeof spokenSummaryInput, SpokenSummary> = workflow({
|
|
113
138
|
description: "Transcribe a recording, summarize it, and read the summary back as audio",
|
|
114
|
-
input:
|
|
115
|
-
// A plain string, because an upload id is what the run really receives.
|
|
116
|
-
// What makes it a file picker rather than a text box is the `uploads` line
|
|
117
|
-
// below.
|
|
118
|
-
recording: z.string().describe("A recording to summarize — WAV, MP3 or M4A"),
|
|
119
|
-
// An enum, so the form renders a SELECT rather than a text box — which is
|
|
120
|
-
// the whole reason the list is derived above rather than left free-form.
|
|
121
|
-
// Optional, so the SDK's own default voice applies when nobody chooses.
|
|
122
|
-
voice: z
|
|
123
|
-
.enum([FIRST_VOICE, ...OTHER_VOICES])
|
|
124
|
-
.optional()
|
|
125
|
-
.describe("Voice to read the summary in"),
|
|
126
|
-
}),
|
|
139
|
+
input: spokenSummaryInput,
|
|
127
140
|
// The one line that makes the form take a file: `<WorkflowFields>` renders a
|
|
128
141
|
// picker for this property, `useWorkflowSubmit` stores the chosen file, and
|
|
129
142
|
// the step that transcribes it reads it back with `readUpload`.
|
|
@@ -36,18 +36,19 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
36
36
|
// ERASED at build time, so naming the agent's own type costs the browser bundle
|
|
37
37
|
// nothing — and it is what stops this file restating a shape
|
|
38
38
|
// `workflows/summarize.ts` already declares.
|
|
39
|
-
import
|
|
39
|
+
import { formatDuration } from "@alexkroman1/aai/utils";
|
|
40
|
+
import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
40
41
|
import {
|
|
41
42
|
createWorkflowApi,
|
|
42
43
|
Form,
|
|
43
44
|
page,
|
|
44
45
|
SubmitButton,
|
|
45
46
|
UploadProgressBar,
|
|
47
|
+
useDownloadUrl,
|
|
46
48
|
useWorkflowSubmit,
|
|
47
49
|
WorkflowFields,
|
|
48
50
|
WorkflowProgress,
|
|
49
51
|
} from "@alexkroman1/aai-ui";
|
|
50
|
-
import { useEffect, useState } from "react";
|
|
51
52
|
import type { spokenSummary } from "./agent.ts";
|
|
52
53
|
|
|
53
54
|
/** What a completed run reports, derived from the workflow rather than restated. */
|
|
@@ -84,58 +85,16 @@ function captionsUrl(text: string, durationMs: number): string {
|
|
|
84
85
|
return `data:text/vtt;charset=utf-8,${encodeURIComponent(vtt)}`;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
/** Seconds a person can read, from the milliseconds a run reports. */
|
|
88
|
-
function duration(ms: number): string {
|
|
89
|
-
const total = Math.round(ms / 1000);
|
|
90
|
-
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The finished run's audio, as something the browser will play.
|
|
95
|
-
*
|
|
96
|
-
* A hook rather than four lines in the component because the CLEANUP is the
|
|
97
|
-
* part worth keeping in one place: an object URL pins its blob for the life of
|
|
98
|
-
* the document, so it is revoked when the id changes and when the page goes
|
|
99
|
-
* away. The `cancelled` flag covers the other half — a second run settling
|
|
100
|
-
* while the first download is still in flight would otherwise set state from
|
|
101
|
-
* the stale one.
|
|
102
|
-
*/
|
|
103
|
-
function useAudioUrl(uploadId: string | undefined): { url?: string; error?: string } {
|
|
104
|
-
const [state, setState] = useState<{ url?: string; error?: string }>({});
|
|
105
|
-
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (uploadId === undefined) {
|
|
108
|
-
setState({});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
let cancelled = false;
|
|
112
|
-
let objectUrl: string | undefined;
|
|
113
|
-
api
|
|
114
|
-
.download(uploadId)
|
|
115
|
-
.then((blob) => {
|
|
116
|
-
if (cancelled) return;
|
|
117
|
-
objectUrl = URL.createObjectURL(blob);
|
|
118
|
-
setState({ url: objectUrl });
|
|
119
|
-
})
|
|
120
|
-
.catch((err: unknown) => {
|
|
121
|
-
if (!cancelled) setState({ error: err instanceof Error ? err.message : String(err) });
|
|
122
|
-
});
|
|
123
|
-
return () => {
|
|
124
|
-
cancelled = true;
|
|
125
|
-
if (objectUrl !== undefined) URL.revokeObjectURL(objectUrl);
|
|
126
|
-
};
|
|
127
|
-
}, [uploadId]);
|
|
128
|
-
|
|
129
|
-
return state;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
88
|
export function App() {
|
|
133
89
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
134
90
|
// `run.output` instead of `unknown`.
|
|
135
91
|
const { submit, run, pending, upload, pauseUpload, resumeUpload, error } =
|
|
136
92
|
useWorkflowSubmit<Summary>(WORKFLOW, { api });
|
|
137
93
|
const output = run?.status === "completed" ? run.output : undefined;
|
|
138
|
-
|
|
94
|
+
// `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
|
|
95
|
+
// bytes have to be FETCHED and handed to the element as an object URL — and the
|
|
96
|
+
// object URL has to be revoked, which is the half a page written by hand forgets.
|
|
97
|
+
const audio = useDownloadUrl(output?.audio, { api });
|
|
139
98
|
|
|
140
99
|
return (
|
|
141
100
|
<main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
|
|
@@ -169,7 +128,7 @@ export function App() {
|
|
|
169
128
|
<div className="flex flex-col gap-1">
|
|
170
129
|
<h2 className="text-xl">{output.headline}</h2>
|
|
171
130
|
<p className="text-sm opacity-70">
|
|
172
|
-
{output.source} · {
|
|
131
|
+
{output.source} · {formatDuration(output.durationMs)} · {output.words} words
|
|
173
132
|
</p>
|
|
174
133
|
</div>
|
|
175
134
|
|
|
@@ -181,8 +140,9 @@ export function App() {
|
|
|
181
140
|
|
|
182
141
|
<section className="flex flex-col gap-2">
|
|
183
142
|
<h3 className="text-sm font-medium opacity-70">
|
|
184
|
-
Read aloud · {
|
|
143
|
+
Read aloud · {formatDuration(output.audioDurationMs)}
|
|
185
144
|
</h3>
|
|
145
|
+
{audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
|
|
186
146
|
{audio.error !== undefined && (
|
|
187
147
|
<p className="text-red-600">Could not load the audio: {audio.error}</p>
|
|
188
148
|
)}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* run. The bytes go to the store, the output carries the id, and the page
|
|
25
25
|
* turns it back into something to play with `api.download(id)`.
|
|
26
26
|
*
|
|
27
|
-
* Both are on `@alexkroman1/aai/
|
|
27
|
+
* Both are on `@alexkroman1/aai/step`, imported from THERE rather than the
|
|
28
28
|
* root: a `workflows/*.ts` module is bundled separately by the WDK builder, so
|
|
29
29
|
* the root barrel's module graph would ride into the step bundle.
|
|
30
30
|
*
|
|
@@ -49,19 +49,17 @@
|
|
|
49
49
|
* expensive one.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
writeUpload,
|
|
60
|
-
} from "@alexkroman1/aai/utils";
|
|
52
|
+
import { report, stepSpeak, TRANSCRIBE_API, writeUpload } from "@alexkroman1/aai/step";
|
|
53
|
+
import { stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
|
|
54
|
+
import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
|
|
55
|
+
// ERASED at build time, so the body can name the schema's own output type without
|
|
56
|
+
// a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
|
|
57
|
+
// for `WorkflowOutputOf`.
|
|
58
|
+
import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
|
|
61
59
|
import { sleep } from "workflow";
|
|
62
60
|
import { z } from "zod";
|
|
61
|
+
import type { spokenSummary } from "../agent.ts";
|
|
63
62
|
import {
|
|
64
|
-
countWords,
|
|
65
63
|
createJob,
|
|
66
64
|
MAX_POLLS,
|
|
67
65
|
POLL_INTERVAL,
|
|
@@ -128,13 +126,9 @@ export type SpokenSummary = {
|
|
|
128
126
|
};
|
|
129
127
|
|
|
130
128
|
/** Transcribe a recording, summarize it, and read the summary back. */
|
|
131
|
-
export async function spokenSummaryFlow(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// is on repo-wide, and what a zod `.optional()` infers is a property that may
|
|
135
|
-
// be PRESENT and undefined.
|
|
136
|
-
voice?: string | undefined;
|
|
137
|
-
}): Promise<SpokenSummary> {
|
|
129
|
+
export async function spokenSummaryFlow(
|
|
130
|
+
input: WorkflowInputOf<typeof spokenSummary>,
|
|
131
|
+
): Promise<SpokenSummary> {
|
|
138
132
|
"use workflow";
|
|
139
133
|
|
|
140
134
|
const transcript = await transcribe(input.recording);
|
|
@@ -188,7 +182,7 @@ export async function summarize(
|
|
|
188
182
|
"use step";
|
|
189
183
|
|
|
190
184
|
await report("Summarizing the transcript.");
|
|
191
|
-
const reply = await
|
|
185
|
+
const reply = await stepGenerateJsonClassified(
|
|
192
186
|
"Summarize this transcript of a recording.\n\n" +
|
|
193
187
|
"Answer with JSON only, in this shape:\n" +
|
|
194
188
|
`{"headline": "...", "points": ["..."], "spoken": "..."}\n\n` +
|
|
@@ -204,9 +198,10 @@ export async function summarize(
|
|
|
204
198
|
system: "You summarize recordings. You answer with JSON and nothing else.",
|
|
205
199
|
schema: SummaryReply,
|
|
206
200
|
},
|
|
207
|
-
// Classified
|
|
208
|
-
//
|
|
209
|
-
|
|
201
|
+
// The `Classified` caller is `stepGenerateJson` plus `throwStepError`, which
|
|
202
|
+
// reads the gateway's own status: a 429 is worth another attempt and a 400
|
|
203
|
+
// is not, and that is what tells the DevKit which.
|
|
204
|
+
);
|
|
210
205
|
|
|
211
206
|
return { headline: reply.headline, points: reply.points.slice(0, POINTS), spoken: reply.spoken };
|
|
212
207
|
}
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* **Every one of them is four lines, because the SDK owns the endpoint.**
|
|
14
14
|
* `stepTranscribeUpload` / `stepTranscribeSubmit` / `stepTranscribePoll` on
|
|
15
|
-
* `@alexkroman1/aai/
|
|
15
|
+
* `@alexkroman1/aai/step` — reached here through their `*Classified` callers on
|
|
16
|
+
* `@alexkroman1/aai/step-errors` — carry the URL, the raw-key auth, the windowed
|
|
16
17
|
* streaming upload, the PLURAL `speech_models` field and the failure
|
|
17
18
|
* classification — all of which this file used to spell out, and all of which
|
|
18
19
|
* `transcription-workflow` used to spell out again, differently worded and
|
|
@@ -37,14 +38,13 @@
|
|
|
37
38
|
* the one leg that should be as boring as possible.
|
|
38
39
|
*/
|
|
39
40
|
|
|
40
|
-
import {
|
|
41
|
+
import { report, uploadInfo } from "@alexkroman1/aai/step";
|
|
41
42
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} from "@alexkroman1/aai/utils";
|
|
43
|
+
stepTranscribePollClassified,
|
|
44
|
+
stepTranscribeSubmitClassified,
|
|
45
|
+
stepTranscribeUploadClassified,
|
|
46
|
+
} from "@alexkroman1/aai/step-errors";
|
|
47
|
+
import { countWords, formatBytes } from "@alexkroman1/aai/utils";
|
|
48
48
|
|
|
49
49
|
/** How long between polls of a submitted job. */
|
|
50
50
|
export const POLL_INTERVAL = "10s";
|
|
@@ -79,16 +79,20 @@ export type Transcript = {
|
|
|
79
79
|
* expires before the next step runs; that costs one fresh upload, once, instead
|
|
80
80
|
* of five.
|
|
81
81
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* The `Classified` callers on `@alexkroman1/aai/step-errors` are the SDK's own
|
|
83
|
+
* `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns
|
|
84
|
+
* the SDK's `TranscribeError` into the DevKit's verdict — a missing key and a
|
|
85
|
+
* 400 stop, a 429 waits as long as the service asked. Every step here ends the
|
|
86
|
+
* same way for the same reason.
|
|
85
87
|
*/
|
|
86
88
|
export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
|
|
87
89
|
"use step";
|
|
88
90
|
|
|
89
91
|
const stored = await uploadInfo(uploadId);
|
|
90
|
-
await report(
|
|
91
|
-
|
|
92
|
+
await report(
|
|
93
|
+
`Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
|
|
94
|
+
);
|
|
95
|
+
return await stepTranscribeUploadClassified(uploadId);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
/** Retries beyond the default 3: an upload is the one call here worth another attempt. */
|
|
@@ -98,7 +102,7 @@ uploadToProvider.maxRetries = 5;
|
|
|
98
102
|
export async function createJob(audioUrl: string): Promise<{ id: string }> {
|
|
99
103
|
"use step";
|
|
100
104
|
|
|
101
|
-
const job = await
|
|
105
|
+
const job = await stepTranscribeSubmitClassified(audioUrl);
|
|
102
106
|
await report(`Transcribing — job ${job.id}.`);
|
|
103
107
|
return job;
|
|
104
108
|
}
|
|
@@ -118,7 +122,7 @@ export async function pollTranscript(
|
|
|
118
122
|
): Promise<{ done: false } | { done: true; transcript: Transcript }> {
|
|
119
123
|
"use step";
|
|
120
124
|
|
|
121
|
-
const progress = await
|
|
125
|
+
const progress = await stepTranscribePollClassified(id);
|
|
122
126
|
if (!progress.done) return { done: false };
|
|
123
127
|
|
|
124
128
|
const stored = await uploadInfo(uploadId);
|
|
@@ -132,14 +136,3 @@ export async function pollTranscript(
|
|
|
132
136
|
},
|
|
133
137
|
};
|
|
134
138
|
}
|
|
135
|
-
|
|
136
|
-
/** Words in a transcript, for the counts a page shows. */
|
|
137
|
-
export function countWords(text: string): number {
|
|
138
|
-
const trimmed = text.trim();
|
|
139
|
-
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** A size a person can read, because the number that matters is the scale. */
|
|
143
|
-
function mb(bytes: number): string {
|
|
144
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
145
|
-
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
// An EVAL: does the line really refuse to say what it cannot ground? Run it
|
|
4
|
+
// with `aai eval`.
|
|
5
|
+
//
|
|
6
|
+
// `agent.test.ts` drives the retriever and each CRAG node directly. What it
|
|
7
|
+
// cannot see is the claim this template exists to make — that a MODEL holding
|
|
8
|
+
// this tool list cannot state a fee it did not just retrieve. So these cases
|
|
9
|
+
// drive a real session and read the verdicts off the tool results: which
|
|
10
|
+
// documents the grader passed, whether the answer was grounded, and whether an
|
|
11
|
+
// answer it could not ground was WITHHELD.
|
|
12
|
+
//
|
|
13
|
+
// **One thing a SCRIPTED run cannot reach: `ctx.generate` with a SCHEMA.** The
|
|
14
|
+
// stub model registers as an LLM provider, so `ctx.generate` resolves it, and it
|
|
15
|
+
// answers a plain text generation from the script. Every node in this template's
|
|
16
|
+
// loop passes a `schema` though (`binaryScore`), and that overload comes back
|
|
17
|
+
// `{"error":"The knowledge base lookup failed: No output generated."}` — so the
|
|
18
|
+
// whole corrective loop is unreachable scripted. That is the template's central
|
|
19
|
+
// tool, so the two cases that run without a key are written as invariants both
|
|
20
|
+
// modes satisfy (a failed lookup is REPORTED, never answered around), and the
|
|
21
|
+
// two that read a real verdict are `{ live: true }`.
|
|
22
|
+
//
|
|
23
|
+
// **`system-prompt.md` is applied HERE, not by `agent.ts`.** The build discovers
|
|
24
|
+
// the file, so an eval driving the raw default export would run this agent with
|
|
25
|
+
// the FRAMEWORK DEFAULT prompt — and the discipline that prompt imposes is the
|
|
26
|
+
// entire subject of this file. Measured against the default: the model answers
|
|
27
|
+
// product questions from its own knowledge and skips the tools the prompt exists
|
|
28
|
+
// to route it through, so a case run that way measures nothing it claims to.
|
|
29
|
+
|
|
30
|
+
import { withSystemPrompt } from "@alexkroman1/aai/manifest";
|
|
31
|
+
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
32
|
+
import { type EvalSession, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
33
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
34
|
+
import { expect } from "vitest";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
import authoredAgent from "./agent.ts";
|
|
37
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
38
|
+
|
|
39
|
+
/** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
|
|
40
|
+
const agentDef = withSystemPrompt(
|
|
41
|
+
withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
|
|
42
|
+
systemPrompt,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What the BROWSER is sent, as this eval reads it.
|
|
47
|
+
*
|
|
48
|
+
* Parsed rather than cast: `state.updated` carries `unknown`, so a projection
|
|
49
|
+
* that stopped matching FAILS naming the field, where the cast this replaced
|
|
50
|
+
* handed the assertions `undefined` and failed a line later on something else.
|
|
51
|
+
* It names only the fields asserted below, so `supportView` may grow without
|
|
52
|
+
* touching this.
|
|
53
|
+
*/
|
|
54
|
+
const ProjectedSupport = z.object({
|
|
55
|
+
ticket: z.string().nullable(),
|
|
56
|
+
trace: z
|
|
57
|
+
.object({
|
|
58
|
+
exhausted: z.boolean(),
|
|
59
|
+
docs: z.array(z.object({ relevant: z.boolean() })),
|
|
60
|
+
steps: z.array(z.object({ node: z.string() })),
|
|
61
|
+
})
|
|
62
|
+
.nullable(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What `answer_question` answered, validated.
|
|
67
|
+
*
|
|
68
|
+
* All-optional, and that is the shape of the CONTRACT rather than laxness: the
|
|
69
|
+
* tool has three legal outcomes — a graded answer, a withheld one carrying
|
|
70
|
+
* guidance, and a reported failure — so a required field would fail two of the
|
|
71
|
+
* three. What the schema still buys is that a field the case reads is the type
|
|
72
|
+
* the case thinks it is.
|
|
73
|
+
*/
|
|
74
|
+
const Lookup = z.object({
|
|
75
|
+
answer: z.string().nullable().optional(),
|
|
76
|
+
sources: z.array(z.string()).optional(),
|
|
77
|
+
grounded: z.boolean().optional(),
|
|
78
|
+
answersTheQuestion: z.boolean().optional(),
|
|
79
|
+
rewrites: z.number().optional(),
|
|
80
|
+
guidance: z.string().optional(),
|
|
81
|
+
error: z.string().optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Every `syncState` frame, in stream order. */
|
|
85
|
+
function frames(session: EvalSession) {
|
|
86
|
+
return session
|
|
87
|
+
.events()
|
|
88
|
+
.flatMap((event) =>
|
|
89
|
+
event.type === "state.updated" ? [ProjectedSupport.parse(event.state)] : [],
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What `answer_question` answered, in call order.
|
|
95
|
+
*
|
|
96
|
+
* `toolResultIn` over a ONE-CALL list per call: the name is that call's own, so
|
|
97
|
+
* the reader's "no such call" and "two calls" throws are unreachable and what is
|
|
98
|
+
* left is the parse, the schema, and its "never completed" failure — which is
|
|
99
|
+
* the half the hand-rolled `JSON.parse(call.result ?? "null")` this replaced got
|
|
100
|
+
* wrong, reading `null` for a tool that never came back.
|
|
101
|
+
*/
|
|
102
|
+
function lookups(session: EvalSession) {
|
|
103
|
+
return session
|
|
104
|
+
.toolCalls()
|
|
105
|
+
.filter((call) => call.name === "answer_question")
|
|
106
|
+
.map((call) => toolResultIn([call], call.name, Lookup));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describeEval(agentDef, (test) => {
|
|
110
|
+
test(
|
|
111
|
+
"nothing about the product is said without going through the knowledge base",
|
|
112
|
+
async ({ session }) => {
|
|
113
|
+
const turn = await session.say("How much notice do I have to give if I want to cancel?");
|
|
114
|
+
|
|
115
|
+
// The prompt's one rule: `answer_question` is the only route to anything
|
|
116
|
+
// about Meridian Fibre. A model that answered a notice period from its own
|
|
117
|
+
// knowledge would sound identical down a phone.
|
|
118
|
+
const asked = turn.toolCalls.filter((call) => call.name === "answer_question");
|
|
119
|
+
// The reply rides in the message, because the failure that matters here is
|
|
120
|
+
// a turn that SPOKE without looking anything up — "let me check that for
|
|
121
|
+
// you" and then nothing, or worse, a notice period from memory.
|
|
122
|
+
expect(
|
|
123
|
+
asked.length,
|
|
124
|
+
`tools called: [${turn.toolCalls.map((c) => c.name).join(", ")}]; said: ${turn.text}`,
|
|
125
|
+
).toBe(1);
|
|
126
|
+
|
|
127
|
+
for (const payload of lookups(session)) {
|
|
128
|
+
// Three legal outcomes, and the invariant that spans them: an answer the
|
|
129
|
+
// caller is given is an answer the hallucination grader passed. `grounded:
|
|
130
|
+
// false` reaching the model is the one thing the whole apparatus exists
|
|
131
|
+
// to prevent — and a broken lookup is REPORTED rather than worked around.
|
|
132
|
+
if (payload.error !== undefined) continue;
|
|
133
|
+
if (payload.answer === null) continue;
|
|
134
|
+
expect(payload.grounded, `ungrounded answer handed to the model: ${payload.answer}`).toBe(
|
|
135
|
+
true,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
stubReply: [{ tool: "answer_question", args: { question: "notice to cancel" } }, "One sec."],
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
test(
|
|
145
|
+
"a logged ticket's callback number never leaves the server",
|
|
146
|
+
async ({ session }) => {
|
|
147
|
+
// Two turns, because that is the shape the prompt asks for: the lookup comes
|
|
148
|
+
// back empty, the line offers a ticket, and only then is there a number to
|
|
149
|
+
// take. Asked for in one breath, a live model reasonably answers the offer
|
|
150
|
+
// instead of acting on it.
|
|
151
|
+
await session.say("Can I add a pay-monthly mobile SIM to my broadband account?");
|
|
152
|
+
await session.say("Yes please, log that one — my callback number is 07700 900123.");
|
|
153
|
+
|
|
154
|
+
const logged = session.toolCalls().find((call) => call.name === "log_ticket");
|
|
155
|
+
expect(
|
|
156
|
+
logged,
|
|
157
|
+
`tools called: ${session
|
|
158
|
+
.toolCalls()
|
|
159
|
+
.map((c) => c.name)
|
|
160
|
+
.join(", ")}`,
|
|
161
|
+
).toBeDefined();
|
|
162
|
+
expect(logged?.result).toMatch(/TCK\d{4}/);
|
|
163
|
+
|
|
164
|
+
const latest = frames(session).at(-1);
|
|
165
|
+
// The projection carries the REFERENCE and nothing else — that is why
|
|
166
|
+
// `supportView` exists rather than `syncState` taking the state itself.
|
|
167
|
+
expect(latest?.ticket).toMatch(/^TCK\d{4}$/);
|
|
168
|
+
for (const frame of frames(session)) {
|
|
169
|
+
expect(JSON.stringify(frame)).not.toMatch(/900123/);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
stubReply: [
|
|
174
|
+
{ tool: "answer_question", args: { question: "add a mobile SIM to my account" } },
|
|
175
|
+
"I don't have that documented. Shall I log a ticket for a callback?",
|
|
176
|
+
{
|
|
177
|
+
tool: "log_ticket",
|
|
178
|
+
args: { question: "add a mobile SIM to my account", callback: "07700 900123" },
|
|
179
|
+
},
|
|
180
|
+
"That's logged — the reference is T C K four thousand and one.",
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
test(
|
|
186
|
+
"the answer comes from the document the caller asked about, not the one next to it",
|
|
187
|
+
async ({ session }) => {
|
|
188
|
+
// Short and single, deliberately: the claim here is which document the
|
|
189
|
+
// grader passed, so the question is phrased to measure that and not the
|
|
190
|
+
// loop's pacing.
|
|
191
|
+
//
|
|
192
|
+
// It used to say the corrective path could not fit the runtime's 30s
|
|
193
|
+
// per-tool deadline. Re-measured against the live gateway, that is no
|
|
194
|
+
// longer true by an order of magnitude — the two-attempt path runs in
|
|
195
|
+
// 2.8s of nine model calls — so the phrasing above is a choice about what
|
|
196
|
+
// this case measures rather than a way around a clock. The numbers and
|
|
197
|
+
// what they do and do not license are on `LOOKUP_BUDGET_MS` in
|
|
198
|
+
// `tools/answer_question.ts`.
|
|
199
|
+
const turn = await session.say("How much notice do I have to give to cancel my contract?");
|
|
200
|
+
|
|
201
|
+
const [payload] = lookups(session);
|
|
202
|
+
expect(
|
|
203
|
+
payload,
|
|
204
|
+
`tools called: [${turn.toolCalls.map((c) => c.name).join(", ")}]; said: ${turn.text}`,
|
|
205
|
+
).toBeDefined();
|
|
206
|
+
// The whole verdict in the message: `grounded: undefined` on its own does
|
|
207
|
+
// not say whether the lookup failed, or ran and refused.
|
|
208
|
+
const verdict = JSON.stringify(payload);
|
|
209
|
+
expect(payload?.grounded, verdict).toBe(true);
|
|
210
|
+
expect(payload?.answersTheQuestion, verdict).toBe(true);
|
|
211
|
+
|
|
212
|
+
const sources = payload?.sources ?? [];
|
|
213
|
+
expect(sources).toContain("Cancelling your contract");
|
|
214
|
+
// THE claim, and the reason the knowledge base is built the way it is:
|
|
215
|
+
// "cancelling your contract" and "cancelling an engineer visit" are two
|
|
216
|
+
// documents, two fees and one word apart. A retriever with no grader hands
|
|
217
|
+
// back both, and sixty pounds gets read down the phone as the answer to a
|
|
218
|
+
// contract question.
|
|
219
|
+
expect(sources).not.toContain("Cancelling or rebooking an engineer visit");
|
|
220
|
+
// Either spelling: the claim is that the notice period came out of D5, not
|
|
221
|
+
// how the answer prompt chose to write a numeral.
|
|
222
|
+
expect(String(payload?.answer)).toMatch(/thirty days|30 days/i);
|
|
223
|
+
expect(String(payload?.answer)).not.toMatch(/sixty pound|abortive/i);
|
|
224
|
+
expect(turn.text).not.toMatch(/sixty|abortive/i);
|
|
225
|
+
},
|
|
226
|
+
// Live only: the grader's verdict IS the measurement.
|
|
227
|
+
//
|
|
228
|
+
// THE CLOCK, and it is no longer the limit it was recorded as here.
|
|
229
|
+
// `answer_question` is up to eleven sequential model calls and the tool
|
|
230
|
+
// executor abandons any tool at `TOOL_EXECUTION_TIMEOUT_MS` (30s) — a
|
|
231
|
+
// deadline neither an agent author nor an eval can raise — so this comment
|
|
232
|
+
// used to say the two-attempt corrective path did not fit, which made this
|
|
233
|
+
// case a coin toss on the gateway rather than a reading of the grader.
|
|
234
|
+
// Re-measured: the whole loop is 2.0-3.1s, the corrective path 2.8s of nine
|
|
235
|
+
// calls, and eight consecutive live runs of this suite were green. The
|
|
236
|
+
// margin belongs to the service, not to us, so the tool keeps its own
|
|
237
|
+
// budget and this case keeps a question that settles on one attempt; if the
|
|
238
|
+
// gateway slows down again the failure says which, because a spent budget
|
|
239
|
+
// comes back as the graph's own exit sentence rather than as a timeout.
|
|
240
|
+
{ live: true },
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
test(
|
|
244
|
+
"a question nothing covers is refused, after the corrective edge has tried",
|
|
245
|
+
async ({ session }) => {
|
|
246
|
+
const turn = await session.say(
|
|
247
|
+
"Can I add a pay-monthly mobile SIM with unlimited data to my broadband account?",
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const [payload] = lookups(session);
|
|
251
|
+
expect(
|
|
252
|
+
payload,
|
|
253
|
+
`tools called: [${turn.toolCalls.map((c) => c.name).join(", ")}]; said: ${turn.text}`,
|
|
254
|
+
).toBeDefined();
|
|
255
|
+
// Withheld, not softened: `answer: null` is the tool refusing to hand the
|
|
256
|
+
// model something to read out, and the guidance is the exit the grading
|
|
257
|
+
// apparatus needs — a support line that can only answer will answer wrong.
|
|
258
|
+
// A lookup that BROKE and one that ran and found nothing are different
|
|
259
|
+
// findings, and neither is readable from "answer is not null".
|
|
260
|
+
expect(payload?.answer, JSON.stringify(payload)).toBeNull();
|
|
261
|
+
expect(String(payload?.guidance)).toMatch(/log_ticket/);
|
|
262
|
+
// The corrective edge really ran: nothing relevant means the RETRIEVAL was
|
|
263
|
+
// wrong, so the query is rewritten and retried before the loop gives up.
|
|
264
|
+
expect(payload?.rewrites).toBeGreaterThanOrEqual(1);
|
|
265
|
+
|
|
266
|
+
const trace = frames(session).at(-1)?.trace;
|
|
267
|
+
expect(trace?.exhausted).toBe(true);
|
|
268
|
+
expect(trace?.docs.every((doc) => !doc.relevant)).toBe(true);
|
|
269
|
+
expect(trace?.steps.map((step) => step.node)).toContain("transform_query");
|
|
270
|
+
},
|
|
271
|
+
{ live: true },
|
|
272
|
+
);
|
|
273
|
+
});
|