@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
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { errorMessage, tool, toolFailure } from "@alexkroman1/aai";
|
|
1
|
+
import { errorMessage, ProcedureNotFinishedError, tool, toolFailure } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { runCorrectiveRag } from "../
|
|
3
|
+
import { runCorrectiveRag } from "../procedure.ts";
|
|
4
4
|
import { recordQuestion, supportSlot } from "../shared.ts";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* How long the whole corrective loop may run before it gives up.
|
|
8
|
+
*
|
|
9
|
+
* Under the runtime's own per-tool deadline (`TOOL_EXECUTION_TIMEOUT_MS`, 30s)
|
|
10
|
+
* with room for this tool's own bookkeeping, and written as a literal because
|
|
11
|
+
* that constant is on `@alexkroman1/aai/internal` — a subpath a template may
|
|
12
|
+
* not import. If the runtime's default ever moves, this is the number to move.
|
|
13
|
+
*/
|
|
14
|
+
const LOOKUP_BUDGET_MS = 28_000;
|
|
15
|
+
|
|
6
16
|
/**
|
|
7
17
|
* The whole graph, as one tool.
|
|
8
18
|
*
|
|
@@ -14,6 +24,33 @@ import { recordQuestion, supportSlot } from "../shared.ts";
|
|
|
14
24
|
* The verdicts ride back with the answer rather than being swallowed. A
|
|
15
25
|
* grounded-but-not-useful answer is still worth speaking, and the caller is the
|
|
16
26
|
* one who should hear that it may not be the whole story.
|
|
27
|
+
*
|
|
28
|
+
* **The loop is given a BUDGET, because the runtime already has one and it is
|
|
29
|
+
* not this tool's.** The corrective path is up to eleven sequential model calls
|
|
30
|
+
* — two attempts of grade/generate/ground/use, plus a regeneration — and the
|
|
31
|
+
* tool executor abandons any tool at `TOOL_EXECUTION_TIMEOUT_MS` (30s), which
|
|
32
|
+
* an author cannot raise. Measured against a live gateway, the two-attempt path
|
|
33
|
+
* really does exceed it: the model was handed
|
|
34
|
+
* `{"error":"Tool \"answer_question\" timed out after 30000ms"}`, which tells
|
|
35
|
+
* it nothing it can act on and reads to a caller as the line going dead.
|
|
36
|
+
* `LOOKUP_BUDGET_MS` lands the overrun on the graph's OWN exit instead — the
|
|
37
|
+
* same "offer to look again, or log a ticket" the design already has for an
|
|
38
|
+
* interrupted run — so the slow path degrades into a sentence rather than into
|
|
39
|
+
* an internal error. It does not make the loop faster; it makes running out
|
|
40
|
+
* something the caller hears about.
|
|
41
|
+
*
|
|
42
|
+
* **Re-measured, and the loop is nowhere near the budget today.** Driven
|
|
43
|
+
* directly against the live gateway on the default model: the single-attempt
|
|
44
|
+
* path is 2.0-3.1s over four or five model calls, and the two-attempt
|
|
45
|
+
* CORRECTIVE path — nine calls, one query rewrite — is 2.8s, with each call
|
|
46
|
+
* 0.3-1.3s. A schema-constrained call is not the expensive kind either (572ms
|
|
47
|
+
* against 553ms for the same prompt without one), which is the guess anyone
|
|
48
|
+
* looking at this would make first. So the budget is a GUARD against a slow
|
|
49
|
+
* gateway rather than a bound the healthy path pushes against, and a question
|
|
50
|
+
* that trips a rewrite fits with room to spare. Keep it: the measurement above
|
|
51
|
+
* happened, and what changed is the service's latency, not the number of round
|
|
52
|
+
* trips. Do not go cutting round trips out of the graph on the strength of the
|
|
53
|
+
* old paragraph — measure first, the same way.
|
|
17
54
|
*/
|
|
18
55
|
export default tool({
|
|
19
56
|
description:
|
|
@@ -26,9 +63,34 @@ export default tool({
|
|
|
26
63
|
async execute(args, ctx) {
|
|
27
64
|
let trace: Awaited<ReturnType<typeof runCorrectiveRag>>;
|
|
28
65
|
try {
|
|
29
|
-
|
|
66
|
+
// `ctx.signal` is what stops the graph on a barge-in: this loop is five
|
|
67
|
+
// to nine model calls, and a caller who interrupts on the second should
|
|
68
|
+
// not be charged for the rest. The budget rides alongside it — see the
|
|
69
|
+
// module doc on `LOOKUP_BUDGET_MS` — and `AbortSignal.any` is what
|
|
70
|
+
// combines the two without any unlink bookkeeping.
|
|
71
|
+
trace = await runCorrectiveRag(
|
|
72
|
+
ctx.generate,
|
|
73
|
+
args.question,
|
|
74
|
+
AbortSignal.any([ctx.signal, AbortSignal.timeout(LOOKUP_BUDGET_MS)]),
|
|
75
|
+
);
|
|
30
76
|
} catch (err: unknown) {
|
|
31
|
-
//
|
|
77
|
+
// An INTERRUPTED lookup is not a broken one, and the difference is worth
|
|
78
|
+
// a sentence: telling the model the knowledge base failed would have it
|
|
79
|
+
// apologize for an outage that did not happen. Which of the two signals
|
|
80
|
+
// fired decides WHICH sentence — a barge-in means the caller is already
|
|
81
|
+
// talking, and a spent budget means they are still waiting.
|
|
82
|
+
if (err instanceof ProcedureNotFinishedError) {
|
|
83
|
+
return toolFailure(
|
|
84
|
+
ctx.signal.aborted
|
|
85
|
+
? "That lookup was cut short before it finished. Offer to look again, " +
|
|
86
|
+
"or to log a ticket with log_ticket."
|
|
87
|
+
: "That lookup ran out of time before it could be graded, so there is no " +
|
|
88
|
+
"answer to give. Say the check is taking too long, offer a narrower " +
|
|
89
|
+
"question, and offer to log a ticket with log_ticket. Do not answer " +
|
|
90
|
+
"from your own knowledge.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
// A broken model call IS the tool's to report: the model can tell the
|
|
32
94
|
// caller the lookup failed, which is a better turn than silence.
|
|
33
95
|
return toolFailure(`The knowledge base lookup failed: ${errorMessage(err)}`);
|
|
34
96
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
// An EVAL for a WORKFLOW APP: does the run actually do the work? Run it with
|
|
3
|
+
// `aai eval`.
|
|
4
|
+
//
|
|
5
|
+
// `agent.test.ts` asserts about the declaration, drives the four steps one at a
|
|
6
|
+
// time, and covers the WAV arithmetic as pure functions. This drives the WHOLE
|
|
7
|
+
// BODY — `transcribeFlow` from the top — and what it is here to check is the one
|
|
8
|
+
// thing no per-step spec can see: that a recording is really planned, really
|
|
9
|
+
// fanned out, and really stitched back into one transcript in the right order.
|
|
10
|
+
//
|
|
11
|
+
// `describeWorkflowEval` picks the providers for you and says which it picked:
|
|
12
|
+
//
|
|
13
|
+
// * with `ASSEMBLYAI_API_KEY` — a LIVE run. The `{ live: true }` case below
|
|
14
|
+
// downloads a real four-minute news clip, converts it with a real ffmpeg,
|
|
15
|
+
// and puts four real requests through the sync endpoint. That spends money
|
|
16
|
+
// and about a minute.
|
|
17
|
+
// * without one — a SCRIPTED run: the same body, the same plan, the same
|
|
18
|
+
// stitch, with the endpoint answered in memory.
|
|
19
|
+
//
|
|
20
|
+
// Two of the three cases are SCRIPTED IN BOTH MODES, deliberately. Their claims
|
|
21
|
+
// are about the PLAN and the SEAM — which window each request got, and what the
|
|
22
|
+
// merge does where two segments overlap — and those are facts about arithmetic
|
|
23
|
+
// that a live provider can neither confirm nor deny. The live case is the one
|
|
24
|
+
// that answers "is the transcript really of the recording".
|
|
25
|
+
//
|
|
26
|
+
// WHAT NO EVAL HERE COVERS: durability. Imported through vitest with no bundler
|
|
27
|
+
// in the path, a `"use workflow"` body is an ordinary async function — no
|
|
28
|
+
// journal, no replay, and no per-step retry, so the resume-after-segment-27
|
|
29
|
+
// property this template exists to demonstrate is NOT exercised here, and a
|
|
30
|
+
// rate-limited live run fails where a deployed one would have ridden it out.
|
|
31
|
+
// `aai-cli`'s `dev-workflow.scenario.test.ts` is the tier that really resumes a
|
|
32
|
+
// run.
|
|
33
|
+
import { encodeWav } from "@alexkroman1/aai/step";
|
|
34
|
+
import { installStubTranscribe, installStubUploads } from "@alexkroman1/aai/testing/vitest";
|
|
35
|
+
import { describeWorkflowEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
36
|
+
import { expect } from "vitest";
|
|
37
|
+
import agentDef, { transcribe } from "./agent.ts";
|
|
38
|
+
import { TRANSCRIPT_STREAM } from "./workflows/stitch.ts";
|
|
39
|
+
import { SEGMENT_SECONDS } from "./workflows/wav.ts";
|
|
40
|
+
|
|
41
|
+
/** The id every case uploads under. */
|
|
42
|
+
const UPLOAD_ID = "upl_eval";
|
|
43
|
+
|
|
44
|
+
/** 16 kHz mono 16-bit — the format `normalize.ts` converts everything TO. */
|
|
45
|
+
const MONO_16K = { sampleRate: 16_000, channels: 1, bitsPerSample: 16 } as const;
|
|
46
|
+
/** Bytes one second of that format occupies. */
|
|
47
|
+
const BYTES_PER_SECOND = MONO_16K.sampleRate * MONO_16K.channels * (MONO_16K.bitsPerSample / 8);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Long enough to force a THREE-segment fan-out, which is the shape worth
|
|
51
|
+
* driving: one segment exercises no plan, two exercise one seam, three exercise
|
|
52
|
+
* a middle segment that is neither first nor last.
|
|
53
|
+
*/
|
|
54
|
+
const SCRIPTED_SECONDS = 200;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A real, parseable, linear-PCM WAV of silence.
|
|
58
|
+
*
|
|
59
|
+
* Silence is fine here because the scripted cases never send it anywhere: what
|
|
60
|
+
* they assert is which BYTE RANGE each request was handed, and the bytes only
|
|
61
|
+
* have to be as long as the header says. It also takes the fast path through
|
|
62
|
+
* `normalizeRecording` — `parseWav` accepts it, so no ffmpeg is involved, which
|
|
63
|
+
* is what keeps these two cases runnable with no binary on `PATH`.
|
|
64
|
+
*/
|
|
65
|
+
const SCRIPTED_WAV = encodeWav(new Uint8Array(SCRIPTED_SECONDS * BYTES_PER_SECOND), MONO_16K);
|
|
66
|
+
|
|
67
|
+
/** The public sample recording — four minutes of real speech, on a real CDN. */
|
|
68
|
+
const LIVE_RECORDING = "https://assembly.ai/wildfires.mp3";
|
|
69
|
+
|
|
70
|
+
/** Publish one in-memory upload store, writable because the converter needs one. */
|
|
71
|
+
function publish(bytes: Uint8Array, name: string, type: string) {
|
|
72
|
+
// `writable: true` is not optional for this template: `normalizeRecording`
|
|
73
|
+
// writes the CONVERTED file back as a new upload, and a read-only store would
|
|
74
|
+
// fail that step by name — which is the store telling the truth, and not what
|
|
75
|
+
// these cases are about.
|
|
76
|
+
return installStubUploads({ [UPLOAD_ID]: { bytes, name, type } }, { writable: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Does this request body carry a WAV header?
|
|
81
|
+
*
|
|
82
|
+
* Scanned as BYTES rather than decoded: the body is a multipart envelope around
|
|
83
|
+
* megabytes of audio, so `String(bytes)` is a comma-joined number list — which
|
|
84
|
+
* makes the assertion pass or fail for the wrong reason and prints five
|
|
85
|
+
* megabytes of it when it fails.
|
|
86
|
+
*/
|
|
87
|
+
function carriesWavHeader(body: Uint8Array | string | undefined): boolean {
|
|
88
|
+
if (!(body instanceof Uint8Array)) return false;
|
|
89
|
+
const riff = [0x52, 0x49, 0x46, 0x46];
|
|
90
|
+
for (let at = 0; at <= body.length - riff.length; at++) {
|
|
91
|
+
if (riff.every((byte, offset) => body[at + offset] === byte)) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The transcript chunks the run streamed, in the order they landed. */
|
|
97
|
+
function streamed(run: { emitted: readonly { namespace: string; chunk: unknown }[] }) {
|
|
98
|
+
return run.emitted
|
|
99
|
+
.filter((one) => one.namespace === TRANSCRIPT_STREAM)
|
|
100
|
+
.map((one) => one.chunk as { index: number; startMs: number; endMs: number; text: string });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describeWorkflowEval(agentDef, (test) => {
|
|
104
|
+
test("plans the fan-out from the header and gives each request its own window", async ({
|
|
105
|
+
app,
|
|
106
|
+
}) => {
|
|
107
|
+
// Scripted in both modes: the claim is which WINDOW each of the three
|
|
108
|
+
// requests was handed, and a live endpoint cannot answer that — it can only
|
|
109
|
+
// transcribe whatever it is sent. This is the case that catches an
|
|
110
|
+
// off-by-one in the plan, which otherwise produces audio the decoder
|
|
111
|
+
// transcribes into confident nonsense.
|
|
112
|
+
publish(SCRIPTED_WAV, "standup.wav", "audio/wav");
|
|
113
|
+
const provider = installStubTranscribe({
|
|
114
|
+
text: ["the first stretch", "the middle stretch", "the last stretch"],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const run = await app.run(transcribe, { recording: UPLOAD_ID });
|
|
118
|
+
|
|
119
|
+
expect(run.error).toBeUndefined();
|
|
120
|
+
expect(run.status).toBe("completed");
|
|
121
|
+
const output = run.output;
|
|
122
|
+
if (output === undefined) expect.fail("a completed run must carry an output");
|
|
123
|
+
|
|
124
|
+
// Three segments over 200 seconds at a 90-second stride, and the durations
|
|
125
|
+
// are the plan: each segment carries the 2-second OVERLAP that stops a cut
|
|
126
|
+
// landing mid-word, so they deliberately do not abut.
|
|
127
|
+
expect(output.segments).toBe(3);
|
|
128
|
+
expect(output.durationMs).toBe(SCRIPTED_SECONDS * 1000);
|
|
129
|
+
expect(streamed(run).map((chunk) => [chunk.index, chunk.startMs, chunk.endMs])).toEqual([
|
|
130
|
+
[0, 0, (SEGMENT_SECONDS + 2) * 1000],
|
|
131
|
+
[1, SEGMENT_SECONDS * 1000, (2 * SEGMENT_SECONDS + 2) * 1000],
|
|
132
|
+
[2, 2 * SEGMENT_SECONDS * 1000, SCRIPTED_SECONDS * 1000],
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// One sync request per segment, each carrying a WHOLE WAV — the endpoint
|
|
136
|
+
// decodes every request independently, so a window with no header put back
|
|
137
|
+
// on it is bytes it refuses.
|
|
138
|
+
const sync = provider.calls.filter((call) => call.leg === "sync");
|
|
139
|
+
expect(sync).toHaveLength(3);
|
|
140
|
+
expect(sync.map((call) => carriesWavHeader(call.body))).toEqual([true, true, true]);
|
|
141
|
+
// The first two segments are a full stride plus the overlap; the last is the
|
|
142
|
+
// remainder, and is therefore much smaller. A plan that ran off the end of
|
|
143
|
+
// the file would show up here as a last segment the size of the others.
|
|
144
|
+
const sizes = sync.map((call) => call.body?.length ?? 0);
|
|
145
|
+
expect(sizes[0]).toBeGreaterThan((SEGMENT_SECONDS + 1) * BYTES_PER_SECOND);
|
|
146
|
+
expect(sizes[2]).toBeLessThan(sizes[0] ?? 0);
|
|
147
|
+
|
|
148
|
+
// Stitched in INDEX order, not completion order.
|
|
149
|
+
expect(output.transcript).toBe("the first stretch the middle stretch the last stretch");
|
|
150
|
+
// Reported before the fan-out, which is what makes a long run legible.
|
|
151
|
+
expect(run.reported).toContain("Split 3:20 of audio into 3 segments.");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("drops the repeated words where two segments overlap", async ({ app }) => {
|
|
155
|
+
// Scripted in both modes for the same reason: the overlap exists so a cut
|
|
156
|
+
// never lands mid-word, and what makes that free is the merge dropping ONE
|
|
157
|
+
// copy of the repeated run. A live provider cannot be asked to produce a
|
|
158
|
+
// seam on demand, and this is where a regression would be invisible — a
|
|
159
|
+
// missed seam repeats a few words, a false one DELETES speech.
|
|
160
|
+
publish(SCRIPTED_WAV, "standup.wav", "audio/wav");
|
|
161
|
+
installStubTranscribe({
|
|
162
|
+
text: [
|
|
163
|
+
"the fire moved north through the valley overnight",
|
|
164
|
+
"through the valley overnight and reached the ridge by dawn",
|
|
165
|
+
"by dawn the crews had contained it",
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const run = await app.run(transcribe, { recording: UPLOAD_ID });
|
|
170
|
+
|
|
171
|
+
expect(run.error).toBeUndefined();
|
|
172
|
+
const transcript = run.output?.transcript ?? "";
|
|
173
|
+
expect(transcript).toBe(
|
|
174
|
+
"the fire moved north through the valley overnight and reached the ridge by dawn the crews had contained it",
|
|
175
|
+
);
|
|
176
|
+
// The repeated runs appear once each, which is the whole claim.
|
|
177
|
+
expect(transcript.match(/overnight/g)).toHaveLength(1);
|
|
178
|
+
expect(transcript.match(/by dawn/g)).toHaveLength(1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test(
|
|
182
|
+
"really transcribes a real recording, end to end",
|
|
183
|
+
async ({ app }) => {
|
|
184
|
+
// LIVE ONLY, and it is the case that earns the template its name. Three
|
|
185
|
+
// separate things have to be real for it to pass: ffmpeg has to convert an
|
|
186
|
+
// MP3 into something the plan can cut, the plan has to cover the whole
|
|
187
|
+
// recording, and every segment's request has to come back with the words
|
|
188
|
+
// that are actually in it.
|
|
189
|
+
//
|
|
190
|
+
// It needs `ffmpeg` on `PATH` (or `AAI_FFMPEG_PATH`), which is what the
|
|
191
|
+
// template's own doc says a developer needs for anything that is not
|
|
192
|
+
// already a WAV. A deployed guest's image installs it.
|
|
193
|
+
const response = await fetch(LIVE_RECORDING);
|
|
194
|
+
expect(response.ok).toBe(true);
|
|
195
|
+
const mp3 = new Uint8Array(await response.arrayBuffer());
|
|
196
|
+
publish(mp3, "wildfires.mp3", "audio/mpeg");
|
|
197
|
+
|
|
198
|
+
const run = await app.run(transcribe, { recording: UPLOAD_ID });
|
|
199
|
+
|
|
200
|
+
expect(run.error).toBeUndefined();
|
|
201
|
+
expect(run.status).toBe("completed");
|
|
202
|
+
const output = run.output;
|
|
203
|
+
if (output === undefined) expect.fail("a completed run must carry an output");
|
|
204
|
+
|
|
205
|
+
// ffmpeg ran, and the run said so before spending minutes on it.
|
|
206
|
+
expect(run.reported.some((line) => line.startsWith("Converting wildfires.mp3"))).toBe(true);
|
|
207
|
+
// The FILENAME a reader sees is the one they uploaded, not the converted
|
|
208
|
+
// artifact's — `mergeTranscript` reads the ORIGINAL id for exactly this.
|
|
209
|
+
expect(output.source).toBe("wildfires.mp3");
|
|
210
|
+
|
|
211
|
+
// Four and a half minutes, so the plan really fanned out rather than
|
|
212
|
+
// sending one request.
|
|
213
|
+
expect(output.segments).toBeGreaterThanOrEqual(3);
|
|
214
|
+
expect(output.durationMs).toBeGreaterThan(250_000);
|
|
215
|
+
|
|
216
|
+
// Every segment came back with words in it, which is how "the whole
|
|
217
|
+
// recording was covered" is checked rather than assumed: a plan that ran
|
|
218
|
+
// past the end would leave a silent tail segment here.
|
|
219
|
+
const chunks = streamed(run);
|
|
220
|
+
expect(chunks).toHaveLength(output.segments);
|
|
221
|
+
for (const chunk of chunks) expect(chunk.text.length).toBeGreaterThan(0);
|
|
222
|
+
|
|
223
|
+
// And the transcript is of THIS recording — a news segment about smoke
|
|
224
|
+
// from Canadian wildfires reaching the US east coast.
|
|
225
|
+
expect(output.transcript).toMatch(/wildfire/i);
|
|
226
|
+
expect(output.transcript).toMatch(/canada/i);
|
|
227
|
+
expect(output.transcript).toMatch(/air quality/i);
|
|
228
|
+
// ~4.5 minutes of speech is several hundred words; a stitch that kept only
|
|
229
|
+
// the first segment would land far under this.
|
|
230
|
+
expect(output.words).toBeGreaterThan(400);
|
|
231
|
+
},
|
|
232
|
+
{ live: true },
|
|
233
|
+
);
|
|
234
|
+
});
|