feat: solution for '8. Самописный поисковый агент на основе deep agents from scratch'
This commit is contained in:
@@ -1,76 +1,114 @@
|
|||||||
# Deep Agent from Scratch
|
# Deep Agents from Scratch – LangChain Search Agent
|
||||||
|
|
||||||
This repository demonstrates a **Deep Agent** implementation using the **LangChain** library.
|
This project demonstrates a **Deep Agent** built from scratch using **LangChain**.
|
||||||
The agent follows the “Deep Agents from Scratch” template and can answer arbitrary questions by leveraging an LLM (OpenAI GPT‑3.5‑Turbo by default). It also showcases how to integrate a simple tool (`Echo`) and use a Planner/Executor pattern for a more realistic agent workflow.
|
The agent can answer user questions by searching the web with DuckDuckGo and
|
||||||
|
providing concise, up‑to‑date responses.
|
||||||
|
|
||||||
|
> **Author**: Artur Kuzakhmetov
|
||||||
|
> **Course**: Deep Agents from Scratch (Lecture: Perplexity, 09.04.2026)
|
||||||
|
> **Deadline**: 31.08.2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Implements the **Planner** and **Executor** pattern from the Deep Agents from Scratch template.
|
- **Custom Search Tool** – queries DuckDuckGo’s instant answer API.
|
||||||
- Uses LangChain’s `OpenAI`, `Tool`, `PromptTemplate`, and `ConversationBufferMemory`.
|
- **Conversation Memory** – keeps context across turns.
|
||||||
- Configurable LLM model, temperature, and token limits.
|
- **REACT Agent** – follows the “Reason → Act → Think” pattern.
|
||||||
- Simple command‑line interface for quick testing.
|
- **CLI** – simple command‑line interface for interactive use.
|
||||||
- Environment‑variable based configuration for API keys and model selection.
|
- **Unit Tests** – basic tests for the search tool.
|
||||||
- Demonstrates tool integration (Echo tool) and the full agent template.
|
|
||||||
|
|
||||||
## Prerequisites
|
---
|
||||||
|
|
||||||
- Node.js 18+ (or any LTS version)
|
|
||||||
- An OpenAI API key
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```bash
|
1. **Clone the repository**
|
||||||
# Clone the repository
|
|
||||||
git clone https://git.brojs.ru/kuzakhmetovartur/8.-samopisnyy-poiskovyy-agent-na-osnove-
|
|
||||||
cd 8.-samopisnyy-poiskovyy-agent-na-osnove-
|
|
||||||
|
|
||||||
# Install dependencies
|
```bash
|
||||||
npm install
|
git clone https://git.brojs.ru/kuzakhmetovartur/8.-samopisnyy-poiskovyy-agent-na-osnove-.git
|
||||||
```
|
cd 8.-samopisnyy-poiskovyy-agent-na-osnove-
|
||||||
|
```
|
||||||
|
|
||||||
Create a `.env` file in the project root:
|
2. **Create a virtual environment**
|
||||||
|
|
||||||
```dotenv
|
```bash
|
||||||
OPENAI_API_KEY=your_openai_api_key_here
|
python -m venv .venv
|
||||||
OPENAI_MODEL=gpt-3.5-turbo # optional, defaults to gpt-3.5-turbo
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Tip:** Keep your `.env` file out of version control. Add it to `.gitignore` if you plan to push the repo.
|
3. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Set up OpenAI API key**
|
||||||
|
|
||||||
|
Create a `.env` file in the project root:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `sk-...` with your actual key.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Run the agent with a question:
|
Run the agent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start -- "What is the tallest mountain in the world?"
|
python -m src.index
|
||||||
```
|
```
|
||||||
|
|
||||||
Or simply:
|
You will see:
|
||||||
|
|
||||||
|
```
|
||||||
|
Deep Agents from Scratch - LangChain Search Agent
|
||||||
|
Type 'exit' or 'quit' to stop.
|
||||||
|
|
||||||
|
Enter your question:
|
||||||
|
```
|
||||||
|
|
||||||
|
Type a question, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
What is the capital of France?
|
||||||
|
```
|
||||||
|
|
||||||
|
The agent will search the web and return an answer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node src/index.js "Your question here"
|
python -m unittest discover -s tests
|
||||||
```
|
```
|
||||||
|
|
||||||
The agent will output the answer to the console.
|
---
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
├── package.json # Project metadata and dependencies
|
├── src
|
||||||
├── src/
|
│ └── index.py # Main agent implementation
|
||||||
│ ├── deepAgent.js # Core DeepAgent implementation (Planner/Executor)
|
├── tests
|
||||||
│ └── index.js # CLI entry point
|
│ └── test_search_tool.py # Unit tests for the search tool
|
||||||
|
├── requirements.txt # Project dependencies
|
||||||
└── README.md # Documentation
|
└── README.md # Documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
## Extending the Agent
|
---
|
||||||
|
|
||||||
- **Add more sophisticated prompts**: Edit the `Planner` prompt in `deepAgent.js`.
|
## Contributing
|
||||||
- **Integrate additional tools**: Use LangChain’s `Tool` and add them to the `tools` array.
|
|
||||||
- **Switch LLM providers**: Replace `OpenAI` with another LangChain LLM implementation (e.g., `AzureOpenAI`, `Anthropic`).
|
Feel free to fork the repository, create a feature branch, and submit a pull request.
|
||||||
|
Please ensure tests pass before merging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT © 2026
|
MIT License.
|
||||||
|
|
||||||
---
|
|
||||||
+4
-5
@@ -1,5 +1,4 @@
|
|||||||
transformers
|
langchain==0.1.0
|
||||||
torch
|
openai==1.3.0
|
||||||
requests
|
python-dotenv==1.0.0
|
||||||
click
|
requests==2.31.0
|
||||||
pytest
|
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from langchain.chat_models import ChatOpenAI
|
||||||
|
from langchain.agents import initialize_agent, AgentType
|
||||||
|
from langchain.memory import ConversationBufferMemory
|
||||||
|
from langchain.tools import BaseTool
|
||||||
|
|
||||||
|
|
||||||
|
class DuckDuckGoSearchTool(BaseTool):
|
||||||
|
"""
|
||||||
|
A simple web search tool that queries DuckDuckGo's instant answer API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = "duckduckgo_search"
|
||||||
|
description: str = (
|
||||||
|
"Use this tool to search the web for up-to-date information. "
|
||||||
|
"Input should be a search query."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run(self, query: str) -> str:
|
||||||
|
"""
|
||||||
|
Execute the search query and return a concise answer.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
query : str
|
||||||
|
The search query string.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
A short answer extracted from the search results.
|
||||||
|
"""
|
||||||
|
if not query:
|
||||||
|
return "No query provided."
|
||||||
|
|
||||||
|
url = "https://api.duckduckgo.com/"
|
||||||
|
params = {
|
||||||
|
"q": query,
|
||||||
|
"format": "json",
|
||||||
|
"no_html": 1,
|
||||||
|
"skip_disambig": 1,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.get(url, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
except Exception as exc:
|
||||||
|
return f"Error during search: {exc}"
|
||||||
|
|
||||||
|
# Prefer abstract text if available
|
||||||
|
abstract = data.get("AbstractText")
|
||||||
|
if abstract:
|
||||||
|
return abstract
|
||||||
|
|
||||||
|
# Fallback to the first related topic
|
||||||
|
topics = data.get("RelatedTopics", [])
|
||||||
|
if topics:
|
||||||
|
first = topics[0]
|
||||||
|
if isinstance(first, dict):
|
||||||
|
return first.get("Text", "No relevant information found.")
|
||||||
|
return "No relevant information found."
|
||||||
|
|
||||||
|
async def _arun(self, query: str) -> str:
|
||||||
|
"""
|
||||||
|
Asynchronous run implementation that delegates to the synchronous _run method.
|
||||||
|
"""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
return await loop.run_in_executor(None, self._run, query)
|
||||||
|
|
||||||
|
|
||||||
|
def create_agent() -> Any:
|
||||||
|
"""
|
||||||
|
Create and configure the Deep Agent using LangChain.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Any
|
||||||
|
The initialized agent executor.
|
||||||
|
"""
|
||||||
|
# Load environment variables (e.g., OPENAI_API_KEY)
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Initialize the LLM
|
||||||
|
llm = ChatOpenAI(temperature=0)
|
||||||
|
|
||||||
|
# Memory to keep conversation context
|
||||||
|
memory = ConversationBufferMemory(memory_key="chat_history")
|
||||||
|
|
||||||
|
# Instantiate the custom search tool
|
||||||
|
search_tool = DuckDuckGoSearchTool()
|
||||||
|
|
||||||
|
# Initialize the agent with the REACT description template
|
||||||
|
agent = initialize_agent(
|
||||||
|
tools=[search_tool],
|
||||||
|
llm=llm,
|
||||||
|
agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
|
||||||
|
memory=memory,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
return agent
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Simple CLI to interact with the Deep Agent.
|
||||||
|
"""
|
||||||
|
agent = create_agent()
|
||||||
|
print("Deep Agents from Scratch - LangChain Search Agent")
|
||||||
|
print("Type 'exit' or 'quit' to stop.\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
query = input("Enter your question: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nExiting.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if query.lower() in {"exit", "quit"}:
|
||||||
|
print("Goodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
print("Please enter a non-empty query.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = agent.run(query)
|
||||||
|
print("\nAnswer:\n", result)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Error: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import unittest
|
||||||
|
from src.index import DuckDuckGoSearchTool
|
||||||
|
|
||||||
|
|
||||||
|
class TestSearchTool(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tool = DuckDuckGoSearchTool()
|
||||||
|
|
||||||
|
def test_run_returns_string(self):
|
||||||
|
result = self.tool.run("Python programming language")
|
||||||
|
self.assertIsInstance(result, str)
|
||||||
|
self.assertTrue(len(result) > 0)
|
||||||
|
|
||||||
|
def test_run_handles_empty_query(self):
|
||||||
|
result = self.tool.run("")
|
||||||
|
self.assertIsInstance(result, str)
|
||||||
|
self.assertTrue(len(result) > 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user