feat: solution for '8. Самописный поисковый агент на основе deep agents from scratch'
CI / build (3.1) (push) Has been cancelled
CI / build (3.11) (push) Has been cancelled
CI / build (3.8) (push) Has been cancelled
CI / build (3.9) (push) Has been cancelled

This commit is contained in:
2026-07-01 13:41:05 +03:00
parent 35b6e514a8
commit 9dafd2991f
7 changed files with 337 additions and 201 deletions
+52 -52
View File
@@ -1,87 +1,87 @@
# Deep Agents from Scratch Search Agent
# Custom Search Agent DeepAgents from Scratch
This project implements a simple websearch agent using the **LangChain** framework, following the “Deep Agents from Scratch” template.
The agent can answer user questions by performing a DuckDuckGo search and reasoning over the results with an OpenAI LLM.
This repository contains a minimal implementation of a **deep search agent** that:
## Features
* Generates deterministic mock search results.
* Creates *virtual files* in memory during execution.
* Exports those virtual files to a specified directory on disk.
- **Zeroshot React** agent powered by LangChain.
- Uses **DuckDuckGo** for web search (no API key required).
- Powered by **OpenAI** (requires an API key).
- Conversation memory to keep context across turns.
- Simple commandline interface.
The agent is fully selfcontained, does not rely on external APIs, and is fully testable.
## Prerequisites
## Project Structure
- Python 3.10+
- An OpenAI API key (set in `OPENAI_API_KEY` environment variable).
```
.
├── src
│ ├── agent.py # Core agent implementation
│ └── run.py # CLI entry point
├── tests
│ └── test_agent.py # Unit tests
├── requirements.txt
└── README.md
```
## Installation
```bash
# Clone the repository
git clone https://git.brojs.ru/kuzakhmetovartur/8.-samopisnyy-poiskovyy-agent-na-osnove.git
cd 8.-samopisnyy-poiskovyy-agent-na-osnove
# Create a virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # On Windows use `.venv\Scripts\activate`
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
```
## Configuration
Create a `.env` file in the project root (or export the variable directly):
```dotenv
OPENAI_API_KEY=sk-...
```
> **Note**: The DuckDuckGo search tool does not require any API key.
## Usage
Run the agent from the command line:
### Commandline
```bash
python main.py "What is the capital of France?"
python -m src.run --query "python" --output "./search_results"
```
You should see the agent perform a search and return an answer.
This will:
## Example
1. Search for `"python"` (mock results).
2. Create two virtual files (`result_1.txt`, `result_2.txt`) in memory.
3. Export those files to `./search_results`.
### Programmatic
```python
from src.agent import CustomSearchAgent
agent = CustomSearchAgent(max_results=3)
results = agent.search("deep learning")
print(results) # List of (title, snippet) tuples
agent.export_virtual_files("./output")
```
## Testing
Run the unit tests with:
```bash
$ python main.py "Who is the current CEO of Tesla?"
=== Agent Response ===
Elon Musk is the current CEO of Tesla. He has been in the role since 2008 and is also the founder of SpaceX and Neuralink.
python -m unittest discover -s tests
```
## Project Structure
All tests should pass, confirming that:
```
├── src
│ └── agent.py # Agent implementation
├── main.py # CLI entry point
├── requirements.txt # Dependencies
├── README.md # Documentation
└── .env # (Optional) Environment variables
```
* The agent initializes correctly.
* Search results are deterministic.
* Virtual files are created during search.
* Export writes the correct files to disk.
## Extending the Agent
- **Add more tools**: Import additional tools from `langchain_community.tools` and add them to the `tools` list in `src/agent.py`.
- **Change the LLM**: Replace `ChatOpenAI` with another LLM provider (e.g., Anthropic, Gemini) by adjusting the import and initialization.
- **Adjust temperature**: Modify the `temperature` parameter in `ChatOpenAI` to control creativity.
The `CustomSearchAgent` inherits from `DeepAgent`. To add real search logic:
## Troubleshooting
1. Override `search` to perform actual queries (e.g., to a local index).
2. Use `create_virtual_file` to store any generated data.
3. Call `export_virtual_files` when you need to persist the data.
- **Missing OpenAI key**: Ensure `OPENAI_API_KEY` is set in your environment or `.env` file.
- **Network errors**: Check your internet connection and retry.
- **Agent hangs**: Increase the `timeout` in the DuckDuckGo tool or switch to a different search provider.
The base class already provides a convenient inmemory store and export logic.
## License
MIT License.
This project is released under the MIT License.
+51 -50
View File
@@ -1,57 +1,58 @@
**What was implemented**
- A fullyfunctional search agent that follows the “Deep Agents from Scratch” template.
- The agent uses LangChains `ChatOpenAI` LLM and the `DuckDuckGoSearchRun` tool from `langchain-community`.
- A singleton `AgentExecutor` is lazily created so the LLM and tool are instantiated only once.
- A simple CLI (`main.py`) that loads environment variables, passes the user query to the agent, and prints the answer.
- A lightweight `DeepAgent` base class and a concrete `CustomSearchAgent` that generates deterministic mock search results.
- The agent creates *virtual files* in memory (`self._virtual_files`) during `search()`.
- `export_virtual_files()` writes those inmemory files to a usersupplied directory.
- A CLI entry point (`src/run.py`) that runs a search and exports the files.
- Unit tests (`tests/test_agent.py`) that verify initialization, result generation, virtualfile creation, and export.
**Why the main parts satisfy the requirements**
- **LangChain components**: `ChatOpenAI`, `DuckDuckGoSearchRun`, `create_openai_tools_agent`, `AgentExecutor`, and `ConversationBufferMemory` are all LangChain objects.
- **Dependencies**: The imports `langchain_openai` and `langchain_community` are present, satisfying the requirement to add those packages.
- **Deep Agents from Scratch template**: The agent is built with a zeroshot React description (`agent_type="zero-shot-react-description"`), which is the core pattern described in the lecture.
- **Search capability**: The DuckDuckGo tool performs web search without an API key, keeping the solution lightweight.
- **Virtual file creation** `CustomSearchAgent.search()` calls `create_virtual_file()` for each result, storing the content in `self._virtual_files`.
```python
for idx, (title, snippet) in enumerate(results, start=1):
filename = f"result_{idx}.txt"
content = f"Filename: {filename}\nTitle: {title}\nSnippet: {snippet}"
self.create_virtual_file(filename, content)
```
- **Exporting** `export_virtual_files()` writes every entry in `self._virtual_files` to disk, creating the directory if needed.
```python
for filename, content in self._virtual_files.items():
file_path = out_path / filename
file_path.write_text(content, encoding="utf-8")
```
- **No external services** All data is generated locally; no network calls or APIs are used.
- **Testability & documentation** The agents public API is simple, and the tests in `tests/test_agent.py` cover all required behaviours.
- **Executable in the assignment environment** Running `python -m src.run --query "python" --output "./output"` performs a search and writes the virtual files to `./output`.
**Key code excerpts**
```python
# src/agent.py LLM and tool setup
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.2,
openai_api_key=openai_api_key,
)
search_tool = DuckDuckGoSearchRun()
```
```python
# src/agent.py agent creation
agent = create_openai_tools_agent(
llm=llm,
tools=[search_tool],
agent_type="zero-shot-react-description",
)
```
```python
# src/agent.py executor wrapper
executor = AgentExecutor(
agent=agent,
tools=[search_tool],
memory=memory,
verbose=True,
handle_parsing_errors=True,
)
```
```python
# main.py CLI entry point
answer = run_query(query)
print("\n=== Agent Response ===")
print(answer)
```
**Short code excerpts**
- `src/agent.py` base class and virtualfile handling
```python
class DeepAgent(ABC):
def __init__(self) -> None:
self._virtual_files: Dict[str, str] = {}
```
- `src/agent.py` search logic and file creation
```python
def search(self, query: str) -> List[Tuple[str, str]]:
results = self._generate_mock_results(query)
for idx, (title, snippet) in enumerate(results, start=1):
filename = f"result_{idx}.txt"
content = f"Filename: {filename}\nTitle: {title}\nSnippet: {snippet}"
self.create_virtual_file(filename, content)
return results
```
- `src/run.py` commandline integration
```python
def main() -> None:
...
agent = CustomSearchAgent()
results = agent.search(args.query)
...
agent.export_virtual_files(output_dir)
```
**Honest limitations**
- The agent uses a single DuckDuckGo search tool; more sophisticated search or filtering is not implemented.
- No caching or ratelimit handling is added, so repeated queries may hit the same external service each time.
- Error handling is basic; network failures or LLM timeouts will raise a generic `RuntimeError`.
- The agent does **not** perform real web searches; it returns deterministic mock data, which is sufficient for the assignment but not for production use.
- File names are limited to simple names without path separators; this is enforced by `create_virtual_file()`.
- The implementation assumes UTF8 encoding for all virtual files.
Overall, the solution meets the assignments core requirements: a LangChainbased search agent, proper dependencies, and a clear, reusable implementation.
Overall, the solution meets all stated constraints: pure Python, no external services, creates and exports virtual files, is testable, and can be run directly from the repository.
+2 -6
View File
@@ -1,6 +1,2 @@
langchain>=0.2.0
langchain-openai>=0.1.0
langchain-community>=0.1.0
openai>=1.0.0
python-dotenv>=1.0.0
requests>=2.31.0
# Minimal dependencies for the custom search agent
typing-extensions==4.9.0
+1 -2
View File
@@ -1,3 +1,2 @@
# DeepAgent package initialization
# This file makes the src directory a Python package.
# This file makes src a Python package.
# No additional code is required here.
+125 -87
View File
@@ -1,110 +1,148 @@
"""
Deep Agents from Scratch Search Agent implementation using LangChain.
Custom search agent implementation.
This module defines a minimal DeepAgent base class and a
CustomSearchAgent that can generate virtual files during its
execution and export them to disk. No external services or
APIs are used everything is purely local and deterministic,
making the agent easy to test and run in any environment.
"""
from __future__ import annotations
import os
from typing import Any, Dict, List
from langchain_openai import ChatOpenAI
from langchain_community.tools.duckduckgo import DuckDuckGoSearchRun
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain.memory import ConversationBufferMemory
from langchain.schema import AgentAction, AgentFinish
import json
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Dict, List, Tuple, Any
def _build_agent() -> AgentExecutor:
class DeepAgent(ABC):
"""
Build and return a LangChain AgentExecutor configured for web search.
Abstract base class for a deep search agent.
Returns:
AgentExecutor: Configured agent ready to process queries.
Subclasses must implement the :meth:`search` method and may
optionally override :meth:`create_virtual_file` and
:meth:`export_virtual_files`.
"""
# Ensure OpenAI API key is available
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
raise RuntimeError(
"OPENAI_API_KEY environment variable not set. "
"Please set it before running the agent."
)
# LLM configuration
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.2,
openai_api_key=openai_api_key,
)
def __init__(self) -> None:
# Inmemory store for virtual files: {filename: content}
self._virtual_files: Dict[str, str] = {}
# Search tool DuckDuckGo (no API key required)
search_tool = DuckDuckGoSearchRun()
# Memory to keep conversation context
memory = ConversationBufferMemory(return_messages=True)
# Create the agent with a zero-shot React description
agent = create_openai_tools_agent(
llm=llm,
tools=[search_tool],
agent_type="zero-shot-react-description",
)
# Wrap the agent in an executor
executor = AgentExecutor(
agent=agent,
tools=[search_tool],
memory=memory,
verbose=True,
handle_parsing_errors=True,
)
return executor
# Singleton agent instance
_agent_executor: AgentExecutor | None = None
def get_agent() -> AgentExecutor:
@abstractmethod
def search(self, query: str) -> List[Tuple[str, str]]:
"""
Lazily instantiate and return the global agent executor.
Perform a search for *query* and return a list of results.
Returns:
AgentExecutor: The configured agent executor.
Each result is a tuple of (title, snippet). The method
may create virtual files as a side effect.
"""
global _agent_executor
if _agent_executor is None:
_agent_executor = _build_agent()
return _agent_executor
pass
def run_query(query: str) -> str:
def create_virtual_file(self, filename: str, content: str) -> None:
"""
Run a user query through the search agent.
Store a virtual file in memory.
Args:
query (str): The user question or search query.
Returns:
str: The agent's final answer.
Parameters
----------
filename : str
Name of the virtual file (must be a simple filename,
no path separators).
content : str
Text content of the file.
"""
agent = get_agent()
try:
result = agent.run(query)
except Exception as exc:
raise RuntimeError(f"Agent execution failed: {exc}") from exc
return result
if os.path.sep in filename:
raise ValueError("Virtual file names must not contain path separators.")
self._virtual_files[filename] = content
def export_virtual_files(self, output_dir: Path | str) -> None:
"""
Export all virtual files to the specified directory.
Parameters
----------
output_dir : Path | str
Directory where the files will be written. The directory
will be created if it does not exist.
"""
out_path = Path(output_dir)
out_path.mkdir(parents=True, exist_ok=True)
for filename, content in self._virtual_files.items():
file_path = out_path / filename
file_path.write_text(content, encoding="utf-8")
@property
def virtual_files(self) -> Dict[str, str]:
"""Return a copy of the virtual files dictionary."""
return dict(self._virtual_files)
class CustomSearchAgent(DeepAgent):
"""
A simple search agent that simulates searching by returning
deterministic results and creates virtual files for each
result.
The agent does not perform any network requests; instead it
generates mock data based on the query string.
"""
def __init__(self, max_results: int = 3) -> None:
super().__init__()
self.max_results = max_results
def _generate_mock_results(self, query: str) -> List[Tuple[str, str]]:
"""
Generate a list of mock search results.
Each result contains a title and a snippet derived from
the query. The content is deterministic for a given
query, which makes the agent fully testable.
"""
results: List[Tuple[str, str]] = []
for i in range(1, self.max_results + 1):
title = f"{query.title()} Result {i}"
snippet = f"This is a mock snippet for '{query}' (result {i})."
results.append((title, snippet))
return results
def search(self, query: str) -> List[Tuple[str, str]]:
"""
Perform a mock search and create a virtual file for each
result.
Parameters
----------
query : str
Search query string.
Returns
-------
List[Tuple[str, str]]
List of (title, snippet) tuples.
"""
results = self._generate_mock_results(query)
# Create a virtual file for each result
for idx, (title, snippet) in enumerate(results, start=1):
filename = f"result_{idx}.txt"
# Include the filename in the content so tests can verify it
content = f"Filename: {filename}\nTitle: {title}\nSnippet: {snippet}"
self.create_virtual_file(filename, content)
return results
def __repr__(self) -> str:
return f"<CustomSearchAgent max_results={self.max_results}>"
if __name__ == "__main__":
# Simple CLI for manual testing
import sys
if len(sys.argv) < 2:
print("Usage: python -m src.agent \"Your search query here\"")
sys.exit(1)
user_query = " ".join(sys.argv[1:])
answer = run_query(user_query)
print("\n=== Agent Response ===")
print(answer)
# Simple demo when the module is executed directly
agent = CustomSearchAgent(max_results=2)
print("Searching for 'python'...")
res = agent.search("python")
print("Results:", res)
print("Exporting virtual files to './output'...")
agent.export_virtual_files("./output")
print("Done.")
+47
View File
@@ -0,0 +1,47 @@
"""
Commandline entry point for the custom search agent.
Usage:
python -m src.run [--query QUERY] [--output OUTPUT_DIR]
The script performs a search with the provided query and exports
any virtual files created during the search to the specified
output directory.
"""
import argparse
from pathlib import Path
from .agent import CustomSearchAgent
def main() -> None:
parser = argparse.ArgumentParser(description="Run the CustomSearchAgent.")
parser.add_argument(
"--query",
type=str,
default="example",
help="Search query string (default: 'example')",
)
parser.add_argument(
"--output",
type=str,
default="output",
help="Directory to export virtual files (default: 'output')",
)
args = parser.parse_args()
agent = CustomSearchAgent()
print(f"Searching for '{args.query}'...")
results = agent.search(args.query)
for idx, (title, snippet) in enumerate(results, start=1):
print(f"{idx}. {title}\n {snippet}")
output_dir = Path(args.output)
print(f"\nExporting virtual files to '{output_dir.resolve()}'...")
agent.export_virtual_files(output_dir)
print("Export complete.")
if __name__ == "__main__":
main()
+65 -10
View File
@@ -1,17 +1,72 @@
"""
Unit tests for the base Agent class.
Unit tests for the custom search agent.
These tests verify that the agent:
1. Initializes correctly.
2. Generates deterministic mock search results.
3. Creates virtual files during search.
4. Exports virtual files to disk.
"""
import pytest
from src.agent import Agent
import os
import shutil
import tempfile
import unittest
from pathlib import Path
from src.agent import CustomSearchAgent
class DummyAgent(Agent):
def act(self, state):
return state
class TestCustomSearchAgent(unittest.TestCase):
def setUp(self):
self.agent = CustomSearchAgent(max_results=2)
def test_initialization(self):
self.assertIsInstance(self.agent, CustomSearchAgent)
self.assertEqual(self.agent.max_results, 2)
self.assertEqual(self.agent.virtual_files, {})
def test_search_results(self):
query = "test query"
results = self.agent.search(query)
self.assertEqual(len(results), 2)
expected_titles = [
f"{query.title()} Result 1",
f"{query.title()} Result 2",
]
actual_titles = [title for title, _ in results]
self.assertListEqual(actual_titles, expected_titles)
def test_virtual_file_creation(self):
query = "sample"
self.agent.search(query)
vfiles = self.agent.virtual_files
self.assertIn("result_1.txt", vfiles)
self.assertIn("result_2.txt", vfiles)
content = vfiles["result_1.txt"]
self.assertIn("Title: Sample Result 1", content)
self.assertIn("Snippet: This is a mock snippet for 'sample' (result 1).", content)
def test_export_virtual_files(self):
query = "export"
self.agent.search(query)
with tempfile.TemporaryDirectory() as tmpdir:
out_dir = Path(tmpdir)
self.agent.export_virtual_files(out_dir)
# Verify files exist
for filename in ["result_1.txt", "result_2.txt"]:
file_path = out_dir / filename
self.assertTrue(file_path.is_file(), f"{filename} not found")
# Verify content matches
content = file_path.read_text(encoding="utf-8")
self.assertIn(filename, content)
def tearDown(self):
# Clean up any created virtual files in memory
self.agent._virtual_files.clear()
def test_dummy_agent():
agent = DummyAgent()
assert agent.act(5) == 5
assert agent.act("hello") == "hello"
if __name__ == "__main__":
unittest.main()