214 lines
9.0 KiB
Python
214 lines
9.0 KiB
Python
"""
|
|
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,
|
|
}
|