Capstone Project
Build a complete AI Agent end-to-end β from design to deployment
Objective
Apply everything youβve learned from the previous 13 lessons to build a complete AI Agent. Youβll create a Personal Research Agent β an agent that researches topics, synthesizes information, and generates reports.
Architecture
βββββββββββββββββββββββββββββββββββββββββββ
β Personal Research Agent β
βββββββββββ¬βββββββββββ¬ββββββββββββ¬βββββββββ€
β Planningβ Research β Synthesis β Memory β
β Agent β Agent β Agent β System β
βββββββββββ΄βββββββββββ΄ββββββββββββ΄βββββββββ€
β Tool Layer β
β ββββββββββ ββββββββ βββββββββββββββββ β
β βWeb β βMCP β βSupabase β β
β βSearch β βTools β β(pgvector + β β
β β β β β β memories) β β
β ββββββββββ ββββββββ βββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββ€
β Observability (Langfuse) β
ββββββββββββββββββββββββββββββββββββββββββββ
Step 1: Setup Project
mkdir research-agent && cd research-agent
npm init -y
npm install ai @ai-sdk/openai zod langfuse @supabase/supabase-js
npm install -D typescript @types/node tsx
npx tsc --init
// .env
OPENAI_API_KEY=sk-...
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=eyJ...
LANGFUSE_PUBLIC_KEY=pk-...
LANGFUSE_SECRET_KEY=sk-...
Step 2: Memory System (Lesson 12)
// src/memory.ts
import { createClient } from "@supabase/supabase-js";
import OpenAI from "openai";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_KEY!
);
const openaiClient = new OpenAI();
export async function getEmbedding(text: string): Promise<number[]> {
const response = await openaiClient.embeddings.create({
model: "text-embedding-3-small",
input: text
});
return response.data[0].embedding;
}
export async function saveResearch(topic: string, content: string, sources: string[]) {
const embedding = await getEmbedding(topic);
await supabase.from("research_memories").insert({
topic,
content,
sources,
embedding,
created_at: new Date().toISOString()
});
}
export async function recallResearch(query: string, limit = 5) {
const embedding = await getEmbedding(query);
const { data } = await supabase.rpc("match_research", {
query_embedding: embedding,
match_count: limit
});
return data || [];
}
Step 3: Tools (Lessons 3, 4, 10)
// src/tools.ts
import { tool } from "ai";
import { z } from "zod";
import { saveResearch, recallResearch } from "./memory";
export const searchWeb = tool({
description: "Search the web for information on a topic",
parameters: z.object({
query: z.string().describe("Search keywords"),
maxResults: z.number().default(5)
}),
execute: async ({ query, maxResults }) => {
const response = await fetch(
`https://api.tavily.com/search?query=${encodeURIComponent(query)}&max_results=${maxResults}`,
{ headers: { Authorization: `Bearer ${process.env.TAVILY_API_KEY}` } }
);
const data = await response.json();
return { results: data.results };
}
});
export const saveNote = tool({
description: "Save important research notes to long-term memory",
parameters: z.object({
topic: z.string(),
content: z.string(),
sources: z.array(z.string())
}),
execute: async ({ topic, content, sources }) => {
await saveResearch(topic, content, sources);
return { saved: true };
}
});
export const recallNotes = tool({
description: "Search for previously saved research notes",
parameters: z.object({
query: z.string()
}),
execute: async ({ query }) => {
const memories = await recallResearch(query);
return { memories };
}
});
Step 4: Agent With Planning + Metacognition (Lessons 6, 8)
// src/agent.ts
import { generateText, generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { searchWeb, saveNote, recallNotes } from "./tools";
const ResearchPlanSchema = z.object({
topic: z.string(),
subQuestions: z.array(z.string()),
approach: z.string(),
estimatedSteps: z.number()
});
const QualityCheckSchema = z.object({
completeness: z.number().min(1).max(10),
accuracy: z.number().min(1).max(10),
missingAreas: z.array(z.string()),
shouldContinue: z.boolean()
});
export async function researchAgent(topic: string): Promise<string> {
// Phase 1: Planning
console.log("π Phase 1: Planning...");
const { object: plan } = await generateObject({
model: openai("gpt-4o"),
schema: ResearchPlanSchema,
prompt: `Create a research plan for: "${topic}".
Break into 3-5 sub-questions to answer.`
});
// Phase 2: Research
console.log("π Phase 2: Researching...");
const findings: string[] = [];
for (const question of plan.subQuestions) {
const result = await generateText({
model: openai("gpt-4o"),
tools: { searchWeb, recallNotes },
maxSteps: 5,
system: "You are a researcher. Find accurate and detailed information.",
prompt: question
});
findings.push(`### ${question}\n${result.text}`);
}
// Phase 3: Synthesis
console.log("βοΈ Phase 3: Synthesizing...");
const report = await generateText({
model: openai("gpt-4o"),
prompt: `Synthesize these findings into a complete report:
Topic: ${topic}
Findings:
${findings.join("\n\n")}`
});
// Phase 4: Metacognition β Self-check
console.log("π§ Phase 4: Quality check...");
const { object: quality } = await generateObject({
model: openai("gpt-4o"),
schema: QualityCheckSchema,
prompt: `Evaluate report quality:\n${report.text}`
});
if (quality.shouldContinue && quality.completeness < 7) {
console.log("π Improving report...");
const improved = await generateText({
model: openai("gpt-4o"),
tools: { searchWeb },
maxSteps: 3,
prompt: `Improve the report. Missing areas: ${quality.missingAreas.join(", ")}
Original: ${report.text}`
});
return improved.text;
}
await saveNote.execute({
topic,
content: report.text,
sources: ["web-search"]
});
return report.text;
}
Step 5: Observability (Lesson 9)
// src/tracing.ts
import { Langfuse } from "langfuse";
export const langfuse = new Langfuse({
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
secretKey: process.env.LANGFUSE_SECRET_KEY!
});
export function createTrace(name: string, userId: string) {
return langfuse.trace({ name, userId });
}
Step 6: Entry Point
// src/index.ts
import { researchAgent } from "./agent";
async function main() {
const topic = process.argv[2] || "AI Agent trends 2025";
console.log(`\nπ Researching: "${topic}"\n`);
const report = await researchAgent(topic);
console.log("\n" + "=".repeat(60));
console.log("π RESEARCH REPORT");
console.log("=".repeat(60));
console.log(report);
}
main().catch(console.error);
Run It
npx tsx src/index.ts "Comparing Vercel AI SDK vs LangChain for production agents"
Concepts Applied
| Lesson | Concept | Used In Project |
|---|---|---|
| 00 | Agent basics | Agent loop, tools, environment |
| 01 | Frameworks | Vercel AI SDK (TypeScript) |
| 03 | Tool Use | searchWeb, saveNote tools |
| 04 | Agentic RAG | recallNotes + semantic search |
| 05 | Trustworthy | Error handling, rate limiting |
| 06 | Planning | ResearchPlanSchema, sub-questions |
| 08 | Metacognition | QualityCheckSchema, self-improvement |
| 09 | Production | Langfuse tracing |
| 11 | Context Eng. | Scratchpad pattern (findings) |
| 12 | Memory | Supabase pgvector memory system |
Next Steps
From here, you can extend:
- Multi-agent: Add writer agent, editor agent (Lesson 07)
- MCP server: Create an MCP server for the agent (Lesson 10)
- Web UI: Create a chat interface with Next.js + Vercel AI SDK
- Deploy: Deploy to Vercel or Cloudflare Workers
Congratulations! Youβve completed the AI Agents series! π