diff --git a/CHAPTER-0.md b/CHAPTER-0.md index 70708d2..461d198 100644 --- a/CHAPTER-0.md +++ b/CHAPTER-0.md @@ -42,31 +42,51 @@ The heart of the application is `/app/(chat)/api/chat/route.ts`. This is where m ### Basic Chat Flow ```typescript -// app/(chat)/api/chat/route.ts (simplified) -import { streamText } from "ai"; +// app/(chat)/api/chat/route.ts (core streaming logic) +import { + convertToModelMessages, + createUIMessageStream, + JsonToSseTransformStream, + smoothStream, + streamText, +} from "ai"; +import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; import { myProvider } from "@/lib/ai/providers"; -import { systemPrompt } from "@/lib/ai/prompts"; -export async function POST(request: Request) { - const { messages } = await request.json(); - - // Stream the AI response - const result = streamText({ - model: myProvider.languageModel("chat-model"), - system: systemPrompt(), - messages, - }); - - return result.toDataStreamResponse(); -} +// Inside POST handler, after authentication and message loading: +const stream = createUIMessageStream({ + execute: ({ writer: dataStream }) => { + const result = streamText({ + model: myProvider.languageModel(selectedChatModel), + system: systemPrompt({ selectedChatModel, requestHints }), + messages: convertToModelMessages(uiMessages), + experimental_transform: smoothStream({ chunking: "word" }), + }); + + result.consumeStream(); + + dataStream.merge( + result.toUIMessageStream({ + sendReasoning: true, + }) + ); + }, + generateId: generateUUID, + onFinish: async ({ messages }) => { + // Save messages to database + }, +}); + +return new Response(stream.pipeThrough(new JsonToSseTransformStream())); ``` ### Key Concepts -1. **`streamText`**: The AI SDK function that sends messages to the model and streams the response token by token. -2. **`myProvider`**: Our configured AI provider (Claude Haiku via AI Gateway). -3. **`systemPrompt`**: Instructions that tell the AI how to behave. -4. **`toDataStreamResponse`**: Converts the stream into a format the frontend can consume. +1. **`createUIMessageStream`**: Creates a stream that handles UI message updates with proper typing. +2. **`streamText`**: The AI SDK function that sends messages to the model and streams the response. +3. **`myProvider`**: Our configured AI provider (Claude Haiku via AI Gateway). +4. **`systemPrompt`**: Function that builds instructions for the AI (takes model and location hints). +5. **`JsonToSseTransformStream`**: Converts the stream into Server-Sent Events format for the frontend. ## How Streaming Works @@ -90,36 +110,56 @@ When you send a message: ## The Frontend Chat Hook -The frontend uses `useChat` from the AI SDK React package: +The frontend uses `useChat` from the AI SDK React package with a custom transport configuration: ```typescript -// Simplified usage in a chat component +// components/chat.tsx (key parts) import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "@ai-sdk/react/internal"; + +export function Chat({ id, initialMessages, selectedChatModel }) { + const { + messages, + setMessages, + sendMessage, + status, + stop, + regenerate, + resumeStream, + } = useChat({ + id, + messages: initialMessages, + experimental_throttle: 100, + generateId: generateUUID, + transport: new DefaultChatTransport({ + api: "/api/chat", + fetch: fetchWithErrorHandlers, + prepareSendMessagesRequest(request) { + return { + ...request, + body: { + id, + message: request.messages[request.messages.length - 1], + selectedChatModel, + selectedVisibilityType: visibilityType, + }, + }; + }, + }), + onFinish: () => { + mutate("/api/history"); + }, + }); -export function Chat() { - const { messages, input, handleSubmit, handleInputChange } = useChat(); - - return ( -
- {messages.map((message) => ( -
- {message.role}: {message.content} -
- ))} -
- - -
-
- ); + // ... component JSX } ``` The `useChat` hook handles: -- Managing message history -- Sending messages to the API -- Streaming response updates -- Input state management +- Managing message history with proper typing +- Sending messages via custom transport +- Streaming response updates with throttling +- Request/response transformation ## Message Format @@ -137,14 +177,40 @@ type Message = { ## The System Prompt -The system prompt shapes the AI's personality and behavior: +The system prompt shapes the AI's personality and behavior. It takes the selected model and geolocation hints as parameters: ```typescript // lib/ai/prompts.ts -export const systemPrompt = () => ` -You are a helpful AI assistant. Be concise and helpful. -Today's date is ${new Date().toLocaleDateString()}. +import type { Geo } from "@vercel/functions"; + +export const regularPrompt = + "You are a friendly study buddy assistant! Keep your responses concise and helpful."; + +export type RequestHints = { + latitude: Geo["latitude"]; + longitude: Geo["longitude"]; + city: Geo["city"]; + country: Geo["country"]; +}; + +export const getRequestPromptFromHints = (requestHints: RequestHints) => `\ +About the origin of user's request: +- lat: ${requestHints.latitude} +- lon: ${requestHints.longitude} +- city: ${requestHints.city} +- country: ${requestHints.country} `; + +export const systemPrompt = ({ + selectedChatModel, + requestHints, +}: { + selectedChatModel: string; + requestHints: RequestHints; +}) => { + const requestPrompt = getRequestPromptFromHints(requestHints); + return `${regularPrompt}\n\n${requestPrompt}`; +}; ``` ## Exercise: Trace a Message diff --git a/CHAPTER-1.md b/CHAPTER-1.md index 31ef7d5..b9314be 100644 --- a/CHAPTER-1.md +++ b/CHAPTER-1.md @@ -48,7 +48,7 @@ const myTool = tool({ ## The Weather Tool -Here's the complete weather tool implementation: +Here's the complete weather tool implementation with city name geocoding: ### File: `lib/ai/tools/get-weather.ts` @@ -56,61 +56,131 @@ Here's the complete weather tool implementation: import { tool } from "ai"; import { z } from "zod"; +// Helper function to convert city names to coordinates +async function geocodeCity( + city: string +): Promise<{ latitude: number; longitude: number } | null> { + try { + const response = await fetch( + `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json` + ); + + if (!response.ok) { + return null; + } + + const data = await response.json(); + + if (!data.results || data.results.length === 0) { + return null; + } + + const result = data.results[0]; + return { + latitude: result.latitude, + longitude: result.longitude, + }; + } catch { + return null; + } +} + export const getWeather = tool({ description: - "Get the current weather at a location. Use this when users ask about weather.", + "Get the current weather at a location. You can provide either coordinates or a city name.", inputSchema: z.object({ - latitude: z.number().describe("Latitude coordinate"), - longitude: z.number().describe("Longitude coordinate"), + latitude: z.number().optional(), + longitude: z.number().optional(), + city: z + .string() + .describe("City name (e.g., 'San Francisco', 'New York', 'London')") + .optional(), }), - execute: async ({ latitude, longitude }) => { - // In production, you'd call a real weather API - // For demo, we return mock data based on coordinates + execute: async (input) => { + let latitude: number; + let longitude: number; + + // If city name provided, geocode it to coordinates + if (input.city) { + const coords = await geocodeCity(input.city); + if (!coords) { + return { + error: `Could not find coordinates for "${input.city}". Please check the city name.`, + }; + } + latitude = coords.latitude; + longitude = coords.longitude; + } else if (input.latitude !== undefined && input.longitude !== undefined) { + latitude = input.latitude; + longitude = input.longitude; + } else { + return { + error: + "Please provide either a city name or both latitude and longitude coordinates.", + }; + } + + // Fetch weather data from Open-Meteo API const response = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto` ); - const data = await response.json(); + const weatherData = await response.json(); - return { - temperature: data.current.temperature_2m, - unit: data.current_units.temperature_2m, - }; + // Include city name in response if provided + if ("city" in input) { + weatherData.cityName = input.city; + } + + return weatherData; }, }); ``` +### Key Features + +1. **Geocoding**: The `geocodeCity` helper converts city names to coordinates using the free Open-Meteo Geocoding API +2. **Flexible Input**: Accepts either a city name OR latitude/longitude coordinates +3. **Error Handling**: Returns helpful error messages if geocoding fails +4. **Real Data**: Uses the Open-Meteo Weather API for actual weather data + ## Wiring Tools into the Chat Route -Add the tool to your chat route: +Add the tool to your chat route. The route already has streaming set up - you just need to add the tool: ### File: `app/(chat)/api/chat/route.ts` ```typescript -import { streamText } from "ai"; -import { myProvider } from "@/lib/ai/providers"; -import { systemPrompt } from "@/lib/ai/prompts"; +// Add this import at the top import { getWeather } from "@/lib/ai/tools/get-weather"; -export async function POST(request: Request) { - const { messages } = await request.json(); - - const result = streamText({ - model: myProvider.languageModel("chat-model"), - system: systemPrompt(), - messages, - // Add tools here! - tools: { - getWeather, - }, - // Let the AI call multiple tools if needed - maxSteps: 5, - }); - - return result.toDataStreamResponse(); -} +// Inside the POST handler, the streamText call should include: +const result = streamText({ + model: myProvider.languageModel(selectedChatModel), + system: systemPrompt({ selectedChatModel, requestHints }), + messages: convertToModelMessages(uiMessages), + // Stop after 5 tool call steps + stopWhen: stepCountIs(5), + // Disable tools for reasoning models + experimental_activeTools: + selectedChatModel === "chat-model-reasoning" ? [] : ["getWeather"], + experimental_transform: smoothStream({ chunking: "word" }), + // Add tools here! + tools: { + getWeather, + }, +}); ``` +The full route handler uses `createUIMessageStream` with `JsonToSseTransformStream` for streaming - the key thing is adding `getWeather` to the `tools` object and `"getWeather"` to `experimental_activeTools`. + +### Key Configuration + +- **`stopWhen: stepCountIs(5)`**: Limits tool call chains to 5 steps +- **`experimental_activeTools`**: Conditionally enables/disables tools (disabled for reasoning model) +- **`tools`**: Object containing all available tools +- **`requestHints`**: Contains user's location (useful for "What's the weather?" without a city) + ## How Tool Calling Works ``` @@ -120,9 +190,12 @@ export async function POST(request: Request) { │ AI thinks: "I should use the getWeather tool" │ │ ↓ │ │ AI generates tool call: │ -│ { name: "getWeather", args: { lat: 48.8, lon: 2.3 }} │ +│ { name: "getWeather", args: { city: "Paris" }} │ │ ↓ │ -│ Tool executes and returns: { temperature: 18, unit: "°C"}│ +│ Tool executes: │ +│ 1. geocodeCity("Paris") → { lat: 48.8, lon: 2.3 } │ +│ 2. fetch weather data from Open-Meteo API │ +│ 3. returns: { temperature: 18, cityName: "Paris" } │ │ ↓ │ │ AI receives result and generates response: │ │ "The current temperature in Paris is 18°C" │ @@ -139,19 +212,26 @@ Tool calls and results appear as special message parts. Here's how to render the "use client"; type WeatherProps = { - temperature: number; - unit: string; + current: { + temperature_2m: number; + }; + current_units: { + temperature_2m: string; + }; + cityName?: string; }; -export function Weather({ temperature, unit }: WeatherProps) { +export function Weather({ current, current_units, cityName }: WeatherProps) { return (
🌡️
-

