UNPKG

wcz-layout

Version:

98 lines (85 loc) • 3.79 kB
--- name: ai description: "Use when adding LLM features (chat, streamed text, structured output, tools, media generation)" metadata: type: convention library: wcz-layout --- > Mechanics are covered by TanStack's own `@tanstack/ai` skills. > This skill adds the wcz-layout conventions on top of that. ## Rules - The gateway needs three server-side inputs: - Ensure that required env are present: `AI_BASE_URL` (`z.url()`) and `AI_SUBSCRIPTION_KEY` (`z.string()`) in `src/env.ts`. - Pass `getAppToken("cognitiveServices")` as the API key. - The subscription key sent as the `Ocp-Apim-Subscription-Key` header. - Build the adapter in one place, `src/server/ai.ts`, and call it per request. `getAppToken` caches and refreshes the token. - Build every chat with `createLayoutChatHook(chatFn)` from `wcz-layout/hooks`. - The model renders MUI components itself (`show_ui`) and asks the user with MUI forms (`ask_user`). The client sends both tools with every request, so the server function only merges them: `tools: mergeAgentTools(serverTools, tools)`. It must also forward `parentRunId` and `resume`, or tool results and approvals never continue the run. - App tools are server tools (`toolDefinition(...).server(...)`) in `serverTools`; they check the signed-in user themselves. Give every tool that writes `needsApproval: true`: the chat shows an Approve / Deny card for it. Their calls show only the progress bar in the transcript. - Structured output (one typed object, e.g. a generated document) stays raw `useChat` with `outputSchema` and `partial`, not the chat. - Render streamed text outside the chat with `@tanstack/markdown` (install if missing): `<Markdown>` from `@tanstack/markdown/react`. ## Example ```ts // src/server/ai.ts import { createOpenaiChat } from "@tanstack/ai-openai"; import { getAppToken } from "wcz-layout/auth"; import { serverEnv } from "~/env"; export const aiText = async () => createOpenaiChat("some-model", await getAppToken("cognitiveServices"), { baseURL: serverEnv.AI_BASE_URL, defaultHeaders: { "Ocp-Apim-Subscription-Key": serverEnv.AI_SUBSCRIPTION_KEY }, }); // src/server/actions/<feature>.ts import { chat, chatParamsFromRequestBody, mergeAgentTools, toServerSentEventsResponse, toolDefinition, } from "@tanstack/ai"; import type { RunAgentInput } from "@tanstack/ai/client"; import { createServerFn } from "@tanstack/react-start"; import z from "zod"; import { authMiddleware } from "wcz-layout/middleware"; import { aiText } from "~/server/ai"; const getOrders = toolDefinition({ name: "get_orders", description: "Orders of the signed-in user", inputSchema: z.object({ status: z.enum(["open", "closed"]) }), }).server(async ({ status }) => findOrders(status)); export const chatFn = createServerFn({ method: "POST" }) .validator(z.custom<RunAgentInput>()) .middleware([authMiddleware()]) .handler(async ({ data }) => { const { messages, threadId, runId, parentRunId, resume, tools } = await chatParamsFromRequestBody(data); const abortController = new AbortController(); const stream = chat({ adapter: await aiText(), threadId, runId, parentRunId, resume, systemPrompts: [SYSTEM_PROMPT], messages, tools: mergeAgentTools([getOrders], tools), abortController, }); return toServerSentEventsResponse(stream, { abortController }); }); ``` ```tsx // client component import { createLayoutChatHook } from "wcz-layout/hooks"; import { chatFn } from "~/server/actions/chat"; const { useLayoutChat, LayoutChat } = createLayoutChatHook(chatFn); const chat = useLayoutChat({ threadId: "assistant" }); ```