// Curated tool surface the assistant can call. The LLM emits a fenced // ```tool block with one JSON object per line; we parse, execute via // arcadia-client, and feed results back as the next user turn. // // Each tool is a named function with documented args. The LLM never sees // raw HTTP — only the menu below. import type { ArcadiaClient } from "@crema/arcadia-client" import type { Tool, ToolCall as LLMToolCall } from "@crema/llm-ui" import { getTenant, listTenants, type Tenant } from "~/lib/arcadia/tenants" export type ToolCall = { name: string args: Record } export type ToolResult = { name: string args: Record ok: boolean data?: unknown error?: string } type ToolDef = { name: string description: string parameters: Record // JSON Schema for OpenAI tool calling isWrite: boolean run: (args: Record, ctx: ToolCtx) => Promise } type ToolCtx = { arcadia: ArcadiaClient } const TOOLS: ToolDef[] = [ { name: "list_tenants", description: "List every tenant on this arcadia deployment. Returns id, slug, name, status, plan, inserted_at. Call this for any question about tenant counts, statuses, or which tenants exist.", parameters: { type: "object", properties: {}, additionalProperties: false, }, isWrite: false, run: async (_args, { arcadia }) => { const tenants = await listTenants(arcadia) return tenants.map(summarize) }, }, { name: "get_tenant", description: "Fetch a single tenant by slug (preferred) or id. Returns the tenant summary or null if not found.", parameters: { type: "object", properties: { slug: { type: "string", description: "The tenant's slug (e.g. 'acme', 'platform-admin')." }, id: { type: "string", description: "The tenant's UUID. Use only when the slug is unknown." }, }, additionalProperties: false, }, isWrite: false, run: async (args, { arcadia }) => { const slug = typeof args.slug === "string" ? args.slug : null const id = typeof args.id === "string" ? args.id : null if (!slug && !id) throw new Error("get_tenant requires { slug } or { id }") if (id) { try { return summarize(await getTenant(arcadia, id)) } catch { return null } } const tenants = await listTenants(arcadia) const found = tenants.find((t) => t.slug === slug) return found ? summarize(found) : null }, }, ] /** OpenAI-format tool list to pass into ChatRequest.tools. */ export function getOpenAITools(): Tool[] { return TOOLS.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters, })) } function summarize(t: Tenant) { return { id: t.id, slug: t.slug, name: t.name, status: t.status, plan: t.plan?.name ?? null, inserted_at: t.inserted_at, } } const TOOL_BY_NAME = new Map(TOOLS.map((t) => [t.name, t])) function safeJson(value: unknown): string { try { const text = JSON.stringify(value, null, 2) if (text.length > 6000) return text.slice(0, 6000) + "\n…(truncated)" return text } catch { return "(unserializable)" } } /** Run a list of provider-native tool calls and return `tool` role messages * ready to push back into useChat history. */ export async function runLLMToolCalls( calls: LLMToolCall[], ctx: ToolCtx, opts: { allowWrites?: boolean } = {}, ): Promise<{ results: ToolResult[] toolMessages: { role: "tool"; content: string; toolCallId: string; name: string }[] }> { const results: ToolResult[] = [] const toolMessages: { role: "tool"; content: string; toolCallId: string; name: string }[] = [] for (const call of calls) { const def = TOOL_BY_NAME.get(call.name) let parsed: Record = {} try { parsed = call.arguments ? (JSON.parse(call.arguments) as Record) : {} } catch { const err = `Could not parse arguments JSON: ${call.arguments}` results.push({ name: call.name, args: {}, ok: false, error: err }) toolMessages.push({ role: "tool", content: JSON.stringify({ error: err }), toolCallId: call.id, name: call.name }) continue } if (!def) { const err = `Unknown tool: ${call.name}` results.push({ name: call.name, args: parsed, ok: false, error: err }) toolMessages.push({ role: "tool", content: JSON.stringify({ error: err }), toolCallId: call.id, name: call.name }) continue } if (def.isWrite && !opts.allowWrites) { const err = "Write tools require user confirmation." results.push({ name: call.name, args: parsed, ok: false, error: err }) toolMessages.push({ role: "tool", content: JSON.stringify({ error: err }), toolCallId: call.id, name: call.name }) continue } try { const data = await def.run(parsed, ctx) results.push({ name: call.name, args: parsed, ok: true, data }) toolMessages.push({ role: "tool", content: safeJson(data), toolCallId: call.id, name: call.name }) } catch (err) { const msg = err instanceof Error ? err.message : String(err) results.push({ name: call.name, args: parsed, ok: false, error: msg }) toolMessages.push({ role: "tool", content: JSON.stringify({ error: msg }), toolCallId: call.id, name: call.name }) } } return { results, toolMessages } }