feat: deep search agent with virtual filesystem and web search
This commit is contained in:
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user