@alexkroman1/aai-cli 13.2.0 → 14.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/bin.mjs +1 -1
- package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
- package/dist/_config.d.ts +25 -1
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +15 -7
- package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
- package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
- package/dist/_output.d.ts +24 -7
- package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
- package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
- package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
- package/dist/_utils.d.ts +35 -0
- package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
- package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-Mxk8gWvX.mjs +108 -0
- package/dist/build.d.ts +5 -0
- package/dist/cli.mjs +172 -116
- package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
- package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
- package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
- package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
- package/dist/eval.d.ts +2 -0
- package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
- package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
- package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/.env.example +17 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +6 -5
- package/dist/scaffold/server.mjs +129 -25
- package/dist/secret-Cjp912yF.mjs +162 -0
- package/dist/secret.d.ts +48 -13
- package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
- package/dist/templates/briefing-desk/agent.test.ts +7 -3
- package/dist/templates/call-audit/agent.test.ts +25 -9
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +40 -58
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/embedded-assets/agent.test.ts +7 -1
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.test.ts +8 -4
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +10 -4
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +42 -36
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/agent.test.ts +6 -1
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +33 -16
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +33 -14
- package/dist/templates/podcast-digest/client.tsx +27 -36
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +49 -32
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +8 -2
- package/dist/templates/redline/client.tsx +47 -48
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +17 -8
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/registry.test.ts +99 -58
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/simple/agent.test.ts +39 -6
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +21 -12
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +36 -47
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +36 -20
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +34 -40
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/test-D032V2cA.mjs +242 -0
- package/dist/test.d.ts +97 -31
- package/dist/typecheck.mjs +2 -2
- package/dist/worker-bundler.mjs +3 -3
- package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
- package/package.json +10 -10
- package/dist/bin.d.mts +0 -2
- package/dist/build-BLlLOrng.mjs +0 -66
- package/dist/secret-B-3T5tvr.mjs +0 -61
- package/dist/test-CiLab-AA.mjs +0 -182
- package/dist/tsdown.config.d.ts +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolContext, ToolDef, ToolInputSchema } from "@alexkroman1/aai";
|
|
2
2
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
3
|
-
import { createToolContext,
|
|
3
|
+
import { createToolContext, expectToolOk } from "@alexkroman1/aai/testing";
|
|
4
4
|
import { describe, expect, test, vi } from "vitest";
|
|
5
5
|
import {
|
|
6
6
|
applyConsequences,
|
|
@@ -24,11 +24,11 @@ import updateState from "./tools/update_state.ts";
|
|
|
24
24
|
|
|
25
25
|
// ── Test doubles ─────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
/** Each call gets its own slot store, which is what makes two contexts two
|
|
28
|
+
* games. `send` is the recorder `createToolContext` installs — no test here
|
|
29
|
+
* asserts on it, and overriding it would only throw the recording away. */
|
|
30
30
|
function makeCtx(): ToolContext {
|
|
31
|
-
return createToolContext(
|
|
31
|
+
return createToolContext();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const SETUP_ARGS = {
|
|
@@ -288,7 +288,7 @@ describe("burn_momentum", () => {
|
|
|
288
288
|
const ctx = makeCtx();
|
|
289
289
|
seedRolledState(8, ctx); // 8 beats both dice (3, 5)
|
|
290
290
|
|
|
291
|
-
const result =
|
|
291
|
+
const result = expectToolOk<Record<string, unknown>>(await callNoArgs(burnMomentum, ctx));
|
|
292
292
|
expect(result.burned).toBe(true);
|
|
293
293
|
expect(result.newResultCode).toBe("STRONG_HIT");
|
|
294
294
|
|
|
@@ -302,7 +302,7 @@ describe("burn_momentum", () => {
|
|
|
302
302
|
test("momentum beating only one die upgrades a MISS to WEAK_HIT", async () => {
|
|
303
303
|
const ctx = makeCtx();
|
|
304
304
|
seedRolledState(4, ctx); // beats 3, not 5
|
|
305
|
-
const result =
|
|
305
|
+
const result = expectToolOk<{ newResultCode: string }>(await callNoArgs(burnMomentum, ctx));
|
|
306
306
|
expect(result.newResultCode).toBe("WEAK_HIT");
|
|
307
307
|
});
|
|
308
308
|
|
|
@@ -528,7 +528,7 @@ describe("update_state", () => {
|
|
|
528
528
|
state.clocks[0]!.filled = 3; // 3 of 4
|
|
529
529
|
seedPlaying(ctx, state);
|
|
530
530
|
|
|
531
|
-
const result =
|
|
531
|
+
const result = expectToolOk<{ clockEvents: { clock: string; trigger: string }[] }>(
|
|
532
532
|
await updateState.execute({ advanceClockName: "Doom" }, ctx),
|
|
533
533
|
);
|
|
534
534
|
expect(result.clockEvents).toEqual([{ clock: "Doom", trigger: "The doom arrives" }]);
|
|
@@ -542,7 +542,7 @@ describe("update_state", () => {
|
|
|
542
542
|
}
|
|
543
543
|
seedPlaying(ctx, state);
|
|
544
544
|
|
|
545
|
-
const result =
|
|
545
|
+
const result = expectToolOk<{ warnings?: string[] }>(
|
|
546
546
|
await updateState.execute({ addNpcName: "One Too Many" }, ctx),
|
|
547
547
|
);
|
|
548
548
|
expect(result.warnings?.[0]).toMatch(/NPC limit/);
|
|
@@ -609,7 +609,7 @@ describe("the story flow", () => {
|
|
|
609
609
|
expect(created.state).toBe("playing.awaitingRoll");
|
|
610
610
|
expect(created.instruction).toMatch(/action_roll/);
|
|
611
611
|
|
|
612
|
-
|
|
612
|
+
expectToolOk(
|
|
613
613
|
await actionRoll.execute(
|
|
614
614
|
{ move: "clash", stat: "iron", position: "risky", effect: "standard", purpose: "swing" },
|
|
615
615
|
ctx,
|
|
@@ -618,7 +618,7 @@ describe("the story flow", () => {
|
|
|
618
618
|
expect(storyFlow.position(ctx).state).toBe("playing.rollResolved");
|
|
619
619
|
|
|
620
620
|
// Moving the scene on SPENDS the roll: the burn window is closed.
|
|
621
|
-
|
|
621
|
+
expectToolOk(await updateState.execute({ location: "The Bridge" }, ctx));
|
|
622
622
|
expect(storyFlow.position(ctx).state).toBe("playing.awaitingRoll");
|
|
623
623
|
expect(isToolFailure(await callNoArgs(burnMomentum, ctx))).toBe(true);
|
|
624
624
|
});
|
|
@@ -644,7 +644,7 @@ describe("the story flow", () => {
|
|
|
644
644
|
// act on it, so a player could keep rolling after both tracks emptied. The
|
|
645
645
|
// WRITE is `gameSlot`'s `after` hook; this tool no longer calls it, which
|
|
646
646
|
// is the point of moving it there.
|
|
647
|
-
|
|
647
|
+
expectToolOk(await updateState.execute({ health: 0, spirit: 0 }, ctx));
|
|
648
648
|
const at = storyFlow.position(ctx);
|
|
649
649
|
expect(at.state).toBe("gameOver");
|
|
650
650
|
expect(at.done).toBe(true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
2
|
import type { DeepReadonly } from "@alexkroman1/aai";
|
|
3
|
-
import {
|
|
3
|
+
import { mountClient, useAgentState } from "@alexkroman1/aai-ui";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import {
|
|
6
6
|
type Clock,
|
|
@@ -782,7 +782,7 @@ function Sidebar() {
|
|
|
782
782
|
|
|
783
783
|
// ── App ──────────────────────────────────────────────────────────────────────
|
|
784
784
|
|
|
785
|
-
|
|
785
|
+
mountClient({
|
|
786
786
|
name: "Solo RPG",
|
|
787
787
|
icon: (
|
|
788
788
|
<span style={{ fontSize: "28px", color: C.accent }}>
|
|
@@ -805,7 +805,7 @@ client({
|
|
|
805
805
|
});
|
|
806
806
|
|
|
807
807
|
// No `onSessionId`/`resumeSessionId` here any more, and their removal is a FIX
|
|
808
|
-
// rather than a simplification. `
|
|
808
|
+
// rather than a simplification. `createBrowserSession` remembers the id in
|
|
809
809
|
// `sessionStorage` by default now — see `session-resume-store.ts`, whose doc
|
|
810
810
|
// names this template as the one client of fourteen that had wired it by hand.
|
|
811
811
|
// What it had wired was `localStorage`, which is the wrong store for a pointer
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* its return value, and audio in one is megabytes replayed on every resume.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { stepReadUpload, stepUploadInfo } from "@alexkroman1/aai/step";
|
|
21
21
|
import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
|
|
22
|
-
import {
|
|
22
|
+
import { createWorkflowContext, stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
23
23
|
import {
|
|
24
24
|
installStubGateway,
|
|
25
25
|
installStubReporter,
|
|
@@ -55,13 +55,22 @@ describe("the declaration", () => {
|
|
|
55
55
|
test("is a workflow app with the one workflow the page starts by name", () => {
|
|
56
56
|
// The page calls `api.start("spokenSummary", …)`, so a rename here is a
|
|
57
57
|
// runtime 400 rather than a compile error. This is what pins it.
|
|
58
|
-
|
|
58
|
+
// `toContain` rather than an exact key list: adding a second workflow is an
|
|
59
|
+
// invited edit and must not redden a test the author did not write. The
|
|
60
|
+
// NAME is still pinned, deliberately — the page starts a run by this
|
|
61
|
+
// string, so renaming the key is a runtime 400 rather than a compile
|
|
62
|
+
// error, and this pin is the only thing that says so. Rename it here and
|
|
63
|
+
// in `client.tsx` together.
|
|
64
|
+
expect(Object.keys(agentDef.workflows ?? {})).toContain("spokenSummary");
|
|
65
|
+
expect(agentDef.workflows?.spokenSummary).toBe(spokenSummary);
|
|
59
66
|
});
|
|
60
67
|
|
|
61
|
-
test("declares
|
|
68
|
+
test("declares the credential its steps read, so a deploy checks for it", () => {
|
|
62
69
|
// A workflow app has no session, so nothing else in its config could name
|
|
63
70
|
// one — and one AssemblyAI key covers transcription, the model and the voice.
|
|
64
|
-
|
|
71
|
+
// `toContain`, so adding a step that reads a second credential and
|
|
72
|
+
// declaring it here does not fail this test.
|
|
73
|
+
expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
|
|
65
74
|
});
|
|
66
75
|
|
|
67
76
|
test("takes the recording as an UPLOAD, which is what makes the form a file picker", () => {
|
|
@@ -206,14 +215,14 @@ describe("speaking", () => {
|
|
|
206
215
|
|
|
207
216
|
const { audio } = await speak("Hello.");
|
|
208
217
|
|
|
209
|
-
await expect(
|
|
218
|
+
await expect(stepUploadInfo(audio)).resolves.toMatchObject({
|
|
210
219
|
name: "summary.wav",
|
|
211
220
|
// The byte route serves this as `Content-Type`, and a browser will not
|
|
212
221
|
// play inline a file it was handed as octet-stream.
|
|
213
222
|
type: "audio/wav",
|
|
214
223
|
size: 44 + 4000,
|
|
215
224
|
});
|
|
216
|
-
const { bytes } = await
|
|
225
|
+
const { bytes } = await stepReadUpload(audio, { end: 12 });
|
|
217
226
|
expect(String.fromCharCode(...bytes.subarray(0, 4))).toBe("RIFF");
|
|
218
227
|
expect(String.fromCharCode(...bytes.subarray(8, 12))).toBe("WAVE");
|
|
219
228
|
});
|
|
@@ -283,7 +292,7 @@ describe("the whole run", () => {
|
|
|
283
292
|
installStubReporter();
|
|
284
293
|
installStubSpeech();
|
|
285
294
|
|
|
286
|
-
const summary = await spokenSummaryFlow({ recording: UPLOAD_ID },
|
|
295
|
+
const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext());
|
|
287
296
|
|
|
288
297
|
expect(summary).toEqual({
|
|
289
298
|
source: "standup.wav",
|
|
@@ -305,7 +314,7 @@ describe("the whole run", () => {
|
|
|
305
314
|
installStubReporter();
|
|
306
315
|
const speech = installStubSpeech();
|
|
307
316
|
|
|
308
|
-
await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" },
|
|
317
|
+
await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowContext());
|
|
309
318
|
|
|
310
319
|
expect(speech.calls[0]).toMatchObject({ text: "Spoken.", voice: "michael" });
|
|
311
320
|
});
|
|
@@ -315,9 +324,9 @@ describe("the whole run", () => {
|
|
|
315
324
|
installStubReporter();
|
|
316
325
|
installStubSpeech();
|
|
317
326
|
|
|
318
|
-
await expect(
|
|
319
|
-
|
|
320
|
-
);
|
|
327
|
+
await expect(
|
|
328
|
+
spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext()),
|
|
329
|
+
).rejects.toThrow("corrupt audio");
|
|
321
330
|
});
|
|
322
331
|
});
|
|
323
332
|
|
|
@@ -24,7 +24,7 @@
|
|
|
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
|
|
26
26
|
* a VALUE.
|
|
27
|
-
* - **`
|
|
27
|
+
* - **`stepWriteUpload`** (same subpath) puts that value where a browser can reach
|
|
28
28
|
* it. A run's OUTPUT is read back as JSON, so audio cannot travel in one —
|
|
29
29
|
* the same rule that keeps a recording's bytes out of a run's INPUT, arriving
|
|
30
30
|
* at the other end of the run.
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* `POST /workflows/uploads` (the browser does this for you: `uploads` below is
|
|
55
55
|
* what makes `<WorkflowFields>` render a file picker, and `useWorkflowSubmit`
|
|
56
56
|
* stores the file before starting the run), the input carries the returned id,
|
|
57
|
-
* and the step that needs the bytes streams them out with `
|
|
57
|
+
* and the step that needs the bytes streams them out with `stepReadUpload`.
|
|
58
58
|
*
|
|
59
59
|
* ## It is scriptable, which is the other half of having an API
|
|
60
60
|
*
|
|
@@ -140,7 +140,7 @@ export const spokenSummary: WorkflowDef<typeof spokenSummaryInput, SpokenSummary
|
|
|
140
140
|
input: spokenSummaryInput,
|
|
141
141
|
// The one line that makes the form take a file: `<WorkflowFields>` renders a
|
|
142
142
|
// picker for this property, `useWorkflowSubmit` stores the chosen file, and
|
|
143
|
-
// the step that transcribes it reads it back with `
|
|
143
|
+
// the step that transcribes it reads it back with `stepReadUpload`.
|
|
144
144
|
uploads: ["recording"],
|
|
145
145
|
run: spokenSummaryFlow,
|
|
146
146
|
});
|
|
@@ -37,14 +37,15 @@
|
|
|
37
37
|
* in React state — so a refresh lost it while the recording carried on being
|
|
38
38
|
* transcribed, summarized and spoken. The bytes were already stored, so the
|
|
39
39
|
* expensive half had happened; what an empty form invites is somebody uploading
|
|
40
|
-
* the recording again and paying for all four legs twice.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* that did
|
|
40
|
+
* the recording again and paying for all four legs twice. Both halves are the
|
|
41
|
+
* SDK's now and this page writes neither: `useWorkflowSubmit` records the run
|
|
42
|
+
* under a correlation KEY it keeps for this page and asks for it again on the
|
|
43
|
+
* next load, and picking the same file again sends only the windows that did
|
|
44
|
+
* not land.
|
|
44
45
|
*
|
|
45
|
-
* **
|
|
46
|
-
* same lifetime as
|
|
47
|
-
* promise. What
|
|
46
|
+
* **That key is `useRunKey()`'s** — opaque, and in `sessionStorage`, which is
|
|
47
|
+
* the same lifetime as the upload recall, so both halves of a reload make the
|
|
48
|
+
* same promise. What it rules out is a `?key=` parameter, and it is worth being
|
|
48
49
|
* plain about the trade because this template exists to produce something
|
|
49
50
|
* sendable: a summary you can listen to is exactly the sort of thing somebody
|
|
50
51
|
* forwards, and a URL is how people forward things. But there is no per-user
|
|
@@ -57,9 +58,9 @@
|
|
|
57
58
|
* the recording would be worse again: two people summarizing one file would
|
|
58
59
|
* recover each other's runs.
|
|
59
60
|
*
|
|
60
|
-
* A real app with accounts passes the account's own id instead, and
|
|
61
|
-
* summary follows the person to another device — a promise only a login
|
|
62
|
-
* keep.
|
|
61
|
+
* A real app with accounts passes the account's own id as `key` instead, and
|
|
62
|
+
* then a summary follows the person to another device — a promise only a login
|
|
63
|
+
* can keep.
|
|
63
64
|
*/
|
|
64
65
|
|
|
65
66
|
import "@alexkroman1/aai-ui/styles.css";
|
|
@@ -68,18 +69,18 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
68
69
|
// `workflows/summarize.ts` already declares.
|
|
69
70
|
import { formatDuration } from "@alexkroman1/aai/utils";
|
|
70
71
|
import {
|
|
72
|
+
BulletList,
|
|
71
73
|
createWorkflowApi,
|
|
74
|
+
Facts,
|
|
72
75
|
Form,
|
|
73
|
-
|
|
76
|
+
mountPage,
|
|
74
77
|
SubmitButton,
|
|
75
78
|
UploadProgressBar,
|
|
76
79
|
useDownloadUrl,
|
|
77
|
-
useRunKey,
|
|
78
80
|
useWorkflowSubmit,
|
|
79
81
|
WorkflowFields,
|
|
80
82
|
WorkflowProgress,
|
|
81
83
|
} from "@alexkroman1/aai-ui";
|
|
82
|
-
import { useState } from "react";
|
|
83
84
|
import type { spokenSummary } from "./agent.ts";
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -128,20 +129,11 @@ function captionsUrl(text: string, durationMs: number): string {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
export function App() {
|
|
131
|
-
// This tab's handle on its own summaries, in the store the upload recall uses
|
|
132
|
-
// — see the module doc for why a `?key=` is the wrong trade here.
|
|
133
|
-
const key = useRunKey();
|
|
134
|
-
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
135
|
-
// the page can tell "working on what you just sent" from "picking up where
|
|
136
|
-
// you left off" — the hook reports the run, not who asked for it.
|
|
137
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
138
132
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
139
|
-
// `run.output` instead of `unknown`.
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
typeof spokenSummary
|
|
144
|
-
>(WORKFLOW, { api, key, recover: true });
|
|
133
|
+
// `run.output` instead of `unknown`. The reload is the hook's own doing — see
|
|
134
|
+
// the module doc for why the key it mints is the right one for this page.
|
|
135
|
+
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
|
|
136
|
+
useWorkflowSubmit<typeof spokenSummary>(WORKFLOW, { api });
|
|
145
137
|
const output = run?.status === "completed" ? run.output : undefined;
|
|
146
138
|
// `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
|
|
147
139
|
// bytes have to be FETCHED and handed to the element as an object URL — and the
|
|
@@ -157,14 +149,7 @@ export function App() {
|
|
|
157
149
|
</p>
|
|
158
150
|
</header>
|
|
159
151
|
|
|
160
|
-
<Form
|
|
161
|
-
onSubmit={(values) => {
|
|
162
|
-
setStartedHere(true);
|
|
163
|
-
return submitForm(values);
|
|
164
|
-
}}
|
|
165
|
-
error={error}
|
|
166
|
-
className="flex flex-col gap-4"
|
|
167
|
-
>
|
|
152
|
+
<Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
|
|
168
153
|
{/* Every control, from the workflow's own input schema. See the module doc. */}
|
|
169
154
|
<WorkflowFields workflow={WORKFLOW} />
|
|
170
155
|
<SubmitButton pending={pending} pendingLabel="Working…">
|
|
@@ -184,25 +169,27 @@ export function App() {
|
|
|
184
169
|
nothing for `<WorkflowProgress>` to read. */}
|
|
185
170
|
<UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
|
|
186
171
|
|
|
187
|
-
{/* What the run itself says, from `
|
|
172
|
+
{/* What the run itself says, from `stepReport()` in the workflow's steps. */}
|
|
188
173
|
<WorkflowProgress runId={run?.runId} api={api} />
|
|
189
174
|
|
|
190
|
-
{
|
|
175
|
+
{/* `role="alert"`, the same contract `<Form>` gives the submit error: this
|
|
176
|
+
is the outcome the reader waited minutes for. */}
|
|
177
|
+
{run?.status === "failed" && (
|
|
178
|
+
<p role="alert" className="text-red-600">
|
|
179
|
+
That one failed: {run.error}
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
191
182
|
|
|
192
183
|
{output !== undefined && (
|
|
193
184
|
<article className="flex flex-col gap-5">
|
|
194
185
|
<div className="flex flex-col gap-1">
|
|
195
186
|
<h2 className="text-xl">{output.headline}</h2>
|
|
196
|
-
<
|
|
197
|
-
{output.source
|
|
198
|
-
|
|
187
|
+
<Facts
|
|
188
|
+
items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
|
|
189
|
+
/>
|
|
199
190
|
</div>
|
|
200
191
|
|
|
201
|
-
<
|
|
202
|
-
{output.points.map((point) => (
|
|
203
|
-
<li key={point}>{point}</li>
|
|
204
|
-
))}
|
|
205
|
-
</ul>
|
|
192
|
+
<BulletList items={output.points} />
|
|
206
193
|
|
|
207
194
|
<section className="flex flex-col gap-2">
|
|
208
195
|
<h3 className="text-sm font-medium opacity-70">
|
|
@@ -210,11 +197,13 @@ export function App() {
|
|
|
210
197
|
</h3>
|
|
211
198
|
{audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
|
|
212
199
|
{audio.error !== undefined && (
|
|
213
|
-
<p className="text-red-600">
|
|
200
|
+
<p role="alert" className="text-red-600">
|
|
201
|
+
Could not load the audio: {audio.error}
|
|
202
|
+
</p>
|
|
214
203
|
)}
|
|
215
204
|
{audio.url !== undefined && (
|
|
216
205
|
<>
|
|
217
|
-
<audio controls src={audio.url} className="w-full">
|
|
206
|
+
<audio aria-label="Summary read aloud" controls src={audio.url} className="w-full">
|
|
218
207
|
{/* A real caption track, not a suppression: the summary was
|
|
219
208
|
written before it was spoken, so the words are already
|
|
220
209
|
here and one cue spanning the clip is an honest
|
|
@@ -248,4 +237,4 @@ export function App() {
|
|
|
248
237
|
);
|
|
249
238
|
}
|
|
250
239
|
|
|
251
|
-
|
|
240
|
+
mountPage({ name: "Spoken Summary", component: App });
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - **`stepSpeak`** synthesizes from inside a step. The session TTS surface
|
|
19
19
|
* cannot: a `TtsSession` is an event stream wired into a live pipeline's
|
|
20
20
|
* playback, and a step has no turn to be part of and has to return a VALUE.
|
|
21
|
-
* - **`
|
|
21
|
+
* - **`stepWriteUpload`** puts that value somewhere. A run's OUTPUT is read back as
|
|
22
22
|
* JSON, so audio cannot travel in one — the same rule that keeps a
|
|
23
23
|
* recording's bytes out of a run's INPUT, arriving at the other end of the
|
|
24
24
|
* run. The bytes go to the store, the output carries the id, and the page
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
* expensive one.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import type {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
52
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
53
|
+
import { stepReport, stepSpeak, stepWriteUpload, TRANSCRIBE_API } from "@alexkroman1/aai/step";
|
|
54
|
+
import { stepGenerateJsonOrFail } from "@alexkroman1/aai/step-errors";
|
|
55
55
|
import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
|
|
56
56
|
// ERASED at build time, so the body can name the schema's own output type without
|
|
57
57
|
// a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
|
|
@@ -128,7 +128,7 @@ export type SpokenSummary = {
|
|
|
128
128
|
/** Transcribe a recording, summarize it, and read the summary back. */
|
|
129
129
|
export async function spokenSummaryFlow(
|
|
130
130
|
input: WorkflowInputOf<typeof spokenSummary>,
|
|
131
|
-
ctx:
|
|
131
|
+
ctx: WorkflowContext,
|
|
132
132
|
): Promise<SpokenSummary> {
|
|
133
133
|
const transcript = await transcribe(input.recording, ctx);
|
|
134
134
|
const summary = await ctx.step("summarize", () => summarize(transcript.text));
|
|
@@ -157,7 +157,7 @@ export async function spokenSummaryFlow(
|
|
|
157
157
|
* re-derives exactly the same sequence. It takes the `ctx` for that reason: a
|
|
158
158
|
* helper that reaches the journal has to be handed the handle.
|
|
159
159
|
*/
|
|
160
|
-
async function transcribe(recording: string, ctx:
|
|
160
|
+
async function transcribe(recording: string, ctx: WorkflowContext): Promise<Transcript> {
|
|
161
161
|
// `maxAttempts: 6` was `uploadToProvider.maxRetries = 5` — five retries after
|
|
162
162
|
// the first attempt. It is the one step here worth extra patience: it streams
|
|
163
163
|
// the whole recording, so a transient failure is expensive to reach again.
|
|
@@ -187,8 +187,8 @@ async function transcribe(recording: string, ctx: WorkflowCtx): Promise<Transcri
|
|
|
187
187
|
export async function summarize(
|
|
188
188
|
text: string,
|
|
189
189
|
): Promise<{ headline: string; points: string[]; spoken: string }> {
|
|
190
|
-
await
|
|
191
|
-
const reply = await
|
|
190
|
+
await stepReport("Summarizing the transcript.");
|
|
191
|
+
const reply = await stepGenerateJsonOrFail(
|
|
192
192
|
"Summarize this transcript of a recording.\n\n" +
|
|
193
193
|
"Answer with JSON only, in this shape:\n" +
|
|
194
194
|
`{"headline": "...", "points": ["..."], "spoken": "..."}\n\n` +
|
|
@@ -204,7 +204,7 @@ export async function summarize(
|
|
|
204
204
|
system: "You summarize recordings. You answer with JSON and nothing else.",
|
|
205
205
|
schema: SummaryReply,
|
|
206
206
|
},
|
|
207
|
-
// The `
|
|
207
|
+
// The `OrFail` caller is `stepGenerateJson` plus `throwStepError`, which
|
|
208
208
|
// reads the gateway's own status: a 429 is worth another attempt and a 400
|
|
209
209
|
// is not, and that is what tells the DevKit which.
|
|
210
210
|
);
|
|
@@ -226,7 +226,7 @@ export async function speak(
|
|
|
226
226
|
voice?: string,
|
|
227
227
|
): Promise<{ audio: string; durationMs: number }> {
|
|
228
228
|
const spoken = await stepSpeak(script, omitUndefined({ voice }));
|
|
229
|
-
const stored = await
|
|
229
|
+
const stored = await stepWriteUpload(spoken.audio, {
|
|
230
230
|
// Named, because this is what a person sees on the download link rather
|
|
231
231
|
// than an opaque id — and typed, because the byte route serves the type it
|
|
232
232
|
// was given and a browser will not play a file it was handed as bytes.
|
|
@@ -234,7 +234,7 @@ export async function speak(
|
|
|
234
234
|
type: "audio/wav",
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
await
|
|
237
|
+
await stepReport(
|
|
238
238
|
`Recorded a ${Math.round(spoken.durationMs / 1000)}s summary in ${spoken.voice}'s voice.`,
|
|
239
239
|
);
|
|
240
240
|
return { audio: stored.id, durationMs: spoken.durationMs };
|
|
@@ -12,7 +12,7 @@
|
|
|
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/step` — reached here through their `*
|
|
15
|
+
* `@alexkroman1/aai/step` — reached here through their `*OrFail` callers on
|
|
16
16
|
* `@alexkroman1/aai/step-errors` — carry the URL, the raw-key auth, the windowed
|
|
17
17
|
* streaming upload, the PLURAL `speech_models` field and the failure
|
|
18
18
|
* classification — all of which this file used to spell out, and all of which
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
* the one leg that should be as boring as possible.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
import {
|
|
43
|
+
import { stepReport, stepUploadInfo } from "@alexkroman1/aai/step";
|
|
44
44
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
stepTranscribePollOrFail,
|
|
46
|
+
stepTranscribeSubmitOrFail,
|
|
47
|
+
stepTranscribeUploadOrFail,
|
|
48
48
|
} from "@alexkroman1/aai/step-errors";
|
|
49
49
|
import { countWords, formatBytes } from "@alexkroman1/aai/utils";
|
|
50
50
|
|
|
@@ -85,24 +85,24 @@ export type Transcript = {
|
|
|
85
85
|
* expires before the next step runs; that costs one fresh upload, once, instead
|
|
86
86
|
* of five.
|
|
87
87
|
*
|
|
88
|
-
* The `
|
|
88
|
+
* The `OrFail` callers on `@alexkroman1/aai/step-errors` are the SDK's own
|
|
89
89
|
* `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns
|
|
90
90
|
* the SDK's `TranscribeError` into the DevKit's verdict — a missing key and a
|
|
91
91
|
* 400 stop, a 429 waits as long as the service asked. Every step here ends the
|
|
92
92
|
* same way for the same reason.
|
|
93
93
|
*/
|
|
94
94
|
export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
|
|
95
|
-
const stored = await
|
|
96
|
-
await
|
|
95
|
+
const stored = await stepUploadInfo(uploadId);
|
|
96
|
+
await stepReport(
|
|
97
97
|
`Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
|
|
98
98
|
);
|
|
99
|
-
return await
|
|
99
|
+
return await stepTranscribeUploadOrFail(uploadId);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** Create the transcription job, and answer with the id that outlives this run. */
|
|
103
103
|
export async function createJob(audioUrl: string): Promise<{ id: string }> {
|
|
104
|
-
const job = await
|
|
105
|
-
await
|
|
104
|
+
const job = await stepTranscribeSubmitOrFail(audioUrl);
|
|
105
|
+
await stepReport(`Transcribing — job ${job.id}.`);
|
|
106
106
|
return job;
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -119,11 +119,11 @@ export async function pollTranscript(
|
|
|
119
119
|
uploadId: string,
|
|
120
120
|
id: string,
|
|
121
121
|
): Promise<{ done: false } | { done: true; transcript: Transcript }> {
|
|
122
|
-
const progress = await
|
|
122
|
+
const progress = await stepTranscribePollOrFail(id);
|
|
123
123
|
if (!progress.done) return { done: false };
|
|
124
124
|
|
|
125
|
-
const stored = await
|
|
126
|
-
await
|
|
125
|
+
const stored = await stepUploadInfo(uploadId);
|
|
126
|
+
await stepReport(`Transcribed ${countWords(progress.transcript.text)} words.`);
|
|
127
127
|
return {
|
|
128
128
|
done: true,
|
|
129
129
|
transcript: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { plural } from "@alexkroman1/aai/utils";
|
|
1
2
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import { AutoScroll,
|
|
3
|
+
import { AutoScroll, mountClient, useAgentState } from "@alexkroman1/aai-ui";
|
|
3
4
|
import { PRODUCT, supportProjection } from "./shared.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -19,7 +20,7 @@ function TraceSidebar() {
|
|
|
19
20
|
<div>
|
|
20
21
|
<h3 className="text-sm font-bold uppercase tracking-wide opacity-60">{support.product}</h3>
|
|
21
22
|
<p className="text-xs opacity-50">
|
|
22
|
-
{support.asked.length}
|
|
23
|
+
{support.asked.length} {plural(support.asked.length, "question")} this call
|
|
23
24
|
{support.ticket ? ` · ticket ${support.ticket}` : ""}
|
|
24
25
|
</p>
|
|
25
26
|
</div>
|
|
@@ -97,7 +98,7 @@ function TraceSidebar() {
|
|
|
97
98
|
);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
mountClient({
|
|
101
102
|
// Derived, not typed twice: `PRODUCT` comes off `knowledge.json`, which is
|
|
102
103
|
// what `agent.ts` names the agent and greets with. A knowledge base swapped
|
|
103
104
|
// for another product otherwise leaves the browser tab advertising the old
|
|
@@ -40,8 +40,6 @@ export const binaryScore = z.object({
|
|
|
40
40
|
reason: z.string().max(200).describe("One short sentence of justification"),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
export type BinaryScore = z.infer<typeof binaryScore>;
|
|
44
|
-
|
|
45
43
|
/** Their `retrieval_grader`: deliberately NOT a stringent test. */
|
|
46
44
|
export const DOC_GRADER_SYSTEM = [
|
|
47
45
|
"You are a grader assessing whether a retrieved support document is relevant",
|