diff --git a/solutions/69de7223f309a98be0007e09_8__Самописный_поисковый_агент_на_основе_/solution.py b/solutions/69de7223f309a98be0007e09_8__Самописный_поисковый_агент_на_основе_/solution.py index d0758ad..aaeb65b 100644 --- a/solutions/69de7223f309a98be0007e09_8__Самописный_поисковый_агент_на_основе_/solution.py +++ b/solutions/69de7223f309a98be0007e09_8__Самописный_поисковый_агент_на_основе_/solution.py @@ -14,30 +14,21 @@ from rich.table import Table # Виртуальная файловая система # --------------------------------------------------------------------------- # class VirtualFileSystem: - """ - Хранит файлы в памяти. Позволяет создавать и экспортировать их. - """ + """Хранит файлы в памяти. Позволяет создавать и экспортировать их.""" def __init__(self): self.files: Dict[str, str] = {} def write(self, filename: str, content: str) -> None: - """Создаёт или перезаписывает файл.""" self.files[filename] = content def read(self, filename: str) -> str | None: - """Возвращает содержимое файла, если он существует.""" return self.files.get(filename) def list_files(self) -> List[str]: - """Список всех файлов.""" return list(self.files.keys()) def export_all(self, directory: str = ".") -> None: - """ - Записывает все файлы в указанную директорию. - Если файл с таким именем уже существует – он будет перезаписан. - """ os.makedirs(directory, exist_ok=True) for name, content in self.files.items(): path = os.path.join(directory, name) @@ -50,13 +41,12 @@ class VirtualFileSystem: # --------------------------------------------------------------------------- # def web_search(query: str) -> str: - """ - Выполняет поиск в интернете через DuckDuckGo и возвращает первые 3 результата. - """ + """Поиск в интернете через DuckDuckGo, возвращает первые 3 результата.""" url = "https://duckduckgo.com/html/" params = {"q": query} try: - resp = requests.get(url, params=params, timeout=10) + resp = requests.get(url, params=params, timeout=10, + headers={"User-Agent": "Mozilla/5.0"}) resp.raise_for_status() except Exception as e: return f"Ошибка при поиске: {e}" @@ -74,53 +64,143 @@ def web_search(query: str) -> str: def create_file(vfs: VirtualFileSystem, filename: str, content: str) -> str: - """ - Создаёт или обновляет виртуальный файл. - """ + """Создаёт или обновляет виртуальный файл.""" vfs.write(filename, content) return f"Файл '{filename}' успешно создан/обновлён." +def read_file(vfs: VirtualFileSystem, filename: str) -> str: + """Читает файл из виртуальной ФС.""" + content = vfs.read(filename) + if content is None: + return f"Файл '{filename}' не найден." + return content + + +def list_files(vfs: VirtualFileSystem) -> str: + """Список файлов в виртуальной ФС.""" + files = vfs.list_files() + if not files: + return "Виртуальная ФС пуста." + return "\n".join(files) + + # --------------------------------------------------------------------------- # -# Основной логика +# Описание инструментов для LLM # --------------------------------------------------------------------------- # -def run_agent(task: str, vfs: VirtualFileSystem) -> str: +TOOLS_DESCRIPTION = """Ты — автономный поисковый агент. Работай в цикле Thought/Action/Observation. + +Доступные инструменты: +1. web_search — поиск в интернете. Action Input: поисковый запрос (строка) +2. create_file — создать файл в виртуальной ФС. Action Input: JSON {"filename": "...", "content": "..."} +3. read_file — прочитать файл из виртуальной ФС. Action Input: имя файла (строка) +4. list_files — список файлов в виртуальной ФС. Action Input: (пусто) +5. finish — завершить работу. Action Input: финальный ответ пользователю + +Формат каждого шага — СТРОГО такой (три строки, никаких отклонений): +Thought: <что ты думаешь, что нужно сделать> +Action: <одно из: web_search | create_file | read_file | list_files | finish> +Action Input: <аргумент> + +Правила: +- Сначала ищи информацию через web_search. +- После сбора информации сохрани результат через create_file. +- Заверши работу командой finish только после того, как файл создан. +- Никогда не выдумывай информацию — используй только то, что получил из web_search. +""" + + +# --------------------------------------------------------------------------- # +# Агентный цикл (ReAct: Reason + Act) +# --------------------------------------------------------------------------- # + +def run_agent(task: str, vfs: VirtualFileSystem, console: Console) -> str: """ - Выполняет задачу с помощью OpenAI LLM. + Агентный цикл ReAct from scratch: + LLM сам решает когда и какой инструмент вызвать, + получает результат (Observation) и продолжает рассуждение. """ client = OpenAI() - # Формируем запрос к модели - prompt = ( - f"Задача: {task}\n" - "Используйте web_search для получения информации.\n" - "Сформируйте краткое резюме (не более 300 слов) и сохраните его в файл summary.txt." - ) - # Получаем ответ модели - try: - response = client.chat.completions.create( - model="gpt-3.5-turbo", - temperature=0, - messages=[{"role": "user", "content": prompt}], - ) - except Exception as e: - return f"Ошибка при вызове OpenAI: {e}" - summary_text = response.choices[0].message.content.strip() - # Сохраняем в виртуальный файл - create_file(vfs, "summary.txt", summary_text) - return summary_text + messages = [ + {"role": "system", "content": TOOLS_DESCRIPTION}, + {"role": "user", "content": task}, + ] + + for step in range(15): + # --- LLM генерирует следующий шаг --- + try: + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=messages, + temperature=0, + ) + except Exception as e: + return f"Ошибка при вызове OpenAI: {e}" + + raw = response.choices[0].message.content.strip() + console.print(f"\n[bold yellow][Шаг {step + 1}][/]\n{raw}") + messages.append({"role": "assistant", "content": raw}) + + # --- Парсинг Action и Action Input --- + action_match = re.search(r"^Action:\s*(\w+)", raw, re.MULTILINE) + input_match = re.search(r"^Action Input:\s*(.+)", raw, re.MULTILINE | re.DOTALL) + + if not action_match: + break + + action = action_match.group(1).strip() + action_input = input_match.group(1).strip() if input_match else "" + + # --- Выполнение инструмента --- + if action == "finish": + return action_input + + elif action == "web_search": + observation = web_search(action_input) + + elif action == "create_file": + try: + args = json.loads(action_input) + observation = create_file(vfs, args["filename"], args["content"]) + except (json.JSONDecodeError, KeyError) as e: + observation = f"Ошибка: неверный формат JSON для create_file: {e}" + + elif action == "read_file": + observation = read_file(vfs, action_input) + + elif action == "list_files": + observation = list_files(vfs) + + else: + observation = ( + f"Неизвестный инструмент: '{action}'. " + "Используй только: web_search, create_file, read_file, list_files, finish." + ) + + # --- Передаём результат обратно в LLM --- + preview = observation[:300] + ("..." if len(observation) > 300 else "") + console.print(f"[dim]Observation: {preview}[/]") + messages.append({"role": "user", "content": f"Observation: {observation}"}) + + return "Агент завершил работу (достигнут лимит шагов)." -def flush_virtual_fs(vfs: VirtualFileSystem) -> str: - """ - Экспортирует все файлы из виртуальной файловой системы на диск. - """ - try: - vfs.export_all(".") - return f"Все файлы выгружены в '{os.path.abspath('.')}'." - except Exception as e: - return f"Ошибка при экспорте файлов: {e}" +# --------------------------------------------------------------------------- # +# Выгрузка виртуальных файлов на диск +# --------------------------------------------------------------------------- # + +def flush_virtual_fs(vfs: VirtualFileSystem, console: Console) -> None: + """Экспортирует все файлы из виртуальной ФС в реальную файловую систему.""" + if not vfs.list_files(): + console.print("[red]Нет виртуальных файлов для выгрузки.[/]") + return + output_dir = "output" + vfs.export_all(output_dir) + console.print(f"\n[green]Файлы выгружены в '{os.path.abspath(output_dir)}':[/]") + for name in vfs.list_files(): + console.print(f" ✓ {name}") # --------------------------------------------------------------------------- # @@ -131,27 +211,35 @@ def main(): console = Console() vfs = VirtualFileSystem() - task = ( - "Найди последние новости и обновления по версии Python 3.11, " - "создай краткое резюме (не более 300 слов) и сохрани его в файле summary.txt." - ) + console.print("[bold cyan]═══ Deep Search Agent ═══[/]\n") + task = input("Введите задачу для агента: ").strip() + if not task: + console.print("[red]Задача не задана. Выход.[/]") + return - console.print("[bold cyan]Запуск агента...[/]") - result = run_agent(task, vfs) + console.print(f"\n[bold]Задача:[/] {task}") + console.print("[bold cyan]\nЗапуск агентного цикла...[/]") - console.print("\n[green]Итоговое сообщение от агента:[/]\n") + # --- Запуск агента --- + result = run_agent(task, vfs, console) + + # --- Финальный ответ --- + console.print("\n[bold green]═══ Финальный ответ агента ═══[/]") console.print(result) + # --- Таблица виртуальных файлов --- console.print("\n[bold magenta]Виртуальные файлы, созданные агентом:[/]") table = Table(show_header=True, header_style="bold blue") - table.add_column("Имя файла", style="dim") + table.add_column("Имя файла", style="dim", min_width=20) + table.add_column("Размер (байт)") for fname in vfs.list_files(): - table.add_row(fname) + size = str(len(vfs.read(fname) or "")) + table.add_row(fname, size) console.print(table) - export_result = flush_virtual_fs(vfs) - console.print("\n[green]Результат экспорта:[/]") - console.print(export_result) + # --- Выгрузка на диск --- + console.print("\n[bold cyan]Выгрузка виртуальных файлов в реальную ФС...[/]") + flush_virtual_fs(vfs, console) if __name__ == "__main__":