Current Temperature

+

+ {cityName ? `Weather in ${cityName}` : "Current Weather"} +

- {temperature} - {unit} + {current.temperature_2m} + {current_units.temperature_2m}

@@ -171,23 +251,39 @@ export function Weather({ temperature, unit }: WeatherProps) { })} ``` -## Updating the System Prompt +## Updating the System Prompt (Optional) + +The AI will use tools based on their `description` field, so you don't *need* to update the system prompt. However, you can optionally add tool documentation to help the AI understand when to use tools. -Help the AI know when to use tools: +The system prompt is built from multiple parts. The simplest way to add tool info is to update the `regularPrompt` constant: ```typescript // lib/ai/prompts.ts -export const systemPrompt = () => ` -You are a helpful AI assistant. + +// Update this constant to include tool documentation +export const regularPrompt = `You are a friendly study buddy assistant! Keep your responses concise and helpful. ## Tools Available - **getWeather**: Use this when users ask about weather conditions. - Ask for a city name if not provided. - -Today's date is ${new Date().toLocaleDateString()}. + You can provide a city name like "Paris" or "Tokyo". `; + +// The systemPrompt function combines regularPrompt with location hints +// No need to change this function - it already works! +export const systemPrompt = ({ + selectedChatModel, + requestHints, +}: { + selectedChatModel: string; + requestHints: RequestHints; +}) => { + const requestPrompt = getRequestPromptFromHints(requestHints); + return `${regularPrompt}\n\n${requestPrompt}`; +}; ``` +**Note**: The `requestHints` add the user's location context (city, country, lat/lon), which is useful for the weather tool - the AI can use the user's location as a default if they just say "What's the weather?" + ## Try It Out: Weather Tool Now that you've wired up the weather tool, test it with these prompts. Click the **"What is the weather in San Francisco?"** button in the chat, or try these variations: @@ -214,7 +310,8 @@ Should I bring an umbrella to Seattle? **Troubleshooting:** - If you see "I don't have access to real-time weather", check that the tool is properly added to the `tools` object in your chat route -- If coordinates seem wrong, the AI is inferring lat/long from city names - this is expected behavior +- If the city isn't found, try using a more common spelling or a larger nearby city +- Check the browser console for any API errors --- diff --git a/CHAPTER-2.md b/CHAPTER-2.md index 21b041a..3e69486 100644 --- a/CHAPTER-2.md +++ b/CHAPTER-2.md @@ -53,22 +53,39 @@ First, let's define the structure for our agents: ### File: `lib/ai/agents/types.ts` ```typescript +import type { UIMessageStreamWriter } from "ai"; import type { Session } from "next-auth"; -import type { DataStreamWriter } from "ai"; +import type { ChatMessage } from "@/lib/types"; -// Props passed to every agent creator -export type CreateAgentProps = { - session: Session | null; - dataStream: DataStreamWriter; +/** + * Context passed to all specialized agents + * Contains session info, data stream for real-time updates, and chat ID + */ +export type AgentContext = { + session: Session; + dataStream: UIMessageStreamWriter; + chatId: string; }; -// Result returned by every agent +/** + * Standard result returned by all agents + * Provides consistent interface for orchestrator to handle agent responses + */ export type AgentResult = { agentName: string; success: boolean; summary: string; data?: Record; }; + +/** + * Props for creating an agent tool + * Same pattern as existing tools (createDocument, requestSuggestions) + */ +export type CreateAgentProps = { + session: Session; + dataStream: UIMessageStreamWriter; +}; ``` ## Model Configuration @@ -79,28 +96,41 @@ Before building agents, understand what `"chat-model"` means. The app uses named |-------------|--------------|---------| | `chat-model` | Claude 3.5 Haiku | Main chat with tool calling | | `chat-model-reasoning` | Claude 3.5 Haiku | Complex analysis (no tools) | -| `artifact-model` | GPT-4o Mini | Content generation for artifacts | -| `title-model` | GPT-4o Mini | Generating chat titles | +| `artifact-model` | Claude 3.5 Haiku | Content generation for artifacts | +| `title-model` | Claude 3.5 Haiku | Generating chat titles | -**Why different models?** -- **Claude Haiku** is fast and great for tool calling (routing to agents) -- **GPT-4o Mini** is cost-effective for content generation +**Why use AI Gateway?** +- Unified API across different model providers +- Easy switching between models +- Built-in monitoring and rate limiting - You can change these in `lib/ai/providers.ts` to use different models ```typescript -// lib/ai/providers.ts (simplified) +// lib/ai/providers.ts +import { gateway } from "@ai-sdk/gateway"; +import { customProvider } from "ai"; + export const myProvider = customProvider({ languageModels: { - "chat-model": anthropic("claude-3-5-haiku-20241022"), - "artifact-model": openai("gpt-4o-mini"), - // Add your own model aliases here + // Multimodal model - supports images, cheap and fast + "chat-model": gateway.languageModel("anthropic/claude-3-5-haiku-latest"), + // Reasoning model - using Haiku (no special reasoning tags needed) + "chat-model-reasoning": gateway.languageModel( + "anthropic/claude-3-5-haiku-latest" + ), + // Simple/cheap model for titles - using Haiku for consistency + "title-model": gateway.languageModel("anthropic/claude-3-5-haiku-latest"), + // Simple/cheap model for artifacts - using Haiku for consistency + "artifact-model": gateway.languageModel( + "anthropic/claude-3-5-haiku-latest" + ), }, }); ``` ## The Tutor Agent -The Tutor agent explains concepts using different teaching approaches: +The Tutor agent explains concepts with examples and analogies: ### File: `lib/ai/agents/tutor.ts` @@ -110,88 +140,67 @@ import { z } from "zod"; import { myProvider } from "../providers"; import type { AgentResult, CreateAgentProps } from "./types"; +const TUTOR_SYSTEM_PROMPT = `You are a patient, encouraging tutor who excels at explaining complex topics. + +Your teaching approach: +- Start with what the student likely already knows +- Use relatable analogies and real-world examples +- Break complex ideas into digestible steps +- Include brief knowledge checks when appropriate +- Encourage curiosity and questions +- Adapt explanation depth based on the topic complexity + +Structure your explanations with: +1. A simple overview (1-2 sentences) +2. The main explanation with examples +3. Key takeaways or summary points + +Keep responses focused and educational. Avoid unnecessary fluff.`; + /** - * Tutor Agent - Explains concepts with different teaching approaches + * Tutor Agent - Explains concepts with examples and analogies * * Triggers: "explain", "teach me", "how does X work", "what is" - * Output: Detailed text explanation streamed to chat + * Output: Returns explanation text that the orchestrator will present + * + * Note: We use generateText instead of streaming because tool results + * are displayed in the chat UI, not the artifact panel. The orchestrator + * (main chat model) can then present the explanation conversationally. */ -export const createTutorAgent = ({ session, dataStream }: CreateAgentProps) => +export const createTutorAgent = (_props: CreateAgentProps) => tool({ description: - "Explain a concept or topic in depth. Use when the user wants to learn or understand something. " + - "Triggers: explain, teach me, how does X work, what is, help me understand.", + "Explain a concept, topic, or idea in detail with examples and analogies. Use when the user asks to understand, learn about, or needs explanation of something. Triggers: explain, teach me, how does X work, what is X.", inputSchema: z.object({ topic: z.string().describe("The topic or concept to explain"), - approach: z - .enum(["eli5", "technical", "analogy", "step-by-step"]) - .default("step-by-step") - .describe( - "Teaching approach: eli5 (simple), technical (detailed), analogy (comparisons), step-by-step" - ), - priorKnowledge: z + depth: z + .enum(["beginner", "intermediate", "advanced"]) + .default("intermediate") + .describe("The depth of explanation needed based on user context"), + context: z .string() .optional() - .describe("What the user already knows about the topic"), + .describe( + "Additional context about what the user already knows or specific aspects to focus on" + ), }), - execute: async ({ - topic, - approach, - priorKnowledge, - }): Promise => { - console.log(`[Tutor] Explaining "${topic}" using ${approach} approach`); - - const approachInstructions = { - eli5: "Explain like I'm 5 years old. Use simple words, fun comparisons, and relatable examples.", - technical: - "Give a thorough technical explanation with precise terminology, underlying mechanisms, and edge cases.", - analogy: - "Explain primarily through analogies and metaphors that connect to everyday experiences.", - "step-by-step": - "Break down the concept into clear, numbered steps. Start from basics and build up.", - }; + execute: async ({ topic, depth, context }): Promise => { + const prompt = `Explain "${topic}" at a ${depth} level.${ + context ? `\n\nAdditional context: ${context}` : "" + }`; - const prompt = `You are an expert tutor. Explain the following topic: - -**Topic**: ${topic} - -**Teaching Approach**: ${approachInstructions[approach]} - -${priorKnowledge ? `**User's Prior Knowledge**: ${priorKnowledge}` : ""} - -Provide a clear, engaging explanation. Use examples where helpful. -Format with markdown for readability.`; - - try { - const { text } = await generateText({ - model: myProvider.languageModel("chat-model"), - prompt, - }); - - console.log(`[Tutor] Generated explanation (${text.length} chars)`); - - return { - agentName: "tutor", - success: true, - summary: text, - data: { - topic, - approach, - characterCount: text.length, - }, - }; - } catch (error) { - console.error(`[Tutor] Error:`, error); - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - - return { - agentName: "tutor", - success: false, - summary: `I had trouble explaining "${topic}". Please try again.`, - data: { error: errorMessage }, - }; - } + const { text } = await generateText({ + model: myProvider.languageModel("chat-model"), + system: TUTOR_SYSTEM_PROMPT, + prompt, + }); + + return { + agentName: "tutor", + success: true, + summary: text, + data: { topic, depth, contentLength: text.length }, + }; }, }); ``` @@ -236,67 +245,96 @@ export const createTutorAgent = ({ session, dataStream }: CreateAgentProps) => ### File: `app/(chat)/api/chat/route.ts` ```typescript -import { createDataStreamResponse, streamText } from "ai"; +import { + convertToModelMessages, + createUIMessageStream, + JsonToSseTransformStream, + smoothStream, + stepCountIs, + streamText, +} from "ai"; +import { auth } from "@/app/(auth)/auth"; +import { createTutorAgent } from "@/lib/ai/agents"; +import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; import { myProvider } from "@/lib/ai/providers"; -import { systemPrompt } from "@/lib/ai/prompts"; import { getWeather } from "@/lib/ai/tools/get-weather"; -import { createTutorAgent } from "@/lib/ai/agents/tutor"; -import { auth } from "@/app/(auth)/auth"; export async function POST(request: Request) { - const session = await auth(); - const { messages } = await request.json(); + // ... authentication and request parsing - return createDataStreamResponse({ - execute: (dataStream) => { + const stream = createUIMessageStream({ + execute: ({ writer: dataStream }) => { const result = streamText({ - model: myProvider.languageModel("chat-model"), - system: systemPrompt(), - messages, + model: myProvider.languageModel(selectedChatModel), + system: systemPrompt({ selectedChatModel, requestHints }), + messages: convertToModelMessages(uiMessages), + stopWhen: stepCountIs(5), + experimental_activeTools: + selectedChatModel === "chat-model-reasoning" + ? [] + : ["getWeather", "tutor"], + experimental_transform: smoothStream({ chunking: "word" }), tools: { getWeather, // Add the tutor agent as a tool! tutor: createTutorAgent({ session, dataStream }), }, - maxSteps: 5, }); - result.mergeIntoDataStream(dataStream); + result.consumeStream(); + + dataStream.merge( + result.toUIMessageStream({ + sendReasoning: true, + }) + ); }, + // ... onFinish, onError handlers }); + + return new Response(stream.pipeThrough(new JsonToSseTransformStream())); } ``` +Note: Agents are imported from the barrel export `@/lib/ai/agents`, not individual files. + ## Updating the System Prompt -Help the orchestrator know when to use the tutor: +The system prompt helps the orchestrator know when to use each agent. The full prompt is built from multiple parts: ### File: `lib/ai/prompts.ts` ```typescript -export const systemPrompt = () => ` -You are a helpful AI assistant with specialized capabilities. +export const regularPrompt = + "You are a friendly study buddy assistant! Keep your responses concise and helpful."; -## Available Tools +export const agentRoutingPrompt = ` +You are a Study Buddy with specialized agents available as tools. Choose the right agent based on what the user needs: -### tutor -Use this when users want to learn or understand something. -- "explain [topic]" -- "teach me about [topic]" -- "how does [thing] work" -- "what is [concept]" +**tutor** - Explain concepts with examples and analogies +Use for: "explain", "teach me", "how does X work", "what is X", understanding concepts -Choose the appropriate teaching approach based on context: -- eli5: For beginners or when simplicity is requested -- technical: For advanced users or detailed explanations -- analogy: When user seems confused or wants relatable examples -- step-by-step: Default, good for most explanations +IMPORTANT ROUTING RULES: +1. Match user intent to the most appropriate agent +2. If the request doesn't clearly match an agent, respond conversationally +3. After using an agent, suggest related follow-ups (e.g., after explaining, offer to quiz) +`; -### getWeather -Use for weather queries. +export const systemPrompt = ({ + selectedChatModel, + requestHints, +}: { + selectedChatModel: string; + requestHints: RequestHints; +}) => { + const requestPrompt = getRequestPromptFromHints(requestHints); + + if (selectedChatModel === "chat-model-reasoning") { + return `${regularPrompt}\n\n${requestPrompt}`; + } -Today's date is ${new Date().toLocaleDateString()}. -`; + return `${regularPrompt}\n\n${agentRoutingPrompt}\n\n${requestPrompt}`; +}; ``` ## How the Agent Response Flows @@ -404,5 +442,5 @@ In Chapter 3, we'll add more agents (Quiz Master and Planner) and see how multip |------|---------| | `lib/ai/agents/types.ts` | New - agent type definitions | | `lib/ai/agents/tutor.ts` | New - tutor agent implementation | -| `app/(chat)/api/chat/route.ts` | Added tutor agent, switched to createDataStreamResponse | +| `app/(chat)/api/chat/route.ts` | Added tutor agent to tools, added to experimental_activeTools | | `lib/ai/prompts.ts` | Added tutor tool documentation | diff --git a/CHAPTER-3.md b/CHAPTER-3.md index 790e226..339dfff 100644 --- a/CHAPTER-3.md +++ b/CHAPTER-3.md @@ -41,18 +41,18 @@ By the end of this chapter, you'll understand: ## The Quiz Master Agent -Creates interactive quizzes to test knowledge: +Creates interactive flashcard quizzes that appear as artifacts: ### File: `lib/ai/agents/quiz-master.ts` ```typescript import { generateObject, tool } from "ai"; import { z } from "zod"; +import { saveDocument } from "@/lib/db/queries"; import { myProvider } from "../providers"; import type { AgentResult, CreateAgentProps } from "./types"; -// Schema for quiz questions -const quizSchema = z.object({ +const flashcardSchema = z.object({ topic: z.string(), questions: z.array( z.object({ @@ -65,10 +65,10 @@ const quizSchema = z.object({ }); /** - * Quiz Master Agent - Creates interactive quizzes + * Quiz Master Agent - Creates interactive flashcard quizzes * - * Triggers: "quiz me", "test my knowledge", "practice questions" - * Output: Structured quiz data (later: flashcard artifact) + * Triggers: "quiz me", "test my knowledge", "practice questions", "assessment" + * Output: Creates a flashcard artifact for interactive testing */ export const createQuizMasterAgent = ({ session, @@ -76,9 +76,7 @@ export const createQuizMasterAgent = ({ }: CreateAgentProps) => tool({ description: - "Create a quiz to test knowledge on a topic. " + - "Use when the user wants to be quizzed, test their knowledge, or practice. " + - "Triggers: quiz me, test me, practice questions, assessment, flashcards.", + "Create a quiz or practice questions to test knowledge on a topic. Use when the user wants to be quizzed, test their knowledge, or practice with questions. Triggers: quiz me, test me, practice questions, assessment, flashcards.", inputSchema: z.object({ topic: z.string().describe("The topic to create quiz questions about"), numberOfQuestions: z @@ -100,90 +98,130 @@ export const createQuizMasterAgent = ({ topic, numberOfQuestions, difficulty, + focusAreas, }): Promise => { + const documentId = crypto.randomUUID(); + const title = `Quiz: ${topic}`; + + console.log(`[QuizMaster] Starting quiz generation for "${topic}"`); console.log( - `[QuizMaster] Creating ${numberOfQuestions} ${difficulty} questions about "${topic}"` + `[QuizMaster] Parameters: ${numberOfQuestions} questions, ${difficulty} difficulty` ); + // Notify UI that we're creating an artifact (opens the panel) + dataStream.write({ type: "data-id", data: documentId }); + dataStream.write({ type: "data-title", data: title }); + dataStream.write({ type: "data-kind", data: "flashcard" }); + dataStream.write({ type: "data-clear", data: null }); + try { + const focusContext = focusAreas?.length + ? `\n\nFocus particularly on: ${focusAreas.join(", ")}` + : ""; + + console.log("[QuizMaster] Calling generateObject..."); + const { object } = await generateObject({ - model: myProvider.languageModel("chat-model"), - schema: quizSchema, + model: myProvider.languageModel("artifact-model"), + schema: flashcardSchema, prompt: `Create a quiz with ${numberOfQuestions} multiple choice questions about: "${topic}" -Difficulty level: ${difficulty} +Difficulty level: ${difficulty}${focusContext} Each question should: - Test understanding, not just memorization - Have 4 options (A, B, C, D) -- Have exactly one correct answer +- Have a clear correct answer - Include a brief explanation of why the answer is correct -For mixed difficulty, vary the questions from easy to hard.`, +Return the quiz as structured JSON.`, }); + const content = JSON.stringify(object, null, 2); console.log( - `[QuizMaster] Generated ${object.questions.length} questions` + `[QuizMaster] Generated ${object.questions.length} questions (${content.length} chars)` ); - // Format as readable text for now - // In Chapter 4, we'll create a proper flashcard artifact - const quizText = object.questions - .map( - (q, i) => ` -**Question ${i + 1}**: ${q.question} - -A) ${q.options[0]} -B) ${q.options[1]} -C) ${q.options[2]} -D) ${q.options[3]} - -
-Show Answer - -**Correct: ${["A", "B", "C", "D"][q.correctAnswer]}** + // Stream the content to the UI + dataStream.write({ + type: "data-flashcardDelta", + data: content, + transient: true, + }); -${q.explanation} -
-` - ) - .join("\n---\n"); + // Signal completion - CRITICAL: always send this + dataStream.write({ type: "data-finish", data: null }); + console.log("[QuizMaster] Sent data-finish signal"); + + // Save to database if user is authenticated + if (session?.user?.id) { + await saveDocument({ + id: documentId, + title, + content, + kind: "flashcard", + userId: session.user.id, + }); + console.log(`[QuizMaster] Saved document ${documentId} to database`); + } return { agentName: "quiz-master", success: true, - summary: `# Quiz: ${topic}\n\n${quizText}`, + summary: `Created an interactive quiz about "${topic}" with ${object.questions.length} questions. The flashcard quiz is now displayed - click through to test your knowledge!`, data: { + documentId, topic, - questionCount: object.questions.length, + numberOfQuestions: object.questions.length, difficulty, + focusAreas, }, }; } catch (error) { - console.error(`[QuizMaster] Error:`, error); + console.error("[QuizMaster] Error generating quiz:", error); + + // CRITICAL: Always send finish signal to unblock UI + dataStream.write({ type: "data-finish", data: null }); + console.log("[QuizMaster] Sent data-finish signal after error"); + + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { agentName: "quiz-master", success: false, - summary: `I couldn't create a quiz about "${topic}". Please try again.`, - data: { error: String(error) }, + summary: `Failed to generate quiz about "${topic}": ${errorMessage}. Please try again.`, + data: { + documentId, + topic, + error: errorMessage, + }, }; } }, }); ``` +**Key patterns for artifact-creating agents:** +1. Generate a unique `documentId` upfront +2. Send `data-id`, `data-title`, `data-kind`, and `data-clear` to open the artifact panel +3. Use `generateObject` with a schema for structured output +4. Stream the content with the appropriate delta type (e.g., `data-flashcardDelta`) +5. **Always** send `data-finish` to unblock the UI (even on errors!) +6. Save to database for persistence + ## The Planner Agent -Creates structured study plans: +Creates interactive study plans with progress tracking that appear as artifacts: ### File: `lib/ai/agents/planner.ts` ```typescript import { generateObject, tool } from "ai"; import { z } from "zod"; +import { saveDocument } from "@/lib/db/queries"; import { myProvider } from "../providers"; import type { AgentResult, CreateAgentProps } from "./types"; -// Schema for study plans const studyPlanSchema = z.object({ topic: z.string(), duration: z.string(), @@ -207,20 +245,15 @@ const studyPlanSchema = z.object({ }); /** - * Planner Agent - Creates study plans and learning roadmaps + * Planner Agent - Creates interactive study plans with progress tracking * - * Triggers: "study plan", "learning roadmap", "how should I learn" - * Output: Structured study plan (later: study-plan artifact) + * Triggers: "create study plan", "learning roadmap", "how should I learn", "study schedule" + * Output: Creates a study-plan artifact for tracking learning progress */ -export const createPlannerAgent = ({ - session, - dataStream, -}: CreateAgentProps) => +export const createPlannerAgent = ({ session, dataStream }: CreateAgentProps) => tool({ description: - "Create a personalized study plan or learning roadmap for a topic. " + - "Use when the user wants to plan their learning, create a study schedule, or get a structured approach. " + - "Triggers: study plan, learning roadmap, how to learn, schedule, curriculum.", + "Create a personalized study plan or learning roadmap for a topic. Use when the user wants to plan their learning, create a study schedule, or get a structured approach to learning something. Triggers: study plan, learning roadmap, how to learn, schedule, curriculum.", inputSchema: z.object({ topic: z .string() @@ -228,7 +261,9 @@ export const createPlannerAgent = ({ timeframe: z .string() .default("2 weeks") - .describe("How long the user has to learn (e.g., '1 week', '30 days')"), + .describe( + "How long the user has to learn (e.g., '1 week', '30 days', '3 months')" + ), hoursPerDay: z .number() .min(0.5) @@ -239,27 +274,48 @@ export const createPlannerAgent = ({ .enum(["complete beginner", "some basics", "intermediate", "advanced"]) .default("complete beginner") .describe("User's current knowledge level"), + goals: z + .array(z.string()) + .optional() + .describe("Specific goals or outcomes the user wants to achieve"), }), execute: async ({ topic, timeframe, hoursPerDay, currentLevel, + goals, }): Promise => { + const documentId = crypto.randomUUID(); + const title = `Study Plan: ${topic}`; + + console.log(`[Planner] Starting study plan generation for "${topic}"`); console.log( - `[Planner] Creating ${timeframe} study plan for "${topic}" (${hoursPerDay}h/day, ${currentLevel})` + `[Planner] Parameters: ${timeframe}, ${hoursPerDay}h/day, level: ${currentLevel}` ); + // Notify UI that we're creating an artifact (opens the panel) + dataStream.write({ type: "data-id", data: documentId }); + dataStream.write({ type: "data-title", data: title }); + dataStream.write({ type: "data-kind", data: "study-plan" }); + dataStream.write({ type: "data-clear", data: null }); + try { + const goalsContext = goals?.length + ? `\n\nSpecific goals to achieve:\n${goals.map((g) => `- ${g}`).join("\n")}` + : ""; + + console.log("[Planner] Calling generateObject..."); + const { object } = await generateObject({ - model: myProvider.languageModel("chat-model"), + model: myProvider.languageModel("artifact-model"), schema: studyPlanSchema, prompt: `Create a structured study plan for learning "${topic}". Student profile: - Current level: ${currentLevel} - Available time: ${hoursPerDay} hours per day -- Timeframe: ${timeframe} +- Timeframe: ${timeframe}${goalsContext} Create a practical, actionable study plan that includes: - A clear overview of what will be learned @@ -271,56 +327,67 @@ Create a practical, actionable study plan that includes: Make it realistic and achievable.`, }); + const content = JSON.stringify(object, null, 2); console.log( - `[Planner] Generated plan with ${object.weeks.length} weeks` + `[Planner] Generated plan with ${object.weeks.length} weeks (${content.length} chars)` ); - // Format as readable text - const planText = ` -# Study Plan: ${object.topic} -**Duration**: ${object.duration} - -## Overview -${object.overview} - -${object.weeks - .map( - (week) => ` -## Week ${week.week}: ${week.title} - -**Goals:** -${week.goals.map((g) => `- ${g}`).join("\n")} - -**Tasks:** -${week.tasks.map((t) => `- [ ] ${t.task} (${t.duration})`).join("\n")} - -**Resources:** -${week.resources.map((r) => `- ${r}`).join("\n")} -` - ) - .join("\n")} + // Stream the content to the UI + dataStream.write({ + type: "data-studyPlanDelta", + data: content, + transient: true, + }); -## Tips for Success -${object.tips.map((t) => `- ${t}`).join("\n")} -`; + // Signal completion - CRITICAL: always send this + dataStream.write({ type: "data-finish", data: null }); + console.log("[Planner] Sent data-finish signal"); + + // Save to database if user is authenticated + if (session?.user?.id) { + await saveDocument({ + id: documentId, + title, + content, + kind: "study-plan", + userId: session.user.id, + }); + console.log(`[Planner] Saved document ${documentId} to database`); + } return { agentName: "planner", success: true, - summary: planText, + summary: `Created a ${timeframe} study plan for "${topic}" with ${object.weeks.length} weeks. The interactive study plan is now displayed - you can track your progress by checking off tasks as you complete them!`, data: { + documentId, topic, timeframe, + hoursPerDay, + currentLevel, + goals, weeksCount: object.weeks.length, }, }; } catch (error) { - console.error(`[Planner] Error:`, error); + console.error("[Planner] Error generating study plan:", error); + + // CRITICAL: Always send finish signal to unblock UI + dataStream.write({ type: "data-finish", data: null }); + console.log("[Planner] Sent data-finish signal after error"); + + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { agentName: "planner", success: false, - summary: `I couldn't create a study plan for "${topic}". Please try again.`, - data: { error: String(error) }, + summary: `Failed to generate study plan for "${topic}": ${errorMessage}. Please try again.`, + data: { + documentId, + topic, + error: errorMessage, + }, }; } }, @@ -329,7 +396,7 @@ ${object.tips.map((t) => `- ${t}`).join("\n")} ## The Analyst Agent -Analyzes content and extracts key insights: +Analyzes content and extracts key insights (returns text, not an artifact): ### File: `lib/ai/agents/analyst.ts` @@ -346,7 +413,16 @@ Your analysis approach: - Extract important facts, figures, and arguments - Note relationships between concepts - Highlight actionable insights -- Provide clear, structured summaries`; +- Provide clear, structured summaries + +For document analysis, provide: +1. Executive summary (2-3 sentences) +2. Key points and main arguments +3. Important details and supporting evidence +4. Connections to broader context +5. Actionable takeaways or study notes + +Be thorough but concise. Focus on what would be most valuable for learning and retention.`; /** * Analyst Agent - Analyzes documents and extracts key insights @@ -357,13 +433,9 @@ Your analysis approach: export const createAnalystAgent = (_props: CreateAgentProps) => tool({ description: - "Analyze content, extract key insights, and create summaries. " + - "Use when the user wants to understand, summarize, or extract key points. " + - "Triggers: analyze, summarize, key points, main ideas, extract insights.", + "Analyze content, extract key insights, and create summaries. Use when the user wants to understand, summarize, or extract key points from text, documents, or concepts. Triggers: analyze, summarize, key points, main ideas, extract insights, break down.", inputSchema: z.object({ - content: z - .string() - .describe("The text or content to analyze"), + content: z.string().describe("The text or content to analyze"), analysisType: z .enum(["summary", "key-points", "deep-analysis", "study-notes"]) .default("summary") @@ -383,8 +455,6 @@ export const createAnalystAgent = (_props: CreateAgentProps) => focusOn, outputLength, }): Promise => { - console.log(`[Analyst] Analyzing content (${analysisType})`); - const focusContext = focusOn ? `\n\nFocus particularly on: ${focusOn}` : ""; @@ -396,10 +466,14 @@ export const createAnalystAgent = (_props: CreateAgentProps) => }; const analysisGuide = { - summary: "Create a clear summary highlighting the main message.", - "key-points": "Extract and list the most important points as bullet points.", - "deep-analysis": "Provide thorough analysis including themes and implications.", - "study-notes": "Create study-friendly notes with headings and key terms.", + summary: + "Create a clear summary highlighting the main message and supporting points.", + "key-points": + "Extract and list the most important points as bullet points with brief explanations.", + "deep-analysis": + "Provide thorough analysis including themes, arguments, evidence, and implications.", + "study-notes": + "Create study-friendly notes with headings, key terms, and memorable takeaways.", }; const prompt = `Analyze the following content: @@ -414,59 +488,73 @@ ${focusContext} ${lengthGuide[outputLength]}`; - try { - const { text } = await generateText({ - model: myProvider.languageModel("chat-model"), - system: ANALYST_SYSTEM_PROMPT, - prompt, - }); + const { text } = await generateText({ + model: myProvider.languageModel("chat-model"), + system: ANALYST_SYSTEM_PROMPT, + prompt, + }); - return { - agentName: "analyst", - success: true, - summary: text, - data: { analysisType, focusOn, outputLength }, - }; - } catch (error) { - console.error(`[Analyst] Error:`, error); - return { - agentName: "analyst", - success: false, - summary: "I couldn't analyze the content. Please try again.", - data: { error: String(error) }, - }; - } + return { + agentName: "analyst", + success: true, + summary: text, + data: { + analysisType, + focusOn, + outputLength, + contentLength: content.length, + }, + }; }, }); ``` **Key difference from Tutor**: The Analyst focuses on breaking down existing content, while the Tutor generates new explanations. Use Analyst for "summarize this article" and Tutor for "explain quantum physics". +**Key difference from Quiz/Planner**: The Analyst returns text directly in the `summary` field (like the Tutor), while Quiz Master and Planner create artifacts with `dataStream.write()` calls. + ## Wiring All Agents Together ### File: `app/(chat)/api/chat/route.ts` +The route handler uses `createUIMessageStream` and barrel imports from `@/lib/ai/agents`: + ```typescript -import { createDataStreamResponse, streamText } from "ai"; +import { + convertToModelMessages, + createUIMessageStream, + JsonToSseTransformStream, + smoothStream, + stepCountIs, + streamText, +} from "ai"; +import { auth } from "@/app/(auth)/auth"; +import { + createAnalystAgent, + createPlannerAgent, + createQuizMasterAgent, + createTutorAgent, +} from "@/lib/ai/agents"; +import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; import { myProvider } from "@/lib/ai/providers"; -import { systemPrompt } from "@/lib/ai/prompts"; import { getWeather } from "@/lib/ai/tools/get-weather"; -import { createTutorAgent } from "@/lib/ai/agents/tutor"; -import { createQuizMasterAgent } from "@/lib/ai/agents/quiz-master"; -import { createPlannerAgent } from "@/lib/ai/agents/planner"; -import { createAnalystAgent } from "@/lib/ai/agents/analyst"; -import { auth } from "@/app/(auth)/auth"; export async function POST(request: Request) { const session = await auth(); - const { messages } = await request.json(); + // ... request parsing, validation, etc. - return createDataStreamResponse({ - execute: (dataStream) => { + const stream = createUIMessageStream({ + execute: ({ writer: dataStream }) => { const result = streamText({ - model: myProvider.languageModel("chat-model"), - system: systemPrompt(), - messages, + model: myProvider.languageModel(selectedChatModel), + system: systemPrompt({ selectedChatModel, requestHints }), + messages: convertToModelMessages(uiMessages), + stopWhen: stepCountIs(5), + experimental_activeTools: + selectedChatModel === "chat-model-reasoning" + ? [] + : ["getWeather", "tutor", "quizMaster", "planner", "analyst"], + experimental_transform: smoothStream({ chunking: "word" }), tools: { getWeather, tutor: createTutorAgent({ session, dataStream }), @@ -474,60 +562,96 @@ export async function POST(request: Request) { planner: createPlannerAgent({ session, dataStream }), analyst: createAnalystAgent({ session, dataStream }), }, - maxSteps: 5, }); - result.mergeIntoDataStream(dataStream); + result.consumeStream(); + + dataStream.merge( + result.toUIMessageStream({ + sendReasoning: true, + }) + ); }, + // ... onFinish, onError handlers }); + + return new Response(stream.pipeThrough(new JsonToSseTransformStream())); } ``` +**Key points:** +- Use barrel import `from "@/lib/ai/agents"` (not individual files) +- `createUIMessageStream` + `JsonToSseTransformStream` for streaming +- `stepCountIs(5)` limits agent call depth +- `experimental_activeTools` disables tools for reasoning model + ## Updated System Prompt ### File: `lib/ai/prompts.ts` +The system prompt is built from multiple parts and takes parameters: + ```typescript -export const systemPrompt = () => ` -You are Study Buddy, an AI assistant that helps users learn effectively. - -## Your Specialized Agents - -### tutor -Use for explanations and teaching. -- "explain [topic]" -- "teach me about [topic]" -- "how does [thing] work" - -### quizMaster -Use for testing knowledge. -- "quiz me on [topic]" -- "test my knowledge" -- "practice questions" - -### planner -Use for creating study plans. -- "create a study plan for [topic]" -- "learning roadmap" -- "how should I learn [topic]" - -### analyst -Use for analyzing and summarizing content. -- "summarize this" -- "key points from [content]" -- "analyze this article" - -## Guidelines - -1. **Route appropriately**: Match user intent to the right agent -2. **Suggest next steps**: After explaining, offer a quiz. After a quiz, suggest a study plan -3. **Be encouraging**: Celebrate learning progress -4. **Stay focused**: Keep responses relevant to learning - -Today's date is ${new Date().toLocaleDateString()}. +export const regularPrompt = + "You are a friendly study buddy assistant! Keep your responses concise and helpful."; + +export const agentRoutingPrompt = ` +You are a Study Buddy with specialized agents available as tools. Choose the right agent based on what the user needs: + +**tutor** - Explain concepts with examples and analogies +Use for: "explain", "teach me", "how does X work", "what is X", understanding concepts + +**quizMaster** - Create quizzes and practice questions (creates interactive flashcard artifact) +Use for: "quiz me", "test my knowledge", "practice questions", "assessment" + +**planner** - Create study plans and learning roadmaps (creates interactive study-plan artifact) +Use for: "study plan", "learning roadmap", "how should I learn", "schedule" + +**analyst** - Analyze content and extract key insights +Use for: "summarize", "key points", "analyze this", "what's important" + +IMPORTANT ROUTING RULES: +1. Match user intent to the most appropriate agent +2. If the request doesn't clearly match an agent, respond conversationally +3. After using an agent, suggest related follow-ups (e.g., after explaining, offer to quiz) +4. You can chain agents - explain first, then offer to create a study plan + +CRITICAL: Agents (quizMaster, planner) create their own artifacts automatically. After using these agents: +- Do NOT call createDocument - the artifact is already created +- Do NOT try to display or reformat the agent's output +- Simply acknowledge the artifact was created and offer follow-up suggestions `; + +export type RequestHints = { + latitude: Geo["latitude"]; + longitude: Geo["longitude"]; + city: Geo["city"]; + country: Geo["country"]; +}; + +export const systemPrompt = ({ + selectedChatModel, + requestHints, +}: { + selectedChatModel: string; + requestHints: RequestHints; +}) => { + const requestPrompt = getRequestPromptFromHints(requestHints); + + if (selectedChatModel === "chat-model-reasoning") { + return `${regularPrompt}\n\n${requestPrompt}`; + } + + return `${regularPrompt}\n\n${agentRoutingPrompt}\n\n${requestPrompt}`; +}; ``` +**Key points:** +- `regularPrompt` for base personality +- `agentRoutingPrompt` for tool/agent descriptions (only for non-reasoning models) +- `requestHints` adds geolocation context +- Reasoning model gets simpler prompt (no tools) + ## Agent Collaboration in Action ``` @@ -622,7 +746,8 @@ Create a new agent that helps review previously created flashcards: | Agent Routing | Based on tool descriptions and user intent | | generateObject | AI SDK function that returns typed data | | Zod Schema | Defines the structure of generated data | -| Multi-step | maxSteps allows agents to be called in sequence | +| stepCountIs | Limits how many agent steps can run in sequence | +| dataStream.write | Sends artifact data to UI in real-time | ## What's Next diff --git a/CHAPTER-4.md b/CHAPTER-4.md index e0bbbc0..f5892ed 100644 --- a/CHAPTER-4.md +++ b/CHAPTER-4.md @@ -81,24 +81,33 @@ Add the new delta types to your custom types: ```typescript export type CustomUIDataTypes = { - // Existing types... + // Content delta types for streaming textDelta: string; - codeDelta: string; + imageDelta: string; sheetDelta: string; + codeDelta: string; + flashcardDelta: string; // JSON string of FlashcardData + studyPlanDelta: string; // JSON string of StudyPlanData - // New artifact types - flashcardDelta: string; // JSON string of FlashcardData - studyPlanDelta: string; // JSON string of StudyPlanData + // Other data types + suggestion: Suggestion; + appendMessage: string; - // Metadata + // Artifact metadata id: string; title: string; - kind: string; + kind: ArtifactKind; + + // Control signals clear: null; finish: null; + error: string; // For error signaling from agents + usage: AppUsage; // Token usage tracking }; ``` +Note: The actual `lib/types.ts` has additional type imports and tool type definitions. This shows the key `CustomUIDataTypes` structure. + ## The Flashcard Artifact ### Step 1: Server Types @@ -342,22 +351,34 @@ export const createQuizMasterAgent = ({ }: CreateAgentProps) => tool({ description: - "Create a quiz to test knowledge on a topic. " + - "Triggers: quiz me, test me, practice questions, flashcards.", + "Create a quiz or practice questions to test knowledge on a topic. Use when the user wants to be quizzed, test their knowledge, or practice with questions. Triggers: quiz me, test me, practice questions, assessment, flashcards.", inputSchema: z.object({ - topic: z.string().describe("The topic to quiz on"), - numberOfQuestions: z.number().min(1).max(10).default(5), - difficulty: z.enum(["easy", "medium", "hard", "mixed"]).default("medium"), + topic: z.string().describe("The topic to create quiz questions about"), + numberOfQuestions: z + .number() + .min(1) + .max(10) + .default(5) + .describe("Number of questions to generate"), + difficulty: z + .enum(["easy", "medium", "hard", "mixed"]) + .default("medium") + .describe("Difficulty level of the questions"), + focusAreas: z + .array(z.string()) + .optional() + .describe("Specific areas within the topic to focus on"), }), execute: async ({ topic, numberOfQuestions, difficulty, + focusAreas, }): Promise => { const documentId = crypto.randomUUID(); const title = `Quiz: ${topic}`; - console.log(`[QuizMaster] Creating quiz for "${topic}"`); + console.log(`[QuizMaster] Starting quiz generation for "${topic}"`); // Signal artifact creation (opens the panel) dataStream.write({ type: "data-id", data: documentId }); @@ -366,10 +387,23 @@ export const createQuizMasterAgent = ({ dataStream.write({ type: "data-clear", data: null }); try { + const focusContext = focusAreas?.length + ? `\n\nFocus particularly on: ${focusAreas.join(", ")}` + : ""; + const { object } = await generateObject({ - model: myProvider.languageModel("chat-model"), + model: myProvider.languageModel("artifact-model"), schema: flashcardSchema, - prompt: `Create ${numberOfQuestions} ${difficulty} quiz questions about "${topic}".`, + prompt: `Create a quiz with ${numberOfQuestions} multiple choice questions about: "${topic}" +Difficulty level: ${difficulty}${focusContext} + +Each question should: +- Test understanding, not just memorization +- Have 4 options (A, B, C, D) +- Have a clear correct answer +- Include a brief explanation of why the answer is correct + +Return the quiz as structured JSON.`, }); const content = JSON.stringify(object, null, 2); @@ -378,12 +412,13 @@ export const createQuizMasterAgent = ({ dataStream.write({ type: "data-flashcardDelta", data: content, + transient: true, }); - // Signal completion + // Signal completion - CRITICAL: always send this dataStream.write({ type: "data-finish", data: null }); - // Save to database + // Save to database if user is authenticated if (session?.user?.id) { await saveDocument({ id: documentId, @@ -397,16 +432,33 @@ export const createQuizMasterAgent = ({ return { agentName: "quiz-master", success: true, - summary: `Created a quiz about "${topic}" with ${object.questions.length} questions!`, - data: { documentId, topic, questionCount: object.questions.length }, + summary: `Created an interactive quiz about "${topic}" with ${object.questions.length} questions. The flashcard quiz is now displayed - click through to test your knowledge!`, + data: { + documentId, + topic, + numberOfQuestions: object.questions.length, + difficulty, + focusAreas, + }, }; } catch (error) { - console.error(`[QuizMaster] Error:`, error); + console.error("[QuizMaster] Error generating quiz:", error); + + // CRITICAL: Always send finish signal to unblock UI dataStream.write({ type: "data-finish", data: null }); + + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { agentName: "quiz-master", success: false, - summary: `Failed to create quiz. Please try again.`, + summary: `Failed to generate quiz about "${topic}": ${errorMessage}. Please try again.`, + data: { + documentId, + topic, + error: errorMessage, + }, }; } }, diff --git a/CHAPTER-5.md b/CHAPTER-5.md index 3f67bcb..49225b1 100644 --- a/CHAPTER-5.md +++ b/CHAPTER-5.md @@ -129,10 +129,12 @@ ai-chatbot/ │ │ ├── tools/ │ │ │ └── get-weather.ts # Weather tool │ │ └── agents/ +│ │ ├── index.ts # Barrel export for all agents │ │ ├── types.ts # Agent type definitions -│ │ ├── tutor.ts # Tutor agent -│ │ ├── quiz-master.ts # Quiz Master agent -│ │ └── planner.ts # Planner agent +│ │ ├── tutor.ts # Tutor agent (text response) +│ │ ├── quiz-master.ts # Quiz Master agent (flashcard artifact) +│ │ ├── planner.ts # Planner agent (study-plan artifact) +│ │ └── analyst.ts # Analyst agent (text response) │ ├── db/ │ │ ├── queries.ts # Database operations │ │ └── types.ts # MongoDB types @@ -168,18 +170,19 @@ ai-chatbot/ │ │ System Prompt: ││ │ │ - You have specialized agents available ││ │ │ - tutor: for explanations ││ -│ │ - quizMaster: for quizzes ││ -│ │ - planner: for study plans ││ +│ │ - quizMaster: for quizzes (creates flashcard artifact) ││ +│ │ - planner: for study plans (creates study-plan artifact) ││ +│ │ - analyst: for summarizing and analyzing content ││ │ │ - Match user intent to the right agent ││ │ └─────────────────────────────────────────────────────────────────────┘│ │ │ │ Available Tools: │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ tutor │ │ quizMaster │ │ planner │ │ getWeather │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ Tool wraps │ │ Tool wraps │ │ Tool wraps │ │ Simple tool │ │ -│ │ agent logic │ │ agent logic │ │ agent logic │ │ (data only) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────┐ │ +│ │ tutor │ │ quizMaster │ │ planner │ │ analyst │ │weather │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ generateText│ │creates │ │creates │ │generateText│ │ Simple │ │ +│ │ → text │ │artifact │ │artifact │ │→ text │ │ tool │ │ +│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -202,25 +205,28 @@ Why wrap agents as tools? export type CustomUIDataTypes = { // Artifact content (each kind has its own delta type) textDelta: string; + imageDelta: string; codeDelta: string; sheetDelta: string; flashcardDelta: string; // Quiz questions JSON studyPlanDelta: string; // Study plan JSON + // Other data types + suggestion: Suggestion; // Document suggestions + appendMessage: string; // Append to message + // Artifact metadata id: string; // Unique document ID title: string; // Display title - kind: string; // Artifact type + kind: ArtifactKind; // Artifact type (typed enum) // Control signals clear: null; // Clear previous content finish: null; // Signal completion + error: string; // Error signaling from agents // Analytics - usage: { // Token usage data - promptTokens: number; - completionTokens: number; - }; + usage: AppUsage; // Token usage data (enriched with costs) }; ``` @@ -282,12 +288,22 @@ db.documents.createIndex({ userId: 1, createdAt: -1 }) export const createCodeReviewerAgent = ({ session, dataStream }: CreateAgentProps) => tool({ description: "Review code for best practices, bugs, and improvements.", - parameters: z.object({ - code: z.string(), - language: z.string(), + inputSchema: z.object({ + code: z.string().describe("The code to review"), + language: z.string().describe("Programming language of the code"), + focusAreas: z + .array(z.string()) + .optional() + .describe("Specific areas to focus on (security, performance, etc.)"), }), - execute: async ({ code, language }) => { - // Generate code review... + execute: async ({ code, language, focusAreas }): Promise => { + // Generate code review with generateText... + return { + agentName: "code-reviewer", + success: true, + summary: reviewText, + data: { language, focusAreas }, + }; }, }); ``` @@ -300,12 +316,21 @@ An agent that can search the web and summarize findings: export const createResearcherAgent = ({ session, dataStream }: CreateAgentProps) => tool({ description: "Research a topic and provide summarized findings.", - parameters: z.object({ - query: z.string(), - depth: z.enum(["quick", "thorough"]), + inputSchema: z.object({ + query: z.string().describe("The topic or question to research"), + depth: z + .enum(["quick", "thorough"]) + .default("quick") + .describe("How deep to research"), }), - execute: async ({ query, depth }) => { - // Fetch from APIs, summarize... + execute: async ({ query, depth }): Promise => { + // Fetch from APIs, summarize with generateText... + return { + agentName: "researcher", + success: true, + summary: researchFindings, + data: { query, depth }, + }; }, }); ``` diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 0187b7d..16c9e3e 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -7,6 +7,13 @@ import { stepCountIs, streamText, } from "ai"; +import { + createAnalystAgent, + createPlannerAgent, + createQuizMasterAgent, + createTutorAgent, +} from "@/lib/ai/agents"; +import { getWeather } from "@/lib/ai/tools/get-weather"; import { unstable_cache as cache } from "next/cache"; import type { ModelCatalog } from "tokenlens/core"; @@ -22,20 +29,10 @@ import { fetchModels } from "tokenlens/fetch"; import { getUsage } from "tokenlens/helpers"; import { auth, type UserType } from "@/app/(auth)/auth"; import type { VisibilityType } from "@/components/visibility-selector"; -import { - createAnalystAgent, - createPlannerAgent, - createQuizMasterAgent, - createTutorAgent, -} from "@/lib/ai/agents"; import { entitlementsByUserType } from "@/lib/ai/entitlements"; import type { ChatModel } from "@/lib/ai/models"; import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; import { myProvider } from "@/lib/ai/providers"; -import { createDocument } from "@/lib/ai/tools/create-document"; -import { getWeather } from "@/lib/ai/tools/get-weather"; -import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; -import { updateDocument } from "@/lib/ai/tools/update-document"; import { isProductionEnvironment } from "@/lib/constants"; import { createStreamId, @@ -192,9 +189,6 @@ export async function POST(request: Request) { ? [] : [ "getWeather", - "createDocument", - "updateDocument", - "requestSuggestions", // Study buddy agents "tutor", "quizMaster", @@ -204,12 +198,6 @@ export async function POST(request: Request) { experimental_transform: smoothStream({ chunking: "word" }), tools: { getWeather, - createDocument: createDocument({ session, dataStream }), - updateDocument: updateDocument({ session, dataStream }), - requestSuggestions: requestSuggestions({ - session, - dataStream, - }), // Study buddy agents tutor: createTutorAgent({ session, dataStream }), quizMaster: createQuizMasterAgent({ session, dataStream }), diff --git a/artifacts/flashcard/client.tsx b/artifacts/flashcard/client.tsx index a542582..0ebf467 100644 --- a/artifacts/flashcard/client.tsx +++ b/artifacts/flashcard/client.tsx @@ -1,223 +1,55 @@ "use client"; -import { useState } from "react"; -import { toast } from "sonner"; import { Artifact } from "@/components/create-artifact"; -import { DocumentSkeleton } from "@/components/document-skeleton"; -import { CopyIcon, RefreshCwIcon } from "@/components/icons"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import type { FlashcardData } from "./server"; -type FlashcardMetadata = Record; - -function FlashcardViewer({ - content, - isLoading, -}: { - content: string; - isLoading: boolean; -}) { - const [currentIndex, setCurrentIndex] = useState(0); - const [selectedAnswer, setSelectedAnswer] = useState(null); - const [showExplanation, setShowExplanation] = useState(false); - const [score, setScore] = useState({ correct: 0, total: 0 }); - - console.log( - `[FlashcardViewer] Render - isLoading: ${isLoading}, content length: ${content?.length || 0}` - ); - - if (isLoading || !content) { - console.log( - `[FlashcardViewer] Showing skeleton (isLoading=${isLoading}, hasContent=${!!content})` - ); - return ; - } - - let data: FlashcardData; - try { - data = JSON.parse(content); - console.log( - `[FlashcardViewer] Parsed ${data.questions?.length || 0} questions for topic: ${data.topic}` - ); - } catch (error) { - console.error("[FlashcardViewer] JSON parse error:", error); - return ( -
-

