wcz-layout
Version:
98 lines (85 loc) • 3.79 kB
Markdown
---
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 `/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 `/markdown` (install if missing):
`<Markdown>` from `/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" });
```