16. Agent Project: Research Assistant

Chapter 16 of 16 · 20 min

This final chapter combines every concept into a working research assistant that can accept a research query, search the web for information, process results, and produce a structured written report.

Project structure

# research_assistant/
# ├── agent.py          # Main agent class
# ├── tools.py          # Tool definitions
# ├── memory.py         # Memory management
# ├── planner.py        # Task planning and decomposition
# ├── evaluator.py      # Test suite
# └── main.py           # Entry point

The agent implementation

# agent.py
import ollama
from tools import WebSearchTool, CalculatorTool
from memory import HierarchicalMemory
from planner import plan_task

class ResearchAssistant:
    def __init__(self, model: str = "llama3.2"):
        self.model = model
        self.tools = [WebSearchTool(), CalculatorTool()]
        self.tool_map = {t.name: t for t in self.tools}
        self.memory = HierarchicalMemory(window_size=15)
        self.max_turns = 20
    
    def research(self, query: str) -> str:
        """Run a complete research task from query to report"""
        steps = plan_task(query, self)
        
        messages = [
            {"role": "system", "content": (
                "You are a research assistant. Use web search to find facts, "
                "use calculator for any needed computations, and provide a "
                "well-structured summary in your final response."
            )},
            {"role": "user", "content": f"Research task: {query}\n\nPlanned steps: {'; '.join(steps)}"}
        ]
        
        for turn in range(self.max_turns):
            tool_schemas = [t.to_openai_schema() for t in self.tools]
            response = ollama.chat(model=self.model, messages=messages, tools=tool_schemas)
            
            if not response.message.tool_calls:
                # Check if this is a final answer
                messages.append({"role": "assistant", "content": response.message.content})
                self.memory.add(query, response.message.content)
                return response.message.content
            
            for call in response.message.tool_calls:
                tool_name = call.function.name
                if tool_name not in self.tool_map:
                    continue
                
                result = self.tool_map[tool_name].invoke(**call.function.arguments)
                messages.append({"role": "assistant", "content": "", "tool_calls": [call]})
                messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
        
        return "Research incomplete due to max turns limit"

Running the project

# Install dependencies
pip install ollama duckduckgo-search requests jsonschema

# Pull a tool-capable model
ollama pull llama3.2

# Run a research query
python main.py "Compare AI regulation in the EU and US"

Example output

For the query "Compare AI regulation in the EU and US," the agent:

  1. Plans three steps: research EU AI Act, research US Executive Order, draft comparison
  2. Calls web_search for "EU AI Act key provisions 2024"
  3. Calls web_search for "US AI Executive Order 2023 key provisions"
  4. Calls calculator for any quantitative comparisons (e.g., fine amounts)
  5. Generates a structured report with sections on scope, enforcement, and impact

Extending the project

  • Add a file writer tool to save reports to disk
  • Add a citation tool that returns structured references
  • Integrate structured memory to track multi-session research threads
  • Add an evaluation harness that runs multiple research queries and scores the outputs
EXERCISE

Extend the research assistant with a file writing tool. Add the tool, test it by writing a research report to disk, and verify the file contents match the agent's final output. Then run the evaluation suite from Chapter 15 against the extended agent. ```