# AI SDK Backend Integration Guidelines ## 1. Overview This document covers backend integration patterns using the Vercel AI SDK (`ai` package) for AI-powered features. ### Supported Providers - **OpenAI**: GPT-4o, GPT-4o-mini, GPT-4-turbo - **Google Gemini**: gemini-1.5-pro, gemini-1.5-flash - **Anthropic**: Claude 3.5 Sonnet, Claude 3 Opus ### Package Dependencies ```bash pnpm add ai @ai-sdk/openai @ai-sdk/google @ai-sdk/anthropic ``` ## 2. Basic Usage ### generateText Use `generateText` for simple text generation tasks where you need a complete response. ```typescript import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; const { text } = await generateText({ model: openai("gpt-4o-mini"), prompt: "Summarize this document...", }); ``` ### generateObject (Structured Output with Zod) Use `generateObject` when you need type-safe structured output. The AI SDK validates the response against your Zod schema automatically. ```typescript import { generateObject } from "ai"; import { openai } from "@ai-sdk/openai"; import { z } from "zod"; const classificationSchema = z.object({ category: z.enum(["urgent", "normal", "low"]), confidence: z.number().min(0).max(1), reasoning: z.string(), }); const { object } = await generateObject({ model: openai("gpt-4o-mini"), schema: classificationSchema, prompt: "Classify the priority of this task...", }); // object is typed as { category: "urgent" | "normal" | "low", confidence: number, reasoning: string } ``` ### streamText (For SSE/Streaming) Use `streamText` for real-time streaming responses, ideal for chat interfaces and long-form content generation. ```typescript import { streamText } from "ai"; import { openai } from "@ai-sdk/openai"; const result = streamText({ model: openai("gpt-4o"), messages: conversationHistory, system: "You are a helpful assistant.", }); // Return as SSE stream return result.toDataStreamResponse(); ``` ## 3. Telemetry Configuration **IMPORTANT**: Always enable telemetry for token tracking and performance monitoring. ```typescript import { generateObject } from "ai"; import { openai } from "@ai-sdk/openai"; const { object } = await generateObject({ model: openai("gpt-4o-mini"), schema: mySchema, prompt, experimental_telemetry: { isEnabled: true, functionId: "orders.classify", // Module.function naming metadata: { orderId, userId, }, }, }); ``` ### Telemetry Naming Convention Use dot-separated format for `functionId`: `module.function` | Module | Example functionId | |--------|-------------------| | Orders | `orders.classify`, `orders.summarize` | | Support | `support.generateReply`, `support.categorize` | | Content | `content.summarize`, `content.translate` | | Users | `users.analyzePreferences` | ### Auto-recorded Metrics When telemetry is enabled, these metrics are automatically tracked: | Metric | Description | |--------|-------------| | `ai.model.id` | Model identifier (e.g., gpt-4o-mini) | | `ai.model.provider` | Provider name (e.g., openai) | | `ai.usage.prompt_tokens` | Input tokens consumed | | `ai.usage.completion_tokens` | Output tokens generated | | `ai.usage.total_tokens` | Total tokens used | | `ai.response.finish_reason` | Completion reason (stop, length, etc.) | ## 4. Tool Calling Define tools that the AI model can invoke to perform actions in your system. ```typescript import { generateText, tool } from "ai"; import { openai } from "@ai-sdk/openai"; import { z } from "zod"; const result = await generateText({ model: openai("gpt-4o"), prompt: "Create a task for the user...", tools: { createTask: tool({ description: "Create a new task in the system", parameters: z.object({ title: z.string(), dueDate: z.string().optional(), priority: z.enum(["high", "medium", "low"]), }), execute: async ({ title, dueDate, priority }) => { const task = await db.insert(tasks).values({ title, dueDate: dueDate ? new Date(dueDate) : null, priority, }).returning(); return { success: true, taskId: task[0].id }; }, }), searchOrders: tool({ description: "Search for orders by criteria", parameters: z.object({ query: z.string(), status: z.enum(["pending", "completed", "cancelled"]).optional(), limit: z.number().default(10), }), execute: async ({ query, status, limit }) => { const orders = await db.query.orders.findMany({ where: and( like(orders.title, `%${query}%`), status ? eq(orders.status, status) : undefined ), limit, }); return { orders }; }, }), }, }); // Access tool results if (result.toolCalls) { for (const toolCall of result.toolCalls) { console.log(`Tool: ${toolCall.toolName}`, toolCall.result); } } ``` ## 5. Error Handling Always implement graceful error handling for AI operations. ```typescript import { generateObject } from "ai"; import { openai } from "@ai-sdk/openai"; import { logger } from "@your-app/logs"; async function classifyOrder(orderData: OrderData) { try { const { object } = await generateObject({ model: openai("gpt-4o-mini"), schema: classificationSchema, prompt: buildClassificationPrompt(orderData), experimental_telemetry: { isEnabled: true, functionId: "orders.classify", }, }); return { success: true, data: object }; } catch (error) { logger.error("AI generation failed", { error, orderId: orderData.id, prompt: buildClassificationPrompt(orderData).slice(0, 100) }); // Return graceful fallback return { success: false, reason: "AI processing failed", error: error instanceof Error ? error.message : "Unknown error", }; } } ``` ### Common Error Types | Error | Cause | Resolution | |-------|-------|------------| | Rate limit exceeded | Too many requests | Implement exponential backoff | | Context length exceeded | Prompt too long | Truncate or summarize input | | Invalid API key | Missing/wrong credentials | Check environment variables | | Schema validation failed | AI output doesn't match schema | Adjust schema or prompt | ## 6. Prompt Engineering Best Practices ### Use XML Structure for Complex Prompts XML tags help the AI model better understand the structure of your request. ```typescript const prompt = ` ${contextData} Analyze the above context and extract key information. Return a JSON object with the following fields: - summary: A brief summary - keyPoints: Array of key points - sentiment: positive, negative, or neutral `; ``` ### System Prompts Define consistent behavior with system prompts. ```typescript import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; const result = await generateText({ model: openai("gpt-4o"), system: `You are a professional assistant. Always respond in a structured format. Be concise and accurate. Never make up information - if unsure, say so.`, messages: userMessages, }); ``` ### Multi-step Prompts For complex tasks, break down into multiple AI calls. ```typescript // Step 1: Extract entities const { object: entities } = await generateObject({ model: openai("gpt-4o-mini"), schema: entitiesSchema, prompt: `Extract entities from: ${document}`, }); // Step 2: Classify based on entities const { object: classification } = await generateObject({ model: openai("gpt-4o-mini"), schema: classificationSchema, prompt: ` ${JSON.stringify(entities, null, 2)} Based on these entities, classify the document category. `, }); ``` ## 7. Provider-Specific Configuration ### OpenAI ```typescript import { openai } from "@ai-sdk/openai"; const model = openai("gpt-4o-mini", { // Optional: custom configuration }); ``` ### Google Gemini ```typescript import { google } from "@ai-sdk/google"; const model = google("gemini-1.5-flash"); ``` ### Anthropic ```typescript import { anthropic } from "@ai-sdk/anthropic"; const model = anthropic("claude-3-5-sonnet-20241022"); ``` ## 8. Best Practices Summary | Rule | Description | |------|-------------| | Always enable telemetry | Track token usage and performance for cost monitoring | | Use generateObject for structured output | Leverage Zod schemas for type safety and validation | | Use XML prompts for complex tasks | Better structure improves AI understanding | | Handle errors gracefully | Return fallback responses, never crash | | Log AI failures | Include context (truncated prompt, IDs) for debugging | | Use appropriate model sizes | Use mini models for simple tasks, larger for complex | | Implement rate limiting | Protect against API quota exhaustion | | Cache responses when appropriate | Reduce costs for repeated queries | ## 9. Environment Variables Required environment variables for AI providers: ```bash # OpenAI OPENAI_API_KEY=sk-... # Google Gemini GOOGLE_GENERATIVE_AI_API_KEY=... # Anthropic ANTHROPIC_API_KEY=sk-ant-... ``` ## Scenario: Elder AI Analysis MVP With LangChain And PostgreSQL JSONB Embeddings ### 1. Scope / Trigger - Trigger: AI features that inspect resident, care, health, family, admission, alert, incident, or knowledge data. - Current project contract: the elder analysis MVP uses LangChain on the server with an OpenAI-compatible provider, not the Vercel AI SDK runtime path. - Keep provider calls under `modules/ai/server/*`; feature modules and API routes must not instantiate model clients directly. ### 2. Signatures - API: - `GET /api/ai/elders/[id]/analyses` - `POST /api/ai/elders/[id]/analyses` - `GET /api/ai/knowledge` - `POST /api/ai/knowledge` - `PATCH /api/ai/knowledge/[id]` - `DELETE /api/ai/knowledge/[id]` - DB: - `ai_knowledge_entries` - `ai_knowledge_chunks` with `jsonb` `embedding` arrays - `elder_ai_analyses` - Runtime env: - `AI_API_KEY` required for generation and embedding. - `AI_BASE_URL` optional OpenAI-compatible endpoint. - `AI_CHAT_MODEL` optional, defaults in `modules/ai/server/config.ts`. - `AI_EMBEDDING_MODEL` optional, defaults in `modules/ai/server/config.ts`. ### 3. Contracts - All API responses use the project API shape: `{ success: boolean, reason: string, ...payload }`. - Knowledge entry input fields: - `scope`: `"platform" | "organization"` - `title`: non-empty string - `category`: string - `tags`: string - `body`: non-empty string - `status`: `"enabled" | "disabled"` - Elder analysis history stores: - `status`: `"completed" | "failed"` - `dataScopes`: JSON array of included families - `resultJson`: only for completed rows - `errorCategory` / `errorReason`: sanitized only for failed rows - Failed rows must not store prompts, full resident context snapshots, API keys, or raw provider errors. - Knowledge retrieval may return enabled platform knowledge and enabled active-organization knowledge only. - Knowledge retrieval first filters enabled platform / active-organization chunks in SQL, then computes cosine similarity in `modules/ai/server/knowledge.ts`; this keeps the MVP deployable on plain PostgreSQL without the `vector` extension. - Completed elder analysis `resultJson.citations` must be derived from citation IDs actually referenced by `keyFindings[].citationIds` and `recommendations[].citationIds`; do not append unused available citations. ### 4. Validation & Error Matrix - Missing `ai:read` -> `403 权限不足`. - Missing `elder:read` -> `403 权限不足`. - Missing active organization for elder analysis -> `400`. - Target elder outside active organization -> `404`. - Missing `knowledge:read` -> knowledge scope is omitted from analysis context. - Missing `knowledge:manage` -> knowledge mutation routes return `403`. - Platform knowledge mutation without `platform:manage` -> `403`. - Missing `AI_API_KEY` -> save failed analysis with `missing_config`, return structured failure. - Provider failure -> save failed analysis with `provider_error` or `timeout`. - Invalid model output -> save failed analysis with `schema_validation_failed`. - Unknown citation IDs in findings or recommendations -> save failed analysis with `schema_validation_failed`, return structured failure. ### 5. Good/Base/Bad Cases - Good: org manager generates elder analysis; context includes only data families allowed by the manager's current permissions; history stores matching `dataScopes`. - Base: caregiver generates analysis; caregiver gets `ai:read` and `knowledge:read` by default, but cannot mutate knowledge. - Bad: viewer has `elder:read` but no `ai:read`; the AI row action must not be exposed and API must return `403`. - Bad: organization user attempts to retrieve another organization's private knowledge; the retrieval query must not include those chunks. ### 6. Tests Required - Permission seeding assertions for `ai:*` and `knowledge:*` default role grants. - Knowledge retrieval tests for platform shared, own organization, other organization, disabled entry filtering, and cosine-score ordering from JSONB embeddings. - Analysis tests for missing config, schema validation failure, failed-history sanitization, and data-scope redaction. - Analysis tests for unknown citation rejection and referenced-only citation persistence. - Route tests for auth/permission failures and standard response shape. ### 7. Wrong vs Correct #### Wrong ```typescript // Do not call a model from a feature route and pass raw resident rows directly. const model = new ChatOpenAI({ apiKey: process.env.AI_API_KEY }); await model.invoke(JSON.stringify(residentRows)); ``` #### Correct ```typescript // Keep model calls in modules/ai/server and persist only citations that findings/recommendations reference. const result = await generateElderAiAnalysis(auth.context, elderId); if (!result.success) { return jsonFailure(result.reason, result.status); } return jsonSuccess("AI 分析已生成", { analysis: result.data.analysis }, 201); ``` ```typescript // Good evidence chain: every cited source ID is from the allowed resident/knowledge citations. const finding = { category: "跌倒风险", severity: "warning", evidence: "夜间徘徊后需要加强通道清理和巡视。", citationIds: ["resident-1", "kb-1"], }; ```