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

FeatureHow It WorksConcepts Used
Answer questions from notesRAG over uploaded documentsRAG, Embeddings, Vector DB
Web researchSearches the internet for current infoTool use, ReAct
Maths solverCalculates expressions step by stepTool use, Chain of Thought
Task managementAdds and lists to-do itemsTool use, State management
Memory across sessionsRemembers user preferencesExternal memory, Long-term memory
REST APIAccessible from any frontendFastAPI, 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

ComponentFileCourse Concept
Agent loopcore/agent.pyReAct, Agent Loop
User memorycore/memory.pyExternal Memory
Document retrievalcore/rag.pyRAG, Vector DB
Web search tooltools/search_tool.pyTools, Function Calling
Calculator tooltools/calculator.pyTools, Chain of Thought
Task manager tooltools/task_manager.pyTools, State Management
REST APIapi/server.pyDeployment, FastAPI
Prompt engineeringcore/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

ModuleTopics Covered
FoundationsWhat is an AI Agent, Agent Loop, Types of Agents, Agent vs Traditional Programs
Core ConceptsLLMs, Prompt Engineering, Tools and Function Calling, Memory
Building BlocksDev Setup, OpenAI API, First Agent, Adding Tools
ArchitecturesReAct Pattern, Planning Agents, Multi-Agent Systems
AdvancedRAG, 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.

Leave a Comment

Your email address will not be published. Required fields are marked *