Invalid flashcard data

-
- ); - } - - const currentQuestion = data.questions[currentIndex]; - const isLastQuestion = currentIndex === data.questions.length - 1; - const isAnswered = selectedAnswer !== null; - - const handleSelectAnswer = (index: number) => { - if (isAnswered) { - return; - } - setSelectedAnswer(index); - setShowExplanation(true); - setScore((prev) => ({ - correct: prev.correct + (index === currentQuestion.correctAnswer ? 1 : 0), - total: prev.total + 1, - })); - }; - - const handleNext = () => { - if (isLastQuestion) { - // Reset quiz - setCurrentIndex(0); - setSelectedAnswer(null); - setShowExplanation(false); - setScore({ correct: 0, total: 0 }); - } else { - setCurrentIndex((prev) => prev + 1); - setSelectedAnswer(null); - setShowExplanation(false); - } - }; - - const optionLabels = ["A", "B", "C", "D"]; - - return ( -
- {/* Header */} -
-
-

{data.topic}

-

- Question {currentIndex + 1} of {data.questions.length} -

-
-
- Score: {score.correct}/{score.total} -
-
- - {/* Question */} -
-

{currentQuestion.question}

-
- - {/* Options */} -
- {currentQuestion.options.map((option, index) => { - const isCorrect = index === currentQuestion.correctAnswer; - const isSelected = selectedAnswer === index; - - return ( - - ); - })} -
- - {/* Explanation */} - {showExplanation && ( -
-

- {selectedAnswer === currentQuestion.correctAnswer - ? "✓ Correct!" - : `✗ Incorrect. The correct answer is ${optionLabels[currentQuestion.correctAnswer]}.`} -

-

- {currentQuestion.explanation} -

-
- )} - - {/* Navigation */} - {isAnswered && ( -
- -
- )} -
- ); -} - -export const flashcardArtifact = new Artifact<"flashcard", FlashcardMetadata>({ +// Placeholder flashcard artifact - full implementation coming in Chapter 4 +export const flashcardArtifact = new Artifact<"flashcard">({ kind: "flashcard", - description: "Interactive flashcard quiz for testing knowledge.", + description: "Interactive quiz flashcards", onStreamPart: ({ streamPart, setArtifact }) => { - console.log(`[FlashcardArtifact] onStreamPart: ${streamPart.type}`); if (streamPart.type === "data-flashcardDelta") { - const contentLength = (streamPart.data as string)?.length || 0; - console.log( - `[FlashcardArtifact] Setting content: ${contentLength} chars` - ); - setArtifact((draft) => ({ - ...draft, + setArtifact((draftArtifact) => ({ + ...draftArtifact, content: streamPart.data, isVisible: true, status: "streaming", })); } }, - content: ({ content, isLoading }) => ( - - ), - actions: [ - { - icon: , - description: "Copy quiz data", - onClick: ({ content }) => { - navigator.clipboard.writeText(content); - toast.success("Quiz data copied to clipboard!"); - }, - }, - ], - toolbar: [ - { - icon: , - description: "Generate new questions", - onClick: ({ sendMessage }) => { - sendMessage({ - role: "user", - parts: [ - { - type: "text", - text: "Generate different questions on the same topic.", - }, - ], - }); - }, - }, - ], + content: ({ content, status }) => { + if (status === "streaming" || !content) { + return ( +
+
Generating quiz...
+
+
+ ); + } + + // Parse and display basic quiz data + try { + const data = JSON.parse(content); + return ( +
+

