@alexkroman1/aai-cli 5.14.0 → 6.1.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-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +8 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +17 -19
- package/dist/templates/dispatch-center/agent.ts +2 -29
- package/dist/templates/dispatch-center/client.tsx +33 -16
- package/dist/templates/dispatch-center/shared.ts +53 -19
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
- package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
- package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +28 -12
- package/dist/templates/infocom-adventure/shared.ts +28 -11
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +52 -47
- package/dist/templates/pizza-ordering/agent.ts +9 -158
- package/dist/templates/pizza-ordering/shared.ts +35 -12
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +236 -172
- package/dist/templates/retail/agent.ts +5 -42
- package/dist/templates/retail/client.tsx +33 -11
- package/dist/templates/retail/registry.test.ts +31 -16
- package/dist/templates/retail/resolve.ts +28 -64
- package/dist/templates/retail/shared.test.ts +4 -3
- package/dist/templates/retail/shared.ts +24 -7
- package/dist/templates/retail/store.test.ts +12 -10
- package/dist/templates/retail/store.ts +69 -30
- package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +5 -7
- package/dist/templates/retail/tools/get_order_details.ts +5 -7
- package/dist/templates/retail/tools/get_product_details.ts +4 -7
- package/dist/templates/retail/tools/get_user_details.ts +4 -7
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
- package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
- package/dist/templates/retail/tools/modify_user_address.ts +4 -9
- package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +152 -29
- package/dist/templates/solo-rpg/agent.ts +3 -21
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +19 -11
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
- package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
- package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- package/dist/build-DQJ1ZSm9.mjs +0 -39
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import type { GenerateFn, ToolContext } from "@alexkroman1/aai";
|
|
4
|
+
import {
|
|
5
|
+
createToolContext,
|
|
6
|
+
runTool,
|
|
7
|
+
stubGenerate,
|
|
8
|
+
withDiscoveredTools,
|
|
9
|
+
} from "@alexkroman1/aai/testing";
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import authoredAgent from "./agent.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
15
|
+
*
|
|
16
|
+
* The glob is written HERE rather than reached for from a shared helper because
|
|
17
|
+
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
18
|
+
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
19
|
+
* the file containing it either way. This is the pattern a user writes.
|
|
20
|
+
*/
|
|
21
|
+
const agentDef = withDiscoveredTools(
|
|
22
|
+
authoredAgent,
|
|
23
|
+
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
import { MAX_ATTEMPTS, runCorrectiveRag } from "./graph.ts";
|
|
27
|
+
import {
|
|
28
|
+
ANSWER_SYSTEM,
|
|
29
|
+
ANSWERS_SYSTEM,
|
|
30
|
+
DOC_GRADER_SYSTEM,
|
|
31
|
+
GROUNDED_SYSTEM,
|
|
32
|
+
REWRITE_SYSTEM,
|
|
33
|
+
} from "./prompts.ts";
|
|
34
|
+
import { retrieve, supportSlot, supportView } from "./shared.ts";
|
|
35
|
+
|
|
36
|
+
// ─── A scripted model ────────────────────────────────────────────────────────
|
|
37
|
+
//
|
|
38
|
+
// Every node in the graph is one `ctx.generate` call, and each call is
|
|
39
|
+
// identified by the system prompt it carries — so a fake that switches on
|
|
40
|
+
// `options.system` can drive the graph down any path, deterministically, with
|
|
41
|
+
// no model and no network. `calls` is the transcript, which is what the routing
|
|
42
|
+
// assertions read: the point of these tests is WHICH nodes ran, not what a
|
|
43
|
+
// model would have said.
|
|
44
|
+
|
|
45
|
+
interface Script {
|
|
46
|
+
/** Whether the grader passes a document, per retrieval attempt (1-based). */
|
|
47
|
+
relevant?: (docId: string, attempt: number) => boolean;
|
|
48
|
+
/** What `transform_query` rewrites to. */
|
|
49
|
+
rewrite?: string;
|
|
50
|
+
/** Answers, consumed one per `generate` node. */
|
|
51
|
+
answers?: string[];
|
|
52
|
+
/** Verdicts, consumed one per grader call. Default: pass. */
|
|
53
|
+
grounded?: ("yes" | "no")[];
|
|
54
|
+
useful?: ("yes" | "no")[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function scriptedModel(script: Script = {}) {
|
|
58
|
+
// The NODE transcript, which is this template's own and is why the routes
|
|
59
|
+
// below are functions: `stubGenerate` records the calls, and what these
|
|
60
|
+
// assertions need is the graph's node names, labelled with their verdicts.
|
|
61
|
+
const calls: string[] = [];
|
|
62
|
+
let attempt = 1;
|
|
63
|
+
const answers = [...(script.answers ?? [])];
|
|
64
|
+
const grounded = [...(script.grounded ?? [])];
|
|
65
|
+
const useful = [...(script.useful ?? [])];
|
|
66
|
+
|
|
67
|
+
const { generate } = stubGenerate({
|
|
68
|
+
[DOC_GRADER_SYSTEM]: (call) => {
|
|
69
|
+
const id = /\[(D\d+)\]/.exec(call.prompt)?.[1] ?? "?";
|
|
70
|
+
calls.push(`grade_documents:${id}`);
|
|
71
|
+
const pass = script.relevant ? script.relevant(id, attempt) : true;
|
|
72
|
+
return { object: { score: pass ? "yes" : "no", reason: `graded ${id}` } };
|
|
73
|
+
},
|
|
74
|
+
[REWRITE_SYSTEM]: () => {
|
|
75
|
+
calls.push("transform_query");
|
|
76
|
+
attempt++;
|
|
77
|
+
return script.rewrite ?? "rewritten query";
|
|
78
|
+
},
|
|
79
|
+
[ANSWER_SYSTEM]: () => {
|
|
80
|
+
calls.push("generate");
|
|
81
|
+
return answers.shift() ?? "The documented answer.";
|
|
82
|
+
},
|
|
83
|
+
[GROUNDED_SYSTEM]: () => {
|
|
84
|
+
const verdict = grounded.shift() ?? "yes";
|
|
85
|
+
calls.push(`grade_generation_v_documents:${verdict}`);
|
|
86
|
+
return { object: { score: verdict, reason: "grounded verdict" } };
|
|
87
|
+
},
|
|
88
|
+
[ANSWERS_SYSTEM]: () => {
|
|
89
|
+
const verdict = useful.shift() ?? "yes";
|
|
90
|
+
calls.push(`grade_generation_v_question:${verdict}`);
|
|
91
|
+
return { object: { score: verdict, reason: "usefulness verdict" } };
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { generate, calls };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function makeCtx(generate: GenerateFn, sessionId?: string) {
|
|
99
|
+
return createToolContext({ generate, ...(sessionId ? { sessionId } : {}) });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup
|
|
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);
|
|
107
|
+
|
|
108
|
+
/** Node names without the per-call suffix, for sequence assertions. */
|
|
109
|
+
function nodes(calls: string[]): string[] {
|
|
110
|
+
return calls.map((call) => call.split(":")[0] ?? call);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The same sequence with consecutive repeats collapsed — `grade_documents`
|
|
114
|
+
* runs once per retrieved document, and the count is `RETRIEVE_K`'s business
|
|
115
|
+
* rather than the routing's. */
|
|
116
|
+
function phases(calls: string[]): string[] {
|
|
117
|
+
return nodes(calls).filter((node, index, all) => node !== all[index - 1]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── 1. The retriever ────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe("retrieval (shared.ts)", () => {
|
|
123
|
+
test("ranks the document a caller's words actually point at", () => {
|
|
124
|
+
expect(retrieve("my broadband light is red")[0]?.doc.id).toBe("D1");
|
|
125
|
+
expect(retrieve("nobody was home for the engineer")[0]?.doc.id).toBe("D6");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("and gets the neighbouring document first often enough to need a grader", () => {
|
|
129
|
+
// "what does it cost to leave my contract early" ranks D7 (moving home,
|
|
130
|
+
// which mentions the contract AND the early exit fee) above D5, the
|
|
131
|
+
// document that actually answers it. Both come back, which is what makes
|
|
132
|
+
// the run recoverable — the document grader is the node that stops the
|
|
133
|
+
// neighbour being spoken as the answer, and this is the retrieval it is
|
|
134
|
+
// grading.
|
|
135
|
+
const ranked = retrieve("what does it cost to leave my contract early").map(
|
|
136
|
+
(one) => one.doc.id,
|
|
137
|
+
);
|
|
138
|
+
expect(ranked[0]).toBe("D7");
|
|
139
|
+
expect(ranked).toContain("D5");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("returns nothing for a query with no content words", () => {
|
|
143
|
+
// Every term is a stopword, so there is nothing to rank — which the graph
|
|
144
|
+
// treats as a failed retrieval rather than as an empty knowledge base.
|
|
145
|
+
expect(retrieve("what about the one that you have")).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a question phrased the way a caller speaks misses, which is the case the rewrite exists for", () => {
|
|
149
|
+
// The bait for the whole template: no document contains "dying", so lexical
|
|
150
|
+
// retrieval cannot reach D10 until `transform_query` supplies the words a
|
|
151
|
+
// document about it would use.
|
|
152
|
+
const spoken = retrieve("my internet keeps dying at night").map((one) => one.doc.id);
|
|
153
|
+
expect(spoken).not.toContain("D10");
|
|
154
|
+
expect(retrieve("evening slowdown congestion peak time")[0]?.doc.id).toBe("D10");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ─── 2. The graph's routing ──────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe("corrective-RAG graph", () => {
|
|
161
|
+
test("the straight path grades, answers, and checks the answer twice", async () => {
|
|
162
|
+
const { generate, calls } = scriptedModel({
|
|
163
|
+
relevant: (id) => id === "D2",
|
|
164
|
+
answers: ["Power the hub off at the wall for thirty seconds."],
|
|
165
|
+
});
|
|
166
|
+
const trace = await runCorrectiveRag(generate, "how do I reboot the hub");
|
|
167
|
+
|
|
168
|
+
expect(phases(calls)).toEqual([
|
|
169
|
+
"grade_documents",
|
|
170
|
+
"generate",
|
|
171
|
+
"grade_generation_v_documents",
|
|
172
|
+
"grade_generation_v_question",
|
|
173
|
+
]);
|
|
174
|
+
expect(trace.answer).toContain("thirty seconds");
|
|
175
|
+
expect(trace.grounded).toBe(true);
|
|
176
|
+
expect(trace.useful).toBe(true);
|
|
177
|
+
expect(trace.rewrites).toBe(0);
|
|
178
|
+
expect(trace.exhausted).toBe(false);
|
|
179
|
+
expect(trace.docs.filter((doc) => doc.relevant).map((doc) => doc.id)).toEqual(["D2"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("nothing relevant rewrites the query and retries — the corrective edge", async () => {
|
|
183
|
+
const { generate, calls } = scriptedModel({
|
|
184
|
+
// First retrieval is graded useless; after the rewrite everything passes.
|
|
185
|
+
relevant: (_id, attempt) => attempt > 1,
|
|
186
|
+
rewrite: "evening slowdown congestion peak time",
|
|
187
|
+
answers: ["There is no traffic management, so evening slowdowns are local."],
|
|
188
|
+
});
|
|
189
|
+
const trace = await runCorrectiveRag(generate, "my internet keeps dying at night");
|
|
190
|
+
|
|
191
|
+
expect(calls.filter((call) => call === "transform_query")).toHaveLength(1);
|
|
192
|
+
expect(trace.rewrites).toBe(1);
|
|
193
|
+
expect(trace.query).toBe("evening slowdown congestion peak time");
|
|
194
|
+
expect(trace.answer).toContain("traffic management");
|
|
195
|
+
// The trace names the nodes the way their graph does, which is what the
|
|
196
|
+
// sidebar renders.
|
|
197
|
+
expect(trace.steps.map((entry) => entry.node)).toContain("transform_query");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a caller question nothing covers ends exhausted, with no answer", async () => {
|
|
201
|
+
const { generate, calls } = scriptedModel({ relevant: () => false });
|
|
202
|
+
const trace = await runCorrectiveRag(generate, "can I get a landline installed");
|
|
203
|
+
|
|
204
|
+
expect(trace.answer).toBeNull();
|
|
205
|
+
expect(trace.exhausted).toBe(true);
|
|
206
|
+
expect(trace.rewrites).toBe(MAX_ATTEMPTS - 1);
|
|
207
|
+
// It never reached the answer node: nothing was graded relevant to answer
|
|
208
|
+
// FROM, which is the whole point of grading before generating.
|
|
209
|
+
expect(nodes(calls)).not.toContain("generate");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("an ungrounded answer is regenerated once and then withheld", async () => {
|
|
213
|
+
const { generate, calls } = scriptedModel({
|
|
214
|
+
relevant: (id) => id === "D5",
|
|
215
|
+
answers: ["The exit fee is nine months.", "The exit fee is still nine months."],
|
|
216
|
+
grounded: ["no", "no"],
|
|
217
|
+
});
|
|
218
|
+
const trace = await runCorrectiveRag(generate, "what is the early exit fee");
|
|
219
|
+
|
|
220
|
+
expect(calls.filter((call) => call === "generate")).toHaveLength(2);
|
|
221
|
+
// Withheld rather than spoken with a caveat: an answer the grader says is
|
|
222
|
+
// not in the documents is the one thing a support line must never say.
|
|
223
|
+
expect(trace.answer).toBeNull();
|
|
224
|
+
expect(trace.grounded).toBe(false);
|
|
225
|
+
expect(trace.exhausted).toBe(true);
|
|
226
|
+
expect(nodes(calls)).not.toContain("grade_generation_v_question");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a grounded but beside-the-point answer rewrites once, then is returned with its verdict", async () => {
|
|
230
|
+
const { generate } = scriptedModel({
|
|
231
|
+
relevant: () => true,
|
|
232
|
+
useful: ["no", "no"],
|
|
233
|
+
// The rewrite has to retrieve something, or the second attempt ends in
|
|
234
|
+
// the no-documents branch instead of the one under test.
|
|
235
|
+
rewrite: "direct debit bill payment date",
|
|
236
|
+
answers: ["Bills go out on the first.", "Bills go out on the first."],
|
|
237
|
+
});
|
|
238
|
+
const trace = await runCorrectiveRag(generate, "when will my money actually leave my account");
|
|
239
|
+
|
|
240
|
+
expect(trace.rewrites).toBe(1);
|
|
241
|
+
expect(trace.useful).toBe(false);
|
|
242
|
+
expect(trace.exhausted).toBe(true);
|
|
243
|
+
// Still returned: it is grounded, and the tool tells the model to offer a
|
|
244
|
+
// ticket alongside it rather than to withhold it.
|
|
245
|
+
expect(trace.answer).toBe("Bills go out on the first.");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ─── 3. The tools ────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
describe("answer_question", () => {
|
|
252
|
+
test("hands back the answer, its sources and its verdicts, and records the trace", async () => {
|
|
253
|
+
const { generate } = scriptedModel({
|
|
254
|
+
relevant: (id) => id === "D8",
|
|
255
|
+
answers: ["Area outages are on the status page, and rebooting will not help."],
|
|
256
|
+
});
|
|
257
|
+
const ctx = makeCtx(generate);
|
|
258
|
+
const result = (await run("answer_question", { question: "is there an outage" }, ctx)) as {
|
|
259
|
+
answer: string;
|
|
260
|
+
sources: string[];
|
|
261
|
+
grounded: boolean;
|
|
262
|
+
answersTheQuestion: boolean;
|
|
263
|
+
guidance?: string;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
expect(result.answer).toContain("status page");
|
|
267
|
+
expect(result.sources).toEqual(["Checking for an outage in your area"]);
|
|
268
|
+
expect(result.grounded).toBe(true);
|
|
269
|
+
expect(result.answersTheQuestion).toBe(true);
|
|
270
|
+
expect(result.guidance).toBeUndefined();
|
|
271
|
+
|
|
272
|
+
const state = supportSlot.get(ctx);
|
|
273
|
+
expect(state.asked).toEqual(["is there an outage"]);
|
|
274
|
+
expect(state.trace?.answer).toBe(result.answer);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("with nothing grounded it returns no answer and points at the ticket", async () => {
|
|
278
|
+
const { generate } = scriptedModel({ relevant: () => false });
|
|
279
|
+
const ctx = makeCtx(generate);
|
|
280
|
+
const result = (await run("answer_question", { question: "do you sell phones" }, ctx)) as {
|
|
281
|
+
answer: null;
|
|
282
|
+
guidance: string;
|
|
283
|
+
};
|
|
284
|
+
expect(result.answer).toBeNull();
|
|
285
|
+
expect(result.guidance).toContain("log_ticket");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("a broken model call is reported rather than thrown at the turn", async () => {
|
|
289
|
+
// ctx.generate rejecting is the default `createToolContext` gives — a bad
|
|
290
|
+
// key in production looks the same from here.
|
|
291
|
+
const ctx = createToolContext({});
|
|
292
|
+
const result = (await run("answer_question", { question: "anything" }, ctx)) as {
|
|
293
|
+
error: string;
|
|
294
|
+
};
|
|
295
|
+
expect(result.error).toContain("knowledge base lookup failed");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("two independent contexts never see each other's traces or tickets", async () => {
|
|
299
|
+
// What this really checks: the state lives in the SLOT and not in a
|
|
300
|
+
// module-level variable. `createToolContext()` hands each call its own
|
|
301
|
+
// detached slot store, so the isolation is per CONTEXT — two distinct
|
|
302
|
+
// session ids would prove nothing extra, and `sessionSlot` could stop
|
|
303
|
+
// keying by session with this still passing.
|
|
304
|
+
const { generate } = scriptedModel({ relevant: (id) => id === "D2", answers: ["Reboot it."] });
|
|
305
|
+
const first = makeCtx(generate);
|
|
306
|
+
const second = makeCtx(generate);
|
|
307
|
+
|
|
308
|
+
await run("answer_question", { question: "how do I reboot" }, first);
|
|
309
|
+
expect(supportSlot.get(second).trace).toBeNull();
|
|
310
|
+
expect(supportSlot.get(first).trace).not.toBeNull();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("log_ticket", () => {
|
|
315
|
+
test("logs a reference and keeps the callback number off the wire", async () => {
|
|
316
|
+
const ctx = createToolContext({});
|
|
317
|
+
const logged = (await run(
|
|
318
|
+
"log_ticket",
|
|
319
|
+
{ question: "landline install", callback: "07700 900123" },
|
|
320
|
+
ctx,
|
|
321
|
+
)) as { reference: string };
|
|
322
|
+
expect(logged.reference).toBe("TCK4001");
|
|
323
|
+
|
|
324
|
+
const state = supportSlot.get(ctx);
|
|
325
|
+
expect(state.ticket?.callback).toBe("07700 900123");
|
|
326
|
+
// The projection is the privacy boundary — only the reference crosses.
|
|
327
|
+
const view = supportView(state);
|
|
328
|
+
expect(view.ticket).toBe("TCK4001");
|
|
329
|
+
expect(JSON.stringify(view)).not.toContain("900123");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("supportView projection", () => {
|
|
334
|
+
test("an untouched call projects an empty trace, not undefined", () => {
|
|
335
|
+
// Exactly the value client.tsx hoists as its fallback.
|
|
336
|
+
expect(supportSlot.projection(supportView)(undefined)).toMatchObject({
|
|
337
|
+
product: "Meridian Fibre",
|
|
338
|
+
trace: null,
|
|
339
|
+
asked: [],
|
|
340
|
+
ticket: null,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { agent } from "@alexkroman1/aai";
|
|
2
|
+
import { PRODUCT, supportSlot, supportView } from "./shared.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A support line that grades its own retrieval before it speaks — the
|
|
6
|
+
* self-RAG / CRAG graph, ported to voice. `graph.ts` holds the loop, `prompts.ts`
|
|
7
|
+
* the attribution and the graders, `shared.ts` the knowledge base and why its
|
|
8
|
+
* retriever is lexical.
|
|
9
|
+
*
|
|
10
|
+
* **Why this shape is worth the model calls on a phone line.** A naive RAG voice
|
|
11
|
+
* agent retrieves, stuffs the documents into the prompt and speaks — and when
|
|
12
|
+
* retrieval missed, it answers from the nearest document it was handed, which
|
|
13
|
+
* over a phone is indistinguishable from a confident correct answer. The
|
|
14
|
+
* template's own knowledge base is built to bait exactly that: "cancelling your
|
|
15
|
+
* contract" and "cancelling an engineer visit" are two documents, two fees, and
|
|
16
|
+
* one word apart. The document grader is what makes the second one not get
|
|
17
|
+
* spoken as the answer to the first.
|
|
18
|
+
*/
|
|
19
|
+
export default agent({
|
|
20
|
+
name: `${PRODUCT} Support`,
|
|
21
|
+
// The trace exists before the first tool call, so a resumed connection has
|
|
22
|
+
// something to project.
|
|
23
|
+
// The projection is also the privacy boundary: a logged ticket carries the
|
|
24
|
+
// caller's callback number, and only its reference crosses to the browser.
|
|
25
|
+
syncState: supportSlot.projection(supportView),
|
|
26
|
+
greeting: `${PRODUCT} support, you're through to the automated line. What's happened?`,
|
|
27
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
+
import { AutoScroll, client, useAgentState, useTheme } from "@alexkroman1/aai-ui";
|
|
3
|
+
import type { SupportView } from "./shared.ts";
|
|
4
|
+
import { PRODUCT, supportSlot, supportView } from "./shared.ts";
|
|
5
|
+
|
|
6
|
+
const EMPTY_SUPPORT: SupportView = supportSlot.projection(supportView)(undefined);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The graph, as it ran for the last question.
|
|
10
|
+
*
|
|
11
|
+
* This is the panel worth having: the whole argument for the corrective loop is
|
|
12
|
+
* work the caller never hears — four documents retrieved, three rejected, the
|
|
13
|
+
* question rewritten once — and a support line whose grading is invisible is a
|
|
14
|
+
* support line nobody can tell apart from one that just guesses well.
|
|
15
|
+
*/
|
|
16
|
+
function TraceSidebar() {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
const support = useAgentState<SupportView>(EMPTY_SUPPORT);
|
|
19
|
+
const trace = support.trace;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex h-full min-h-0 flex-col gap-4 p-4" style={{ color: theme.text }}>
|
|
23
|
+
<div>
|
|
24
|
+
<h3 className="text-sm font-bold uppercase tracking-wide opacity-60">{support.product}</h3>
|
|
25
|
+
<p className="text-xs opacity-50">
|
|
26
|
+
{support.asked.length} question{support.asked.length === 1 ? "" : "s"} this call
|
|
27
|
+
{support.ticket ? ` · ticket ${support.ticket}` : ""}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{!trace && (
|
|
32
|
+
<p className="text-sm opacity-50">
|
|
33
|
+
Ask a question and the retrieval, the grades and the verdicts appear here.
|
|
34
|
+
</p>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
{trace && (
|
|
38
|
+
<AutoScroll
|
|
39
|
+
scrollClassName="min-h-0 overflow-y-auto"
|
|
40
|
+
contentClassName="flex flex-col gap-4 pr-1"
|
|
41
|
+
>
|
|
42
|
+
<div className="rounded-lg p-3" style={{ background: theme.surface }}>
|
|
43
|
+
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Question</p>
|
|
44
|
+
<p className="text-sm">{trace.question}</p>
|
|
45
|
+
{trace.rewrites > 0 && (
|
|
46
|
+
<p className="mt-1 text-xs opacity-60">
|
|
47
|
+
rewritten to <span className="italic">{trace.query}</span>
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className="flex flex-col gap-1.5">
|
|
53
|
+
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">
|
|
54
|
+
Retrieved & graded
|
|
55
|
+
</p>
|
|
56
|
+
{trace.docs.length === 0 && <p className="text-xs opacity-50">Nothing retrieved.</p>}
|
|
57
|
+
{trace.docs.map((doc) => (
|
|
58
|
+
<div
|
|
59
|
+
key={doc.id}
|
|
60
|
+
className="flex items-start gap-2 rounded-lg p-2"
|
|
61
|
+
style={{ background: theme.surface, opacity: doc.relevant ? 1 : 0.5 }}
|
|
62
|
+
>
|
|
63
|
+
<span style={{ color: doc.relevant ? theme.primary : theme.text }}>
|
|
64
|
+
{doc.relevant ? "✓" : "×"}
|
|
65
|
+
</span>
|
|
66
|
+
<div className="min-w-0">
|
|
67
|
+
<p className="text-sm">{doc.title}</p>
|
|
68
|
+
<p className="text-xs opacity-60">{doc.reason}</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="flex flex-col gap-1">
|
|
75
|
+
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Graph</p>
|
|
76
|
+
{trace.steps.map((entry, index) => (
|
|
77
|
+
// Steps are append-only within a run, so the index is stable.
|
|
78
|
+
<p key={`${index}-${entry.node}`} className="text-xs">
|
|
79
|
+
<span className="font-mono opacity-80">{entry.node}</span>
|
|
80
|
+
<span className="opacity-50"> — {entry.detail}</span>
|
|
81
|
+
</p>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="rounded-lg p-3" style={{ background: theme.surface }}>
|
|
86
|
+
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Verdict</p>
|
|
87
|
+
{trace.answer ? (
|
|
88
|
+
<p className="text-sm">{trace.answer}</p>
|
|
89
|
+
) : (
|
|
90
|
+
<p className="text-sm opacity-60">No answer could be grounded.</p>
|
|
91
|
+
)}
|
|
92
|
+
<p className="mt-2 text-xs opacity-60">
|
|
93
|
+
grounded: {String(trace.grounded)} · answers the question: {String(trace.useful)}
|
|
94
|
+
{trace.exhausted ? " · budget exhausted" : ""}
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
</AutoScroll>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
client({
|
|
104
|
+
// Derived, not typed twice: `PRODUCT` comes off `knowledge.json`, which is
|
|
105
|
+
// what `agent.ts` names the agent and greets with. A knowledge base swapped
|
|
106
|
+
// for another product otherwise leaves the browser tab advertising the old
|
|
107
|
+
// one.
|
|
108
|
+
name: `${PRODUCT} Support`,
|
|
109
|
+
sidebar: TraceSidebar,
|
|
110
|
+
theme: {
|
|
111
|
+
bg: "#0b1220",
|
|
112
|
+
primary: "#4ea8de",
|
|
113
|
+
text: "#e8eef6",
|
|
114
|
+
surface: "#141d2e",
|
|
115
|
+
border: "#1f2b3f",
|
|
116
|
+
},
|
|
117
|
+
tools: {
|
|
118
|
+
answer_question: { icon: "\u{1F50D}", label: "Checking the knowledge base" },
|
|
119
|
+
list_topics: { icon: "\u{1F4DA}", label: "Listing topics" },
|
|
120
|
+
log_ticket: { icon: "\u{1F4DD}", label: "Logging a ticket" },
|
|
121
|
+
},
|
|
122
|
+
});
|