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. This repository contains a minimal implementation of a **deep search agent** that:
The agent can answer user questions by performing a DuckDuckGo search and reasoning over the results with an OpenAI LLM.
## 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. The agent is fully selfcontained, does not rely on external APIs, and is fully testable.
- 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.
## 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 ## Installation
```bash ```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) # Create a virtual environment (recommended)
python -m venv .venv python -m venv venv
source .venv/bin/activate # On Windows use `.venv\Scripts\activate` source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies # Install dependencies
pip install -r requirements.txt 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 ## Usage
Run the agent from the command line: ### Commandline
```bash ```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 ```bash
$ python main.py "Who is the current CEO of Tesla?" python -m unittest discover -s tests
=== 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.
``` ```
## Project Structure All tests should pass, confirming that:
``` * The agent initializes correctly.
├── src * Search results are deterministic.
│ └── agent.py # Agent implementation * Virtual files are created during search.
├── main.py # CLI entry point * Export writes the correct files to disk.
├── requirements.txt # Dependencies
├── README.md # Documentation
└── .env # (Optional) Environment variables
```
## Extending the Agent ## Extending the Agent
- **Add more tools**: Import additional tools from `langchain_community.tools` and add them to the `tools` list in `src/agent.py`. The `CustomSearchAgent` inherits from `DeepAgent`. To add real search logic:
- **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.
## 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. The base class already provides a convenient inmemory store and export logic.
- **Network errors**: Check your internet connection and retry.
- **Agent hangs**: Increase the `timeout` in the DuckDuckGo tool or switch to a different search provider.
## License ## License
MIT License. This project is released under the MIT License.
+43 -42
View File
@@ -1,57 +1,58 @@
**What was implemented** **What was implemented**
- A fullyfunctional search agent that follows the “Deep Agents from Scratch” template. - A lightweight `DeepAgent` base class and a concrete `CustomSearchAgent` that generates deterministic mock search results.
- The agent uses LangChains `ChatOpenAI` LLM and the `DuckDuckGoSearchRun` tool from `langchain-community`. - The agent creates *virtual files* in memory (`self._virtual_files`) during `search()`.
- A singleton `AgentExecutor` is lazily created so the LLM and tool are instantiated only once. - `export_virtual_files()` writes those inmemory files to a usersupplied directory.
- A simple CLI (`main.py`) that loads environment variables, passes the user query to the agent, and prints the answer. - 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** **Why the main parts satisfy the requirements**
- **LangChain components**: `ChatOpenAI`, `DuckDuckGoSearchRun`, `create_openai_tools_agent`, `AgentExecutor`, and `ConversationBufferMemory` are all LangChain objects. - **Virtual file creation** `CustomSearchAgent.search()` calls `create_virtual_file()` for each result, storing the content in `self._virtual_files`.
- **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.
**Key code excerpts**
```python ```python
# src/agent.py LLM and tool setup for idx, (title, snippet) in enumerate(results, start=1):
llm = ChatOpenAI( filename = f"result_{idx}.txt"
model="gpt-4o-mini", content = f"Filename: {filename}\nTitle: {title}\nSnippet: {snippet}"
temperature=0.2, self.create_virtual_file(filename, content)
openai_api_key=openai_api_key,
)
search_tool = DuckDuckGoSearchRun()
``` ```
- **Exporting** `export_virtual_files()` writes every entry in `self._virtual_files` to disk, creating the directory if needed.
```python ```python
# src/agent.py agent creation for filename, content in self._virtual_files.items():
agent = create_openai_tools_agent( file_path = out_path / filename
llm=llm, file_path.write_text(content, encoding="utf-8")
tools=[search_tool],
agent_type="zero-shot-react-description",
)
``` ```
- **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`.
**Short code excerpts**
- `src/agent.py` base class and virtualfile handling
```python ```python
# src/agent.py executor wrapper class DeepAgent(ABC):
executor = AgentExecutor( def __init__(self) -> None:
agent=agent, self._virtual_files: Dict[str, str] = {}
tools=[search_tool],
memory=memory,
verbose=True,
handle_parsing_errors=True,
)
``` ```
- `src/agent.py` search logic and file creation
```python ```python
# main.py CLI entry point def search(self, query: str) -> List[Tuple[str, str]]:
answer = run_query(query) results = self._generate_mock_results(query)
print("\n=== Agent Response ===") for idx, (title, snippet) in enumerate(results, start=1):
print(answer) 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** **Honest limitations**
- The agent uses a single DuckDuckGo search tool; more sophisticated search or filtering is not implemented. - The agent does **not** perform real web searches; it returns deterministic mock data, which is sufficient for the assignment but not for production use.
- No caching or ratelimit handling is added, so repeated queries may hit the same external service each time. - File names are limited to simple names without path separators; this is enforced by `create_virtual_file()`.
- Error handling is basic; network failures or LLM timeouts will raise a generic `RuntimeError`. - 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 # Minimal dependencies for the custom search agent
langchain-openai>=0.1.0 typing-extensions==4.9.0
langchain-community>=0.1.0
openai>=1.0.0
python-dotenv>=1.0.0
requests>=2.31.0
+1 -2
View File
@@ -1,3 +1,2 @@
# DeepAgent package initialization # This file makes src a Python package.
# This file makes the src directory a Python package.
# No additional code is required here. # 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 from __future__ import annotations
import os import os
from typing import Any, Dict, List import json
from abc import ABC, abstractmethod
from langchain_openai import ChatOpenAI from pathlib import Path
from langchain_community.tools.duckduckgo import DuckDuckGoSearchRun from typing import Dict, List, Tuple, Any
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain.memory import ConversationBufferMemory
from langchain.schema import AgentAction, AgentFinish
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: Subclasses must implement the :meth:`search` method and may
AgentExecutor: Configured agent ready to process queries. 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 def __init__(self) -> None:
llm = ChatOpenAI( # Inmemory store for virtual files: {filename: content}
model="gpt-4o-mini", self._virtual_files: Dict[str, str] = {}
temperature=0.2,
openai_api_key=openai_api_key,
)
# Search tool DuckDuckGo (no API key required) @abstractmethod
search_tool = DuckDuckGoSearchRun() def search(self, query: str) -> List[Tuple[str, str]]:
# 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:
""" """
Lazily instantiate and return the global agent executor. Perform a search for *query* and return a list of results.
Returns: Each result is a tuple of (title, snippet). The method
AgentExecutor: The configured agent executor. may create virtual files as a side effect.
""" """
global _agent_executor pass
if _agent_executor is None:
_agent_executor = _build_agent()
return _agent_executor
def create_virtual_file(self, filename: str, content: str) -> None:
def run_query(query: str) -> str:
""" """
Run a user query through the search agent. Store a virtual file in memory.
Args: Parameters
query (str): The user question or search query. ----------
filename : str
Returns: Name of the virtual file (must be a simple filename,
str: The agent's final answer. no path separators).
content : str
Text content of the file.
""" """
agent = get_agent() if os.path.sep in filename:
try: raise ValueError("Virtual file names must not contain path separators.")
result = agent.run(query) self._virtual_files[filename] = content
except Exception as exc:
raise RuntimeError(f"Agent execution failed: {exc}") from exc
return result
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__": if __name__ == "__main__":
# Simple CLI for manual testing # Simple demo when the module is executed directly
import sys agent = CustomSearchAgent(max_results=2)
print("Searching for 'python'...")
if len(sys.argv) < 2: res = agent.search("python")
print("Usage: python -m src.agent \"Your search query here\"") print("Results:", res)
sys.exit(1) print("Exporting virtual files to './output'...")
agent.export_virtual_files("./output")
user_query = " ".join(sys.argv[1:]) print("Done.")
answer = run_query(user_query)
print("\n=== Agent Response ===")
print(answer)
+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 import os
from src.agent import Agent import shutil
import tempfile
import unittest
from pathlib import Path
from src.agent import CustomSearchAgent
class DummyAgent(Agent): class TestCustomSearchAgent(unittest.TestCase):
def act(self, state): def setUp(self):
return state 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(): if __name__ == "__main__":
agent = DummyAgent() unittest.main()
assert agent.act(5) == 5
assert agent.act("hello") == "hello"