Capstone: Building a Full AI Agent Application
This final topic brings together everything from the course — LLMs, tools, memory, ReAct, planning, multi-agent systems, RAG, and deployment — into a single, complete, production-quality AI Agent application.
The project being built is a Personal Productivity AI Agent called StudyMate — an intelligent assistant that can answer questions from a knowledge base, help with research, solve maths, manage tasks, and remember user preferences.
StudyMate — Feature Overview
| Feature | How It Works | Concepts Used |
|---|---|---|
| Answer questions from notes | RAG over uploaded documents | RAG, Embeddings, Vector DB |
| Web research | Searches the internet for current info | Tool use, ReAct |
| Maths solver | Calculates expressions step by step | Tool use, Chain of Thought |
| Task management | Adds and lists to-do items | Tool use, State management |
| Memory across sessions | Remembers user preferences | External memory, Long-term memory |
| REST API | Accessible from any frontend | FastAPI, Deployment |
Project Structure
studymate/ │ ├── .env ← API keys ├── requirements.txt ← All dependencies │ ├── core/ │ ├── agent.py ← Main agent loop │ ├── memory.py ← User memory management │ └── rag.py ← Document indexing and retrieval │ ├── tools/ │ ├── __init__.py ← Tool registry │ ├── search_tool.py ← Web search │ ├── calculator.py ← Maths solver │ ├── task_manager.py ← To-do list manager │ └── knowledge_base.py ← RAG search tool │ ├── api/ │ └── server.py ← FastAPI web server │ ├── data/ │ └── knowledge/ ← Upload documents here │ ├── tests/ │ ├── test_tools.py │ └── eval_suite.py │ └── run.py ← CLI entry point
Core: The Memory Manager
# core/memory.py
import json
import os
MEMORY_PATH = "data/user_memories.json"
def load_memories(user_id: str) -> list:
if not os.path.exists(MEMORY_PATH):
return []
with open(MEMORY_PATH, "r") as f:
all_memories = json.load(f)
return all_memories.get(user_id, [])
def save_memory(user_id: str, fact: str):
all_memories = {}
if os.path.exists(MEMORY_PATH):
with open(MEMORY_PATH, "r") as f:
all_memories = json.load(f)
if user_id not in all_memories:
all_memories[user_id] = []
# Avoid duplicate memories
if fact not in all_memories[user_id]:
all_memories[user_id].append(fact)
os.makedirs("data", exist_ok=True)
with open(MEMORY_PATH, "w") as f:
json.dump(all_memories, f, indent=2)
def build_memory_context(user_id: str) -> str:
memories = load_memories(user_id)
if not memories:
return ""
lines = ["What you know about this user:"]
for m in memories:
lines.append(f"- {m}")
return "\n".join(lines)
Tools: Task Manager
# tools/task_manager.py
import json
import os
from langchain_core.tools import tool
TASKS_PATH = "data/tasks.json"
def _load_tasks(user_id: str) -> list:
if not os.path.exists(TASKS_PATH):
return []
with open(TASKS_PATH, "r") as f:
all_tasks = json.load(f)
return all_tasks.get(user_id, [])
def _save_tasks(user_id: str, tasks: list):
all_tasks = {}
if os.path.exists(TASKS_PATH):
with open(TASKS_PATH, "r") as f:
all_tasks = json.load(f)
all_tasks[user_id] = tasks
os.makedirs("data", exist_ok=True)
with open(TASKS_PATH, "w") as f:
json.dump(all_tasks, f, indent=2)
def add_task(task: str, user_id: str = "default") -> str:
"""Add a new task to the user's to-do list."""
tasks = _load_tasks(user_id)
tasks.append({"id": len(tasks) + 1, "task": task, "done": False})
_save_tasks(user_id, tasks)
return f"Task added: '{task}'. You now have {len(tasks)} task(s)."
def list_tasks(user_id: str = "default") -> str:
"""List all pending tasks."""
tasks = _load_tasks(user_id)
pending = [t for t in tasks if not t["done"]]
if not pending:
return "No pending tasks."
result = "Pending tasks:\n"
for t in pending:
result += f" {t['id']}. {t['task']}\n"
return result
def complete_task(task_id: int, user_id: str = "default") -> str:
"""Mark a task as complete."""
tasks = _load_tasks(user_id)
for t in tasks:
if t["id"] == task_id:
t["done"] = True
_save_tasks(user_id, tasks)
return f"Task {task_id} marked as complete: '{t['task']}'"
return f"Task {task_id} not found."
Core: The Main Agent
# core/agent.py
import os
import json
from dotenv import load_dotenv
import openai
from core.memory import build_memory_context, save_memory
from tools import ALL_TOOLS, TOOL_DEFINITIONS, TOOL_MAP
load_dotenv()
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
BASE_SYSTEM_PROMPT = """You are StudyMate, an intelligent personal productivity assistant.
CAPABILITIES:
- Answer questions from the user's uploaded notes (use search_knowledge_base)
- Search the web for current information (use web_search)
- Solve mathematical calculations (use calculate)
- Manage tasks: add, list, and complete tasks
BEHAVIOUR:
- Use tools whenever factual or current information is needed
- Think step by step before giving complex answers
- Keep answers clear and concise
- If unsure, say so — never fabricate information
- If the user shares personal preferences, remember them
MEMORY: {memory_context}"""
def run_studymate(question: str, user_id: str = "default",
conversation_history: list = None) -> dict:
"""
Run the StudyMate agent.
Returns: {"answer": str, "updated_history": list}
"""
memory_context = build_memory_context(user_id)
system_prompt = BASE_SYSTEM_PROMPT.format(
memory_context=memory_context if memory_context else "No memories yet."
)
messages = [{"role": "system", "content": system_prompt}]
# Add conversation history if provided
if conversation_history:
messages.extend(conversation_history)
messages.append({"role": "user", "content": question})
# Check if agent should save a memory
memory_check = _should_save_memory(question)
if memory_check:
save_memory(user_id, memory_check)
# Run agent loop
final_answer = _agent_loop(messages)
# Update history
updated_history = (conversation_history or []) + [
{"role": "user", "content": question},
{"role": "assistant", "content": final_answer}
]
return {
"answer": final_answer,
"updated_history": updated_history
}
def _agent_loop(messages: list) -> str:
for _ in range(6): # Max 6 tool calls
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto",
temperature=0.2,
max_tokens=1000
)
message = response.choices[0].message
messages.append(message)
if message.tool_calls:
for tool_call in message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
if tool_name in TOOL_MAP:
result = TOOL_MAP[tool_name](**tool_args)
else:
result = json.dumps({"error": f"Unknown tool: {tool_name}"})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result if isinstance(result, str) else json.dumps(result)
})
else:
return message.content
return "I was unable to complete the task. Please try again."
def _should_save_memory(question: str) -> str | None:
"""Check if the user's message contains something worth remembering."""
memory_triggers = [
"my name is", "i am from", "i prefer", "i like", "i don't like",
"i work as", "call me", "remember that", "my goal is"
]
question_lower = question.lower()
for trigger in memory_triggers:
if trigger in question_lower:
return question # Save the full statement as a memory
return None
API: The Web Server
# api/server.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from core.agent import run_studymate
app = FastAPI(title="StudyMate AI Agent", version="1.0.0")
# Store conversation histories in memory (use Redis in production)
conversations: dict = {}
class ChatRequest(BaseModel):
user_id: str = "default"
message: str
class ChatResponse(BaseModel):
answer: str
user_id: str
@app.get("/health")
def health():
return {"status": "ok", "agent": "StudyMate"}
@app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
if not request.message.strip():
raise HTTPException(status_code=400, detail="Message cannot be empty")
history = conversations.get(request.user_id, [])
result = run_studymate(
question=request.message,
user_id=request.user_id,
conversation_history=history
)
conversations[request.user_id] = result["updated_history"][-20:] # Keep last 10 turns
return ChatResponse(answer=result["answer"], user_id=request.user_id)
@app.delete("/chat/{user_id}/history")
def clear_history(user_id: str):
conversations.pop(user_id, None)
return {"message": f"History cleared for {user_id}"}
Running StudyMate
# Start the server
uvicorn api.server:app --reload --port 8000
# Test with curl
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "student_001", "message": "My name is Ananya and I am studying for GATE exam."}'
# Response:
# {"answer": "Nice to meet you, Ananya! I'll remember that you're preparing for GATE.
# How can I help you study today?", "user_id": "student_001"}
# Second message
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "student_001", "message": "Add a task: Revise data structures chapter"}'
What Was Built — Full Architecture Summary
| Component | File | Course Concept |
|---|---|---|
| Agent loop | core/agent.py | ReAct, Agent Loop |
| User memory | core/memory.py | External Memory |
| Document retrieval | core/rag.py | RAG, Vector DB |
| Web search tool | tools/search_tool.py | Tools, Function Calling |
| Calculator tool | tools/calculator.py | Tools, Chain of Thought |
| Task manager tool | tools/task_manager.py | Tools, State Management |
| REST API | api/server.py | Deployment, FastAPI |
| Prompt engineering | core/agent.py (SYSTEM_PROMPT) | Prompt Engineering |
What to Build Next
With the foundation from this course, here are natural next projects to extend the skills:
- Add a web UI — Build a React or Streamlit frontend that connects to the API
- Add voice input — Use OpenAI Whisper to convert speech to text
- Add image understanding — Use GPT-4o's vision capabilities
- Add email integration — Allow the agent to read and send emails via Gmail API
- Multi-user support — Use Redis for session storage instead of in-memory dictionaries
- Scheduled tasks — Use APScheduler to run agent tasks at specified times
Course Summary — What Was Learned
| Module | Topics Covered |
|---|---|
| Foundations | What is an AI Agent, Agent Loop, Types of Agents, Agent vs Traditional Programs |
| Core Concepts | LLMs, Prompt Engineering, Tools and Function Calling, Memory |
| Building Blocks | Dev Setup, OpenAI API, First Agent, Adding Tools |
| Architectures | ReAct Pattern, Planning Agents, Multi-Agent Systems |
| Advanced | RAG, LangChain, Orchestration, Evaluation, Deployment, Capstone Project |
Final Thoughts
AI Agents represent one of the most significant shifts in software development in recent history. The combination of powerful LLMs, flexible tool use, persistent memory, and structured reasoning patterns makes it possible to build software that was previously impossible — software that can understand goals, plan, act, and learn.
The concepts in this course are not theoretical — they are the exact patterns being used by teams at major technology companies to build the next generation of intelligent products. Building these skills today is an investment in one of the most exciting areas of computer science.
Keep building, keep experimenting, and keep learning. The best AI Agent is always the next one.