{data.topic}

+

+ {data.questions?.length || 0} questions generated +

+
+

+ Full flashcard UI coming in Chapter 4 +

+
+
+ ); + } catch { + return ( +
+

Loading flashcard data...

+
+ ); + } + }, + actions: [], + toolbar: [], }); diff --git a/artifacts/flashcard/server.ts b/artifacts/flashcard/server.ts index 1e2359f..7787f7e 100644 --- a/artifacts/flashcard/server.ts +++ b/artifacts/flashcard/server.ts @@ -1,9 +1,18 @@ -import { generateObject } from "ai"; +// TODO CHAPTER 4: Implement flashcard server handler +// +// This file should: +// 1. Define FlashcardData type schema +// 2. Create document handler for flashcard artifacts +// 3. Handle creation and updates of flashcard quizzes +// +// See CHAPTER-4.md for the complete implementation. + import { z } from "zod"; -import { myProvider } from "@/lib/ai/providers"; -import { createDocumentHandler } from "@/lib/artifacts/server"; -const flashcardSchema = z.object({ +/** + * Schema for flashcard quiz data + */ +export const flashcardSchema = z.object({ topic: z.string(), questions: z.array( z.object({ @@ -17,56 +26,9 @@ const flashcardSchema = z.object({ export type FlashcardData = z.infer; -export const flashcardDocumentHandler = createDocumentHandler<"flashcard">({ - kind: "flashcard", - onCreateDocument: async ({ title, dataStream }) => { - const { object } = await generateObject({ - model: myProvider.languageModel("artifact-model"), - schema: flashcardSchema, - prompt: `Create a quiz with 5 multiple choice questions about: "${title}" - -Each question should: -- Test understanding, not just memorization -- Have 4 options (A, B, C, D) -- Have a clear correct answer -- Include a brief explanation of why the answer is correct - -Return the quiz as structured JSON.`, - }); - - const content = JSON.stringify(object, null, 2); - - // Stream the content as a single delta - dataStream.write({ - type: "data-flashcardDelta", - data: content, - transient: true, - }); - - return content; - }, - onUpdateDocument: async ({ document, description, dataStream }) => { - const currentData = JSON.parse(document.content || "{}") as FlashcardData; - - const { object } = await generateObject({ - model: myProvider.languageModel("artifact-model"), - schema: flashcardSchema, - prompt: `Update this quiz based on the request: "${description}" - -Current quiz: -${JSON.stringify(currentData, null, 2)} - -Make the requested changes while maintaining the quiz structure.`, - }); - - const content = JSON.stringify(object, null, 2); - - dataStream.write({ - type: "data-flashcardDelta", - data: content, - transient: true, - }); - - return content; - }, -}); +// TODO: Add createDocumentHandler for flashcard artifacts +// export const flashcardDocumentHandler = createDocumentHandler<"flashcard">({ +// kind: "flashcard", +// onCreateDocument: async ({ title, dataStream }) => { ... }, +// onUpdateDocument: async ({ document, description, dataStream }) => { ... }, +// }); diff --git a/artifacts/study-plan/client.tsx b/artifacts/study-plan/client.tsx index 36dc323..e784d26 100644 --- a/artifacts/study-plan/client.tsx +++ b/artifacts/study-plan/client.tsx @@ -1,281 +1,56 @@ "use client"; -import { useState } from "react"; -import { toast } from "sonner"; import { Artifact } from "@/components/create-artifact"; -import { DocumentSkeleton } from "@/components/document-skeleton"; -import { - CheckCircleIcon, - CircleIcon, - CopyIcon, - PenIcon, -} from "@/components/icons"; -import { cn } from "@/lib/utils"; -import type { StudyPlanData } from "./server"; -type StudyPlanMetadata = Record; - -function StudyPlanViewer({ - content, - isLoading, - onSaveContent, -}: { - content: string; - isLoading: boolean; - onSaveContent: (content: string, debounce: boolean) => void; -}) { - const [expandedWeek, setExpandedWeek] = useState(0); - - console.log( - `[StudyPlanViewer] Render - isLoading: ${isLoading}, content length: ${content?.length || 0}` - ); - - if (isLoading || !content) { - console.log( - `[StudyPlanViewer] Showing skeleton (isLoading=${isLoading}, hasContent=${!!content})` - ); - return ; - } - - let data: StudyPlanData; - try { - data = JSON.parse(content); - console.log( - `[StudyPlanViewer] Parsed plan for: ${data.topic}, ${data.weeks?.length || 0} weeks` - ); - } catch (error) { - console.error("[StudyPlanViewer] JSON parse error:", error); - return ( -
-

