commit 85d4ae32a800f8051ee86ab1ea71c75e47720727 Author: marat Date: Fri May 8 16:11:45 2026 +0300 feat: deep search agent with virtual filesystem and web search diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b9635d8 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +OPENAI_API_KEY=your-api-key-here +OPENAI_API_BASE=https://api.openai.com/v1 +OPENAI_MODEL=gpt-4o-mini diff --git a/README.md b/README.md new file mode 100644 index 0000000..9df8393 --- /dev/null +++ b/README.md @@ -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()` для выгрузки на реальный диск diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..bf195df --- /dev/null +++ b/agent.py @@ -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, + } diff --git a/main.py b/main.py new file mode 100644 index 0000000..bc63fc3 --- /dev/null +++ b/main.py @@ -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']}/") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5ee393b --- /dev/null +++ b/requirements.txt @@ -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