@alexkroman1/aai-cli 5.1.1 → 5.2.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.d.ts +39 -0
- package/dist/_api-client.d.ts +37 -0
- package/dist/_bundler.d.ts +46 -0
- package/dist/_config.d.ts +57 -0
- package/dist/_default-html.d.ts +12 -0
- package/dist/_deploy.d.ts +28 -0
- package/dist/_dev-server.d.ts +74 -0
- package/dist/{_init-B-LEVTHx.mjs → _init-DU-sXH6S.mjs} +44 -65
- package/dist/_init.d.ts +7 -0
- package/dist/_mock-api.d.ts +22 -0
- package/dist/_mock-registry.d.ts +17 -0
- package/dist/_output.d.ts +50 -0
- package/dist/_server-common.d.ts +17 -0
- package/dist/_slug-api.d.ts +15 -0
- package/dist/_templates.d.ts +21 -0
- package/dist/_typecheck-gate.d.ts +7 -0
- package/dist/_ui.d.ts +22 -0
- package/dist/_utils.d.ts +48 -0
- package/dist/_vite-env.d.ts +8 -0
- package/dist/build.d.ts +16 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +1 -1
- package/dist/client-bundler.d.ts +21 -0
- package/dist/delete.d.ts +18 -0
- package/dist/deploy.d.ts +15 -0
- package/dist/dev.d.ts +13 -0
- package/dist/{init-tc3Rbgrf.mjs → init-BjeK8crW.mjs} +1 -1
- package/dist/init.d.ts +24 -0
- package/dist/scaffold/.env.example +11 -0
- package/dist/scaffold/CLAUDE.md +919 -0
- package/dist/scaffold/global.d.ts +1 -0
- package/dist/scaffold/package.json +32 -0
- package/dist/scaffold/pnpm-workspace.yaml +13 -0
- package/dist/scaffold/tsconfig.json +19 -0
- package/dist/scaffold/vite.config.ts +14 -0
- package/dist/scaffold/vitest.config.ts +24 -0
- package/dist/secret.d.ts +25 -0
- package/dist/storage.d.ts +22 -0
- package/dist/templates/code-interpreter/agent.ts +11 -0
- package/dist/templates/code-interpreter/system-prompt.md +18 -0
- package/dist/templates/dispatch-center/agent.test.ts +153 -0
- package/dist/templates/dispatch-center/agent.ts +50 -0
- package/dist/templates/dispatch-center/client.tsx +378 -0
- package/dist/templates/dispatch-center/shared.ts +631 -0
- package/dist/templates/dispatch-center/system-prompt.md +39 -0
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +27 -0
- package/dist/templates/dispatch-center/tools/incident_create.ts +79 -0
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +88 -0
- package/dist/templates/dispatch-center/tools/incident_get.ts +36 -0
- package/dist/templates/dispatch-center/tools/incident_triage.ts +78 -0
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +68 -0
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +52 -0
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +27 -0
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +149 -0
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +97 -0
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +36 -0
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +62 -0
- package/dist/templates/embedded-assets/agent.ts +55 -0
- package/dist/templates/embedded-assets/knowledge.json +20 -0
- package/dist/templates/health-assistant/agent.ts +174 -0
- package/dist/templates/health-assistant/system-prompt.md +17 -0
- package/dist/templates/infocom-adventure/agent.ts +119 -0
- package/dist/templates/infocom-adventure/client.tsx +279 -0
- package/dist/templates/infocom-adventure/shared.ts +38 -0
- package/dist/templates/infocom-adventure/system-prompt.md +43 -0
- package/dist/templates/math-buddy/agent.ts +16 -0
- package/dist/templates/math-buddy/system-prompt.md +12 -0
- package/dist/templates/night-owl/agent.ts +76 -0
- package/dist/templates/night-owl/client.tsx +130 -0
- package/dist/templates/night-owl/shared.ts +5 -0
- package/dist/templates/personal-finance/agent.ts +11 -0
- package/dist/templates/personal-finance/system-prompt.md +17 -0
- package/dist/templates/pipeline-simple/agent.test.ts +32 -0
- package/dist/templates/pipeline-simple/agent.ts +13 -0
- package/dist/templates/pizza-ordering/agent.test.ts +235 -0
- package/dist/templates/pizza-ordering/agent.ts +174 -0
- package/dist/templates/pizza-ordering/client.tsx +120 -0
- package/dist/templates/pizza-ordering/shared.ts +142 -0
- package/dist/templates/pizza-ordering/system-prompt.md +17 -0
- package/dist/templates/simple/agent.ts +5 -0
- package/dist/templates/solo-rpg/agent.test.ts +477 -0
- package/dist/templates/solo-rpg/agent.ts +37 -0
- package/dist/templates/solo-rpg/client.tsx +809 -0
- package/dist/templates/solo-rpg/shared.ts +747 -0
- package/dist/templates/solo-rpg/system-prompt.md +115 -0
- package/dist/templates/solo-rpg/tools/action_roll.ts +98 -0
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +69 -0
- package/dist/templates/solo-rpg/tools/check_state.ts +11 -0
- package/dist/templates/solo-rpg/tools/load_game.ts +25 -0
- package/dist/templates/solo-rpg/tools/oracle.ts +248 -0
- package/dist/templates/solo-rpg/tools/save_game.ts +20 -0
- package/dist/templates/solo-rpg/tools/setup_character.ts +208 -0
- package/dist/templates/solo-rpg/tools/update_state.ts +170 -0
- package/dist/templates/web-researcher/agent.ts +11 -0
- package/dist/test.d.ts +39 -0
- package/dist/tsdown.config.d.ts +2 -0
- package/dist/typecheck.d.ts +26 -0
- package/dist/worker-bundler.d.ts +29 -0
- package/package.json +9 -6
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getState, RESOURCE_TYPES } from "../shared.ts";
|
|
4
|
+
|
|
5
|
+
export const resourcesGetAvailable = tool({
|
|
6
|
+
description: "List available resources, optionally filtered by type.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
type: z
|
|
9
|
+
.enum([...RESOURCE_TYPES, "all"])
|
|
10
|
+
.describe("Filter by resource type, or 'all'")
|
|
11
|
+
.optional(),
|
|
12
|
+
}),
|
|
13
|
+
async execute(args, ctx) {
|
|
14
|
+
const state = getState(ctx);
|
|
15
|
+
let resources = state.resources;
|
|
16
|
+
if (args.type && args.type !== "all") {
|
|
17
|
+
resources = resources.filter((r) => r.type === args.type);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
resources: resources.map((r) => ({
|
|
22
|
+
callsign: r.callsign,
|
|
23
|
+
type: r.type,
|
|
24
|
+
status: r.status,
|
|
25
|
+
assignedIncident: r.assignedIncident,
|
|
26
|
+
eta: r.eta,
|
|
27
|
+
capabilities: r.capabilities,
|
|
28
|
+
})),
|
|
29
|
+
summary: {
|
|
30
|
+
total: resources.length,
|
|
31
|
+
available: resources.filter((r) => r.status === "available").length,
|
|
32
|
+
committed: resources.filter((r) => r.status !== "available").length,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { logEvent, RESOURCE_STATUSES, updateState } from "../shared.ts";
|
|
4
|
+
|
|
5
|
+
export const resourcesUpdateStatus = tool({
|
|
6
|
+
description: "Update a resource unit's status when it radios in.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
callsign: z.string().max(50).describe("The resource callsign"),
|
|
9
|
+
status: z.enum(RESOURCE_STATUSES).describe("New status"),
|
|
10
|
+
notes: z.string().max(1000).describe("Status notes").optional(),
|
|
11
|
+
}),
|
|
12
|
+
async execute(args, ctx) {
|
|
13
|
+
return updateState(ctx, (state) => {
|
|
14
|
+
const resource = state.resources.find(
|
|
15
|
+
(r) => r.callsign.toLowerCase() === args.callsign.toLowerCase(),
|
|
16
|
+
);
|
|
17
|
+
if (!resource) {
|
|
18
|
+
return { error: `Resource ${args.callsign} not found` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const previousStatus = resource.status;
|
|
22
|
+
|
|
23
|
+
// Log to the incident timeline BEFORE unassigning, so the
|
|
24
|
+
// return-to-available transition is recorded too.
|
|
25
|
+
if (resource.assignedIncident) {
|
|
26
|
+
const inc = state.incidents[resource.assignedIncident];
|
|
27
|
+
if (inc) {
|
|
28
|
+
logEvent(
|
|
29
|
+
inc,
|
|
30
|
+
`${resource.callsign}: ${previousStatus} → ${args.status}${args.notes ? ` (${args.notes})` : ""}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
resource.status = args.status;
|
|
36
|
+
|
|
37
|
+
if (args.status === "available") {
|
|
38
|
+
// Detach from the old incident on BOTH sides — leaving the id in
|
|
39
|
+
// assignedResources lets that incident later yank a unit that has
|
|
40
|
+
// been re-dispatched elsewhere.
|
|
41
|
+
if (resource.assignedIncident) {
|
|
42
|
+
const inc = state.incidents[resource.assignedIncident];
|
|
43
|
+
if (inc) {
|
|
44
|
+
inc.assignedResources = inc.assignedResources.filter((id) => id !== resource.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
resource.assignedIncident = null;
|
|
48
|
+
resource.eta = null;
|
|
49
|
+
}
|
|
50
|
+
if (args.status === "on_scene") {
|
|
51
|
+
resource.eta = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
callsign: resource.callsign,
|
|
56
|
+
previousStatus,
|
|
57
|
+
newStatus: args.status,
|
|
58
|
+
assignedIncident: resource.assignedIncident,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { agent, assemblyAIPipeline, tool } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import knowledge from "./knowledge.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
type FaqEntry = { question: string; answer: string };
|
|
6
|
+
const faqs: FaqEntry[] = knowledge.faqs;
|
|
7
|
+
|
|
8
|
+
// Search text is static, so it's normalized once at module load — per-call
|
|
9
|
+
// work stays O(query) even when the knowledge base grows.
|
|
10
|
+
const searchable = faqs.map((entry) => ({
|
|
11
|
+
entry,
|
|
12
|
+
text: `${entry.question} ${entry.answer}`.toLowerCase(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
export default agent({
|
|
16
|
+
name: "FAQ Bot",
|
|
17
|
+
...assemblyAIPipeline(),
|
|
18
|
+
systemPrompt:
|
|
19
|
+
"You are a friendly FAQ assistant. Answer questions using ONLY the information from your embedded knowledge base. If the user asks something not covered by your knowledge base, say you don't have that information and suggest they check the official documentation.\n\nRules:\n- Keep answers concise and conversational — this is a voice agent\n- Quote the knowledge base accurately, do not embellish\n- If a question is ambiguous, ask the user to clarify\n- Use 'search_knowledge' to find answers to specific questions\n- Use 'list_topics' to see all available FAQ topics\n- Always be helpful and polite",
|
|
20
|
+
greeting:
|
|
21
|
+
"Hi! I'm your FAQ assistant. Ask me anything about the AAI agent framework and I'll look it up in my knowledge base.",
|
|
22
|
+
|
|
23
|
+
tools: {
|
|
24
|
+
list_topics: tool({
|
|
25
|
+
description: "List all available topics in the embedded FAQ knowledge base.",
|
|
26
|
+
async execute() {
|
|
27
|
+
return faqs.map((f) => f.question);
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
search_knowledge: tool({
|
|
32
|
+
description:
|
|
33
|
+
"Search the embedded FAQ knowledge base for an answer matching the user's question.",
|
|
34
|
+
parameters: z.object({
|
|
35
|
+
query: z.string().describe("The user's question to search for"),
|
|
36
|
+
}),
|
|
37
|
+
async execute(args) {
|
|
38
|
+
// Score by word overlap — natural questions rarely match an FAQ
|
|
39
|
+
// entry as an exact substring.
|
|
40
|
+
const words = args.query
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.split(/\W+/)
|
|
43
|
+
.filter((w) => w.length > 2);
|
|
44
|
+
if (words.length === 0) return { result: "No matching FAQ found." };
|
|
45
|
+
|
|
46
|
+
let best: { entry: FaqEntry; score: number } | null = null;
|
|
47
|
+
for (const { entry, text } of searchable) {
|
|
48
|
+
const score = words.filter((w) => text.includes(w)).length;
|
|
49
|
+
if (score > 0 && (!best || score > best.score)) best = { entry, score };
|
|
50
|
+
}
|
|
51
|
+
return best?.entry ?? { result: "No matching FAQ found." };
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"faqs": [
|
|
3
|
+
{
|
|
4
|
+
"question": "What is AAI?",
|
|
5
|
+
"answer": "AAI is a framework for building voice-powered AI agents that run on Node.js."
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"question": "How do agents handle tools?",
|
|
9
|
+
"answer": "Agents define tools with a schema and handler function. The LLM decides when to call them based on the conversation."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"question": "What speech providers are supported?",
|
|
13
|
+
"answer": "AAI supports AssemblyAI for speech-to-text and text-to-speech."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"question": "Can agents access the internet?",
|
|
17
|
+
"answer": "Yes, agents run with network access and can make HTTP requests via the fetch API or built-in tools like web_search."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { agent, assemblyAIPipeline, tool } from "@alexkroman1/aai";
|
|
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
|
+
}
|
|
76
|
+
|
|
77
|
+
export default agent({
|
|
78
|
+
name: "Dr. Sage",
|
|
79
|
+
...assemblyAIPipeline(),
|
|
80
|
+
systemPrompt,
|
|
81
|
+
greeting:
|
|
82
|
+
"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.",
|
|
83
|
+
builtinTools: ["web_search", "run_code"],
|
|
84
|
+
|
|
85
|
+
tools: {
|
|
86
|
+
check_drug_interaction: tool({
|
|
87
|
+
description:
|
|
88
|
+
"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.",
|
|
89
|
+
parameters: z.object({
|
|
90
|
+
drugs: z
|
|
91
|
+
.array(z.string().min(1))
|
|
92
|
+
.min(2)
|
|
93
|
+
.describe("Medication names to check, e.g. ['ibuprofen', 'warfarin']"),
|
|
94
|
+
}),
|
|
95
|
+
async execute(args) {
|
|
96
|
+
const names = args.drugs.map((d) => d.trim().toLowerCase()).filter((d) => d.length > 0);
|
|
97
|
+
if (names.length < 2) {
|
|
98
|
+
return { error: "Provide at least two medication names to check." };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const labels = await Promise.all(names.map((n) => fetchFdaLabel(n)));
|
|
102
|
+
const unresolved = names.filter((_, i) => labels[i] === null);
|
|
103
|
+
if (unresolved.length > 0) {
|
|
104
|
+
// Never silently drop a drug from an interaction check — a partial
|
|
105
|
+
// answer would read as "no interaction" for the missing one.
|
|
106
|
+
return {
|
|
107
|
+
error: `Could not find FDA label data for: ${unresolved.join(", ")}. Check the spelling, or try the generic name.`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The unresolved check above guarantees every label resolved.
|
|
112
|
+
const drugs = names.map((name, i) => toDrugInfo(name, labels[i]!));
|
|
113
|
+
|
|
114
|
+
const interactions: Array<{ drug: string; mentions: string; excerpt: string }> = [];
|
|
115
|
+
for (const a of drugs) {
|
|
116
|
+
if (!a.interactionsText) continue;
|
|
117
|
+
for (const b of drugs) {
|
|
118
|
+
if (a === b) continue;
|
|
119
|
+
const hit = b.aliases.find((alias) => a.interactionsText.includes(alias));
|
|
120
|
+
if (hit) {
|
|
121
|
+
interactions.push({
|
|
122
|
+
drug: a.name,
|
|
123
|
+
mentions: b.name,
|
|
124
|
+
excerpt: excerptAround(a.interactionsText, hit).slice(0, 400),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
drugs: names,
|
|
132
|
+
interactions_found: interactions.length,
|
|
133
|
+
interactions: interactions.slice(0, 5),
|
|
134
|
+
note:
|
|
135
|
+
interactions.length === 0
|
|
136
|
+
? "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."
|
|
137
|
+
: "Based on FDA label Drug Interactions sections. Confirm with a pharmacist or doctor.",
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
medication_lookup: tool({
|
|
143
|
+
description:
|
|
144
|
+
"Look up detailed information about a single medication, including purpose, warnings, dosage, side effects, and manufacturer. Works with both generic and brand names.",
|
|
145
|
+
parameters: z.object({
|
|
146
|
+
name: z
|
|
147
|
+
.string()
|
|
148
|
+
.describe("Medication name (generic or brand, e.g. 'ibuprofen' or 'Advil')"),
|
|
149
|
+
}),
|
|
150
|
+
async execute(args) {
|
|
151
|
+
const drug = await fetchFdaLabel(args.name);
|
|
152
|
+
if (!drug) {
|
|
153
|
+
return { error: `No FDA data found for: ${args.name}` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const openfda = drug.openfda ?? {};
|
|
157
|
+
return {
|
|
158
|
+
name: openfda.generic_name?.[0] ?? args.name,
|
|
159
|
+
brand_names: openfda.brand_name ?? [],
|
|
160
|
+
purpose:
|
|
161
|
+
first(drug.purpose as string[] | undefined) ??
|
|
162
|
+
first(drug.indications_and_usage as string[] | undefined) ??
|
|
163
|
+
"N/A",
|
|
164
|
+
warnings: first(drug.warnings as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
165
|
+
dosage:
|
|
166
|
+
first(drug.dosage_and_administration as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
167
|
+
side_effects:
|
|
168
|
+
first(drug.adverse_reactions as string[] | undefined)?.slice(0, 500) ?? "N/A",
|
|
169
|
+
manufacturer: openfda.manufacturer_name?.[0] ?? "N/A",
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
You are Dr. Sage, a friendly health information assistant. You help people understand symptoms, look up medication details, check drug interactions, and calculate basic health metrics.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- You are NOT a doctor and cannot diagnose or prescribe. Always remind users to consult a healthcare provider for medical decisions.
|
|
5
|
+
- Be clear and calm when discussing symptoms — avoid alarming language
|
|
6
|
+
- When discussing medications, always mention common side effects
|
|
7
|
+
- Use plain language first, then mention the medical term
|
|
8
|
+
- Keep responses concise — this is a voice conversation
|
|
9
|
+
- If symptoms sound urgent (chest pain, difficulty breathing, sudden numbness), advise calling emergency services immediately
|
|
10
|
+
- Use web_search to look up current symptom information when needed
|
|
11
|
+
- Use medication_lookup to get details on a single medication
|
|
12
|
+
- Use check_drug_interaction to check interactions between multiple drugs. It reads FDA drug labels — if it finds no mention, say that no interaction was found on the labels but that this does not prove the combination is safe, and recommend asking a pharmacist
|
|
13
|
+
|
|
14
|
+
Use run_code for health calculations:
|
|
15
|
+
- BMI: weight_kg / (height_m * height_m). Categories: <18.5 underweight, 18.5-25 normal, 25-30 overweight, >30 obese
|
|
16
|
+
Unit conversions: 1 lb = 0.453592 kg, 1 in = 0.0254 m, 1 ft = 0.3048 m, 1 cm = 0.01 m
|
|
17
|
+
- Weight-based dosage: dose_mg = weight_kg * dose_per_kg. Always note this is an estimate.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { agent, assemblyAIPipeline, tool } from "@alexkroman1/aai";
|
|
2
|
+
import { assemblyAI as assemblyAITts } from "@alexkroman1/aai/tts";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getGameState, resetGameState } from "./shared.ts";
|
|
5
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
6
|
+
|
|
7
|
+
export default agent({
|
|
8
|
+
name: "Cavern Adventure",
|
|
9
|
+
...assemblyAIPipeline(),
|
|
10
|
+
// Overriding one stage of the preset: a narrator wants a narrative voice,
|
|
11
|
+
// and the other two stages stay as the preset set them.
|
|
12
|
+
tts: assemblyAITts({ voice: "paul" }),
|
|
13
|
+
systemPrompt,
|
|
14
|
+
// The opening scene here must agree with DEFAULT_GAME_STATE.currentRoom
|
|
15
|
+
// (shared.ts) and the world map in system-prompt.md.
|
|
16
|
+
greeting:
|
|
17
|
+
"Welcome, adventurer. You are standing at the mouth of a weathered cave at the edge of a pine forest. A cold wind carries the smell of damp stone up from the darkness below. A rusted lantern hangs from an iron hook beside the entrance. What would you like to do?",
|
|
18
|
+
|
|
19
|
+
tools: {
|
|
20
|
+
game_state_drop: tool({
|
|
21
|
+
description: "Remove an item from the player's inventory.",
|
|
22
|
+
parameters: z.object({
|
|
23
|
+
value: z.string().describe("Item name to drop"),
|
|
24
|
+
}),
|
|
25
|
+
async execute(args, ctx) {
|
|
26
|
+
const g = getGameState(ctx);
|
|
27
|
+
g.inventory = g.inventory.filter((i) => i !== args.value);
|
|
28
|
+
return { inventory: g.inventory };
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
game_state_flag: tool({
|
|
33
|
+
description: "Set a game flag to true, used for tracking puzzle and event state.",
|
|
34
|
+
parameters: z.object({
|
|
35
|
+
value: z.string().describe("Flag name to set"),
|
|
36
|
+
}),
|
|
37
|
+
async execute(args, ctx) {
|
|
38
|
+
const g = getGameState(ctx);
|
|
39
|
+
g.flags[args.value] = true;
|
|
40
|
+
return { flags: g.flags };
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
game_state_get: tool({
|
|
45
|
+
description:
|
|
46
|
+
"Read the current game state including inventory, current room, score, moves, flags, and recent history.",
|
|
47
|
+
async execute(_args, ctx) {
|
|
48
|
+
const g = getGameState(ctx);
|
|
49
|
+
return {
|
|
50
|
+
currentRoom: g.currentRoom,
|
|
51
|
+
inventory: g.inventory,
|
|
52
|
+
score: g.score,
|
|
53
|
+
moves: g.moves,
|
|
54
|
+
flags: g.flags,
|
|
55
|
+
recentHistory: g.history.slice(-5),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
game_state_history: tool({
|
|
61
|
+
description: "Log a player command to the history and increment the move counter.",
|
|
62
|
+
parameters: z.object({
|
|
63
|
+
value: z.string().describe("Command text to log"),
|
|
64
|
+
}),
|
|
65
|
+
async execute(args, ctx) {
|
|
66
|
+
const g = getGameState(ctx);
|
|
67
|
+
g.history.push(args.value);
|
|
68
|
+
g.moves++;
|
|
69
|
+
return { moves: g.moves, recentHistory: g.history.slice(-5) };
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
game_state_move: tool({
|
|
74
|
+
description: "Move the player to a new room and increment the move counter.",
|
|
75
|
+
parameters: z.object({
|
|
76
|
+
value: z.string().describe("Room name to move to"),
|
|
77
|
+
}),
|
|
78
|
+
async execute(args, ctx) {
|
|
79
|
+
const g = getGameState(ctx);
|
|
80
|
+
g.currentRoom = args.value;
|
|
81
|
+
g.moves++;
|
|
82
|
+
return { currentRoom: g.currentRoom, moves: g.moves };
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
game_state_restart: tool({
|
|
87
|
+
description:
|
|
88
|
+
"Reset the game to the beginning: empty inventory, zero score and moves, all flags cleared. Use when the player asks to restart, quit, or start a new game.",
|
|
89
|
+
async execute(_args, ctx) {
|
|
90
|
+
const g = resetGameState(ctx);
|
|
91
|
+
return { restarted: true, currentRoom: g.currentRoom };
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
game_state_score: tool({
|
|
96
|
+
description: "Add points to the player's score.",
|
|
97
|
+
parameters: z.object({
|
|
98
|
+
value: z.number().describe("Points to add"),
|
|
99
|
+
}),
|
|
100
|
+
async execute(args, ctx) {
|
|
101
|
+
const g = getGameState(ctx);
|
|
102
|
+
g.score += args.value;
|
|
103
|
+
return { score: g.score };
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
game_state_take: tool({
|
|
108
|
+
description: "Add an item to the player's inventory.",
|
|
109
|
+
parameters: z.object({
|
|
110
|
+
value: z.string().describe("Item name to take"),
|
|
111
|
+
}),
|
|
112
|
+
async execute(args, ctx) {
|
|
113
|
+
const g = getGameState(ctx);
|
|
114
|
+
if (!g.inventory.includes(args.value)) g.inventory.push(args.value);
|
|
115
|
+
return { inventory: g.inventory };
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
});
|