G1 — Design System: 14 UI primitives (Button, Card, Modal, Sheet, Input, Textarea, Select, EmptyState, LoadingShimmer, ConfirmDialog, WashiTape, Badge, Avatar, Tabs), PageTransition with Framer Motion, sun/moon CSS vars, Caveat font, /dev/components visual showcase. G2 — Memories Pipeline: R2 presigned uploads, Sharp thumbnail generation, LiteLLM vision captions + pgvector embeddings, CSS masonry gallery with infinite scroll, private toggle, semantic search fallback to ILIKE. G3 — Medical: dose log + correction audit trail, IAP vaccine bulk import, emergency escalation page, pediatrician phone in settings. G4 — AI Brain: keyword guardrail → LLM classifier → structured DB tool-use (7 tools) → memory search → general parenting handler; ai_usage table; 22-case medical bypass safety test suite. DB migrations: 0011_memories, 0012_medical_doses, 0013_ai_usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3 KiB
TypeScript
91 lines
3 KiB
TypeScript
import { TOOL_DEFINITIONS, executeTool, type ToolContext } from "./db-tools";
|
|
|
|
const LITELLM_URL = process.env.LITELLM_BASE_URL;
|
|
const LITELLM_KEY = process.env.LITELLM_API_KEY;
|
|
const QUERY_MODEL = process.env.QUERY_MODEL || "minimax-2.7";
|
|
|
|
interface Message {
|
|
role: "system" | "user" | "assistant" | "tool";
|
|
content: string;
|
|
tool_calls?: ToolCall[];
|
|
tool_call_id?: string;
|
|
name?: string;
|
|
}
|
|
|
|
interface ToolCall {
|
|
id: string;
|
|
type: "function";
|
|
function: { name: string; arguments: string };
|
|
}
|
|
|
|
export async function answerStructuredQuery(query: string, ctx: ToolContext): Promise<string> {
|
|
if (!LITELLM_URL || !LITELLM_KEY) return "AI not configured.";
|
|
|
|
const now = new Date();
|
|
const systemPrompt = `You are a data assistant for ${ctx.childName}'s baby tracking app. Today is ${now.toLocaleDateString("en-IN", { weekday: "long", day: "numeric", month: "long" })}. Use the available tools to fetch data and answer the parent's question with specific numbers and times. Keep responses concise and warm. Use "she/her" or "he/his" based on context if unknown use their name. Never interpret symptoms or give medical advice.`;
|
|
|
|
const messages: Message[] = [
|
|
{ role: "system", content: systemPrompt },
|
|
{ role: "user", content: query },
|
|
];
|
|
|
|
// Tool-use loop (max 3 iterations)
|
|
for (let i = 0; i < 3; i++) {
|
|
const res = await fetch(`${LITELLM_URL}/v1/chat/completions`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${LITELLM_KEY}` },
|
|
body: JSON.stringify({
|
|
model: QUERY_MODEL,
|
|
messages,
|
|
tools: TOOL_DEFINITIONS,
|
|
tool_choice: "auto",
|
|
temperature: 0,
|
|
max_tokens: 400,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.text();
|
|
console.error("[structured-query] LLM error:", err);
|
|
break;
|
|
}
|
|
|
|
const data = await res.json();
|
|
const choice = data.choices?.[0];
|
|
const msg = choice?.message;
|
|
|
|
if (!msg) break;
|
|
|
|
// If the model wants to call tools
|
|
if (msg.tool_calls?.length) {
|
|
messages.push({ role: "assistant", content: msg.content || "", tool_calls: msg.tool_calls });
|
|
|
|
// Execute each tool call in parallel
|
|
const toolResults = await Promise.all(
|
|
msg.tool_calls.map(async (tc: ToolCall) => {
|
|
let args: Record<string, unknown> = {};
|
|
try { args = JSON.parse(tc.function.arguments); } catch { /* empty args */ }
|
|
const result = await executeTool(tc.function.name, args, ctx).catch(e => ({ error: String(e) }));
|
|
return {
|
|
role: "tool" as const,
|
|
tool_call_id: tc.id,
|
|
name: tc.function.name,
|
|
content: JSON.stringify(result),
|
|
};
|
|
})
|
|
);
|
|
|
|
messages.push(...toolResults);
|
|
continue;
|
|
}
|
|
|
|
// Final text response
|
|
if (choice?.finish_reason === "stop" && msg.content) {
|
|
return msg.content;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return "I couldn't retrieve that data right now. Please try again.";
|
|
}
|