Invalid study plan data

-
- ); - } - - const toggleTask = (weekIndex: number, taskIndex: number) => { - console.log( - `[StudyPlanViewer] Toggling task: week ${weekIndex}, task ${taskIndex}` - ); - const newData = { ...data }; - newData.weeks[weekIndex].tasks[taskIndex].completed = - !newData.weeks[weekIndex].tasks[taskIndex].completed; - onSaveContent(JSON.stringify(newData, null, 2), true); - }; - - const totalTasks = data.weeks.reduce( - (acc, week) => acc + week.tasks.length, - 0 - ); - const completedTasks = data.weeks.reduce( - (acc, week) => acc + week.tasks.filter((t) => t.completed).length, - 0 - ); - const progressPercent = - totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; - - return ( -
- {/* Header */} -
-

{data.topic}

-

{data.duration}

-
- - {/* Progress Bar */} -
-
- Progress - - {completedTasks}/{totalTasks} tasks ({Math.round(progressPercent)}%) - -
-
-
-
-
- - {/* Overview */} -
-

Overview

-

{data.overview}

-
- - {/* Weeks */} -
- {data.weeks.map((week, weekIndex) => { - const weekCompleted = week.tasks.filter((t) => t.completed).length; - const isExpanded = expandedWeek === weekIndex; - - return ( -
- - - {isExpanded && ( -
- {/* Goals */} -
-

Goals

-
    - {week.goals.map((goal) => ( -
  • {goal}
  • - ))} -
-
- - {/* Tasks */} -
-

Tasks

-
- {week.tasks.map((task, taskIndex) => ( - - ))} -
-
- - {/* Resources */} -
-

Resources

-
    - {week.resources.map((resource) => ( -
  • {resource}
  • - ))} -
-
-
- )} -
- ); - })} -
- - {/* Tips */} -
-

- Tips for Success -

-
    - {data.tips.map((tip) => ( -
  • {tip}
  • - ))} -
-
-
- ); -} - -export const studyPlanArtifact = new Artifact<"study-plan", StudyPlanMetadata>({ +// Placeholder study-plan artifact - full implementation coming in Chapter 4 +export const studyPlanArtifact = new Artifact<"study-plan">({ kind: "study-plan", - description: "Structured study plan with progress tracking.", + description: "Interactive study plan with progress tracking", onStreamPart: ({ streamPart, setArtifact }) => { - console.log(`[StudyPlanArtifact] onStreamPart: ${streamPart.type}`); if (streamPart.type === "data-studyPlanDelta") { - const contentLength = (streamPart.data as string)?.length || 0; - console.log( - `[StudyPlanArtifact] Setting content: ${contentLength} chars` - ); - setArtifact((draft) => ({ - ...draft, + setArtifact((draftArtifact) => ({ + ...draftArtifact, content: streamPart.data, isVisible: true, status: "streaming", })); } }, - content: ({ content, isLoading, onSaveContent }) => ( - - ), - actions: [ - { - icon: , - description: "Copy plan", - onClick: ({ content }) => { - navigator.clipboard.writeText(content); - toast.success("Study plan copied to clipboard!"); - }, - }, - ], - toolbar: [ - { - icon: , - description: "Adjust plan", - onClick: ({ sendMessage }) => { - sendMessage({ - role: "user", - parts: [ - { - type: "text", - text: "Please adjust the study plan to be more detailed with specific daily activities.", - }, - ], - }); - }, - }, - ], + content: ({ content, status }) => { + if (status === "streaming" || !content) { + return ( +
+
Creating study plan...
+
+
+ ); + } + + // Parse and display basic study plan data + try { + const data = JSON.parse(content); + return ( +
+

