@alexkroman1/aai-cli 5.13.2 → 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 +32 -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 +21 -30
- package/dist/templates/dispatch-center/agent.ts +4 -31
- package/dist/templates/dispatch-center/client.tsx +41 -28
- package/dist/templates/dispatch-center/shared.ts +73 -66
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +13 -15
- package/dist/templates/dispatch-center/tools/incident_create.ts +42 -45
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +60 -62
- package/dist/templates/dispatch-center/tools/incident_get.ts +10 -6
- package/dist/templates/dispatch-center/tools/incident_triage.ts +44 -46
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +40 -42
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +3 -6
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +21 -24
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +53 -55
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +3 -5
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +40 -43
- 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 +32 -28
- package/dist/templates/infocom-adventure/shared.ts +36 -16
- 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 +59 -58
- package/dist/templates/pizza-ordering/agent.ts +10 -159
- package/dist/templates/pizza-ordering/client.tsx +4 -4
- package/dist/templates/pizza-ordering/shared.ts +44 -27
- 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 +319 -256
- package/dist/templates/retail/agent.ts +6 -46
- package/dist/templates/retail/authenticate.ts +2 -2
- package/dist/templates/retail/client.tsx +45 -24
- package/dist/templates/retail/registry.test.ts +40 -25
- package/dist/templates/retail/resolve.test.ts +17 -16
- package/dist/templates/retail/resolve.ts +32 -67
- package/dist/templates/retail/shared.test.ts +21 -19
- package/dist/templates/retail/shared.ts +61 -23
- package/dist/templates/retail/store.test.ts +47 -52
- package/dist/templates/retail/store.ts +92 -79
- package/dist/templates/retail/swap.test.ts +20 -17
- package/dist/templates/retail/swap.ts +7 -14
- package/dist/templates/retail/tools/cancel_pending_order.ts +7 -11
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +26 -14
- 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 +7 -8
- package/dist/templates/retail/tools/get_order_details.ts +7 -8
- package/dist/templates/retail/tools/get_product_details.ts +6 -8
- package/dist/templates/retail/tools/get_user_details.ts +6 -8
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +6 -10
- package/dist/templates/retail/tools/modify_pending_order_items.ts +8 -20
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +7 -13
- package/dist/templates/retail/tools/modify_user_address.ts +6 -10
- package/dist/templates/retail/tools/return_delivered_order_items.ts +7 -13
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +169 -54
- package/dist/templates/solo-rpg/agent.ts +4 -22
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +20 -20
- package/dist/templates/solo-rpg/tools/action_roll.ts +3 -5
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +3 -5
- package/dist/templates/solo-rpg/tools/check_state.ts +7 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +3 -3
- package/dist/templates/solo-rpg/tools/oracle.ts +19 -12
- package/dist/templates/solo-rpg/tools/save_game.ts +11 -8
- package/dist/templates/solo-rpg/tools/setup_character.ts +3 -3
- package/dist/templates/solo-rpg/tools/update_state.ts +13 -13
- 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
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import type { StateSlot } from "./shared.ts";
|
|
3
2
|
import { storeView } from "./shared.ts";
|
|
4
|
-
import {
|
|
5
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
6
|
-
import { cancelPendingOrder } from "./tools/cancel_pending_order.ts";
|
|
7
|
-
import { exchangeDeliveredOrderItems } from "./tools/exchange_delivered_order_items.ts";
|
|
8
|
-
import { findUserIdByEmail } from "./tools/find_user_id_by_email.ts";
|
|
9
|
-
import { findUserIdByNameZip } from "./tools/find_user_id_by_name_zip.ts";
|
|
10
|
-
import { getItemDetails } from "./tools/get_item_details.ts";
|
|
11
|
-
import { getOrderDetails } from "./tools/get_order_details.ts";
|
|
12
|
-
import { getProductDetails } from "./tools/get_product_details.ts";
|
|
13
|
-
import { getUserDetails } from "./tools/get_user_details.ts";
|
|
14
|
-
import { listAllProductTypes } from "./tools/list_all_product_types.ts";
|
|
15
|
-
import { modifyPendingOrderAddress } from "./tools/modify_pending_order_address.ts";
|
|
16
|
-
import { modifyPendingOrderItems } from "./tools/modify_pending_order_items.ts";
|
|
17
|
-
import { modifyPendingOrderPayment } from "./tools/modify_pending_order_payment.ts";
|
|
18
|
-
import { modifyUserAddress } from "./tools/modify_user_address.ts";
|
|
19
|
-
import { returnDeliveredOrderItems } from "./tools/return_delivered_order_items.ts";
|
|
20
|
-
import { transferToHumanAgents } from "./tools/transfer_to_human_agents.ts";
|
|
3
|
+
import { retailSlot } from "./store.ts";
|
|
21
4
|
|
|
22
5
|
export default agent({
|
|
23
6
|
name: "Retail Support",
|
|
@@ -26,44 +9,21 @@ export default agent({
|
|
|
26
9
|
// filled from the all-AssemblyAI pipeline at parse time. Only `stt` is
|
|
27
10
|
// overridden below; `llm` and `tts` take the defaults.
|
|
28
11
|
|
|
29
|
-
// The store lives in
|
|
30
|
-
// not see each other's cancellations.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// and `StateSlot`'s `retail` field is optional (a "weak type"), so handing
|
|
34
|
-
// it a bare `RetailState` with no properties in common fails that inference
|
|
35
|
-
// rather than an ordinary structural check.
|
|
36
|
-
state: (): StateSlot => ({ retail: createDefaultState() }),
|
|
12
|
+
// The store lives in one `sessionSlot` (`store.ts`), a pristine copy per
|
|
13
|
+
// session — callers must not see each other's cancellations. Nothing declares
|
|
14
|
+
// it here: the slot installs itself on first access, and its `projection`
|
|
15
|
+
// below is what gives a session that has run no tool something to render.
|
|
37
16
|
|
|
38
17
|
// One projection pushed after every tool call. It is a projection, not a
|
|
39
18
|
// flag, because the state holds all six seeded customers and only the
|
|
40
19
|
// authenticated one may reach the browser.
|
|
41
|
-
syncState: storeView,
|
|
20
|
+
syncState: retailSlot.projection(storeView),
|
|
42
21
|
|
|
43
22
|
// Callers read order numbers and ten-digit item numbers in bursts with pauses
|
|
44
23
|
// inside one utterance ("W seven six seven … eight oh seven two"). The default
|
|
45
24
|
// pipeline's `max_turn_silence` already tolerates that; reach for
|
|
46
25
|
// `assemblyAIStt({ maxTurnSilenceMs })` only if your callers pause longer.
|
|
47
|
-
systemPrompt,
|
|
48
26
|
greeting:
|
|
49
27
|
"Thanks for calling. Before I can look anything up I'll need to find your account — " +
|
|
50
28
|
"what's the email address on it?",
|
|
51
|
-
|
|
52
|
-
tools: {
|
|
53
|
-
cancel_pending_order: cancelPendingOrder,
|
|
54
|
-
exchange_delivered_order_items: exchangeDeliveredOrderItems,
|
|
55
|
-
find_user_id_by_email: findUserIdByEmail,
|
|
56
|
-
find_user_id_by_name_zip: findUserIdByNameZip,
|
|
57
|
-
get_item_details: getItemDetails,
|
|
58
|
-
get_order_details: getOrderDetails,
|
|
59
|
-
get_product_details: getProductDetails,
|
|
60
|
-
get_user_details: getUserDetails,
|
|
61
|
-
list_all_product_types: listAllProductTypes,
|
|
62
|
-
modify_pending_order_address: modifyPendingOrderAddress,
|
|
63
|
-
modify_pending_order_items: modifyPendingOrderItems,
|
|
64
|
-
modify_pending_order_payment: modifyPendingOrderPayment,
|
|
65
|
-
modify_user_address: modifyUserAddress,
|
|
66
|
-
return_delivered_order_items: returnDeliveredOrderItems,
|
|
67
|
-
transfer_to_human_agents: transferToHumanAgents,
|
|
68
|
-
},
|
|
69
29
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { ToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import type { RetailState, User } from "./shared.ts";
|
|
2
|
-
import type { ErrorResult } from "./store.ts";
|
|
3
3
|
|
|
4
4
|
export interface AuthResult {
|
|
5
5
|
user_id: string;
|
|
@@ -16,7 +16,7 @@ export interface AuthResult {
|
|
|
16
16
|
* order you want. Re-running a finder for the SAME customer succeeds, because a
|
|
17
17
|
* caller repeating their email should not hit an error.
|
|
18
18
|
*/
|
|
19
|
-
export function authenticateAs(state: RetailState, user: User): AuthResult |
|
|
19
|
+
export function authenticateAs(state: RetailState, user: User): AuthResult | ToolFailure {
|
|
20
20
|
const current = state.authenticatedUserId;
|
|
21
21
|
if (current && current !== user.user_id) {
|
|
22
22
|
return {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import type { ChatMessage } from "@alexkroman1/aai-ui";
|
|
3
|
-
import {
|
|
2
|
+
import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
|
|
3
|
+
import {
|
|
4
|
+
AutoScroll,
|
|
5
|
+
client,
|
|
6
|
+
useAgentState,
|
|
7
|
+
useSession,
|
|
8
|
+
useUserTranscript,
|
|
9
|
+
} from "@alexkroman1/aai-ui";
|
|
4
10
|
import type { ReactNode } from "react";
|
|
5
|
-
import { useEffect, useRef } from "react";
|
|
6
11
|
import type {
|
|
7
12
|
OrderStatus,
|
|
8
13
|
OrderView,
|
|
@@ -10,7 +15,7 @@ import type {
|
|
|
10
15
|
StoreView,
|
|
11
16
|
SwapOptionView,
|
|
12
17
|
} from "./shared.ts";
|
|
13
|
-
import { DEMO_PERSONAS, storeView } from "./shared.ts";
|
|
18
|
+
import { DEMO_PERSONAS, emptyRetailState, storeView } from "./shared.ts";
|
|
14
19
|
|
|
15
20
|
const CSS = `
|
|
16
21
|
@keyframes rt-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
|
|
@@ -38,15 +43,30 @@ const statusColors: Record<string, string> = {
|
|
|
38
43
|
} satisfies Record<OrderStatus, string>;
|
|
39
44
|
|
|
40
45
|
// The sidebar before the first tool call, derived from the projection itself so
|
|
41
|
-
// a new StoreView field can't miss the pre-first-call render.
|
|
42
|
-
|
|
46
|
+
// a new StoreView field can't miss the pre-first-call render. Built from
|
|
47
|
+
// `emptyRetailState` rather than `retailSlot.projection(...)(undefined)`,
|
|
48
|
+
// because the slot's factory lives in `store.ts` and pulls the 107 KB seed —
|
|
49
|
+
// importing it here would ship the whole catalog to the browser.
|
|
50
|
+
const EMPTY_VIEW: StoreView = storeView(emptyRetailState());
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
// The dot's colour per session state, as an EXHAUSTIVE map rather than an
|
|
53
|
+
// if-chain with a grey default.
|
|
54
|
+
//
|
|
55
|
+
// The palette is this template's own — every client here paints the same six
|
|
56
|
+
// states in its own colours, so the SDK has nothing to share but the union. What
|
|
57
|
+
// the SDK does own is `AgentState`, and `satisfies Record<AgentState, string>`
|
|
58
|
+
// is what borrows it: a state added there stops compiling here, where the
|
|
59
|
+
// `state === "…"` chain this replaced answered a new state with a silent grey
|
|
60
|
+
// badge in three separate files and no way to notice.
|
|
61
|
+
const STATE_COLORS = {
|
|
62
|
+
disconnected: "#a1a1aa",
|
|
63
|
+
connecting: "#a1a1aa",
|
|
64
|
+
ready: "#16a34a",
|
|
65
|
+
listening: "#16a34a",
|
|
66
|
+
thinking: "#ca8a04",
|
|
67
|
+
speaking: "#2563eb",
|
|
68
|
+
error: "#a1a1aa",
|
|
69
|
+
} satisfies Record<AgentState, string>;
|
|
50
70
|
|
|
51
71
|
function Panel({ title, children }: { title: string; children: ReactNode }) {
|
|
52
72
|
return (
|
|
@@ -193,13 +213,12 @@ function SwapOptions({ option }: { option: SwapOptionView }) {
|
|
|
193
213
|
|
|
194
214
|
function App() {
|
|
195
215
|
const session = useSession();
|
|
196
|
-
|
|
216
|
+
// `speaking` and `text` in place of a `userTranscript !== null` check: `null`
|
|
217
|
+
// is silence and `""` is speech detected with no words back yet, and reading
|
|
218
|
+
// them as one falsy value hides the indicator at the start of every turn.
|
|
219
|
+
const { speaking, text: userTranscript } = useUserTranscript();
|
|
197
220
|
// The agent's own store, projected by `syncState` after every tool call.
|
|
198
|
-
const view = useAgentState<StoreView>()
|
|
199
|
-
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
202
|
-
}, [session.messages]);
|
|
221
|
+
const view = useAgentState<StoreView>(EMPTY_VIEW);
|
|
203
222
|
|
|
204
223
|
const lastAction = view.activity.at(-1);
|
|
205
224
|
|
|
@@ -220,7 +239,7 @@ function App() {
|
|
|
220
239
|
<span
|
|
221
240
|
className="w-2 h-2 rounded-full inline-block"
|
|
222
241
|
style={{
|
|
223
|
-
background:
|
|
242
|
+
background: STATE_COLORS[session.state],
|
|
224
243
|
animation:
|
|
225
244
|
session.state === "listening"
|
|
226
245
|
? "rt-pulse 1.5s ease-in-out infinite"
|
|
@@ -255,7 +274,10 @@ function App() {
|
|
|
255
274
|
className="flex flex-col overflow-hidden"
|
|
256
275
|
style={{ borderRight: "1px solid #e4e4e7" }}
|
|
257
276
|
>
|
|
258
|
-
<
|
|
277
|
+
<AutoScroll
|
|
278
|
+
scrollClassName="rt-scroll overflow-y-auto"
|
|
279
|
+
contentClassName="p-4 flex flex-col gap-2"
|
|
280
|
+
>
|
|
259
281
|
{session.messages.length === 0 && (
|
|
260
282
|
<div className="text-center p-10 text-[13px]" style={{ color: "#a1a1aa" }}>
|
|
261
283
|
Press start, then read one of the emails from “Who to be” to the agent.
|
|
@@ -277,10 +299,9 @@ function App() {
|
|
|
277
299
|
{message.content}
|
|
278
300
|
</div>
|
|
279
301
|
))}
|
|
280
|
-
|
|
281
|
-
</div>
|
|
302
|
+
</AutoScroll>
|
|
282
303
|
|
|
283
|
-
{
|
|
304
|
+
{speaking && (
|
|
284
305
|
<div
|
|
285
306
|
className="flex items-center px-4 py-2 text-xs italic min-h-8"
|
|
286
307
|
style={{ background: "#fafafa", borderTop: "1px solid #e4e4e7", color: "#71717a" }}
|
|
@@ -289,7 +310,7 @@ function App() {
|
|
|
289
310
|
className="w-2 h-2 rounded-full inline-block mr-2"
|
|
290
311
|
style={{ background: "#16a34a", animation: "rt-pulse 1.5s ease-in-out infinite" }}
|
|
291
312
|
/>
|
|
292
|
-
{
|
|
313
|
+
{userTranscript}
|
|
293
314
|
</div>
|
|
294
315
|
)}
|
|
295
316
|
{session.error && (
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
4
|
+
import { createToolContext, withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
2
5
|
import { describe, expect, test } from "vitest";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
6
|
+
import authoredAgent from "./agent.ts";
|
|
7
|
+
import { retailSlot } from "./store.ts";
|
|
5
8
|
|
|
6
9
|
/** Tools that legitimately run before the caller is identified. Everything
|
|
7
10
|
* else must refuse. Listed here so ADDING an unauthenticated tool is a
|
|
@@ -15,18 +18,24 @@ const PUBLIC_TOOLS = new Set([
|
|
|
15
18
|
"transfer_to_human_agents",
|
|
16
19
|
]);
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
23
|
+
*
|
|
24
|
+
* The glob is written HERE rather than reached for from a shared helper because
|
|
25
|
+
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
26
|
+
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
27
|
+
* the file containing it either way. This is the pattern a user writes.
|
|
28
|
+
*/
|
|
29
|
+
const retailAgent = withDiscoveredTools(
|
|
30
|
+
authoredAgent,
|
|
31
|
+
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
32
|
+
);
|
|
33
|
+
const registry = Object.entries(retailAgent.tools);
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
send: () => {},
|
|
25
|
-
env: {},
|
|
26
|
-
state: {},
|
|
27
|
-
messages: [],
|
|
28
|
-
} as unknown as ToolContext;
|
|
29
|
-
}
|
|
35
|
+
// `createToolContext()` rather than a cast: it carries a real slot store (the
|
|
36
|
+
// same storability check and freeze the deployed one applies), and each call is a
|
|
37
|
+
// distinct session, which is what these per-tool cases assume.
|
|
38
|
+
const makeCtx = (): ToolContext => createToolContext();
|
|
30
39
|
|
|
31
40
|
/** Minimal args satisfying each tool's schema. Deliberately plausible-shaped
|
|
32
41
|
* but wrong — these calls are expected to fail; what is asserted is that they
|
|
@@ -126,9 +135,9 @@ describe("the UI-update invariant", () => {
|
|
|
126
135
|
// retailTool(): it would work, and the sidebar would sit still through it.
|
|
127
136
|
test.each(registry)("%s increments callSeq and logs activity", async (name, def) => {
|
|
128
137
|
const ctx = makeCtx();
|
|
129
|
-
const before =
|
|
138
|
+
const before = retailSlot.get(ctx).callSeq;
|
|
130
139
|
await def.execute(SAMPLE_ARGS[name] ?? {}, ctx);
|
|
131
|
-
const state =
|
|
140
|
+
const state = retailSlot.get(ctx);
|
|
132
141
|
expect(state.callSeq, `${name} did not bump callSeq`).toBe(before + 1);
|
|
133
142
|
expect(state.activity.at(-1)?.tool, `${name} logged the wrong tool name`).toBe(name);
|
|
134
143
|
expect(state.activity.at(-1)?.summary).toBeTruthy();
|
|
@@ -139,7 +148,7 @@ describe("the UI-update invariant", () => {
|
|
|
139
148
|
// disagree — the activity feed would then attribute calls to the wrong tool.
|
|
140
149
|
const ctx = makeCtx();
|
|
141
150
|
await def.execute(SAMPLE_ARGS[name] ?? {}, ctx);
|
|
142
|
-
expect(
|
|
151
|
+
expect(retailSlot.get(ctx).activity.at(-1)?.tool).toBe(name);
|
|
143
152
|
});
|
|
144
153
|
});
|
|
145
154
|
|
|
@@ -148,8 +157,8 @@ describe("the authentication gate", () => {
|
|
|
148
157
|
"%s refuses before the caller is identified",
|
|
149
158
|
async (name, def) => {
|
|
150
159
|
const result = await def.execute(SAMPLE_ARGS[name] ?? {}, makeCtx());
|
|
151
|
-
expect(
|
|
152
|
-
if (!
|
|
160
|
+
expect(isToolFailure(result), `${name} did not refuse`).toBe(true);
|
|
161
|
+
if (!isToolFailure(result)) return;
|
|
153
162
|
expect(result.error, `${name} refused for the wrong reason`).toContain(
|
|
154
163
|
"find_user_id_by_email",
|
|
155
164
|
);
|
|
@@ -162,9 +171,9 @@ describe("the authentication gate", () => {
|
|
|
162
171
|
const result = await def.execute(SAMPLE_ARGS[name] ?? {}, makeCtx());
|
|
163
172
|
// A public tool may still fail on its own (deliberately bogus) arguments;
|
|
164
173
|
// it must not fail on the GATE. Written as one unconditional assertion —
|
|
165
|
-
// an `if (
|
|
174
|
+
// an `if (isToolFailure(result))` wrapper would pass vacuously for the tools
|
|
166
175
|
// that succeed, which is precisely the set most at risk of regressing.
|
|
167
|
-
const blockedByGate =
|
|
176
|
+
const blockedByGate = isToolFailure(result) && result.error.includes("find_user_id_by_email");
|
|
168
177
|
expect(blockedByGate, `${name} was blocked by the auth gate`).toBe(false);
|
|
169
178
|
},
|
|
170
179
|
);
|
|
@@ -175,10 +184,16 @@ describe("agent config", () => {
|
|
|
175
184
|
expect(typeof retailAgent.syncState).toBe("function");
|
|
176
185
|
});
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
// The slot owns the default now — there is no `state` factory on the agent to
|
|
188
|
+
// forget to declare, which is what four of five slot-backed templates used to.
|
|
189
|
+
// What still has to hold is that the factory really is called per session: the
|
|
190
|
+
// 107 KB seed is one module-level object, so a shared clone would let one
|
|
191
|
+
// caller's cancellation show up in another's.
|
|
192
|
+
test("the slot's factory clones the seed, so sessions do not share a store", () => {
|
|
193
|
+
const a = retailSlot.create();
|
|
194
|
+
const b = retailSlot.create();
|
|
182
195
|
expect(a).not.toBe(b);
|
|
196
|
+
expect(a.store.orders).not.toBe(b.store.orders);
|
|
197
|
+
expect(a.store).toEqual(b.store);
|
|
183
198
|
});
|
|
184
199
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { describe, expect, test } from "vitest";
|
|
2
3
|
import { normalizeItemId, normalizeOrderId, resolveOrder, resolveVariantId } from "./resolve.ts";
|
|
3
|
-
import { createDefaultState, findProduct
|
|
4
|
+
import { createDefaultState, findProduct } from "./store.ts";
|
|
4
5
|
|
|
5
6
|
function stateFor(userId: string) {
|
|
6
7
|
const state = createDefaultState();
|
|
@@ -39,13 +40,13 @@ describe("resolveOrder — canonical ids", () => {
|
|
|
39
40
|
const state = stateFor("olivia_ito_3591");
|
|
40
41
|
for (const spoken of ["#W5866402", "W 5866402", "5866402"]) {
|
|
41
42
|
const order = resolveOrder(state, spoken);
|
|
42
|
-
expect.soft(
|
|
43
|
+
expect.soft(isToolFailure(order) ? null : order.order_id, spoken).toBe("#W5866402");
|
|
43
44
|
}
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
test("refuses another customer's order id", () => {
|
|
47
48
|
const state = stateFor("olivia_ito_3591");
|
|
48
|
-
expect(
|
|
49
|
+
expect(isToolFailure(resolveOrder(state, "#W4316152"))).toBe(true);
|
|
49
50
|
});
|
|
50
51
|
});
|
|
51
52
|
|
|
@@ -54,14 +55,14 @@ describe("resolveOrder — shorthand", () => {
|
|
|
54
55
|
const state = stateFor("olivia_ito_3591");
|
|
55
56
|
for (const spoken of ["the delivered one", "my delivered order", "delivered"]) {
|
|
56
57
|
const order = resolveOrder(state, spoken);
|
|
57
|
-
expect.soft(
|
|
58
|
+
expect.soft(isToolFailure(order) ? null : order.order_id, spoken).toBe("#W5866402");
|
|
58
59
|
}
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
test("three pending orders make bare 'my pending order' ambiguous, and it lists them", () => {
|
|
62
63
|
const state = stateFor("olivia_ito_3591");
|
|
63
64
|
const result = resolveOrder(state, "my pending order");
|
|
64
|
-
if (!
|
|
65
|
+
if (!isToolFailure(result)) throw new Error("expected ambiguity");
|
|
65
66
|
for (const id of ["#W5442520", "#W7941031", "#W3657213"]) {
|
|
66
67
|
expect.soft(result.error, `ambiguity message omits ${id}`).toContain(id);
|
|
67
68
|
}
|
|
@@ -71,38 +72,38 @@ describe("resolveOrder — shorthand", () => {
|
|
|
71
72
|
const state = stateFor("olivia_ito_3591");
|
|
72
73
|
const first = resolveOrder(state, "my first pending order");
|
|
73
74
|
const second = resolveOrder(state, "the second pending order");
|
|
74
|
-
expect(
|
|
75
|
-
expect(
|
|
75
|
+
expect(isToolFailure(first) ? null : first.order_id).toBe("#W5442520");
|
|
76
|
+
expect(isToolFailure(second) ? null : second.order_id).toBe("#W7941031");
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
test("'last' picks the final one", () => {
|
|
79
80
|
const state = stateFor("olivia_ito_3591");
|
|
80
81
|
const result = resolveOrder(state, "my last pending order");
|
|
81
|
-
expect(
|
|
82
|
+
expect(isToolFailure(result) ? null : result.order_id).toBe("#W3657213");
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
test("a bare ordinal with no status word indexes all of the caller's orders", () => {
|
|
85
86
|
const state = stateFor("emma_smith_8564");
|
|
86
87
|
const result = resolveOrder(state, "the first one");
|
|
87
|
-
expect(
|
|
88
|
+
expect(isToolFailure(result) ? null : result.order_id).toBe("#W2417020");
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
test("a status the caller has none of says what they do have", () => {
|
|
91
92
|
const state = stateFor("harper_brown_7363");
|
|
92
93
|
const result = resolveOrder(state, "my cancelled order");
|
|
93
|
-
if (!
|
|
94
|
+
if (!isToolFailure(result)) throw new Error("expected refusal");
|
|
94
95
|
expect(result.error).toContain("delivered");
|
|
95
96
|
expect(result.error).toContain("pending");
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
test("an out-of-range ordinal is refused, not clamped", () => {
|
|
99
100
|
const state = stateFor("emma_smith_8564");
|
|
100
|
-
expect(
|
|
101
|
+
expect(isToolFailure(resolveOrder(state, "the fourth pending order"))).toBe(true);
|
|
101
102
|
});
|
|
102
103
|
|
|
103
104
|
test("unresolvable input is refused before authentication too", () => {
|
|
104
105
|
const state = createDefaultState();
|
|
105
|
-
expect(
|
|
106
|
+
expect(isToolFailure(resolveOrder(state, "my pending order"))).toBe(true);
|
|
106
107
|
});
|
|
107
108
|
});
|
|
108
109
|
|
|
@@ -110,7 +111,7 @@ describe("resolveVariantId", () => {
|
|
|
110
111
|
const state = createDefaultState();
|
|
111
112
|
function teaKettle() {
|
|
112
113
|
const product = findProduct(state, "9832717871");
|
|
113
|
-
if (
|
|
114
|
+
if (isToolFailure(product)) throw new Error(product.error);
|
|
114
115
|
return product;
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -120,7 +121,7 @@ describe("resolveVariantId", () => {
|
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
test("refuses an item id belonging to a different product", () => {
|
|
123
|
-
expect(
|
|
124
|
+
expect(isToolFailure(resolveVariantId(teaKettle(), "4725166838"))).toBe(true);
|
|
124
125
|
});
|
|
125
126
|
|
|
126
127
|
test("matches a spoken option phrase", () => {
|
|
@@ -135,13 +136,13 @@ describe("resolveVariantId", () => {
|
|
|
135
136
|
|
|
136
137
|
test("an ambiguous phrase lists the candidates with their options", () => {
|
|
137
138
|
const result = resolveVariantId(teaKettle(), "glass");
|
|
138
|
-
if (!
|
|
139
|
+
if (!isToolFailure(result)) throw new Error("expected ambiguity");
|
|
139
140
|
expect(result.error).toContain("glass");
|
|
140
141
|
expect(result.error.match(/\d{10}/g)?.length).toBeGreaterThan(1);
|
|
141
142
|
});
|
|
142
143
|
|
|
143
144
|
test("a phrase matching nothing is refused", () => {
|
|
144
|
-
expect(
|
|
145
|
+
expect(isToolFailure(resolveVariantId(teaKettle(), "titanium"))).toBe(true);
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
test("availableOnly excludes unavailable variants from matching", () => {
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spoken references to this store's things.
|
|
3
|
+
*
|
|
4
|
+
* The generic half — reading digits out of an utterance, reading a position out
|
|
5
|
+
* of one, and the never-guess contract that turns a list of candidates into one
|
|
6
|
+
* of them or into a failure that lists them — is `resolveOne`, `spokenDigits`
|
|
7
|
+
* and `spokenOrdinal` (`@alexkroman1/aai`). What stays here is the STORE's
|
|
8
|
+
* vocabulary: what an order id looks like when a caller reads it aloud, which
|
|
9
|
+
* words name a status, and what a variant's options are matched against.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { isToolFailure, resolveOne, spokenDigits, type ToolFailure } from "@alexkroman1/aai";
|
|
1
13
|
import type { Order, OrderStatus, Product, RetailState } from "./shared.ts";
|
|
2
|
-
import { authenticatedUser
|
|
14
|
+
import { authenticatedUser } from "./store.ts";
|
|
3
15
|
|
|
4
16
|
/** `#W5866402` from anything STT plausibly produces for it. */
|
|
5
17
|
export function normalizeOrderId(spoken: string): string {
|
|
@@ -9,27 +21,13 @@ export function normalizeOrderId(spoken: string): string {
|
|
|
9
21
|
|
|
10
22
|
/** Item ids are 10 digits; callers read them in groups. */
|
|
11
23
|
export function normalizeItemId(spoken: string): string {
|
|
12
|
-
return spoken
|
|
24
|
+
return spokenDigits(spoken);
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
/** A digit run long enough to be an id rather than an ordinal. */
|
|
16
28
|
const LOOKS_LIKE_ORDER_ID = /\d[\d\s-]{5,}/;
|
|
17
29
|
const LOOKS_LIKE_ITEM_ID = /(?:\d[\s-]*){8,}/;
|
|
18
30
|
|
|
19
|
-
const ORDINALS: Record<string, number> = {
|
|
20
|
-
first: 0,
|
|
21
|
-
"1st": 0,
|
|
22
|
-
second: 1,
|
|
23
|
-
"2nd": 1,
|
|
24
|
-
third: 2,
|
|
25
|
-
"3rd": 2,
|
|
26
|
-
fourth: 3,
|
|
27
|
-
"4th": 3,
|
|
28
|
-
fifth: 4,
|
|
29
|
-
"5th": 4,
|
|
30
|
-
last: -1,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
31
|
/** Spoken status words → the statuses they select. `pending` deliberately
|
|
34
32
|
* excludes `pending (item modified)`, which no tool will accept anyway. */
|
|
35
33
|
const STATUS_WORDS: [string, OrderStatus[]][] = [
|
|
@@ -53,9 +51,9 @@ function describeOrder(order: Order): string {
|
|
|
53
51
|
* Ambiguity is always an error listing the candidates, never a guess — the
|
|
54
52
|
* consequence of guessing here is cancelling the wrong order.
|
|
55
53
|
*/
|
|
56
|
-
export function resolveOrder(state: RetailState, spoken: string): Order |
|
|
54
|
+
export function resolveOrder(state: RetailState, spoken: string): Order | ToolFailure {
|
|
57
55
|
const user = authenticatedUser(state);
|
|
58
|
-
if (
|
|
56
|
+
if (isToolFailure(user)) return user;
|
|
59
57
|
|
|
60
58
|
const owned = user.orders
|
|
61
59
|
.map((id) => state.store.orders[id])
|
|
@@ -71,6 +69,9 @@ export function resolveOrder(state: RetailState, spoken: string): Order | ErrorR
|
|
|
71
69
|
const statusEntry = STATUS_WORDS.find(([word]) => text.includes(word));
|
|
72
70
|
const candidates = statusEntry ? owned.filter((o) => statusEntry[1].includes(o.status)) : owned;
|
|
73
71
|
|
|
72
|
+
// The empty case is the store's to answer, not `resolveOne`'s: "no cancelled
|
|
73
|
+
// order" is only useful said alongside the statuses this customer DOES have,
|
|
74
|
+
// which is what lets the model ask its next question without another tool call.
|
|
74
75
|
if (candidates.length === 0) {
|
|
75
76
|
const have = [...new Set(owned.map((o) => o.status))].join(", ");
|
|
76
77
|
return {
|
|
@@ -78,28 +79,9 @@ export function resolveOrder(state: RetailState, spoken: string): Order | ErrorR
|
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const picked = index === -1 ? candidates.at(-1) : candidates[index];
|
|
85
|
-
if (!picked) {
|
|
86
|
-
return {
|
|
87
|
-
error: `There is no ${ordinalWord} such order — the customer has ${candidates.length}: ${candidates
|
|
88
|
-
.map(describeOrder)
|
|
89
|
-
.join("; ")}.`,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return picked;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const only = candidates[0];
|
|
96
|
-
if (candidates.length === 1 && only) return only;
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
error: `That is ambiguous — ${candidates.length} orders match. Ask which one: ${candidates
|
|
100
|
-
.map(describeOrder)
|
|
101
|
-
.join("; ")}.`,
|
|
102
|
-
};
|
|
82
|
+
// A position, then exactly-one, then an ambiguity that lists them — and never
|
|
83
|
+
// a guess, because the consequence here is cancelling the wrong order.
|
|
84
|
+
return resolveOne(candidates, spoken, { label: "order", describe: describeOrder });
|
|
103
85
|
}
|
|
104
86
|
|
|
105
87
|
/**
|
|
@@ -110,7 +92,7 @@ export function resolveVariantId(
|
|
|
110
92
|
product: Product,
|
|
111
93
|
spoken: string,
|
|
112
94
|
opts: { availableOnly?: boolean } = {},
|
|
113
|
-
): string |
|
|
95
|
+
): string | ToolFailure {
|
|
114
96
|
if (LOOKS_LIKE_ITEM_ID.test(spoken)) {
|
|
115
97
|
const itemId = normalizeItemId(spoken);
|
|
116
98
|
const variant = product.variants[itemId];
|
|
@@ -126,33 +108,16 @@ export function resolveVariantId(
|
|
|
126
108
|
return itemId;
|
|
127
109
|
}
|
|
128
110
|
|
|
129
|
-
const text = spoken.toLowerCase();
|
|
130
111
|
const pool = Object.values(product.variants).filter((v) => !opts.availableOnly || v.available);
|
|
131
112
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
113
|
+
// A variant is named by its OPTIONS ("the blue medium"), so the score is how
|
|
114
|
+
// many of them the caller said. Everything after that — no match, one match, a
|
|
115
|
+
// tie — is `resolveOne`'s, and a tie is answered rather than broken.
|
|
116
|
+
const picked = resolveOne(pool, spoken, {
|
|
117
|
+
label: `${product.name} option`,
|
|
118
|
+
describe: (v) => `${v.item_id} (${Object.values(v.options).join(", ")})`,
|
|
119
|
+
score: (variant, text) =>
|
|
120
|
+
Object.values(variant.options).filter((value) => text.includes(value.toLowerCase())).length,
|
|
139
121
|
});
|
|
140
|
-
|
|
141
|
-
if (best === 0) {
|
|
142
|
-
return {
|
|
143
|
-
error: `No ${product.name} option matches "${spoken}". Options available: ${pool
|
|
144
|
-
.map((v) => `${v.item_id} (${Object.values(v.options).join(", ")})`)
|
|
145
|
-
.join("; ")}.`,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const winners = scored.filter((s) => s.score === best);
|
|
150
|
-
const winner = winners[0]?.variant;
|
|
151
|
-
if (winners.length === 1 && winner) return winner.item_id;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
error: `"${spoken}" matches ${winners.length} ${product.name} options. Ask which: ${winners
|
|
155
|
-
.map((s) => `${s.variant.item_id} (${Object.values(s.variant.options).join(", ")})`)
|
|
156
|
-
.join("; ")}.`,
|
|
157
|
-
};
|
|
122
|
+
return isToolFailure(picked) ? picked : picked.item_id;
|
|
158
123
|
}
|