feat: deep search agent with virtual filesystem and web search
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
OPENAI_API_KEY=your-api-key-here
|
||||||
|
OPENAI_API_BASE=https://api.openai.com/v1
|
||||||
|
OPENAI_MODEL=gpt-4o-mini
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Задание 3: Самописный поисковый агент (Deep Agent)
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Deep search agent на основе курса "deep agents from scratch".
|
||||||
|
Агент умеет:
|
||||||
|
1. Искать информацию в интернете (DuckDuckGo)
|
||||||
|
2. Создавать и управлять виртуальными файлами в памяти
|
||||||
|
3. Выгружать виртуальные файлы в реальную файловую систему
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- langchain-core, langchain-openai, langgraph, duckduckgo-search, pydantic
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
cp .env.example .env
|
||||||
|
# Заполнить OPENAI_API_KEY в .env
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
- `agent.py` — виртуальная файловая система, инструменты агента, сборка ReAct-агента
|
||||||
|
- `main.py` — точка входа с примером задания (исследование темы LangGraph)
|
||||||
|
|
||||||
|
## Как работает
|
||||||
|
|
||||||
|
1. Агент получает задачу
|
||||||
|
2. Ищет информацию через `search_web()`
|
||||||
|
3. Сохраняет результаты в виртуальную ФС через `write_file()`
|
||||||
|
4. В конце вызывает `export_files()` для выгрузки на реальный диск
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
"""
|
||||||
|
Deep Search Agent — самописный поисковый агент на основе deep agents from scratch.
|
||||||
|
|
||||||
|
Возможности:
|
||||||
|
1. Поиск информации в интернете (DuckDuckGo)
|
||||||
|
2. Виртуальная файловая система в памяти
|
||||||
|
3. Выгрузка виртуальных файлов в реальную файловую систему
|
||||||
|
|
||||||
|
Основан на: https://github.com/langchain-ai/deep-agents-from-scratch
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langgraph.prebuilt import create_react_agent
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Виртуальная файловая система
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class VirtualFileSystem:
|
||||||
|
"""Простая файловая система в памяти."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.files: dict[str, str] = {}
|
||||||
|
|
||||||
|
def read(self, file_path: str) -> str:
|
||||||
|
"""Прочитать файл из виртуальной ФС."""
|
||||||
|
if file_path not in self.files:
|
||||||
|
return f"Error: File '{file_path}' not found."
|
||||||
|
return self.files[file_path]
|
||||||
|
|
||||||
|
def write(self, file_path: str, content: str) -> str:
|
||||||
|
"""Записать (или перезаписать) файл в виртуальной ФС."""
|
||||||
|
self.files[file_path] = content
|
||||||
|
return f"Successfully wrote {len(content)} bytes to '{file_path}'."
|
||||||
|
|
||||||
|
def list_files(self) -> str:
|
||||||
|
"""Список всех файлов в виртуальной ФС."""
|
||||||
|
if not self.files:
|
||||||
|
return "No files in virtual filesystem."
|
||||||
|
lines = [f" {path} ({len(content)} bytes)"
|
||||||
|
for path, content in self.files.items()]
|
||||||
|
return "Virtual filesystem:\n" + "\n".join(lines)
|
||||||
|
|
||||||
|
def export_to_disk(self, output_dir: str = "./output") -> str:
|
||||||
|
"""Выгрузить все виртуальные файлы на реальный диск."""
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
exported = []
|
||||||
|
for path, content in self.files.items():
|
||||||
|
full_path = os.path.join(output_dir, path)
|
||||||
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||||
|
with open(full_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
exported.append(full_path)
|
||||||
|
return f"Exported {len(exported)} files to '{output_dir}/':\n" + \
|
||||||
|
"\n".join(f" {p}" for p in exported)
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр виртуальной ФС
|
||||||
|
vfs = VirtualFileSystem()
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Инструменты агента
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search_web(query: str) -> str:
|
||||||
|
"""
|
||||||
|
Поиск информации в интернете через DuckDuckGo.
|
||||||
|
Используй для поиска фактов, новостей, документации и любой информации.
|
||||||
|
Вход: поисковый запрос (строка).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from duckduckgo_search import DDGS
|
||||||
|
with DDGS() as ddgs:
|
||||||
|
results = list(ddgs.text(query, max_results=5))
|
||||||
|
if not results:
|
||||||
|
return "No results found."
|
||||||
|
|
||||||
|
output = []
|
||||||
|
for i, r in enumerate(results, 1):
|
||||||
|
output.append(
|
||||||
|
f"[{i}] {r.get('title', 'No title')}\n"
|
||||||
|
f" URL: {r.get('href', 'N/A')}\n"
|
||||||
|
f" {r.get('body', 'No description')}"
|
||||||
|
)
|
||||||
|
return "\n\n".join(output)
|
||||||
|
except ImportError:
|
||||||
|
return ("Error: duckduckgo_search not installed. "
|
||||||
|
"Run: pip install duckduckgo-search")
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def write_file(file_path: str, content: str) -> str:
|
||||||
|
"""
|
||||||
|
Записать файл в виртуальную файловую систему.
|
||||||
|
Вход: file_path — путь файла (например, 'report.md', 'data/notes.txt'),
|
||||||
|
content — содержимое файла.
|
||||||
|
"""
|
||||||
|
return vfs.write(file_path, content)
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def read_file(file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Прочитать файл из виртуальной файловой системы.
|
||||||
|
Вход: file_path — путь файла.
|
||||||
|
"""
|
||||||
|
return vfs.read(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def list_files() -> str:
|
||||||
|
"""Показать список всех файлов в виртуальной файловой системе."""
|
||||||
|
return vfs.list_files()
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def export_files(output_dir: str = "./output") -> str:
|
||||||
|
"""
|
||||||
|
Выгрузить все виртуальные файлы на реальный диск.
|
||||||
|
Вход: output_dir — директория для выгрузки (по умолчанию './output').
|
||||||
|
"""
|
||||||
|
return vfs.export_to_disk(output_dir)
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS = [search_web, write_file, read_file, list_files, export_files]
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Сборка агента
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """Ты — Deep Search Agent, автономный поисковый агент.
|
||||||
|
|
||||||
|
Твои возможности:
|
||||||
|
- Искать информацию в интернете (search_web)
|
||||||
|
- Создавать и редактировать файлы в виртуальной файловой системе (write_file, read_file)
|
||||||
|
- Просматривать список файлов (list_files)
|
||||||
|
- Выгружать файлы на реальный диск (export_files)
|
||||||
|
|
||||||
|
Правила работы:
|
||||||
|
1. Сначала исследуй тему через search_web, собери информацию
|
||||||
|
2. Сохраняй полезные находки в виртуальную файловую систему через write_file
|
||||||
|
3. При необходимости читай ранее сохранённые файлы через read_file
|
||||||
|
4. В конце работы ВСЕГДА выгружай файлы на диск через export_files
|
||||||
|
5. Дай краткий итог выполненной работы
|
||||||
|
|
||||||
|
Будь методичен: ищи → анализируй → сохраняй → выгружай.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def build_agent() -> tuple:
|
||||||
|
"""Создать и вернуть агента + виртуальную ФС."""
|
||||||
|
api_base = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||||
|
api_key = os.environ.get("OPENAI_API_KEY", "")
|
||||||
|
model_name = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
||||||
|
|
||||||
|
llm = ChatOpenAI(
|
||||||
|
model=model_name,
|
||||||
|
openai_api_base=api_base,
|
||||||
|
openai_api_key=api_key,
|
||||||
|
temperature=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
agent = create_react_agent(
|
||||||
|
model=llm,
|
||||||
|
tools=TOOLS,
|
||||||
|
prompt=SYSTEM_PROMPT,
|
||||||
|
)
|
||||||
|
|
||||||
|
return agent, vfs
|
||||||
|
|
||||||
|
|
||||||
|
def run_agent(task: str, output_dir: str = "./output") -> dict:
|
||||||
|
"""
|
||||||
|
Запустить агента на выполнение задачи.
|
||||||
|
Возвращает словарь с результатом и списком файлов.
|
||||||
|
"""
|
||||||
|
agent, vfs = build_agent()
|
||||||
|
|
||||||
|
# Добавляем инструкцию о выгрузке в конец задачи
|
||||||
|
full_task = (
|
||||||
|
f"{task}\n\n"
|
||||||
|
f"После завершения исследования ОБЯЗАТЕЛЬНО:\n"
|
||||||
|
f"1. Вызови list_files() чтобы увидеть все созданные файлы\n"
|
||||||
|
f"2. Вызови export_files('{output_dir}') чтобы выгрузить файлы на диск"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = agent.invoke(
|
||||||
|
{"messages": [{"role": "user", "content": full_task}]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Собираем ответ
|
||||||
|
messages = result.get("messages", [])
|
||||||
|
last_ai_msg = None
|
||||||
|
for msg in reversed(messages):
|
||||||
|
if hasattr(msg, "content") and msg.type == "ai":
|
||||||
|
last_ai_msg = msg.content
|
||||||
|
break
|
||||||
|
|
||||||
|
return {
|
||||||
|
"response": last_ai_msg,
|
||||||
|
"files_created": list(vfs.files.keys()),
|
||||||
|
"output_dir": output_dir,
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
Deep Search Agent — точка входа.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
Переменные окружения:
|
||||||
|
OPENAI_API_BASE — URL API (по умолчанию https://api.openai.com/v1)
|
||||||
|
OPENAI_API_KEY — ключ API
|
||||||
|
OPENAI_MODEL — модель (по умолчанию gpt-4o-mini)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from agent import run_agent
|
||||||
|
|
||||||
|
|
||||||
|
TASK = (
|
||||||
|
"Исследуй тему: 'Что такое LangGraph и как он работает?'. "
|
||||||
|
"Найди основную информацию, ключевые концепции и примеры использования. "
|
||||||
|
"Сохрани результаты в файл 'research/langgraph_overview.md'. "
|
||||||
|
"Также сохрани краткое резюме в 'research/summary.txt'."
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Deep Search Agent — запуск")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
result = run_agent(TASK, output_dir="./output")
|
||||||
|
|
||||||
|
print("\n--- Итог работы агента ---")
|
||||||
|
print(result["response"])
|
||||||
|
|
||||||
|
print(f"\n--- Созданные файлы ({len(result['files_created'])}) ---")
|
||||||
|
for f in result["files_created"]:
|
||||||
|
print(f" {f}")
|
||||||
|
|
||||||
|
print(f"\nФайлы выгружены в: {result['output_dir']}/")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
langchain-core>=0.3.0
|
||||||
|
langchain-openai>=0.2.0
|
||||||
|
langgraph>=0.2.0
|
||||||
|
duckduckgo-search>=6.0.0
|
||||||
|
pydantic>=2.0.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
Reference in New Issue
Block a user