{data.topic}

+

+ {data.duration} - {data.weeks?.length || 0} weeks +

+

{data.overview}

+
+

+ Full study plan UI coming in Chapter 4 +

+
+
+ ); + } catch { + return ( +
+

Loading study plan data...

+
+ ); + } + }, + actions: [], + toolbar: [], }); diff --git a/artifacts/study-plan/server.ts b/artifacts/study-plan/server.ts index af850f3..648c741 100644 --- a/artifacts/study-plan/server.ts +++ b/artifacts/study-plan/server.ts @@ -1,9 +1,18 @@ -import { generateObject } from "ai"; +// TODO CHAPTER 4: Implement study-plan server handler +// +// This file should: +// 1. Define StudyPlanData type schema +// 2. Create document handler for study-plan artifacts +// 3. Handle creation and updates with progress tracking +// +// See CHAPTER-4.md for the complete implementation. + import { z } from "zod"; -import { myProvider } from "@/lib/ai/providers"; -import { createDocumentHandler } from "@/lib/artifacts/server"; -const studyPlanSchema = z.object({ +/** + * Schema for study plan data + */ +export const studyPlanSchema = z.object({ topic: z.string(), duration: z.string(), overview: z.string(), @@ -27,56 +36,9 @@ const studyPlanSchema = z.object({ export type StudyPlanData = z.infer; -export const studyPlanDocumentHandler = createDocumentHandler<"study-plan">({ - kind: "study-plan", - onCreateDocument: async ({ title, dataStream }) => { - const { object } = await generateObject({ - model: myProvider.languageModel("artifact-model"), - schema: studyPlanSchema, - prompt: `Create a structured study plan for: "${title}" - -Create a practical, actionable study plan that includes: -- A clear overview of what will be learned -- Weekly breakdown with specific goals -- Daily tasks with estimated durations -- Recommended resources (types of materials, not specific URLs) -- Tips for staying on track - -Make it realistic and achievable. Default to a 2-week plan unless specified otherwise.`, - }); - - const content = JSON.stringify(object, null, 2); - - dataStream.write({ - type: "data-studyPlanDelta", - data: content, - transient: true, - }); - - return content; - }, - onUpdateDocument: async ({ document, description, dataStream }) => { - const currentData = JSON.parse(document.content || "{}") as StudyPlanData; - - const { object } = await generateObject({ - model: myProvider.languageModel("artifact-model"), - schema: studyPlanSchema, - prompt: `Update this study plan based on the request: "${description}" - -Current plan: -${JSON.stringify(currentData, null, 2)} - -Make the requested changes while maintaining the plan structure.`, - }); - - const content = JSON.stringify(object, null, 2); - - dataStream.write({ - type: "data-studyPlanDelta", - data: content, - transient: true, - }); - - return content; - }, -}); +// TODO: Add createDocumentHandler for study-plan artifacts +// export const studyPlanDocumentHandler = createDocumentHandler<"study-plan">({ +// kind: "study-plan", +// onCreateDocument: async ({ title, dataStream }) => { ... }, +// onUpdateDocument: async ({ document, description, dataStream }) => { ... }, +// }); diff --git a/components/document-preview.tsx b/components/document-preview.tsx index 4ea4749..494ee75 100644 --- a/components/document-preview.tsx +++ b/components/document-preview.tsx @@ -275,6 +275,22 @@ const DocumentContent = ({ document }: { document: Document }) => {
+ ) : document.kind === "flashcard" ? ( +
+
📝
+

Interactive Quiz

+

+ Click to start the flashcard quiz +

+
+ ) : document.kind === "study-plan" ? ( +
+
📚
+

Study Plan

+

+ Click to view your personalized study plan +

+
) : null}
); diff --git a/components/message.tsx b/components/message.tsx index be35208..df00228 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -6,6 +6,7 @@ import { memo, useState } from "react"; import type { Vote } from "@/lib/db/types"; import type { ChatMessage } from "@/lib/types"; import { cn, sanitizeText } from "@/lib/utils"; +import type { ArtifactKind } from "./artifact"; import { useDataStream } from "./data-stream-provider"; import { DocumentToolResult } from "./document"; import { DocumentPreview } from "./document-preview"; @@ -244,7 +245,7 @@ const PurePreviewMessage = ({ {state === "input-available" && ( )} - {state === "output-available" && ( + {state === "output-available" && part.output && ( + ); + } + return null; + } + + if (type === "tool-planner") { + const { toolCallId, state, output } = part; + + if ( + state === "output-available" && + output?.success && + output?.data?.documentId + ) { + return ( + + ); + } + return null; + } + return null; })} diff --git a/components/weather.tsx b/components/weather.tsx index 074ba69..9e30ff6 100644 --- a/components/weather.tsx +++ b/components/weather.tsx @@ -80,7 +80,7 @@ const CloudIcon = ({ size = 24 }: { size?: number }) => ( ); -type WeatherAtLocation = { +export type WeatherAtLocation = { latitude: number; longitude: number; generationtime_ms: number; diff --git a/lib/ai/prompts.ts b/lib/ai/prompts.ts index 1c3030d..c51beaf 100644 --- a/lib/ai/prompts.ts +++ b/lib/ai/prompts.ts @@ -1,37 +1,6 @@ import type { Geo } from "@vercel/functions"; import type { ArtifactKind } from "@/components/artifact"; -export const artifactsPrompt = ` -Artifacts is a special user interface mode that helps users with writing, editing, and other content creation tasks. When artifact is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the artifacts and visible to the user. - -When asked to write code, always use artifacts. When writing code, specify the language in the backticks, e.g. \`\`\`python\`code here\`\`\`. The default language is Python. Other languages are not yet supported, so let the user know if they request a different language. - -DO NOT UPDATE DOCUMENTS IMMEDIATELY AFTER CREATING THEM. WAIT FOR USER FEEDBACK OR REQUEST TO UPDATE IT. - -This is a guide for using artifacts tools: \`createDocument\` and \`updateDocument\`, which render content on a artifacts beside the conversation. - -**When to use \`createDocument\`:** -- For substantial content (>10 lines) or code -- For content users will likely save/reuse (emails, code, essays, etc.) -- When explicitly requested to create a document -- For when content contains a single code snippet - -**When NOT to use \`createDocument\`:** -- For informational/explanatory content -- For conversational responses -- When asked to keep it in chat - -**Using \`updateDocument\`:** -- Default to full document rewrites for major changes -- Use targeted updates only for specific, isolated changes -- Follow user instructions for which parts to modify - -**When NOT to use \`updateDocument\`:** -- Immediately after creating a document - -Do not update document right after creating it. Wait for user feedback or request to update it. -`; - export const regularPrompt = "You are a friendly study buddy assistant! Keep your responses concise and helpful."; @@ -60,12 +29,6 @@ CRITICAL: Agents (quizMaster, planner) create their own artifacts automatically. - Do NOT call createDocument - the artifact is already created - Do NOT try to display or reformat the agent's output - Simply acknowledge the artifact was created and offer follow-up suggestions - -Example flows: -- "Explain machine learning" → use tutor -- "Quiz me on what we just discussed" → use quizMaster (creates flashcard artifact automatically) -- "Create a study plan for learning Python" → use planner (creates study-plan artifact automatically) -- "Summarize this article" → use analyst `; export type RequestHints = { @@ -96,7 +59,7 @@ export const systemPrompt = ({ return `${regularPrompt}\n\n${requestPrompt}`; } - return `${regularPrompt}\n\n${agentRoutingPrompt}\n\n${requestPrompt}\n\n${artifactsPrompt}`; + return `${regularPrompt}\n\n${agentRoutingPrompt}\n\n${requestPrompt}`; }; export const codePrompt = ` diff --git a/lib/artifacts/server.ts b/lib/artifacts/server.ts index 0a4bfd1..bf6480f 100644 --- a/lib/artifacts/server.ts +++ b/lib/artifacts/server.ts @@ -1,9 +1,7 @@ import type { UIMessageStreamWriter } from "ai"; import type { Session } from "next-auth"; import { codeDocumentHandler } from "@/artifacts/code/server"; -import { flashcardDocumentHandler } from "@/artifacts/flashcard/server"; import { sheetDocumentHandler } from "@/artifacts/sheet/server"; -import { studyPlanDocumentHandler } from "@/artifacts/study-plan/server"; import { textDocumentHandler } from "@/artifacts/text/server"; import type { ArtifactKind } from "@/components/artifact"; import { saveDocument } from "../db/queries"; @@ -95,8 +93,6 @@ export const documentHandlersByArtifactKind: DocumentHandler[] = [ textDocumentHandler, codeDocumentHandler, sheetDocumentHandler, - flashcardDocumentHandler, - studyPlanDocumentHandler, ]; export const artifactKinds = [ diff --git a/lib/types.ts b/lib/types.ts index 2137cc6..09d427c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -7,10 +7,7 @@ import type { createQuizMasterAgent, createTutorAgent, } from "./ai/agents"; -import type { createDocument } from "./ai/tools/create-document"; import type { getWeather } from "./ai/tools/get-weather"; -import type { requestSuggestions } from "./ai/tools/request-suggestions"; -import type { updateDocument } from "./ai/tools/update-document"; import type { Suggestion } from "./db/types"; import type { AppUsage } from "./usage"; @@ -22,13 +19,8 @@ export const messageMetadataSchema = z.object({ export type MessageMetadata = z.infer; -// Tool types +// Tool types - inferred from actual tool definitions type weatherTool = InferUITool; -type createDocumentTool = InferUITool>; -type updateDocumentTool = InferUITool>; -type requestSuggestionsTool = InferUITool< - ReturnType ->; // Agent tool types type tutorTool = InferUITool>; @@ -36,16 +28,33 @@ type quizMasterTool = InferUITool>; type plannerTool = InferUITool>; type analystTool = InferUITool>; +// Placeholder types for tools not yet implemented +// UITools expects { input, output } shape for each tool +type DocumentResult = { + id: string; + title: string; + kind: ArtifactKind; +}; + export type ChatTools = { getWeather: weatherTool; - createDocument: createDocumentTool; - updateDocument: updateDocumentTool; - requestSuggestions: requestSuggestionsTool; // Study buddy agents tutor: tutorTool; quizMaster: quizMasterTool; planner: plannerTool; analyst: analystTool; + createDocument: { + input: { title: string; kind: ArtifactKind }; + output: DocumentResult | { error: string }; + }; + updateDocument: { + input: { id: string; description: string }; + output: DocumentResult | { error: string }; + }; + requestSuggestions: { + input: { documentId: string }; + output: DocumentResult | { error: string }; + }; }; export type CustomUIDataTypes = { @@ -62,7 +71,7 @@ export type CustomUIDataTypes = { kind: ArtifactKind; clear: null; finish: null; - error: string; // For error signaling from agents + error: string; usage: AppUsage; }; diff --git a/tsconfig.json b/tsconfig.json index e11ae50..b0699ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,5 +31,5 @@ "next.config.js", ".next/dev/types/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "tempfiles"] }