@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.
Files changed (231) hide show
  1. package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
  2. package/dist/_agent.d.ts +18 -0
  3. package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
  4. package/dist/_api-client.d.ts +39 -0
  5. package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
  6. package/dist/_bundler.d.ts +43 -5
  7. package/dist/_cli-common.d.ts +67 -3
  8. package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
  9. package/dist/_deploy.d.ts +2 -5
  10. package/dist/_dev-restart.d.ts +6 -1
  11. package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
  12. package/dist/_dev-server.d.ts +30 -2
  13. package/dist/_e2e-test-utils.d.ts +80 -0
  14. package/dist/_fault-mode.d.ts +175 -0
  15. package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
  16. package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
  17. package/dist/_output.d.ts +9 -2
  18. package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
  19. package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
  20. package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
  21. package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
  22. package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
  23. package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
  24. package/dist/_utils.d.ts +18 -2
  25. package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
  26. package/dist/_vite-env.d.ts +30 -0
  27. package/dist/build-BO1Ni6oJ.mjs +62 -0
  28. package/dist/build.d.ts +19 -0
  29. package/dist/cli-workflow.d.ts +10 -0
  30. package/dist/cli.d.ts +1 -2
  31. package/dist/cli.mjs +406 -224
  32. package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
  33. package/dist/client-bundler.mjs +1 -1
  34. package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
  35. package/dist/delete.d.ts +2 -8
  36. package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
  37. package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
  38. package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
  39. package/dist/eject.d.ts +30 -3
  40. package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
  41. package/dist/init.d.ts +11 -0
  42. package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
  43. package/dist/project-config.mjs +1 -1
  44. package/dist/scaffold/CLAUDE.md +498 -91
  45. package/dist/scaffold/package.json +11 -8
  46. package/dist/scaffold/pnpm-workspace.yaml +8 -0
  47. package/dist/scaffold/server.mjs +53 -57
  48. package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
  49. package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
  50. package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
  51. package/dist/templates/code-interpreter/agent.ts +0 -2
  52. package/dist/templates/dispatch-center/agent.test.ts +17 -19
  53. package/dist/templates/dispatch-center/agent.ts +2 -29
  54. package/dist/templates/dispatch-center/client.tsx +33 -16
  55. package/dist/templates/dispatch-center/shared.ts +53 -19
  56. package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
  57. package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
  58. package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
  59. package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
  60. package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
  61. package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
  62. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
  63. package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
  66. package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
  67. package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
  68. package/dist/templates/embedded-assets/agent.test.ts +83 -0
  69. package/dist/templates/embedded-assets/agent.ts +1 -48
  70. package/dist/templates/embedded-assets/shared.ts +17 -0
  71. package/dist/templates/embedded-assets/system-prompt.md +13 -0
  72. package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
  73. package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
  74. package/dist/templates/health-assistant/agent.test.ts +169 -0
  75. package/dist/templates/health-assistant/agent.ts +1 -166
  76. package/dist/templates/health-assistant/fda.ts +100 -0
  77. package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
  78. package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
  79. package/dist/templates/infocom-adventure/agent.test.ts +205 -0
  80. package/dist/templates/infocom-adventure/agent.ts +3 -106
  81. package/dist/templates/infocom-adventure/client.tsx +28 -12
  82. package/dist/templates/infocom-adventure/shared.ts +28 -11
  83. package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
  84. package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
  85. package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
  86. package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
  87. package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
  88. package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
  89. package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
  90. package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
  91. package/dist/templates/link-digest/agent.test.ts +223 -0
  92. package/dist/templates/link-digest/agent.ts +79 -0
  93. package/dist/templates/link-digest/client.tsx +146 -0
  94. package/dist/templates/link-digest/workflows/digest.ts +248 -0
  95. package/dist/templates/math-buddy/agent.ts +0 -2
  96. package/dist/templates/night-owl/agent.test.ts +80 -0
  97. package/dist/templates/night-owl/agent.ts +1 -68
  98. package/dist/templates/night-owl/system-prompt.md +10 -0
  99. package/dist/templates/night-owl/tools/recommend.ts +64 -0
  100. package/dist/templates/personal-finance/agent.ts +0 -2
  101. package/dist/templates/pizza-ordering/agent.test.ts +52 -47
  102. package/dist/templates/pizza-ordering/agent.ts +9 -158
  103. package/dist/templates/pizza-ordering/shared.ts +35 -12
  104. package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
  105. package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
  106. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
  107. package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
  108. package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
  109. package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
  110. package/dist/templates/plan-and-execute/agent.test.ts +411 -0
  111. package/dist/templates/plan-and-execute/agent.ts +23 -0
  112. package/dist/templates/plan-and-execute/client.tsx +115 -0
  113. package/dist/templates/plan-and-execute/graph.ts +178 -0
  114. package/dist/templates/plan-and-execute/prompts.ts +110 -0
  115. package/dist/templates/plan-and-execute/shared.ts +161 -0
  116. package/dist/templates/plan-and-execute/system-prompt.md +18 -0
  117. package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
  118. package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
  119. package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
  120. package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
  121. package/dist/templates/recap-workflow/agent.test.ts +762 -0
  122. package/dist/templates/recap-workflow/agent.ts +95 -0
  123. package/dist/templates/recap-workflow/shared.ts +43 -0
  124. package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
  125. package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
  126. package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
  127. package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
  128. package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
  129. package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
  130. package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
  131. package/dist/templates/redline/agent.test.ts +222 -0
  132. package/dist/templates/redline/agent.ts +89 -0
  133. package/dist/templates/redline/client.tsx +184 -0
  134. package/dist/templates/redline/workflows/prompts.ts +68 -0
  135. package/dist/templates/redline/workflows/redline.ts +258 -0
  136. package/dist/templates/research-workflow/agent.test.ts +525 -0
  137. package/dist/templates/research-workflow/agent.ts +94 -0
  138. package/dist/templates/research-workflow/shared.ts +35 -0
  139. package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
  140. package/dist/templates/research-workflow/tools/request_research.ts +33 -0
  141. package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
  142. package/dist/templates/research-workflow/tools/research_status.ts +40 -0
  143. package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
  144. package/dist/templates/research-workflow/workflows/research.ts +558 -0
  145. package/dist/templates/retail/agent.test.ts +236 -172
  146. package/dist/templates/retail/agent.ts +5 -42
  147. package/dist/templates/retail/client.tsx +33 -11
  148. package/dist/templates/retail/registry.test.ts +31 -16
  149. package/dist/templates/retail/resolve.ts +28 -64
  150. package/dist/templates/retail/shared.test.ts +4 -3
  151. package/dist/templates/retail/shared.ts +24 -7
  152. package/dist/templates/retail/store.test.ts +12 -10
  153. package/dist/templates/retail/store.ts +69 -30
  154. package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
  155. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
  156. package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
  157. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
  158. package/dist/templates/retail/tools/get_item_details.ts +5 -7
  159. package/dist/templates/retail/tools/get_order_details.ts +5 -7
  160. package/dist/templates/retail/tools/get_product_details.ts +4 -7
  161. package/dist/templates/retail/tools/get_user_details.ts +4 -7
  162. package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
  163. package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
  164. package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
  165. package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
  166. package/dist/templates/retail/tools/modify_user_address.ts +4 -9
  167. package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
  168. package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
  169. package/dist/templates/solo-rpg/agent.test.ts +152 -29
  170. package/dist/templates/solo-rpg/agent.ts +3 -21
  171. package/dist/templates/solo-rpg/client.tsx +19 -6
  172. package/dist/templates/solo-rpg/shared.ts +19 -11
  173. package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
  174. package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
  175. package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
  176. package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
  177. package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
  178. package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
  179. package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
  180. package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
  181. package/dist/templates/support-line/agent.test.ts +343 -0
  182. package/dist/templates/support-line/agent.ts +27 -0
  183. package/dist/templates/support-line/client.tsx +122 -0
  184. package/dist/templates/support-line/graph.ts +224 -0
  185. package/dist/templates/support-line/knowledge.json +65 -0
  186. package/dist/templates/support-line/prompts.ts +95 -0
  187. package/dist/templates/support-line/shared.ts +240 -0
  188. package/dist/templates/support-line/system-prompt.md +17 -0
  189. package/dist/templates/support-line/tools/answer_question.ts +72 -0
  190. package/dist/templates/support-line/tools/list_topics.ts +22 -0
  191. package/dist/templates/support-line/tools/log_ticket.ts +30 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +563 -0
  193. package/dist/templates/transcription-workflow/agent.ts +92 -0
  194. package/dist/templates/transcription-workflow/client.tsx +234 -0
  195. package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
  196. package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
  197. package/dist/templates/travel-concierge/agent.test.ts +338 -0
  198. package/dist/templates/travel-concierge/agent.ts +29 -0
  199. package/dist/templates/travel-concierge/client.tsx +161 -0
  200. package/dist/templates/travel-concierge/routing.ts +138 -0
  201. package/dist/templates/travel-concierge/shared.ts +547 -0
  202. package/dist/templates/travel-concierge/system-prompt.md +24 -0
  203. package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
  204. package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
  205. package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
  206. package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
  207. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
  208. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
  209. package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
  210. package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
  211. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
  212. package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
  213. package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
  214. package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
  215. package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
  216. package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
  217. package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
  218. package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
  219. package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
  220. package/dist/templates/web-researcher/agent.ts +0 -2
  221. package/dist/templates/web-researcher/system-prompt.md +10 -0
  222. package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
  223. package/dist/typecheck.mjs +17 -15
  224. package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
  225. package/dist/worker-bundler.d.ts +14 -0
  226. package/dist/worker-bundler.mjs +1 -108
  227. package/dist/workflow-B-O97AqI.mjs +125 -0
  228. package/dist/workflow-bundler.d.ts +133 -0
  229. package/dist/workflow.d.ts +67 -0
  230. package/package.json +13 -9
  231. package/dist/build-DQJ1ZSm9.mjs +0 -39
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The corrective-RAG loop, node by node.
3
+ *
4
+ * This is the port of the self-RAG / CRAG graph (see `prompts.ts` for the
5
+ * attribution). Their control flow is edges between nodes; here it is a `while`
6
+ * loop in one tool body, and the node names survive in {@link AnswerTrace.steps}
7
+ * so a run is still readable as the graph it came from:
8
+ *
9
+ * ```text
10
+ * retrieve → grade_documents → decide_to_generate
11
+ * ├─ no relevant docs → transform_query → retrieve
12
+ * └─ generate → grade_generation_v_documents
13
+ * ├─ not grounded → generate (once)
14
+ * └─ grade_generation_v_question
15
+ * ├─ not useful → transform_query
16
+ * └─ done
17
+ * ```
18
+ *
19
+ * **The budget is the mechanism, not the prompt.** A graph with a
20
+ * `transform_query` edge can cycle, and their notebooks bound it with a recursion
21
+ * limit that raises on hit. A caller is holding the line, so the bound here is
22
+ * two attempts and one regeneration — {@link MAX_ATTEMPTS} — and running out is
23
+ * an ANSWER (`exhausted`, the caller is offered a ticket) rather than an error.
24
+ * An agent that cannot say "I don't have that documented" is the failure the
25
+ * whole grading apparatus exists to prevent, so it has to be a reachable state.
26
+ *
27
+ * **Documents are graded CONCURRENTLY.** Their loop is sequential because a
28
+ * notebook does not care; four serial grader calls is four round trips of dead
29
+ * air on a phone. The grades are independent, so the fan-out is free.
30
+ */
31
+
32
+ import type { GenerateFn } from "@alexkroman1/aai";
33
+ import {
34
+ ANSWER_SYSTEM,
35
+ ANSWERS_SYSTEM,
36
+ binaryScore,
37
+ DOC_GRADER_SYSTEM,
38
+ GROUNDED_SYSTEM,
39
+ REWRITE_SYSTEM,
40
+ } from "./prompts.ts";
41
+ import type { AnswerTrace, Doc, GradedDoc, TraceStep } from "./shared.ts";
42
+ import { formatDoc, retrieve } from "./shared.ts";
43
+
44
+ /** Retrieve-and-answer attempts, i.e. one query rewrite. */
45
+ export const MAX_ATTEMPTS = 2;
46
+ /** Regenerations after a "not grounded" verdict, within one attempt. */
47
+ export const MAX_REGENERATIONS = 1;
48
+
49
+ function step(trace: AnswerTrace, node: string, detail: string): void {
50
+ const entry: TraceStep = { node, detail };
51
+ trace.steps.push(entry);
52
+ }
53
+
54
+ /** `grade_documents` — one closed question per document, all at once. */
55
+ async function gradeDocuments(
56
+ generate: GenerateFn,
57
+ question: string,
58
+ docs: Doc[],
59
+ ): Promise<GradedDoc[]> {
60
+ return await Promise.all(
61
+ docs.map(async (doc) => {
62
+ const { object } = await generate({
63
+ system: DOC_GRADER_SYSTEM,
64
+ prompt: `Caller's question: ${question}\n\nDocument:\n${formatDoc(doc)}`,
65
+ schema: binaryScore,
66
+ });
67
+ return {
68
+ id: doc.id,
69
+ title: doc.title,
70
+ relevant: object.score === "yes",
71
+ reason: object.reason,
72
+ };
73
+ }),
74
+ );
75
+ }
76
+
77
+ /** `transform_query` — their question re-writer, aimed at a keyword index. */
78
+ async function transformQuery(generate: GenerateFn, question: string): Promise<string> {
79
+ const { text } = await generate({
80
+ system: REWRITE_SYSTEM,
81
+ prompt: `Caller's question: ${question}`,
82
+ });
83
+ const rewritten = text.trim().replace(/^["']|["']$/g, "");
84
+ // A rewriter that returns nothing usable must not empty the query — that
85
+ // would retrieve zero documents and read as "we have nothing on this".
86
+ return rewritten.length > 0 ? rewritten : question;
87
+ }
88
+
89
+ /** `generate` — the answer, from the graded-relevant documents only. */
90
+ async function generateAnswer(
91
+ generate: GenerateFn,
92
+ question: string,
93
+ docs: Doc[],
94
+ ): Promise<string> {
95
+ const { text } = await generate({
96
+ system: ANSWER_SYSTEM,
97
+ prompt: `Documents:\n\n${docs.map(formatDoc).join("\n\n")}\n\nCaller's question: ${question}`,
98
+ });
99
+ return text.trim();
100
+ }
101
+
102
+ /** The two generation graders — same closed question, different subject. */
103
+ async function grade(
104
+ generate: GenerateFn,
105
+ system: string,
106
+ prompt: string,
107
+ ): Promise<{ pass: boolean; reason: string }> {
108
+ const { object } = await generate({ system, prompt, schema: binaryScore });
109
+ return { pass: object.score === "yes", reason: object.reason };
110
+ }
111
+
112
+ /**
113
+ * Run the graph for one caller question. Never throws for a bad ANSWER — only
114
+ * for a broken model call, which is the tool's to report.
115
+ */
116
+ export async function runCorrectiveRag(
117
+ generate: GenerateFn,
118
+ question: string,
119
+ ): Promise<AnswerTrace> {
120
+ const trace: AnswerTrace = {
121
+ question,
122
+ query: question,
123
+ rewrites: 0,
124
+ steps: [],
125
+ docs: [],
126
+ answer: null,
127
+ grounded: null,
128
+ useful: null,
129
+ exhausted: false,
130
+ };
131
+
132
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
133
+ const retrieved = retrieve(trace.query);
134
+ step(trace, "retrieve", `${retrieved.length} document(s) for "${trace.query}"`);
135
+
136
+ const graded = await gradeDocuments(
137
+ generate,
138
+ question,
139
+ retrieved.map((one) => one.doc),
140
+ );
141
+ trace.docs = graded;
142
+ const relevant = retrieved.filter((one) => graded.find((g) => g.id === one.doc.id)?.relevant);
143
+ step(trace, "grade_documents", `${relevant.length} of ${graded.length} relevant`);
144
+
145
+ // `decide_to_generate`: nothing relevant means the retrieval was wrong, not
146
+ // that the answer is unknown — so rewrite and try once more before giving up.
147
+ if (relevant.length === 0) {
148
+ if (attempt < MAX_ATTEMPTS) {
149
+ trace.query = await transformQuery(generate, question);
150
+ trace.rewrites++;
151
+ step(trace, "transform_query", `retrying as "${trace.query}"`);
152
+ continue;
153
+ }
154
+ trace.exhausted = true;
155
+ step(trace, "decide_to_generate", "no relevant documents, and no attempts left");
156
+ return trace;
157
+ }
158
+
159
+ const docs = relevant.map((one) => one.doc);
160
+ const facts = docs.map(formatDoc).join("\n\n");
161
+ let answer = await generateAnswer(generate, question, docs);
162
+ step(trace, "generate", `answered from ${docs.map((d) => d.id).join(", ")}`);
163
+
164
+ // `grade_generation_v_documents`: is it grounded? A regeneration is worth
165
+ // one shot — the same documents with the ungrounded attempt now visible.
166
+ let grounded = await grade(
167
+ generate,
168
+ GROUNDED_SYSTEM,
169
+ `Facts:\n\n${facts}\n\nAnswer: ${answer}`,
170
+ );
171
+ for (let redo = 0; !grounded.pass && redo < MAX_REGENERATIONS; redo++) {
172
+ step(trace, "grade_generation_v_documents", `not grounded: ${grounded.reason}`);
173
+ answer = await generateAnswer(generate, question, docs);
174
+ step(trace, "generate", "regenerated");
175
+ grounded = await grade(generate, GROUNDED_SYSTEM, `Facts:\n\n${facts}\n\nAnswer: ${answer}`);
176
+ }
177
+ trace.answer = answer;
178
+ trace.grounded = grounded.pass;
179
+ step(
180
+ trace,
181
+ "grade_generation_v_documents",
182
+ grounded.pass ? "grounded" : `still not grounded: ${grounded.reason}`,
183
+ );
184
+ if (!grounded.pass) {
185
+ // An ungrounded answer is never spoken. Two regenerations that both
186
+ // invented something is a signal about the documents, not the phrasing.
187
+ trace.answer = null;
188
+ trace.exhausted = true;
189
+ return trace;
190
+ }
191
+
192
+ // `grade_generation_v_question`: grounded is not the same as useful.
193
+ const useful = await grade(
194
+ generate,
195
+ ANSWERS_SYSTEM,
196
+ `Question: ${question}\n\nAnswer: ${answer}`,
197
+ );
198
+ trace.useful = useful.pass;
199
+ step(
200
+ trace,
201
+ "grade_generation_v_question",
202
+ useful.pass ? "useful" : `not useful: ${useful.reason}`,
203
+ );
204
+ if (useful.pass) return trace;
205
+
206
+ if (attempt < MAX_ATTEMPTS) {
207
+ trace.query = await transformQuery(generate, question);
208
+ trace.rewrites++;
209
+ step(trace, "transform_query", `retrying as "${trace.query}"`);
210
+ trace.answer = null;
211
+ trace.grounded = null;
212
+ trace.useful = null;
213
+ continue;
214
+ }
215
+ // Out of attempts holding an answer that is grounded but beside the point.
216
+ // It is still the best thing we have, so it is returned WITH its verdict —
217
+ // the tool tells the model to offer a ticket alongside it.
218
+ trace.exhausted = true;
219
+ return trace;
220
+ }
221
+
222
+ trace.exhausted = true;
223
+ return trace;
224
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "product": "Meridian Fibre",
3
+ "docs": [
4
+ {
5
+ "id": "D1",
6
+ "title": "Router lights and what they mean",
7
+ "topic": "connection",
8
+ "text": "The Meridian hub has three lights. A solid white power light means the hub is on. A solid blue broadband light means the line is synced. A flashing blue light means the hub is still syncing, which can take up to fifteen minutes after a power cut. A red broadband light means the line is down: check the fibre cable is pushed fully into the grey socket, then leave the hub powered on for fifteen minutes before doing anything else."
9
+ },
10
+ {
11
+ "id": "D2",
12
+ "title": "Rebooting the hub",
13
+ "topic": "connection",
14
+ "text": "Power the hub off at the wall, wait thirty seconds, and power it back on. Do not use the pinhole reset button unless an agent asks you to: that clears your wifi name and password as well, and every device in the house then has to be reconnected. A normal reboot keeps all your settings."
15
+ },
16
+ {
17
+ "id": "D3",
18
+ "title": "Slow speeds over wifi",
19
+ "topic": "speed",
20
+ "text": "Wifi speed drops with distance and with walls. Keep the hub off the floor, out of a cupboard, and away from the microwave and the TV. A wired device plugged into a yellow ethernet port is the only way to measure your real line speed; a speed test over wifi measures the wifi, not the fibre. If a wired test is more than twenty percent below your plan speed for three days running, that is a line fault and we will send an engineer."
21
+ },
22
+ {
23
+ "id": "D4",
24
+ "title": "Your bill and when it is taken",
25
+ "topic": "billing",
26
+ "text": "Bills are issued on the first of the month and the direct debit is taken eight working days later. Your first bill also carries a part month charge for the days between your activation date and the first of the month, so it is usually larger than the ones that follow. You can change the payment date once every twelve months from the account page."
27
+ },
28
+ {
29
+ "id": "D5",
30
+ "title": "Cancelling your contract",
31
+ "topic": "account",
32
+ "text": "You can cancel at any time by giving thirty days notice. If you are still inside your minimum term, an early exit fee applies: it is the remaining months of the term multiplied by your monthly price, capped at six months. Notice starts on the day we receive it, not the day you post the equipment back. The hub must be returned within fourteen days of the service ending or a fifty pound non return fee is charged."
33
+ },
34
+ {
35
+ "id": "D6",
36
+ "title": "Cancelling or rebooking an engineer visit",
37
+ "topic": "appointments",
38
+ "text": "An engineer visit can be moved or cancelled free of charge until midnight the day before. After that, and for a missed appointment where nobody over eighteen was at the property, a sixty pound abortive visit charge applies. Rebooking is done from the appointments page or by asking us on the phone; the next available slot is usually within three working days."
39
+ },
40
+ {
41
+ "id": "D7",
42
+ "title": "Moving home",
43
+ "topic": "account",
44
+ "text": "Tell us at least four weeks before you move. If Meridian Fibre is available at the new address we move the service and your contract continues unchanged, with a one off activation charge of thirty five pounds. If we cannot serve the new address, the contract ends with no early exit fee, which is the one case where the fee is waived."
45
+ },
46
+ {
47
+ "id": "D8",
48
+ "title": "Checking for an outage in your area",
49
+ "topic": "connection",
50
+ "text": "Area outages are listed on the status page and the phone line plays a recorded message naming the affected postcodes. During a confirmed outage there is nothing to fix at your end: rebooting the hub will not help and an engineer cannot be booked until the area work is closed. Compensation of eight pounds per full day starts after the second full day of a confirmed total loss of service."
51
+ },
52
+ {
53
+ "id": "D9",
54
+ "title": "Static IP addresses",
55
+ "topic": "advanced",
56
+ "text": "A static IP address is available on business plans only, at five pounds a month. It cannot be added to a residential plan; upgrading to business is the only route, and that starts a new twelve month term. Port forwarding, by contrast, works on every plan and needs no static IP."
57
+ },
58
+ {
59
+ "id": "D10",
60
+ "title": "Data caps and traffic management",
61
+ "topic": "speed",
62
+ "text": "There is no data cap on any Meridian Fibre plan and no traffic management at peak times. If your speed drops every evening at the same time, that is congestion in your own home, usually a device backing up or streaming in the background, and not something we apply."
63
+ }
64
+ ]
65
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The graders, the rewriter and the answer prompt — and where they come from.
3
+ *
4
+ * **Adapted from LangGraph's self-RAG and corrective-RAG notebooks** (MIT,
5
+ * <https://github.com/langchain-ai/langgraph>,
6
+ * `docs/docs/tutorials/rag/langgraph_self_rag.ipynb` and
7
+ * `langgraph_crag.ipynb`), whose graders are `ChatPromptTemplate`s bound to
8
+ * small pydantic models with one field: a binary `"yes"`/`"no"` score.
9
+ *
10
+ * | self-RAG / CRAG node | here |
11
+ * | --- | --- |
12
+ * | `retrieval_grader` (`GradeDocuments`) | {@link DOC_GRADER_SYSTEM} + {@link binaryScore} |
13
+ * | `question_rewriter` | {@link REWRITE_SYSTEM} |
14
+ * | `rag_chain` (`rlm/rag-prompt`) | {@link ANSWER_SYSTEM} |
15
+ * | `hallucination_grader` (`GradeHallucinations`) | {@link GROUNDED_SYSTEM} |
16
+ * | `answer_grader` (`GradeAnswer`) | {@link ANSWERS_SYSTEM} |
17
+ *
18
+ * **The binary score is the load-bearing part and it is kept verbatim in
19
+ * shape.** Their whole design rests on asking a model one closed question at a
20
+ * time rather than "is this good?", because a closed question is one a small
21
+ * model answers reliably and a caller's answer can be routed on. `reason` is an
22
+ * addition: it costs a few tokens, it is what the sidebar renders per document,
23
+ * and asking for it is the cheapest way to see WHY a grader is being strict.
24
+ *
25
+ * What is adapted is the audience. Theirs answer into a notebook and may take
26
+ * three sentences of markdown; ours is read down a phone by a support line, so
27
+ * {@link ANSWER_SYSTEM} asks for two spoken sentences and forbids the "based on
28
+ * the provided context" preamble that a caller hears as evasion.
29
+ */
30
+
31
+ import { z } from "zod";
32
+
33
+ /**
34
+ * Their `GradeDocuments` / `GradeHallucinations` / `GradeAnswer`, as one
35
+ * schema — all three are the same closed question with a different subject, and
36
+ * `ctx.generate({ schema })` constrains the model to it.
37
+ */
38
+ export const binaryScore = z.object({
39
+ score: z.enum(["yes", "no"]).describe("Binary score, 'yes' or 'no'"),
40
+ reason: z.string().max(200).describe("One short sentence of justification"),
41
+ });
42
+
43
+ export type BinaryScore = z.infer<typeof binaryScore>;
44
+
45
+ /** Their `retrieval_grader`: deliberately NOT a stringent test. */
46
+ export const DOC_GRADER_SYSTEM = [
47
+ "You are a grader assessing whether a retrieved support document is relevant",
48
+ "to a caller's question. If the document contains keywords or meaning related",
49
+ "to the question, grade it relevant. This does not need to be a stringent",
50
+ "test — the goal is to filter out erroneous retrievals, not to pick the single",
51
+ "best document. Documents about a NEIGHBOURING subject (cancelling an",
52
+ "appointment when the caller asked about cancelling a contract) are NOT",
53
+ "relevant; answering from one of those is the specific mistake you exist to",
54
+ "prevent.",
55
+ "Give a binary score, 'yes' or 'no'.",
56
+ ].join(" ");
57
+
58
+ /** Their `question_rewriter`, retargeted from a vectorstore to a keyword index. */
59
+ export const REWRITE_SYSTEM = [
60
+ "You rewrite a caller's spoken question into a better search query for a",
61
+ "support knowledge base that matches on WORDS, not meaning.",
62
+ "Reason about what the caller actually means, then write the terms a support",
63
+ "document about it would itself use — including the words they did not say.",
64
+ '"my internet keeps dying at night" should become something like',
65
+ '"evening slowdown congestion speed drops peak time".',
66
+ "Reply with the query alone: no quotes, no explanation, under fifteen words.",
67
+ ].join(" ");
68
+
69
+ /** Their `rag_chain` prompt, rewritten for a voice line. */
70
+ export const ANSWER_SYSTEM = [
71
+ "You are a support agent answering a caller using ONLY the documents given to",
72
+ "you. If the documents do not contain the answer, say you do not have it",
73
+ "documented — do not fill the gap from general knowledge.",
74
+ "Two sentences maximum, spoken plainly, as you would say them out loud.",
75
+ "Never say 'based on the provided context' or 'according to the documents':",
76
+ "the caller hears that as evasion. Just answer.",
77
+ "Give the specific number, fee or step where the documents have one.",
78
+ ].join(" ");
79
+
80
+ /** Their `hallucination_grader`. */
81
+ export const GROUNDED_SYSTEM = [
82
+ "You are a grader assessing whether an answer is grounded in and supported by",
83
+ "a set of facts. Give a binary score, 'yes' or 'no'.",
84
+ "'Yes' means every claim in the answer is supported by the facts.",
85
+ "An answer that says it does not know is grounded — refusing to guess is",
86
+ "supported by any set of facts.",
87
+ ].join(" ");
88
+
89
+ /** Their `answer_grader`: grounded is not the same as useful. */
90
+ export const ANSWERS_SYSTEM = [
91
+ "You are a grader assessing whether an answer actually resolves the caller's",
92
+ "question. Give a binary score, 'yes' or 'no'.",
93
+ "'Yes' means the caller now knows what to do or what is true.",
94
+ "An answer that is correct but about something adjacent scores 'no'.",
95
+ ].join(" ");
@@ -0,0 +1,240 @@
1
+ /**
2
+ * The knowledge base, the retriever, and what the browser is shown.
3
+ *
4
+ * **The retriever is lexical, and that is a deliberate difference from the
5
+ * notebooks this template ports.** Their self-RAG and CRAG graphs retrieve from
6
+ * a Chroma vectorstore over OpenAI embeddings; this SDK has no vector store
7
+ * (`ctx.vector` was removed — see the root guide), and a template that needed
8
+ * one would be a template nobody can run. So retrieval here is term overlap
9
+ * with an inverse-document-frequency weight, which is genuinely worse at
10
+ * meaning and genuinely fine at words.
11
+ *
12
+ * That makes the corrective loop MORE valuable rather than less, which is the
13
+ * point worth taking away: CRAG exists because retrieval is imperfect, and a
14
+ * weaker retriever is exactly the case its grader and its query rewriter were
15
+ * designed for. A caller saying "my internet keeps dying at night" retrieves
16
+ * nothing useful by words alone — the rewrite step is what turns it into
17
+ * "evening slowdown congestion peak time" and finds D10.
18
+ */
19
+
20
+ import { type DeepReadonly, pushCapped, sessionSlot } from "@alexkroman1/aai";
21
+ import knowledge from "./knowledge.json" with { type: "json" };
22
+
23
+ export interface Doc {
24
+ id: string;
25
+ title: string;
26
+ topic: string;
27
+ text: string;
28
+ }
29
+
30
+ export const PRODUCT: string = knowledge.product;
31
+ export const DOCS: Doc[] = knowledge.docs;
32
+
33
+ export const TOPICS: string[] = [...new Set(DOCS.map((doc) => doc.topic))].sort();
34
+
35
+ // ─── Retrieval ───────────────────────────────────────────────────────────────
36
+
37
+ /** Words that match everything and therefore rank nothing. */
38
+ const STOPWORDS = new Set([
39
+ "the",
40
+ "and",
41
+ "for",
42
+ "you",
43
+ "your",
44
+ "are",
45
+ "was",
46
+ "with",
47
+ "that",
48
+ "this",
49
+ "have",
50
+ "has",
51
+ "can",
52
+ "not",
53
+ "but",
54
+ "how",
55
+ "why",
56
+ "what",
57
+ "when",
58
+ "will",
59
+ "from",
60
+ "into",
61
+ "out",
62
+ "get",
63
+ "does",
64
+ "did",
65
+ "any",
66
+ "all",
67
+ "our",
68
+ "their",
69
+ "there",
70
+ "then",
71
+ "than",
72
+ "about",
73
+ "just",
74
+ "been",
75
+ "were",
76
+ "they",
77
+ "them",
78
+ "his",
79
+ "her",
80
+ "its",
81
+ "one",
82
+ "two",
83
+ "who",
84
+ "whom",
85
+ "some",
86
+ ]);
87
+
88
+ function tokenize(text: string): string[] {
89
+ return text
90
+ .toLowerCase()
91
+ .split(/[^a-z0-9]+/)
92
+ .filter((word) => word.length > 2 && !STOPWORDS.has(word));
93
+ }
94
+
95
+ /** Term → how many documents contain it. Computed once at module load. */
96
+ const DOC_FREQUENCY = new Map<string, number>();
97
+ const DOC_TOKENS = new Map<string, string[]>();
98
+ for (const doc of DOCS) {
99
+ const tokens = tokenize(`${doc.title} ${doc.topic} ${doc.text}`);
100
+ DOC_TOKENS.set(doc.id, tokens);
101
+ for (const term of new Set(tokens)) {
102
+ DOC_FREQUENCY.set(term, (DOC_FREQUENCY.get(term) ?? 0) + 1);
103
+ }
104
+ }
105
+
106
+ /** How many documents one retrieval returns before grading. */
107
+ export const RETRIEVE_K = 4;
108
+
109
+ export interface Retrieved {
110
+ doc: Doc;
111
+ score: number;
112
+ }
113
+
114
+ /**
115
+ * The `retrieve` node: top-k by idf-weighted term overlap.
116
+ *
117
+ * A term in one document is worth much more than a term in eight — without the
118
+ * weight, "fibre" and "hub" (in nearly every document) drown the one word that
119
+ * actually distinguishes the caller's question.
120
+ */
121
+ export function retrieve(query: string, k: number = RETRIEVE_K): Retrieved[] {
122
+ const terms = new Set(tokenize(query));
123
+ if (terms.size === 0) return [];
124
+ const scored: Retrieved[] = [];
125
+ for (const doc of DOCS) {
126
+ const tokens = DOC_TOKENS.get(doc.id) ?? [];
127
+ let score = 0;
128
+ for (const term of terms) {
129
+ const hits = tokens.filter((token) => token === term).length;
130
+ if (hits === 0) continue;
131
+ const frequency = DOC_FREQUENCY.get(term) ?? 1;
132
+ // Diminishing returns per repeat, so one long document cannot win on
133
+ // repetition alone.
134
+ score += Math.log(1 + hits) * Math.log(DOCS.length / frequency + 1);
135
+ }
136
+ if (score > 0) scored.push({ doc, score });
137
+ }
138
+ return scored.sort((a, b) => b.score - a.score).slice(0, k);
139
+ }
140
+
141
+ /** One document as the answer prompt sees it. */
142
+ export function formatDoc(doc: Doc): string {
143
+ return `[${doc.id}] ${doc.title}\n${doc.text}`;
144
+ }
145
+
146
+ // ─── The trace ───────────────────────────────────────────────────────────────
147
+ // Their graph is watched by streaming node names to a notebook. A caller hears
148
+ // none of that, so the run records itself and the browser renders it.
149
+
150
+ export interface GradedDoc {
151
+ id: string;
152
+ title: string;
153
+ relevant: boolean;
154
+ reason: string;
155
+ }
156
+
157
+ export interface TraceStep {
158
+ /** The node's name, spelled as their graph spells it. */
159
+ node: string;
160
+ detail: string;
161
+ }
162
+
163
+ export interface AnswerTrace {
164
+ question: string;
165
+ /** The query retrieval actually ran on — rewritten, if it was. */
166
+ query: string;
167
+ rewrites: number;
168
+ steps: TraceStep[];
169
+ docs: GradedDoc[];
170
+ answer: string | null;
171
+ /** `null` until the hallucination grader has run. */
172
+ grounded: boolean | null;
173
+ /** `null` until the answer grader has run. */
174
+ useful: boolean | null;
175
+ /** True when the loop gave up and the caller should be offered a ticket. */
176
+ exhausted: boolean;
177
+ }
178
+
179
+ export interface Ticket {
180
+ reference: string;
181
+ question: string;
182
+ /** Server-side only — the projection never carries it. */
183
+ callback: string;
184
+ }
185
+
186
+ export interface SupportState {
187
+ /** The most recent run, which is what the sidebar renders. */
188
+ trace: AnswerTrace | null;
189
+ /** Every question this call has asked, capped. */
190
+ asked: string[];
191
+ ticket: Ticket | null;
192
+ ticketCounter: number;
193
+ }
194
+
195
+ export const MAX_ASKED = 20;
196
+
197
+ export function emptySupportState(): SupportState {
198
+ return { trace: null, asked: [], ticket: null, ticketCounter: 0 };
199
+ }
200
+
201
+ export const supportSlot = sessionSlot("support", emptySupportState);
202
+
203
+ /**
204
+ * The call as a READ hands it out: deep-frozen, and typed to say so.
205
+ *
206
+ * {@link supportView} takes this rather than {@link SupportState}, which is the
207
+ * widening a deep-readonly slot forces and the reason it is worth doing: a
208
+ * mutable state still satisfies it, so a call with an `update` draft is
209
+ * unaffected, while a projection that WOULD have mutated stops compiling
210
+ * instead of throwing at its first call.
211
+ */
212
+ export type FrozenSupportState = DeepReadonly<SupportState>;
213
+
214
+ export function recordQuestion(state: SupportState, question: string): void {
215
+ pushCapped(state.asked, question, MAX_ASKED);
216
+ }
217
+
218
+ // ─── The projection ──────────────────────────────────────────────────────────
219
+
220
+ export interface SupportView {
221
+ product: string;
222
+ trace: DeepReadonly<AnswerTrace> | null;
223
+ asked: readonly string[];
224
+ /** The reference only — the callback number stays on the server. */
225
+ ticket: string | null;
226
+ }
227
+
228
+ /**
229
+ * What the browser sees. The `ticket` field is why this is a projection rather
230
+ * than the state itself: a ticket carries the caller's phone number, and
231
+ * `syncState` is where you decide what leaves the server.
232
+ */
233
+ export function supportView(state: FrozenSupportState): SupportView {
234
+ return {
235
+ product: PRODUCT,
236
+ trace: state.trace,
237
+ asked: state.asked,
238
+ ticket: state.ticket?.reference ?? null,
239
+ };
240
+ }
@@ -0,0 +1,17 @@
1
+ You are the automated support line for Meridian Fibre, a home broadband provider. You are on a phone call. Answers are one or two sentences, spoken plainly, no lists and no markdown.
2
+
3
+ The one rule that matters:
4
+
5
+ - You do not know anything about Meridian Fibre. Everything you say about the product, its prices, its fees, its equipment or its process must come from `answer_question`. If you find yourself about to state a fee or a timescale you did not just get from that tool, stop and call it.
6
+ - `answer_question` checks itself. It retrieves, grades what it retrieved, rewrites the question and retries if the retrieval was poor, and refuses to hand you an answer it could not ground in a document. Trust its verdict over your own instinct.
7
+
8
+ Running a call:
9
+
10
+ - The caller is waiting while the lookup runs, so say a short "let me check that for you" before calling `answer_question` — never in silence.
11
+ - Give the answer it returns in your own words, keeping every number exactly as it came back.
12
+ - When it returns no answer, say plainly that you do not have that documented. Do not guess, and do not soften it into a maybe. Offer to log a ticket.
13
+ - When it returns an answer with a caveat, give the answer and then offer the ticket.
14
+ - To log a ticket, ask for a callback number, call `log_ticket`, and read the reference back digit by digit.
15
+ - Use `list_topics` when the caller does not know what to ask, or to say what you can help with after a failed lookup.
16
+
17
+ Manners: one question at a time, no jargon the caller did not use first, and never blame the caller for the fault.