From dcdd374372231972e81dc10e80b93eb5fd9d38cd Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Tue, 25 Nov 2025 21:38:58 +0000 Subject: [PATCH 1/8] Chapter 0: Basic chat starting point (tool-free) - Remove all AI tools (weather, create/update document, suggestions) - Remove all agent definitions (tutor, quiz master, planner, analyst) - Remove custom artifacts (flashcard, study-plan) - Simplify prompts to basic chat without tool routing - Keep UI components with placeholder types for rendering - Exclude tempfiles from TypeScript compilation --- app/(chat)/api/chat/route.ts | 40 ----- artifacts/flashcard/client.tsx | 223 ------------------------- artifacts/flashcard/server.ts | 72 -------- artifacts/study-plan/client.tsx | 281 -------------------------------- artifacts/study-plan/server.ts | 82 ---------- components/artifact.tsx | 4 - components/message.tsx | 2 +- components/weather.tsx | 2 +- lib/ai/agents/analyst.ts | 106 ------------ lib/ai/agents/index.ts | 8 - lib/ai/agents/planner.ts | 176 -------------------- lib/ai/agents/quiz-master.ts | 153 ----------------- lib/ai/agents/tutor.ts | 68 -------- lib/ai/agents/types.ts | 33 ---- lib/ai/prompts.ts | 70 +------- lib/ai/tools/get-weather.ts | 78 --------- lib/artifacts/server.ts | 12 +- lib/db/types.ts | 2 +- lib/types.ts | 63 +++---- tsconfig.json | 2 +- 20 files changed, 33 insertions(+), 1444 deletions(-) delete mode 100644 artifacts/flashcard/client.tsx delete mode 100644 artifacts/flashcard/server.ts delete mode 100644 artifacts/study-plan/client.tsx delete mode 100644 artifacts/study-plan/server.ts delete mode 100644 lib/ai/agents/analyst.ts delete mode 100644 lib/ai/agents/index.ts delete mode 100644 lib/ai/agents/planner.ts delete mode 100644 lib/ai/agents/quiz-master.ts delete mode 100644 lib/ai/agents/tutor.ts delete mode 100644 lib/ai/agents/types.ts delete mode 100644 lib/ai/tools/get-weather.ts diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 0187b7d..03e4dbe 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -4,7 +4,6 @@ import { createUIMessageStream, JsonToSseTransformStream, smoothStream, - stepCountIs, streamText, } from "ai"; import { unstable_cache as cache } from "next/cache"; @@ -22,20 +21,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, @@ -186,36 +175,7 @@ export async function POST(request: Request) { model: myProvider.languageModel(selectedChatModel), system: systemPrompt({ selectedChatModel, requestHints }), messages: convertToModelMessages(uiMessages), - stopWhen: stepCountIs(5), - experimental_activeTools: - selectedChatModel === "chat-model-reasoning" - ? [] - : [ - "getWeather", - "createDocument", - "updateDocument", - "requestSuggestions", - // Study buddy agents - "tutor", - "quizMaster", - "planner", - "analyst", - ], 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 }), - planner: createPlannerAgent({ session, dataStream }), - analyst: createAnalystAgent({ session, dataStream }), - }, experimental_telemetry: { isEnabled: isProductionEnvironment, functionId: "stream-text", diff --git a/artifacts/flashcard/client.tsx b/artifacts/flashcard/client.tsx deleted file mode 100644 index a542582..0000000 --- a/artifacts/flashcard/client.tsx +++ /dev/null @@ -1,223 +0,0 @@ -"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>({ - kind: "flashcard", - description: "Interactive flashcard quiz for testing knowledge.", - 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, - 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.", - }, - ], - }); - }, - }, - ], -}); diff --git a/artifacts/flashcard/server.ts b/artifacts/flashcard/server.ts deleted file mode 100644 index 1e2359f..0000000 --- a/artifacts/flashcard/server.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { generateObject } from "ai"; -import { z } from "zod"; -import { myProvider } from "@/lib/ai/providers"; -import { createDocumentHandler } from "@/lib/artifacts/server"; - -const flashcardSchema = z.object({ - topic: z.string(), - questions: z.array( - z.object({ - question: z.string(), - options: z.array(z.string()).length(4), - correctAnswer: z.number().min(0).max(3), - explanation: z.string(), - }) - ), -}); - -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; - }, -}); diff --git a/artifacts/study-plan/client.tsx b/artifacts/study-plan/client.tsx deleted file mode 100644 index 36dc323..0000000 --- a/artifacts/study-plan/client.tsx +++ /dev/null @@ -1,281 +0,0 @@ -"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>({ - kind: "study-plan", - description: "Structured 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, - 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.", - }, - ], - }); - }, - }, - ], -}); diff --git a/artifacts/study-plan/server.ts b/artifacts/study-plan/server.ts deleted file mode 100644 index af850f3..0000000 --- a/artifacts/study-plan/server.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { generateObject } from "ai"; -import { z } from "zod"; -import { myProvider } from "@/lib/ai/providers"; -import { createDocumentHandler } from "@/lib/artifacts/server"; - -const studyPlanSchema = z.object({ - topic: z.string(), - duration: z.string(), - overview: z.string(), - weeks: z.array( - z.object({ - week: z.number(), - title: z.string(), - goals: z.array(z.string()), - tasks: z.array( - z.object({ - task: z.string(), - duration: z.string(), - completed: z.boolean().default(false), - }) - ), - resources: z.array(z.string()), - }) - ), - tips: z.array(z.string()), -}); - -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; - }, -}); diff --git a/components/artifact.tsx b/components/artifact.tsx index fb5a7f2..4871c55 100644 --- a/components/artifact.tsx +++ b/components/artifact.tsx @@ -13,9 +13,7 @@ import { import useSWR, { useSWRConfig } from "swr"; import { useDebounceCallback, useWindowSize } from "usehooks-ts"; import { codeArtifact } from "@/artifacts/code/client"; -import { flashcardArtifact } from "@/artifacts/flashcard/client"; import { sheetArtifact } from "@/artifacts/sheet/client"; -import { studyPlanArtifact } from "@/artifacts/study-plan/client"; import { textArtifact } from "@/artifacts/text/client"; import { useArtifact } from "@/hooks/use-artifact"; import type { Document, Vote } from "@/lib/db/types"; @@ -34,8 +32,6 @@ export const artifactDefinitions = [ textArtifact, codeArtifact, sheetArtifact, - flashcardArtifact, - studyPlanArtifact, ]; export type ArtifactKind = (typeof artifactDefinitions)[number]["kind"]; diff --git a/components/message.tsx b/components/message.tsx index be35208..ee737b4 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -244,7 +244,7 @@ const PurePreviewMessage = ({ {state === "input-available" && ( )} - {state === "output-available" && ( + {state === "output-available" && part.output && ( ( ); -type WeatherAtLocation = { +export type WeatherAtLocation = { latitude: number; longitude: number; generationtime_ms: number; diff --git a/lib/ai/agents/analyst.ts b/lib/ai/agents/analyst.ts deleted file mode 100644 index 276eab6..0000000 --- a/lib/ai/agents/analyst.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { generateText, tool } from "ai"; -import { z } from "zod"; -import { myProvider } from "../providers"; -import type { AgentResult, CreateAgentProps } from "./types"; - -const ANALYST_SYSTEM_PROMPT = `You are a document analyst who excels at extracting insights and summarizing content. - -Your analysis approach: -- Identify the main themes and key points -- Extract important facts, figures, and arguments -- Note relationships between concepts -- Highlight actionable insights -- 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 - * - * Triggers: "analyze this", "summarize", "key points", "what's important" - * Output: Returns analysis that the orchestrator will present - */ -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 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"), - analysisType: z - .enum(["summary", "key-points", "deep-analysis", "study-notes"]) - .default("summary") - .describe("Type of analysis to perform"), - focusOn: z - .string() - .optional() - .describe("Specific aspect to focus the analysis on"), - outputLength: z - .enum(["brief", "moderate", "detailed"]) - .default("moderate") - .describe("Desired length of the analysis output"), - }), - execute: async ({ - content, - analysisType, - focusOn, - outputLength, - }): Promise => { - const focusContext = focusOn - ? `\n\nFocus particularly on: ${focusOn}` - : ""; - - const lengthGuide = { - brief: "Keep the analysis concise, around 100-200 words.", - moderate: "Provide a balanced analysis, around 300-500 words.", - detailed: "Provide a comprehensive analysis, around 600-800 words.", - }; - - const analysisGuide = { - 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: - ---- -${content} ---- - -Analysis type: ${analysisType} -${analysisGuide[analysisType]} -${focusContext} - -${lengthGuide[outputLength]}`; - - 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, - contentLength: content.length, - }, - }; - }, - }); diff --git a/lib/ai/agents/index.ts b/lib/ai/agents/index.ts deleted file mode 100644 index 11c0cf1..0000000 --- a/lib/ai/agents/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Agent type definitions - -// Specialized agents -export { createAnalystAgent } from "./analyst"; -export { createPlannerAgent } from "./planner"; -export { createQuizMasterAgent } from "./quiz-master"; -export { createTutorAgent } from "./tutor"; -export type { AgentContext, AgentResult, CreateAgentProps } from "./types"; diff --git a/lib/ai/agents/planner.ts b/lib/ai/agents/planner.ts deleted file mode 100644 index c944215..0000000 --- a/lib/ai/agents/planner.ts +++ /dev/null @@ -1,176 +0,0 @@ -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"; - -const studyPlanSchema = z.object({ - topic: z.string(), - duration: z.string(), - overview: z.string(), - weeks: z.array( - z.object({ - week: z.number(), - title: z.string(), - goals: z.array(z.string()), - tasks: z.array( - z.object({ - task: z.string(), - duration: z.string(), - completed: z.boolean().default(false), - }) - ), - resources: z.array(z.string()), - }) - ), - tips: z.array(z.string()), -}); - -/** - * Planner Agent - Creates interactive study plans with progress tracking - * - * 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) => - 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 to learning something. Triggers: study plan, learning roadmap, how to learn, schedule, curriculum.", - inputSchema: z.object({ - topic: z - .string() - .describe("The topic or skill to create a study plan for"), - timeframe: z - .string() - .default("2 weeks") - .describe( - "How long the user has to learn (e.g., '1 week', '30 days', '3 months')" - ), - hoursPerDay: z - .number() - .min(0.5) - .max(8) - .default(1) - .describe("Hours available for study per day"), - currentLevel: z - .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] 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("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}${goalsContext} - -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.`, - }); - - const content = JSON.stringify(object, null, 2); - console.log( - `[Planner] Generated plan with ${object.weeks.length} weeks (${content.length} chars)` - ); - - // Stream the content to the UI - dataStream.write({ - type: "data-studyPlanDelta", - data: content, - transient: true, - }); - - // 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: `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 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: `Failed to generate study plan for "${topic}": ${errorMessage}. Please try again.`, - data: { - documentId, - topic, - error: errorMessage, - }, - }; - } - }, - }); diff --git a/lib/ai/agents/quiz-master.ts b/lib/ai/agents/quiz-master.ts deleted file mode 100644 index cdaf902..0000000 --- a/lib/ai/agents/quiz-master.ts +++ /dev/null @@ -1,153 +0,0 @@ -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"; - -const flashcardSchema = z.object({ - topic: z.string(), - questions: z.array( - z.object({ - question: z.string(), - options: z.array(z.string()).length(4), - correctAnswer: z.number().min(0).max(3), - explanation: z.string(), - }) - ), -}); - -/** - * Quiz Master Agent - Creates interactive flashcard quizzes - * - * Triggers: "quiz me", "test my knowledge", "practice questions", "assessment" - * Output: Creates a flashcard artifact for interactive testing - */ -export const createQuizMasterAgent = ({ - session, - dataStream, -}: CreateAgentProps) => - tool({ - description: - "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 - .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] Starting quiz generation for "${topic}"`); - console.log( - `[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("artifact-model"), - schema: flashcardSchema, - 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); - console.log( - `[QuizMaster] Generated ${object.questions.length} questions (${content.length} chars)` - ); - - // Stream the content to the UI - dataStream.write({ - type: "data-flashcardDelta", - data: content, - transient: true, - }); - - // 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: `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 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: `Failed to generate quiz about "${topic}": ${errorMessage}. Please try again.`, - data: { - documentId, - topic, - error: errorMessage, - }, - }; - } - }, - }); diff --git a/lib/ai/agents/tutor.ts b/lib/ai/agents/tutor.ts deleted file mode 100644 index 5a92d9f..0000000 --- a/lib/ai/agents/tutor.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { generateText, tool } from "ai"; -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 examples and analogies - * - * Triggers: "explain", "teach me", "how does X work", "what is" - * 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 = (_props: CreateAgentProps) => - tool({ - description: - "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"), - depth: z - .enum(["beginner", "intermediate", "advanced"]) - .default("intermediate") - .describe("The depth of explanation needed based on user context"), - context: z - .string() - .optional() - .describe( - "Additional context about what the user already knows or specific aspects to focus on" - ), - }), - execute: async ({ topic, depth, context }): Promise => { - const prompt = `Explain "${topic}" at a ${depth} level.${ - context ? `\n\nAdditional context: ${context}` : "" - }`; - - 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 }, - }; - }, - }); diff --git a/lib/ai/agents/types.ts b/lib/ai/agents/types.ts deleted file mode 100644 index f090c99..0000000 --- a/lib/ai/agents/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { UIMessageStreamWriter } from "ai"; -import type { Session } from "next-auth"; -import type { ChatMessage } from "@/lib/types"; - -/** - * 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; -}; - -/** - * 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; -}; diff --git a/lib/ai/prompts.ts b/lib/ai/prompts.ts index 1c3030d..a2fdbf3 100644 --- a/lib/ai/prompts.ts +++ b/lib/ai/prompts.ts @@ -1,73 +1,9 @@ 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."; -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 - -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 = { latitude: Geo["latitude"]; longitude: Geo["longitude"]; @@ -92,11 +28,7 @@ export const systemPrompt = ({ }) => { const requestPrompt = getRequestPromptFromHints(requestHints); - if (selectedChatModel === "chat-model-reasoning") { - return `${regularPrompt}\n\n${requestPrompt}`; - } - - return `${regularPrompt}\n\n${agentRoutingPrompt}\n\n${requestPrompt}\n\n${artifactsPrompt}`; + return `${regularPrompt}\n\n${requestPrompt}`; }; export const codePrompt = ` diff --git a/lib/ai/tools/get-weather.ts b/lib/ai/tools/get-weather.ts deleted file mode 100644 index 3e616e0..0000000 --- a/lib/ai/tools/get-weather.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { tool } from "ai"; -import { z } from "zod"; - -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. You can provide either coordinates or a city name.", - inputSchema: z.object({ - latitude: z.number().optional(), - longitude: z.number().optional(), - city: z - .string() - .describe("City name (e.g., 'San Francisco', 'New York', 'London')") - .optional(), - }), - execute: async (input) => { - let latitude: number; - let longitude: number; - - 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.", - }; - } - - 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 weatherData = await response.json(); - - if ("city" in input) { - weatherData.cityName = input.city; - } - - return weatherData; - }, -}); diff --git a/lib/artifacts/server.ts b/lib/artifacts/server.ts index 0a4bfd1..e36046d 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,14 +93,6 @@ export const documentHandlersByArtifactKind: DocumentHandler[] = [ textDocumentHandler, codeDocumentHandler, sheetDocumentHandler, - flashcardDocumentHandler, - studyPlanDocumentHandler, ]; -export const artifactKinds = [ - "text", - "code", - "sheet", - "flashcard", - "study-plan", -] as const; +export const artifactKinds = ["text", "code", "sheet"] as const; diff --git a/lib/db/types.ts b/lib/db/types.ts index 38e9411..bbf3543 100644 --- a/lib/db/types.ts +++ b/lib/db/types.ts @@ -47,7 +47,7 @@ export type Document = { id: string; title: string; content?: string; - kind: "text" | "code" | "sheet" | "flashcard" | "study-plan"; + kind: "text" | "code" | "sheet"; userId: string; embedding?: number[]; createdAt: Date; diff --git a/lib/types.ts b/lib/types.ts index 2137cc6..54f9e52 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,16 +1,7 @@ -import type { InferUITool, UIMessage } from "ai"; +import type { UIMessage } from "ai"; import { z } from "zod"; import type { ArtifactKind } from "@/components/artifact"; -import type { - createAnalystAgent, - createPlannerAgent, - 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 { WeatherAtLocation } from "@/components/weather"; import type { Suggestion } from "./db/types"; import type { AppUsage } from "./usage"; @@ -22,30 +13,32 @@ export const messageMetadataSchema = z.object({ export type MessageMetadata = z.infer; -// Tool types -type weatherTool = InferUITool; -type createDocumentTool = InferUITool>; -type updateDocumentTool = InferUITool>; -type requestSuggestionsTool = InferUITool< - ReturnType ->; - -// Agent tool types -type tutorTool = InferUITool>; -type quizMasterTool = InferUITool>; -type plannerTool = InferUITool>; -type analystTool = InferUITool>; +// Tool type definitions for UI rendering +// These are placeholders in Chapter 0 - tools not registered in the API +// 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; + getWeather: { + input: { latitude: number; longitude: number }; + output: WeatherAtLocation; + }; + 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 = { @@ -53,8 +46,6 @@ export type CustomUIDataTypes = { imageDelta: string; sheetDelta: string; codeDelta: string; - flashcardDelta: string; - studyPlanDelta: string; suggestion: Suggestion; appendMessage: string; id: string; @@ -62,7 +53,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"] } From f990ce0d51e6a86c488caba064709b0d5645fa83 Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Tue, 25 Nov 2025 21:40:32 +0000 Subject: [PATCH 2/8] Chapter 1: Add weather tool (first tool) - Add lib/ai/tools/get-weather.ts with geocoding support - Update route.ts to import and use weather tool - Add stepCountIs for multi-step tool conversations - Add experimental_activeTools for reasoning model compatibility - Update types.ts to use InferUITool for weather tool --- app/(chat)/api/chat/route.ts | 10 +++++ lib/ai/tools/get-weather.ts | 78 ++++++++++++++++++++++++++++++++++++ lib/types.ts | 15 ++++--- 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 lib/ai/tools/get-weather.ts diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 03e4dbe..601d1d5 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -4,8 +4,10 @@ import { createUIMessageStream, JsonToSseTransformStream, smoothStream, + stepCountIs, streamText, } from "ai"; +import { getWeather } from "@/lib/ai/tools/get-weather"; import { unstable_cache as cache } from "next/cache"; import type { ModelCatalog } from "tokenlens/core"; @@ -175,7 +177,15 @@ export async function POST(request: Request) { model: myProvider.languageModel(selectedChatModel), system: systemPrompt({ selectedChatModel, requestHints }), messages: convertToModelMessages(uiMessages), + stopWhen: stepCountIs(5), + experimental_activeTools: + selectedChatModel === "chat-model-reasoning" + ? [] + : ["getWeather"], experimental_transform: smoothStream({ chunking: "word" }), + tools: { + getWeather, + }, experimental_telemetry: { isEnabled: isProductionEnvironment, functionId: "stream-text", diff --git a/lib/ai/tools/get-weather.ts b/lib/ai/tools/get-weather.ts new file mode 100644 index 0000000..3e616e0 --- /dev/null +++ b/lib/ai/tools/get-weather.ts @@ -0,0 +1,78 @@ +import { tool } from "ai"; +import { z } from "zod"; + +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. You can provide either coordinates or a city name.", + inputSchema: z.object({ + latitude: z.number().optional(), + longitude: z.number().optional(), + city: z + .string() + .describe("City name (e.g., 'San Francisco', 'New York', 'London')") + .optional(), + }), + execute: async (input) => { + let latitude: number; + let longitude: number; + + 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.", + }; + } + + 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 weatherData = await response.json(); + + if ("city" in input) { + weatherData.cityName = input.city; + } + + return weatherData; + }, +}); diff --git a/lib/types.ts b/lib/types.ts index 54f9e52..5be01e1 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,7 @@ -import type { UIMessage } from "ai"; +import type { InferUITool, UIMessage } from "ai"; import { z } from "zod"; import type { ArtifactKind } from "@/components/artifact"; -import type { WeatherAtLocation } from "@/components/weather"; +import type { getWeather } from "./ai/tools/get-weather"; import type { Suggestion } from "./db/types"; import type { AppUsage } from "./usage"; @@ -13,8 +13,10 @@ export const messageMetadataSchema = z.object({ export type MessageMetadata = z.infer; -// Tool type definitions for UI rendering -// These are placeholders in Chapter 0 - tools not registered in the API +// Tool types - inferred from actual tool definitions +type weatherTool = InferUITool; + +// Placeholder types for tools not yet implemented // UITools expects { input, output } shape for each tool type DocumentResult = { id: string; @@ -23,10 +25,7 @@ type DocumentResult = { }; export type ChatTools = { - getWeather: { - input: { latitude: number; longitude: number }; - output: WeatherAtLocation; - }; + getWeather: weatherTool; createDocument: { input: { title: string; kind: ArtifactKind }; output: DocumentResult | { error: string }; From 6b8545267fc58c1bb9bd1d967e637523707fbfe4 Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Tue, 25 Nov 2025 21:42:03 +0000 Subject: [PATCH 3/8] Chapter 2: Add tutor agent (first agent) - Add lib/ai/agents/ directory with types and index - Add tutor agent that explains concepts with examples - Update route.ts to import and register tutor agent - Update types.ts with tutor tool type inference --- app/(chat)/api/chat/route.ts | 4 ++- lib/ai/agents/index.ts | 5 +++ lib/ai/agents/tutor.ts | 68 ++++++++++++++++++++++++++++++++++++ lib/ai/agents/types.ts | 33 +++++++++++++++++ lib/types.ts | 3 ++ 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 lib/ai/agents/index.ts create mode 100644 lib/ai/agents/tutor.ts create mode 100644 lib/ai/agents/types.ts diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 601d1d5..799ddee 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -7,6 +7,7 @@ import { stepCountIs, streamText, } from "ai"; +import { 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"; @@ -181,10 +182,11 @@ export async function POST(request: Request) { experimental_activeTools: selectedChatModel === "chat-model-reasoning" ? [] - : ["getWeather"], + : ["getWeather", "tutor"], experimental_transform: smoothStream({ chunking: "word" }), tools: { getWeather, + tutor: createTutorAgent({ session, dataStream }), }, experimental_telemetry: { isEnabled: isProductionEnvironment, diff --git a/lib/ai/agents/index.ts b/lib/ai/agents/index.ts new file mode 100644 index 0000000..03f57dd --- /dev/null +++ b/lib/ai/agents/index.ts @@ -0,0 +1,5 @@ +// Agent type definitions + +// Specialized agents +export { createTutorAgent } from "./tutor"; +export type { AgentContext, AgentResult, CreateAgentProps } from "./types"; diff --git a/lib/ai/agents/tutor.ts b/lib/ai/agents/tutor.ts new file mode 100644 index 0000000..5a92d9f --- /dev/null +++ b/lib/ai/agents/tutor.ts @@ -0,0 +1,68 @@ +import { generateText, tool } from "ai"; +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 examples and analogies + * + * Triggers: "explain", "teach me", "how does X work", "what is" + * 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 = (_props: CreateAgentProps) => + tool({ + description: + "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"), + depth: z + .enum(["beginner", "intermediate", "advanced"]) + .default("intermediate") + .describe("The depth of explanation needed based on user context"), + context: z + .string() + .optional() + .describe( + "Additional context about what the user already knows or specific aspects to focus on" + ), + }), + execute: async ({ topic, depth, context }): Promise => { + const prompt = `Explain "${topic}" at a ${depth} level.${ + context ? `\n\nAdditional context: ${context}` : "" + }`; + + 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 }, + }; + }, + }); diff --git a/lib/ai/agents/types.ts b/lib/ai/agents/types.ts new file mode 100644 index 0000000..f090c99 --- /dev/null +++ b/lib/ai/agents/types.ts @@ -0,0 +1,33 @@ +import type { UIMessageStreamWriter } from "ai"; +import type { Session } from "next-auth"; +import type { ChatMessage } from "@/lib/types"; + +/** + * 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; +}; + +/** + * 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; +}; diff --git a/lib/types.ts b/lib/types.ts index 5be01e1..2666c7f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,7 @@ import type { InferUITool, UIMessage } from "ai"; import { z } from "zod"; import type { ArtifactKind } from "@/components/artifact"; +import type { createTutorAgent } from "./ai/agents"; import type { getWeather } from "./ai/tools/get-weather"; import type { Suggestion } from "./db/types"; import type { AppUsage } from "./usage"; @@ -15,6 +16,7 @@ export type MessageMetadata = z.infer; // Tool types - inferred from actual tool definitions type weatherTool = InferUITool; +type tutorTool = InferUITool>; // Placeholder types for tools not yet implemented // UITools expects { input, output } shape for each tool @@ -26,6 +28,7 @@ type DocumentResult = { export type ChatTools = { getWeather: weatherTool; + tutor: tutorTool; createDocument: { input: { title: string; kind: ArtifactKind }; output: DocumentResult | { error: string }; From 509ecd6eb5c8f4f37820b7ce5810381d52e1000d Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Wed, 26 Nov 2025 06:59:33 +0000 Subject: [PATCH 4/8] workshop: add Chapter 3 preview stubs (multi-agent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add lib/ai/agents/quiz-master.ts with TODO placeholder - Add lib/ai/agents/planner.ts with TODO placeholder - Add lib/ai/agents/analyst.ts with TODO placeholder - Update index.ts with commented exports for Chapter 3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/ai/agents/analyst.ts | 62 ++++++++++++++++++++++++++++++++ lib/ai/agents/index.ts | 6 ++++ lib/ai/agents/planner.ts | 68 ++++++++++++++++++++++++++++++++++++ lib/ai/agents/quiz-master.ts | 62 ++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 lib/ai/agents/analyst.ts create mode 100644 lib/ai/agents/planner.ts create mode 100644 lib/ai/agents/quiz-master.ts diff --git a/lib/ai/agents/analyst.ts b/lib/ai/agents/analyst.ts new file mode 100644 index 0000000..0802812 --- /dev/null +++ b/lib/ai/agents/analyst.ts @@ -0,0 +1,62 @@ +import { tool } from "ai"; +import { z } from "zod"; + +import type { AgentResult, CreateAgentProps } from "./types"; + +// TODO CHAPTER 3: Implement the Analyst Agent +// +// The Analyst analyzes content and extracts key insights. +// It should: +// 1. Accept content and analysis type (summary, key-points, deep-analysis, study-notes) +// 2. Use generateText to analyze the content +// 3. Return structured analysis or summary +// +// Triggers: "analyze", "summarize", "key points", "what's important" +// +// See CHAPTER-3.md for the complete implementation. + +/** + * Analyst Agent - Analyzes content and extracts insights + * + * @param _props - Agent props (session, dataStream) + * @returns AI SDK tool that analyzes and summarizes content + */ +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.", + inputSchema: z.object({ + content: z.string().describe("The text or content to analyze"), + analysisType: z + .enum(["summary", "key-points", "deep-analysis", "study-notes"]) + .default("summary") + .describe("Type of analysis to perform"), + focusOn: z + .string() + .optional() + .describe("Specific aspect to focus the analysis on"), + outputLength: z + .enum(["brief", "moderate", "detailed"]) + .default("moderate") + .describe("Desired length of the analysis output"), + }), + execute: async ({ analysisType }): Promise => { + // TODO: Implement in Chapter 3 + // 1. Build prompt based on analysis type + // 2. Use generateText to analyze content + // 3. Return structured analysis result + + console.log(`[Analyst] TODO: Perform ${analysisType} analysis`); + + // Placeholder await to satisfy linter (remove when implementing) + await Promise.resolve(); + + return { + agentName: "analyst", + success: false, + summary: `TODO: Implement analyst in Chapter 3 to perform ${analysisType} analysis`, + }; + }, + }); diff --git a/lib/ai/agents/index.ts b/lib/ai/agents/index.ts index 03f57dd..a7547a0 100644 --- a/lib/ai/agents/index.ts +++ b/lib/ai/agents/index.ts @@ -2,4 +2,10 @@ // Specialized agents export { createTutorAgent } from "./tutor"; + +// TODO CHAPTER 3: Uncomment these exports when implementing multi-agent system +// export { createQuizMasterAgent } from "./quiz-master"; +// export { createPlannerAgent } from "./planner"; +// export { createAnalystAgent } from "./analyst"; + export type { AgentContext, AgentResult, CreateAgentProps } from "./types"; diff --git a/lib/ai/agents/planner.ts b/lib/ai/agents/planner.ts new file mode 100644 index 0000000..6496db9 --- /dev/null +++ b/lib/ai/agents/planner.ts @@ -0,0 +1,68 @@ +import { tool } from "ai"; +import { z } from "zod"; + +import type { AgentResult, CreateAgentProps } from "./types"; + +// TODO CHAPTER 3: Implement the Planner Agent +// +// The Planner creates personalized study plans and learning roadmaps. +// It should: +// 1. Accept a topic, timeframe, hours/day, and current level +// 2. Use generateObject to create structured study plan data +// 3. Return weekly breakdown with goals, tasks, and resources +// +// In Chapter 4, this will create a study-plan artifact with checkboxes. +// +// Triggers: "study plan", "learning roadmap", "how should I learn" +// +// See CHAPTER-3.md for the complete implementation. + +/** + * Planner Agent - Creates study plans and learning roadmaps + * + * @param _props - Agent props (session, dataStream) + * @returns AI SDK tool that generates study plans + */ +export const createPlannerAgent = (_props: 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.", + inputSchema: z.object({ + topic: z + .string() + .describe("The topic or skill to create a study plan for"), + timeframe: z + .string() + .default("2 weeks") + .describe("How long the user has to learn (e.g., '1 week', '30 days')"), + hoursPerDay: z + .number() + .min(0.5) + .max(8) + .default(1) + .describe("Hours available for study per day"), + currentLevel: z + .enum(["complete beginner", "some basics", "intermediate", "advanced"]) + .default("complete beginner") + .describe("User's current knowledge level"), + }), + execute: async ({ topic }): Promise => { + // TODO: Implement in Chapter 3 + // 1. Use generateObject with a study plan schema + // 2. Create weekly breakdown with goals and tasks + // 3. Format as markdown or create artifact in Chapter 4 + + console.log(`[Planner] TODO: Create study plan for "${topic}"`); + + // Placeholder await to satisfy linter (remove when implementing) + await Promise.resolve(); + + return { + agentName: "planner", + success: false, + summary: `TODO: Implement planner in Chapter 3 to create study plan for "${topic}"`, + }; + }, + }); diff --git a/lib/ai/agents/quiz-master.ts b/lib/ai/agents/quiz-master.ts new file mode 100644 index 0000000..b4272d4 --- /dev/null +++ b/lib/ai/agents/quiz-master.ts @@ -0,0 +1,62 @@ +import { tool } from "ai"; +import { z } from "zod"; + +import type { AgentResult, CreateAgentProps } from "./types"; + +// TODO CHAPTER 3: Implement the Quiz Master Agent +// +// The Quiz Master creates interactive quizzes to test knowledge. +// It should: +// 1. Accept a topic, number of questions, and difficulty level +// 2. Use generateObject to create structured quiz data +// 3. Return quiz questions with multiple choice options +// +// In Chapter 4, this will create a flashcard artifact instead of text. +// +// Triggers: "quiz me", "test my knowledge", "practice questions" +// +// See CHAPTER-3.md for the complete implementation. + +/** + * Quiz Master Agent - Creates quizzes to test knowledge + * + * @param _props - Agent props (session, dataStream) + * @returns AI SDK tool that generates quiz questions + */ +export const createQuizMasterAgent = (_props: 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.", + inputSchema: z.object({ + 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"), + }), + execute: async ({ topic }): Promise => { + // TODO: Implement in Chapter 3 + // 1. Use generateObject with a quiz schema + // 2. Generate questions with options and explanations + // 3. Format as markdown or create artifact in Chapter 4 + + console.log(`[QuizMaster] TODO: Create quiz about "${topic}"`); + + // Placeholder await to satisfy linter (remove when implementing) + await Promise.resolve(); + + return { + agentName: "quiz-master", + success: false, + summary: `TODO: Implement quiz master in Chapter 3 to create quiz about "${topic}"`, + }; + }, + }); From 82cb86a5143a5715a3b5f1952e136c55b4f0745c Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Wed, 26 Nov 2025 07:06:21 +0000 Subject: [PATCH 5/8] docs: update CHAPTER-1.md to match full weather implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show full geocodeCity implementation - Update route.ts example with stepCountIs, experimental_activeTools - Fix Weather component props to match API response - Update flow diagram to show city name lookup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHAPTER-1.md | 138 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 111 insertions(+), 27 deletions(-) diff --git a/CHAPTER-1.md b/CHAPTER-1.md index 31ef7d5..18ff6ac 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,30 +56,94 @@ 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: @@ -87,30 +151,39 @@ Add the tool to your chat route: ### File: `app/(chat)/api/chat/route.ts` ```typescript -import { streamText } from "ai"; +import { stepCountIs, streamText } from "ai"; import { myProvider } from "@/lib/ai/providers"; import { systemPrompt } from "@/lib/ai/prompts"; import { getWeather } from "@/lib/ai/tools/get-weather"; export async function POST(request: Request) { - const { messages } = await request.json(); + const { messages, selectedChatModel } = await request.json(); const result = streamText({ - model: myProvider.languageModel("chat-model"), - system: systemPrompt(), + model: myProvider.languageModel(selectedChatModel), + system: systemPrompt({ selectedChatModel }), messages, + // Stop after 5 tool call steps + stopWhen: stepCountIs(5), + // Disable tools for reasoning models + experimental_activeTools: + selectedChatModel === "chat-model-reasoning" ? [] : ["getWeather"], // Add tools here! tools: { getWeather, }, - // Let the AI call multiple tools if needed - maxSteps: 5, }); return result.toDataStreamResponse(); } ``` +### 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 + ## How Tool Calling Works ``` @@ -120,9 +193,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 +215,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}

@@ -214,7 +297,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 --- From 7bf7e4b6ec0b1ec773533ba862b10c7530b79590 Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Wed, 26 Nov 2025 07:33:00 +0000 Subject: [PATCH 6/8] docs: fix all chapter documentation to match actual code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHAPTER-0: Updated to show createUIMessageStream pattern, full useChat config with transport, correct systemPrompt signature - CHAPTER-2: Fixed agent types (UIMessageStreamWriter), gateway.languageModel pattern, tutor params (depth/context), route handler structure - CHAPTER-3: Rewrote quiz-master and planner to show artifact creation with dataStream.write(), correct models (artifact-model), DB save, error handling - CHAPTER-4: Fixed CustomUIDataTypes, added focusAreas param, artifact-model - CHAPTER-5: Added analyst.ts to file structure, fixed inputSchema usage, updated architecture diagrams, added analyst to orchestrator tools All code snippets are now copy-paste ready and match the actual implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHAPTER-0.md | 156 ++++++++++++----- CHAPTER-2.md | 276 ++++++++++++++++------------- CHAPTER-3.md | 481 ++++++++++++++++++++++++++++++++------------------- CHAPTER-4.md | 94 +++++++--- CHAPTER-5.md | 77 ++++++--- 5 files changed, 695 insertions(+), 389 deletions(-) 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-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 }, + }; }, }); ``` From 8762bcf8e4487e57dcb31754cff74cfe8c41846a Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Wed, 26 Nov 2025 08:22:00 +0000 Subject: [PATCH 7/8] docs: fix CHAPTER-1 to show correct systemPrompt and route patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated system prompt section to show updating regularPrompt constant - Updated route handler to show correct systemPrompt({ selectedChatModel, requestHints }) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHAPTER-1.md | 75 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/CHAPTER-1.md b/CHAPTER-1.md index 18ff6ac..b9314be 100644 --- a/CHAPTER-1.md +++ b/CHAPTER-1.md @@ -146,43 +146,40 @@ export const getWeather = tool({ ## 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 { stepCountIs, 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, selectedChatModel } = await request.json(); - - const result = streamText({ - model: myProvider.languageModel(selectedChatModel), - system: systemPrompt({ selectedChatModel }), - messages, - // Stop after 5 tool call steps - stopWhen: stepCountIs(5), - // Disable tools for reasoning models - experimental_activeTools: - selectedChatModel === "chat-model-reasoning" ? [] : ["getWeather"], - // Add tools here! - tools: { - getWeather, - }, - }); - - 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 @@ -254,23 +251,39 @@ export function Weather({ current, current_units, cityName }: 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: From 2c6bffca36fe25e634e9d2f5a068ce2bcb68c23e Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Wed, 26 Nov 2025 09:10:38 +0000 Subject: [PATCH 8/8] Add agentRoutingPrompt for tutor agent routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add agentRoutingPrompt with tutor agent description - Update systemPrompt to include routing for non-reasoning models 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/ai/prompts.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/ai/prompts.ts b/lib/ai/prompts.ts index a2fdbf3..f3da7aa 100644 --- a/lib/ai/prompts.ts +++ b/lib/ai/prompts.ts @@ -4,6 +4,18 @@ import type { ArtifactKind } from "@/components/artifact"; 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 + +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) +`; + export type RequestHints = { latitude: Geo["latitude"]; longitude: Geo["longitude"]; @@ -28,7 +40,11 @@ export const systemPrompt = ({ }) => { const requestPrompt = getRequestPromptFromHints(requestHints); - return `${regularPrompt}\n\n${requestPrompt}`; + if (selectedChatModel === "chat-model-reasoning") { + return `${regularPrompt}\n\n${requestPrompt}`; + } + + return `${regularPrompt}\n\n${agentRoutingPrompt}\n\n${requestPrompt}`; }; export const codePrompt = `