@alexkroman1/aai-cli 6.11.0 → 7.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/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
- package/dist/cli.mjs +30 -25
- 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-DvYeD8ia.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
- package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
- package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +40 -19
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +1 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
- 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/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.test.ts +17 -14
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.test.ts +4 -6
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- 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/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/pizza-ordering/agent.test.ts +27 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- 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.test.ts +746 -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 +411 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +209 -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.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.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/agent.test.ts +26 -23
- package/dist/templates/retail/client.tsx +226 -117
- package/dist/templates/retail/registry.test.ts +38 -6
- package/dist/templates/retail/store.test.ts +82 -15
- package/dist/templates/retail/store.ts +174 -47
- package/dist/templates/retail/system-prompt.md +11 -2
- package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
- 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 +2 -2
- package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
- package/dist/templates/retail/tools/modify_user_address.ts +2 -2
- package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
- 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.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.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/tools/answer_question.ts +17 -4
- 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.test.ts +64 -33
- 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 +70 -3
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
- package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- 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
|
-
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
-
import type { GenerateFn, ToolContext } from "@alexkroman1/aai";
|
|
4
3
|
import {
|
|
5
4
|
createToolContext,
|
|
6
|
-
runTool,
|
|
7
5
|
stubGenerate,
|
|
6
|
+
toolRunner,
|
|
8
7
|
withDiscoveredTools,
|
|
9
8
|
} from "@alexkroman1/aai/testing";
|
|
10
9
|
import { describe, expect, test } from "vitest";
|
|
@@ -23,7 +22,7 @@ const agentDef = withDiscoveredTools(
|
|
|
23
22
|
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
24
23
|
);
|
|
25
24
|
|
|
26
|
-
import { MAX_ATTEMPTS, runCorrectiveRag } from "./
|
|
25
|
+
import { MAX_ATTEMPTS, runCorrectiveRag } from "./procedure.ts";
|
|
27
26
|
import {
|
|
28
27
|
ANSWER_SYSTEM,
|
|
29
28
|
ANSWERS_SYSTEM,
|
|
@@ -95,15 +94,11 @@ function scriptedModel(script: Script = {}) {
|
|
|
95
94
|
return { generate, calls };
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* and its "no such tool" message are `runTool`'s (`@alexkroman1/aai/testing`);
|
|
104
|
-
* what is local is only which agent they run against. */
|
|
105
|
-
const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
|
|
106
|
-
runTool(agentDef, name, args, ctx);
|
|
97
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup,
|
|
98
|
+
* its "no such tool" message and the args-or-context shape are all
|
|
99
|
+
* `toolRunner`'s (`@alexkroman1/aai/testing`); what is local is only which
|
|
100
|
+
* agent it runs against. */
|
|
101
|
+
const run = toolRunner(agentDef);
|
|
107
102
|
|
|
108
103
|
/** Node names without the per-call suffix, for sequence assertions. */
|
|
109
104
|
function nodes(calls: string[]): string[] {
|
|
@@ -254,7 +249,7 @@ describe("answer_question", () => {
|
|
|
254
249
|
relevant: (id) => id === "D8",
|
|
255
250
|
answers: ["Area outages are on the status page, and rebooting will not help."],
|
|
256
251
|
});
|
|
257
|
-
const ctx =
|
|
252
|
+
const ctx = createToolContext({ generate });
|
|
258
253
|
const result = (await run("answer_question", { question: "is there an outage" }, ctx)) as {
|
|
259
254
|
answer: string;
|
|
260
255
|
sources: string[];
|
|
@@ -276,7 +271,7 @@ describe("answer_question", () => {
|
|
|
276
271
|
|
|
277
272
|
test("with nothing grounded it returns no answer and points at the ticket", async () => {
|
|
278
273
|
const { generate } = scriptedModel({ relevant: () => false });
|
|
279
|
-
const ctx =
|
|
274
|
+
const ctx = createToolContext({ generate });
|
|
280
275
|
const result = (await run("answer_question", { question: "do you sell phones" }, ctx)) as {
|
|
281
276
|
answer: null;
|
|
282
277
|
guidance: string;
|
|
@@ -302,8 +297,8 @@ describe("answer_question", () => {
|
|
|
302
297
|
// session ids would prove nothing extra, and `sessionSlot` could stop
|
|
303
298
|
// keying by session with this still passing.
|
|
304
299
|
const { generate } = scriptedModel({ relevant: (id) => id === "D2", answers: ["Reboot it."] });
|
|
305
|
-
const first =
|
|
306
|
-
const second =
|
|
300
|
+
const first = createToolContext({ generate });
|
|
301
|
+
const second = createToolContext({ generate });
|
|
307
302
|
|
|
308
303
|
await run("answer_question", { question: "how do I reboot" }, first);
|
|
309
304
|
expect(supportSlot.get(second).trace).toBeNull();
|
|
@@ -3,7 +3,7 @@ import { PRODUCT, supportProjection } from "./shared.ts";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A support line that grades its own retrieval before it speaks — the
|
|
6
|
-
* self-RAG / CRAG graph, ported to voice. `
|
|
6
|
+
* self-RAG / CRAG graph, ported to voice. `procedure.ts` holds the loop, `prompts.ts`
|
|
7
7
|
* the attribution and the graders, `shared.ts` the knowledge base and why its
|
|
8
8
|
* retriever is lexical.
|
|
9
9
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import { AutoScroll, client, useAgentState
|
|
2
|
+
import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
|
|
3
3
|
import { PRODUCT, supportProjection } from "./shared.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -11,12 +11,11 @@ import { PRODUCT, supportProjection } from "./shared.ts";
|
|
|
11
11
|
* support line nobody can tell apart from one that just guesses well.
|
|
12
12
|
*/
|
|
13
13
|
function TraceSidebar() {
|
|
14
|
-
const theme = useTheme();
|
|
15
14
|
const support = useAgentState(supportProjection);
|
|
16
15
|
const trace = support.trace;
|
|
17
16
|
|
|
18
17
|
return (
|
|
19
|
-
<div className="flex h-full min-h-0 flex-col gap-4 p-4
|
|
18
|
+
<div className="flex h-full min-h-0 flex-col gap-4 p-4 text-aai-text">
|
|
20
19
|
<div>
|
|
21
20
|
<h3 className="text-sm font-bold uppercase tracking-wide opacity-60">{support.product}</h3>
|
|
22
21
|
<p className="text-xs opacity-50">
|
|
@@ -36,7 +35,7 @@ function TraceSidebar() {
|
|
|
36
35
|
scrollClassName="min-h-0 overflow-y-auto"
|
|
37
36
|
contentClassName="flex flex-col gap-4 pr-1"
|
|
38
37
|
>
|
|
39
|
-
<div className="rounded-lg p-3
|
|
38
|
+
<div className="rounded-lg p-3 bg-aai-surface">
|
|
40
39
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Question</p>
|
|
41
40
|
<p className="text-sm">{trace.question}</p>
|
|
42
41
|
{trace.rewrites > 0 && (
|
|
@@ -54,10 +53,11 @@ function TraceSidebar() {
|
|
|
54
53
|
{trace.docs.map((doc) => (
|
|
55
54
|
<div
|
|
56
55
|
key={doc.id}
|
|
57
|
-
className=
|
|
58
|
-
|
|
56
|
+
className={`flex items-start gap-2 rounded-lg p-2 bg-aai-surface ${
|
|
57
|
+
doc.relevant ? "" : "opacity-50"
|
|
58
|
+
}`}
|
|
59
59
|
>
|
|
60
|
-
<span
|
|
60
|
+
<span className={doc.relevant ? "text-aai-primary" : "text-aai-text"}>
|
|
61
61
|
{doc.relevant ? "✓" : "×"}
|
|
62
62
|
</span>
|
|
63
63
|
<div className="min-w-0">
|
|
@@ -69,7 +69,7 @@ function TraceSidebar() {
|
|
|
69
69
|
</div>
|
|
70
70
|
|
|
71
71
|
<div className="flex flex-col gap-1">
|
|
72
|
-
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">
|
|
72
|
+
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Procedure</p>
|
|
73
73
|
{trace.steps.map((entry, index) => (
|
|
74
74
|
// Steps are append-only within a run, so the index is stable.
|
|
75
75
|
<p key={`${index}-${entry.node}`} className="text-xs">
|
|
@@ -79,7 +79,7 @@ function TraceSidebar() {
|
|
|
79
79
|
))}
|
|
80
80
|
</div>
|
|
81
81
|
|
|
82
|
-
<div className="rounded-lg p-3
|
|
82
|
+
<div className="rounded-lg p-3 bg-aai-surface">
|
|
83
83
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Verdict</p>
|
|
84
84
|
{trace.answer ? (
|
|
85
85
|
<p className="text-sm">{trace.answer}</p>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CRAG nodes' BODIES — one model call each, and nothing about routing.
|
|
3
|
+
*
|
|
4
|
+
* Split from `procedure.ts` when that file became an actual machine: the machine
|
|
5
|
+
* there declares which node runs next, and these are what each node does. The
|
|
6
|
+
* seam is worth having beyond the line count — everything here is a plain async
|
|
7
|
+
* function of its arguments, so a spec can drive one directly, and the machine
|
|
8
|
+
* holds no prompt text at all.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { GenerateFn } from "@alexkroman1/aai";
|
|
12
|
+
import {
|
|
13
|
+
ANSWER_SYSTEM,
|
|
14
|
+
ANSWERS_SYSTEM,
|
|
15
|
+
binaryScore,
|
|
16
|
+
DOC_GRADER_SYSTEM,
|
|
17
|
+
GROUNDED_SYSTEM,
|
|
18
|
+
REWRITE_SYSTEM,
|
|
19
|
+
} from "./prompts.ts";
|
|
20
|
+
import type { Doc, GradedDoc } from "./shared.ts";
|
|
21
|
+
import { formatDoc } from "./shared.ts";
|
|
22
|
+
|
|
23
|
+
/** A closed verdict from one of the graders. */
|
|
24
|
+
export interface Verdict {
|
|
25
|
+
pass: boolean;
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `grade_documents` — one closed question per document, all at once. */
|
|
30
|
+
export async function gradeDocuments(
|
|
31
|
+
generate: GenerateFn,
|
|
32
|
+
question: string,
|
|
33
|
+
docs: readonly Doc[],
|
|
34
|
+
): Promise<GradedDoc[]> {
|
|
35
|
+
return await Promise.all(
|
|
36
|
+
docs.map(async (doc) => {
|
|
37
|
+
const { object } = await generate({
|
|
38
|
+
system: DOC_GRADER_SYSTEM,
|
|
39
|
+
prompt: `Caller's question: ${question}\n\nDocument:\n${formatDoc(doc)}`,
|
|
40
|
+
schema: binaryScore,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
id: doc.id,
|
|
44
|
+
title: doc.title,
|
|
45
|
+
relevant: object.score === "yes",
|
|
46
|
+
reason: object.reason,
|
|
47
|
+
};
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `transform_query` — their question re-writer, aimed at a keyword index. */
|
|
53
|
+
export async function transformQuery(generate: GenerateFn, question: string): Promise<string> {
|
|
54
|
+
const { text } = await generate({
|
|
55
|
+
system: REWRITE_SYSTEM,
|
|
56
|
+
prompt: `Caller's question: ${question}`,
|
|
57
|
+
});
|
|
58
|
+
const rewritten = text.trim().replace(/^["']|["']$/g, "");
|
|
59
|
+
// A rewriter that returns nothing usable must not empty the query — that
|
|
60
|
+
// would retrieve zero documents and read as "we have nothing on this".
|
|
61
|
+
return rewritten.length > 0 ? rewritten : question;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `generate` — the answer, from the graded-relevant documents only. */
|
|
65
|
+
export async function generateAnswer(
|
|
66
|
+
generate: GenerateFn,
|
|
67
|
+
question: string,
|
|
68
|
+
docs: readonly Doc[],
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const { text } = await generate({
|
|
71
|
+
system: ANSWER_SYSTEM,
|
|
72
|
+
prompt: `Documents:\n\n${docs.map(formatDoc).join("\n\n")}\n\nCaller's question: ${question}`,
|
|
73
|
+
});
|
|
74
|
+
return text.trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The two generation graders — same closed question, different subject. */
|
|
78
|
+
async function grade(generate: GenerateFn, system: string, prompt: string): Promise<Verdict> {
|
|
79
|
+
const { object } = await generate({ system, prompt, schema: binaryScore });
|
|
80
|
+
return { pass: object.score === "yes", reason: object.reason };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `grade_generation_v_documents` — is the answer IN the documents? */
|
|
84
|
+
export async function gradeGrounded(
|
|
85
|
+
generate: GenerateFn,
|
|
86
|
+
docs: readonly Doc[],
|
|
87
|
+
answer: string,
|
|
88
|
+
): Promise<Verdict> {
|
|
89
|
+
const facts = docs.map(formatDoc).join("\n\n");
|
|
90
|
+
return await grade(generate, GROUNDED_SYSTEM, `Facts:\n\n${facts}\n\nAnswer: ${answer}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `grade_generation_v_question` — grounded is not the same as useful. */
|
|
94
|
+
export async function gradeUseful(
|
|
95
|
+
generate: GenerateFn,
|
|
96
|
+
question: string,
|
|
97
|
+
answer: string,
|
|
98
|
+
): Promise<Verdict> {
|
|
99
|
+
return await grade(generate, ANSWERS_SYSTEM, `Question: ${question}\n\nAnswer: ${answer}`);
|
|
100
|
+
}
|