@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,169 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
4
|
+
import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
5
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
6
|
+
import authoredAgent from "./agent.ts";
|
|
7
|
+
import { excerptAround, type FdaLabel, toDrugInfo } from "./fda.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Only the NETWORK half of `fda.ts` is faked.
|
|
11
|
+
*
|
|
12
|
+
* `fetchFdaLabel` is the one function in this template that leaves the process,
|
|
13
|
+
* so it is the one thing a unit test may not run — everything the tools are
|
|
14
|
+
* actually about (the cross-mention scan, the refuse-on-a-missing-drug rule,
|
|
15
|
+
* the field folding) is pure and stays real. `importActual` rather than a whole
|
|
16
|
+
* module stub for exactly that reason: a fully mocked `fda.ts` would leave
|
|
17
|
+
* `toDrugInfo` returning `undefined` and the tools passing over nothing.
|
|
18
|
+
*/
|
|
19
|
+
vi.mock("./fda.ts", async (importOriginal) => ({
|
|
20
|
+
...(await importOriginal<typeof import("./fda.ts")>()),
|
|
21
|
+
fetchFdaLabel: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const { fetchFdaLabel } = await import("./fda.ts");
|
|
25
|
+
const label = vi.mocked(fetchFdaLabel);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
29
|
+
*
|
|
30
|
+
* The glob is written HERE rather than reached for from a shared helper because
|
|
31
|
+
* this file SHIPS — a scaffolded project has no repo helper to import.
|
|
32
|
+
*/
|
|
33
|
+
const agentDef = withDiscoveredTools(
|
|
34
|
+
authoredAgent,
|
|
35
|
+
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const run = (name: string, args: Record<string, unknown>, ctx: ToolContext = createToolContext()) =>
|
|
39
|
+
runTool(agentDef, name, args, ctx);
|
|
40
|
+
|
|
41
|
+
const IBUPROFEN: FdaLabel = {
|
|
42
|
+
openfda: { generic_name: ["IBUPROFEN"], brand_name: ["Advil"], manufacturer_name: ["Acme"] },
|
|
43
|
+
purpose: ["Pain reliever"],
|
|
44
|
+
warnings: ["Stomach bleeding warning"],
|
|
45
|
+
dosage_and_administration: ["One tablet every 6 hours"],
|
|
46
|
+
adverse_reactions: ["Nausea"],
|
|
47
|
+
drug_interactions: ["Ask a doctor before use if taking WARFARIN, a blood thinner."],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const WARFARIN: FdaLabel = {
|
|
51
|
+
openfda: { generic_name: ["WARFARIN SODIUM"], brand_name: ["Coumadin"] },
|
|
52
|
+
purpose: ["Anticoagulant"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
// `restoreMocks` restores `vi.spyOn` mocks; it does not clear a `vi.fn()`'s
|
|
57
|
+
// history or its implementation, so an implementation set by one test would
|
|
58
|
+
// otherwise still be installed in the next.
|
|
59
|
+
label.mockReset();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("health-assistant template", () => {
|
|
63
|
+
test("both tools are discovered from tools/", () => {
|
|
64
|
+
// `agent()` takes no `tools` field: a file in `tools/` IS the tool. A
|
|
65
|
+
// template whose tools are never resolved ships a model with none.
|
|
66
|
+
expect(Object.keys(agentDef.tools ?? {}).sort()).toEqual([
|
|
67
|
+
"check_drug_interaction",
|
|
68
|
+
"medication_lookup",
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("medication_lookup", () => {
|
|
74
|
+
test("folds the label's array fields into one flat answer", async () => {
|
|
75
|
+
label.mockResolvedValue(IBUPROFEN);
|
|
76
|
+
const result = await run("medication_lookup", { name: "advil" });
|
|
77
|
+
expect(result).toMatchObject({
|
|
78
|
+
name: "IBUPROFEN",
|
|
79
|
+
brand_names: ["Advil"],
|
|
80
|
+
purpose: "Pain reliever",
|
|
81
|
+
manufacturer: "Acme",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("an unknown drug is a tool FAILURE, not an empty answer", async () => {
|
|
86
|
+
// The model has to be able to tell "no such drug" from "a drug with no
|
|
87
|
+
// warnings", which is why this is a failure rather than a record of "N/A".
|
|
88
|
+
label.mockResolvedValue(null);
|
|
89
|
+
const result = await run("medication_lookup", { name: "sparkleforin" });
|
|
90
|
+
expect(isToolFailure(result) && result.error).toContain("sparkleforin");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a missing section reads N/A rather than undefined", async () => {
|
|
94
|
+
label.mockResolvedValue(WARFARIN);
|
|
95
|
+
expect(await run("medication_lookup", { name: "warfarin" })).toMatchObject({
|
|
96
|
+
warnings: "N/A",
|
|
97
|
+
dosage: "N/A",
|
|
98
|
+
side_effects: "N/A",
|
|
99
|
+
manufacturer: "N/A",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("check_drug_interaction", () => {
|
|
105
|
+
test("reports a cross-mention with the excerpt that justifies it", async () => {
|
|
106
|
+
label.mockImplementation(async (name: string) =>
|
|
107
|
+
name.includes("ibuprofen") ? IBUPROFEN : WARFARIN,
|
|
108
|
+
);
|
|
109
|
+
const result = await run("check_drug_interaction", { drugs: ["ibuprofen", "warfarin"] });
|
|
110
|
+
expect(result).toMatchObject({ interactions_found: 1 });
|
|
111
|
+
const [first] = (result as { interactions: { drug: string; mentions: string }[] }).interactions;
|
|
112
|
+
expect(first).toMatchObject({ drug: "ibuprofen", mentions: "warfarin" });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("matches on a BRAND alias, not only the name the caller used", async () => {
|
|
116
|
+
// `toDrugInfo` folds generic and brand names into one alias list precisely
|
|
117
|
+
// so a label naming "Coumadin" still matches a caller who said "warfarin".
|
|
118
|
+
const coumadinMention: FdaLabel = {
|
|
119
|
+
openfda: { generic_name: ["IBUPROFEN"] },
|
|
120
|
+
drug_interactions: ["Do not combine with COUMADIN."],
|
|
121
|
+
};
|
|
122
|
+
label.mockImplementation(async (name: string) =>
|
|
123
|
+
name.includes("ibuprofen") ? coumadinMention : WARFARIN,
|
|
124
|
+
);
|
|
125
|
+
const result = await run("check_drug_interaction", { drugs: ["ibuprofen", "warfarin"] });
|
|
126
|
+
expect(result).toMatchObject({ interactions_found: 1 });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a drug that cannot be resolved REFUSES the whole check", async () => {
|
|
130
|
+
// The rule that makes this tool safe to have: a partial answer would read
|
|
131
|
+
// as "no interaction" for the drug that was silently dropped.
|
|
132
|
+
label.mockImplementation(async (name: string) =>
|
|
133
|
+
name.includes("ibuprofen") ? IBUPROFEN : null,
|
|
134
|
+
);
|
|
135
|
+
const result = await run("check_drug_interaction", { drugs: ["ibuprofen", "sparkleforin"] });
|
|
136
|
+
expect(isToolFailure(result) && result.error).toContain("sparkleforin");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("two drugs with no cross-mention are reported as such, with the caveat", async () => {
|
|
140
|
+
label.mockResolvedValue(WARFARIN);
|
|
141
|
+
const result = await run("check_drug_interaction", { drugs: ["warfarin", "aspirin"] });
|
|
142
|
+
expect(result).toMatchObject({ interactions_found: 0 });
|
|
143
|
+
expect((result as { note: string }).note).toContain("does not guarantee");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("whitespace-only names are refused before any lookup happens", async () => {
|
|
147
|
+
// The schema's `min(2)` counts entries, not real names, so the body
|
|
148
|
+
// re-checks after trimming — and must do so BEFORE touching the network.
|
|
149
|
+
const result = await run("check_drug_interaction", { drugs: ["ibuprofen", " "] });
|
|
150
|
+
expect(isToolFailure(result) && result.error).toContain("at least two");
|
|
151
|
+
expect(label).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("fda.ts helpers", () => {
|
|
156
|
+
test("toDrugInfo lowercases every alias so cross-matching is case-blind", () => {
|
|
157
|
+
const info = toDrugInfo("advil", IBUPROFEN);
|
|
158
|
+
expect(info.aliases).toEqual(["advil", "ibuprofen"]);
|
|
159
|
+
expect(info.interactionsText).toContain("warfarin");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("excerptAround ellipsizes only the ends it actually cut", () => {
|
|
163
|
+
const text = `${"a".repeat(300)}warfarin${"b".repeat(300)}`;
|
|
164
|
+
const excerpt = excerptAround(text, "warfarin");
|
|
165
|
+
expect(excerpt.startsWith("…")).toBe(true);
|
|
166
|
+
expect(excerpt.endsWith("…")).toBe(true);
|
|
167
|
+
expect(excerptAround("warfarin", "warfarin")).toBe("warfarin");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -1,173 +1,8 @@
|
|
|
1
|
-
import { agent
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
4
|
-
|
|
5
|
-
function first(arr: string[] | undefined): string | undefined {
|
|
6
|
-
return arr?.[0];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
type FdaLabel = Record<string, unknown> & { openfda?: Record<string, string[]> };
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Fetch a drug's FDA label (generic OR brand name match) from openFDA.
|
|
13
|
-
* Returns null when the drug can't be found or the API is unreachable.
|
|
14
|
-
*
|
|
15
|
-
* Memoized per drug name: a voice session naturally asks several questions
|
|
16
|
-
* about the same drugs, and labels are static, so repeats skip the network
|
|
17
|
-
* round-trip. A null result (not found, or a transient network failure) is
|
|
18
|
-
* NOT cached, so the next call retries instead of pinning the failure.
|
|
19
|
-
*/
|
|
20
|
-
const labelCache = new Map<string, Promise<FdaLabel | null>>();
|
|
21
|
-
|
|
22
|
-
function fetchFdaLabel(name: string): Promise<FdaLabel | null> {
|
|
23
|
-
const key = name.toLowerCase();
|
|
24
|
-
let p = labelCache.get(key);
|
|
25
|
-
if (!p) {
|
|
26
|
-
p = fetchFdaLabelUncached(key).then((label) => {
|
|
27
|
-
if (label === null) labelCache.delete(key);
|
|
28
|
-
return label;
|
|
29
|
-
});
|
|
30
|
-
labelCache.set(key, p);
|
|
31
|
-
}
|
|
32
|
-
return p;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function fetchFdaLabelUncached(name: string): Promise<FdaLabel | null> {
|
|
36
|
-
const q = encodeURIComponent(name);
|
|
37
|
-
try {
|
|
38
|
-
const resp = await fetch(
|
|
39
|
-
`https://api.fda.gov/drug/label.json?search=openfda.generic_name:"${q}"+openfda.brand_name:"${q}"&limit=1`,
|
|
40
|
-
);
|
|
41
|
-
if (!resp.ok) return null;
|
|
42
|
-
const raw = (await resp.json()) as { results?: FdaLabel[] };
|
|
43
|
-
return raw.results?.[0] ?? null;
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type DrugInfo = {
|
|
50
|
-
/** The name the user asked about. */
|
|
51
|
-
name: string;
|
|
52
|
-
/** All known names, lowercased — generic + brands — used for cross-matching. */
|
|
53
|
-
aliases: string[];
|
|
54
|
-
/** The label's "Drug Interactions" section, lowercased. */
|
|
55
|
-
interactionsText: string;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
function toDrugInfo(name: string, label: FdaLabel): DrugInfo {
|
|
59
|
-
const openfda = label.openfda ?? {};
|
|
60
|
-
const generic = openfda.generic_name ?? [];
|
|
61
|
-
const brands = openfda.brand_name ?? [];
|
|
62
|
-
const aliases = [...new Set([name, ...generic, ...brands].map((n) => n.toLowerCase()))];
|
|
63
|
-
const interactionsText = ((label.drug_interactions as string[] | undefined) ?? [])
|
|
64
|
-
.join(" ")
|
|
65
|
-
.toLowerCase();
|
|
66
|
-
return { name, aliases, interactionsText };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Pull a short excerpt around the first mention of `alias` in `text`. */
|
|
70
|
-
function excerptAround(text: string, alias: string): string {
|
|
71
|
-
const idx = text.indexOf(alias);
|
|
72
|
-
const start = Math.max(0, idx - 100);
|
|
73
|
-
const end = Math.min(text.length, idx + alias.length + 200);
|
|
74
|
-
return `${start > 0 ? "…" : ""}${text.slice(start, end)}${end < text.length ? "…" : ""}`;
|
|
75
|
-
}
|
|
1
|
+
import { agent } from "@alexkroman1/aai";
|
|
76
2
|
|
|
77
3
|
export default agent({
|
|
78
4
|
name: "Dr. Sage",
|
|
79
|
-
systemPrompt,
|
|
80
5
|
greeting:
|
|
81
6
|
"Hey, I'm Dr. Sage. Try asking me something like, what are the side effects of ibuprofen, can I take aspirin and warfarin together, or calculate my BMI. Just remember, I'm not a real doctor, so always check with your healthcare provider.",
|
|
82
7
|
builtinTools: ["web_search", "run_code"],
|
|
83
|
-
|
|
84
|
-
tools: {
|
|
85
|
-
check_drug_interaction: tool({
|
|
86
|
-
description:
|
|
87
|
-
"Check for interactions between two or more medications using FDA drug label data. Looks up each drug's official label and reports where one drug's Drug Interactions section mentions another. Absence of a mention does not guarantee safety.",
|
|
88
|
-
inputSchema: z.object({
|
|
89
|
-
drugs: z
|
|
90
|
-
.array(z.string().min(1))
|
|
91
|
-
.min(2)
|
|
92
|
-
.describe("Medication names to check, e.g. ['ibuprofen', 'warfarin']"),
|
|
93
|
-
}),
|
|
94
|
-
async execute(args) {
|
|
95
|
-
const names = args.drugs.map((d) => d.trim().toLowerCase()).filter((d) => d.length > 0);
|
|
96
|
-
if (names.length < 2) {
|
|
97
|
-
return { error: "Provide at least two medication names to check." };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const labels = await Promise.all(names.map((n) => fetchFdaLabel(n)));
|
|
101
|
-
const unresolved = names.filter((_, i) => labels[i] === null);
|
|
102
|
-
if (unresolved.length > 0) {
|
|
103
|
-
// Never silently drop a drug from an interaction check — a partial
|
|
104
|
-
// answer would read as "no interaction" for the missing one.
|
|
105
|
-
return {
|
|
106
|
-
error: `Could not find FDA label data for: ${unresolved.join(", ")}. Check the spelling, or try the generic name.`,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// The unresolved check above guarantees every label resolved.
|
|
111
|
-
const drugs = names.map((name, i) => toDrugInfo(name, labels[i]!));
|
|
112
|
-
|
|
113
|
-
const interactions: Array<{ drug: string; mentions: string; excerpt: string }> = [];
|
|
114
|
-
for (const a of drugs) {
|
|
115
|
-
if (!a.interactionsText) continue;
|
|
116
|
-
for (const b of drugs) {
|
|
117
|
-
if (a === b) continue;
|
|
118
|
-
const hit = b.aliases.find((alias) => a.interactionsText.includes(alias));
|
|
119
|
-
if (hit) {
|
|
120
|
-
interactions.push({
|
|
121
|
-
drug: a.name,
|
|
122
|
-
mentions: b.name,
|
|
123
|
-
excerpt: excerptAround(a.interactionsText, hit).slice(0, 400),
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
drugs: names,
|
|
131
|
-
interactions_found: interactions.length,
|
|
132
|
-
interactions: interactions.slice(0, 5),
|
|
133
|
-
note:
|
|
134
|
-
interactions.length === 0
|
|
135
|
-
? "No cross-mentions found in the FDA label Drug Interactions sections. This does not guarantee the combination is safe — confirm with a pharmacist or doctor."
|
|
136
|
-
: "Based on FDA label Drug Interactions sections. Confirm with a pharmacist or doctor.",
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
}),
|
|
140
|
-
|
|
141
|
-
medication_lookup: tool({
|
|
142
|
-
description:
|
|
143
|
-
"Look up detailed information about a single medication, including purpose, warnings, dosage, side effects, and manufacturer. Works with both generic and brand names.",
|
|
144
|
-
inputSchema: z.object({
|
|
145
|
-
name: z
|
|
146
|
-
.string()
|
|
147
|
-
.describe("Medication name (generic or brand, e.g. 'ibuprofen' or 'Advil')"),
|
|
148
|
-
}),
|
|
149
|
-
async execute(args) {
|
|
150
|
-
const drug = await fetchFdaLabel(args.name);
|
|
151
|
-
if (!drug) {
|
|
152
|
-
return { error: `No FDA data found for: ${args.name}` };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const openfda = drug.openfda ?? {};
|
|
156
|
-
return {
|
|
157
|
-
name: openfda.generic_name?.[0] ?? args.name,
|
|
158
|
-
brand_names: openfda.brand_name ?? [],
|
|
159
|
-
purpose:
|
|
160
|
-
first(drug.purpose as string[] | undefined) ??
|
|
161
|
-
first(drug.indications_and_usage as string[] | undefined) ??
|
|
162
|
-
"N/A",
|
|
163
|
-
warnings: first(drug.warnings as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
164
|
-
dosage:
|
|
165
|
-
first(drug.dosage_and_administration as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
166
|
-
side_effects:
|
|
167
|
-
first(drug.adverse_reactions as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
168
|
-
manufacturer: openfda.manufacturer_name?.[0] ?? "N/A",
|
|
169
|
-
};
|
|
170
|
-
},
|
|
171
|
-
}),
|
|
172
|
-
},
|
|
173
8
|
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openFDA drug labels, and the one cache both tools read.
|
|
3
|
+
*
|
|
4
|
+
* This module exists because the memoization below has to be shared: with a
|
|
5
|
+
* cache per tool file, a session that looks a drug up and then checks it for
|
|
6
|
+
* interactions pays the network round-trip twice and the "labels are static"
|
|
7
|
+
* argument stops being true.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
11
|
+
import { fetchJson } from "@alexkroman1/aai/tools";
|
|
12
|
+
|
|
13
|
+
export type FdaLabel = Record<string, unknown> & { openfda?: Record<string, string[]> };
|
|
14
|
+
|
|
15
|
+
export function first(arr: string[] | undefined): string | undefined {
|
|
16
|
+
return arr?.[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Memoized per drug name: a voice session naturally asks several questions
|
|
21
|
+
* about the same drugs, and labels are static, so repeats skip the network
|
|
22
|
+
* round-trip. A null result (not found, or a transient network failure) is
|
|
23
|
+
* NOT cached, so the next call retries instead of pinning the failure.
|
|
24
|
+
*/
|
|
25
|
+
const labelCache = new Map<string, Promise<FdaLabel | null>>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fetch a drug's FDA label (generic OR brand name match) from openFDA.
|
|
29
|
+
* Returns null when the drug can't be found or the API is unreachable.
|
|
30
|
+
*/
|
|
31
|
+
export function fetchFdaLabel(name: string): Promise<FdaLabel | null> {
|
|
32
|
+
const key = name.toLowerCase();
|
|
33
|
+
let p = labelCache.get(key);
|
|
34
|
+
if (!p) {
|
|
35
|
+
p = fetchFdaLabelUncached(key).then((label) => {
|
|
36
|
+
if (label === null) labelCache.delete(key);
|
|
37
|
+
return label;
|
|
38
|
+
});
|
|
39
|
+
labelCache.set(key, p);
|
|
40
|
+
}
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One openFDA lookup, through the SDK's own REST call rather than a bare
|
|
46
|
+
* `fetch`.
|
|
47
|
+
*
|
|
48
|
+
* `fetchJson` (`@alexkroman1/aai/tools`) is the same implementation behind the
|
|
49
|
+
* model-facing `fetch_json` builtin, so this inherits the three things a bare
|
|
50
|
+
* `fetch` here had none of: a request DEADLINE (`check_drug_interaction` fires
|
|
51
|
+
* a `Promise.all` over one of these per drug the caller named, and a stalled
|
|
52
|
+
* one held the whole tool call open for as long as the far side wanted), a
|
|
53
|
+
* bounded read so an unexpected body cannot be buffered whole, and URL
|
|
54
|
+
* screening on a developer's own machine under `aai dev`.
|
|
55
|
+
*
|
|
56
|
+
* It ANSWERS with `{ error }` rather than throwing for an HTTP failure, which
|
|
57
|
+
* is the builtin contract — narrowed with `isToolFailure` and folded into the
|
|
58
|
+
* same `null` this has always returned. The `catch` still earns its keep: a
|
|
59
|
+
* connection failure, a timeout or a refused URL is a throw.
|
|
60
|
+
*/
|
|
61
|
+
async function fetchFdaLabelUncached(name: string): Promise<FdaLabel | null> {
|
|
62
|
+
const q = encodeURIComponent(name);
|
|
63
|
+
try {
|
|
64
|
+
const raw = await fetchJson<{ results?: FdaLabel[] }>(
|
|
65
|
+
`https://api.fda.gov/drug/label.json?search=openfda.generic_name:"${q}"+openfda.brand_name:"${q}"&limit=1`,
|
|
66
|
+
);
|
|
67
|
+
if (isToolFailure(raw)) return null;
|
|
68
|
+
return raw.results?.[0] ?? null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type DrugInfo = {
|
|
75
|
+
/** The name the user asked about. */
|
|
76
|
+
name: string;
|
|
77
|
+
/** All known names, lowercased — generic + brands — used for cross-matching. */
|
|
78
|
+
aliases: string[];
|
|
79
|
+
/** The label's "Drug Interactions" section, lowercased. */
|
|
80
|
+
interactionsText: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function toDrugInfo(name: string, label: FdaLabel): DrugInfo {
|
|
84
|
+
const openfda = label.openfda ?? {};
|
|
85
|
+
const generic = openfda.generic_name ?? [];
|
|
86
|
+
const brands = openfda.brand_name ?? [];
|
|
87
|
+
const aliases = [...new Set([name, ...generic, ...brands].map((n) => n.toLowerCase()))];
|
|
88
|
+
const interactionsText = ((label.drug_interactions as string[] | undefined) ?? [])
|
|
89
|
+
.join(" ")
|
|
90
|
+
.toLowerCase();
|
|
91
|
+
return { name, aliases, interactionsText };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Pull a short excerpt around the first mention of `alias` in `text`. */
|
|
95
|
+
export function excerptAround(text: string, alias: string): string {
|
|
96
|
+
const idx = text.indexOf(alias);
|
|
97
|
+
const start = Math.max(0, idx - 100);
|
|
98
|
+
const end = Math.min(text.length, idx + alias.length + 200);
|
|
99
|
+
return `${start > 0 ? "…" : ""}${text.slice(start, end)}${end < text.length ? "…" : ""}`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { tool, toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { excerptAround, fetchFdaLabel, toDrugInfo } from "../fda.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
description:
|
|
7
|
+
"Check for interactions between two or more medications using FDA drug label data. Looks up each drug's official label and reports where one drug's Drug Interactions section mentions another. Absence of a mention does not guarantee safety.",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
drugs: z
|
|
10
|
+
.array(z.string().min(1))
|
|
11
|
+
.min(2)
|
|
12
|
+
.describe("Medication names to check, e.g. ['ibuprofen', 'warfarin']"),
|
|
13
|
+
}),
|
|
14
|
+
async execute(args) {
|
|
15
|
+
const names = args.drugs.map((d) => d.trim().toLowerCase()).filter((d) => d.length > 0);
|
|
16
|
+
if (names.length < 2) {
|
|
17
|
+
return toolFailure("Provide at least two medication names to check.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const labels = await Promise.all(names.map((n) => fetchFdaLabel(n)));
|
|
21
|
+
const unresolved = names.filter((_, i) => labels[i] === null);
|
|
22
|
+
if (unresolved.length > 0) {
|
|
23
|
+
// Never silently drop a drug from an interaction check — a partial
|
|
24
|
+
// answer would read as "no interaction" for the missing one.
|
|
25
|
+
return toolFailure(
|
|
26
|
+
`Could not find FDA label data for: ${unresolved.join(", ")}. Check the spelling, or try the generic name.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The unresolved check above guarantees every label resolved.
|
|
31
|
+
const drugs = names.map((name, i) => toDrugInfo(name, labels[i]!));
|
|
32
|
+
|
|
33
|
+
const interactions: Array<{ drug: string; mentions: string; excerpt: string }> = [];
|
|
34
|
+
for (const a of drugs) {
|
|
35
|
+
if (!a.interactionsText) continue;
|
|
36
|
+
for (const b of drugs) {
|
|
37
|
+
if (a === b) continue;
|
|
38
|
+
const hit = b.aliases.find((alias) => a.interactionsText.includes(alias));
|
|
39
|
+
if (hit) {
|
|
40
|
+
interactions.push({
|
|
41
|
+
drug: a.name,
|
|
42
|
+
mentions: b.name,
|
|
43
|
+
excerpt: excerptAround(a.interactionsText, hit).slice(0, 400),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
drugs: names,
|
|
51
|
+
interactions_found: interactions.length,
|
|
52
|
+
interactions: interactions.slice(0, 5),
|
|
53
|
+
note:
|
|
54
|
+
interactions.length === 0
|
|
55
|
+
? "No cross-mentions found in the FDA label Drug Interactions sections. This does not guarantee the combination is safe — confirm with a pharmacist or doctor."
|
|
56
|
+
: "Based on FDA label Drug Interactions sections. Confirm with a pharmacist or doctor.",
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { tool, toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { fetchFdaLabel, first } from "../fda.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
description:
|
|
7
|
+
"Look up detailed information about a single medication, including purpose, warnings, dosage, side effects, and manufacturer. Works with both generic and brand names.",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
name: z.string().describe("Medication name (generic or brand, e.g. 'ibuprofen' or 'Advil')"),
|
|
10
|
+
}),
|
|
11
|
+
async execute(args) {
|
|
12
|
+
const drug = await fetchFdaLabel(args.name);
|
|
13
|
+
if (!drug) {
|
|
14
|
+
return toolFailure(`No FDA data found for: ${args.name}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const openfda = drug.openfda ?? {};
|
|
18
|
+
return {
|
|
19
|
+
name: openfda.generic_name?.[0] ?? args.name,
|
|
20
|
+
brand_names: openfda.brand_name ?? [],
|
|
21
|
+
purpose:
|
|
22
|
+
first(drug.purpose as string[] | undefined) ??
|
|
23
|
+
first(drug.indications_and_usage as string[] | undefined) ??
|
|
24
|
+
"N/A",
|
|
25
|
+
warnings: first(drug.warnings as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
26
|
+
dosage: first(drug.dosage_and_administration as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
27
|
+
side_effects: first(drug.adverse_reactions as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
28
|
+
manufacturer: openfda.manufacturer_name?.[0] ?? "N/A",
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
});
|