diff --git a/src/subagents/__init__.py b/src/subagents/__init__.py new file mode 100644 index 0000000..06d7405 Binary files /dev/null and b/src/subagents/__init__.py differ diff --git a/src/subagents/analyzer.py b/src/subagents/analyzer.py new file mode 100644 index 0000000..fc8e405 --- /dev/null +++ b/src/subagents/analyzer.py @@ -0,0 +1,75 @@ +""" +Субагент-аналитик: читает условие задания и возвращает список файлов для создания. +""" + +import re +from langchain_core.messages import SystemMessage, HumanMessage +from ..utils import parse_json + +PROMPT = """Ты — аналитик заданий по программированию. Прочитай условие задания и составь ПОЛНЫЙ список файлов которые нужно создать. + +## Правила анализа + +1. Сначала найди ВСЕ упоминания файлов в условии — ищи слова вида "файл X.py", "создай X.py", "X.py должен", "files_to_create", списки файлов. +2. Включи в план КАЖДЫЙ упомянутый файл — пропускать нельзя. +3. Если в условии названы server.py и client.py — в плане должны быть ОБА. +4. Если в условии упоминается конкретный стек (FastMCP, Qdrant и т.д.) — учти его при выборе файлов. +5. ВСЕГДА добавляй requirements.txt и README.md. +6. Не придумывай файлы, которых нет в условии; если имя не указано — используй solution.py. + +## ОБЯЗАТЕЛЬНО +- В плане ДОЛЖНЫ быть ВСЕ кодовые файлы из условия, не только первый. +- Если условие требует 3 файла — в плане будет 3 кодовых файла + requirements.txt + README.md. +- README.md и requirements.txt НЕ считаются кодовыми файлами. + +Верни ТОЛЬКО валидный JSON без markdown и пояснений: +{ + "files": [ + {"filename": "server.py", "purpose": "FastMCP сервер с инструментами get/set/delete"}, + {"filename": "client.py", "purpose": "клиентский скрипт для тестирования сервера"}, + {"filename": "requirements.txt", "purpose": "зависимости проекта"}, + {"filename": "README.md", "purpose": "документация"} + ] +}""" + +_SERVICE_FILES = {"requirements.txt", "README.md"} + + +def analyze_task(llm, task_title: str, task_description: str) -> list[dict]: + """ + Анализирует условие задания и возвращает список кодовых файлов для создания. + Каждый элемент: {"filename": "...", "purpose": "..."}. + """ + print(f"[ANALYZER] 🔍 Анализирую файлы для: «{task_title}»") + + r = llm.invoke([ + SystemMessage(content=PROMPT), + HumanMessage(content=f"Задание: {task_title}\n\nУсловие:\n{task_description}"), + ]) + plan = parse_json(r.content) + + raw_files = plan.get("files", []) if plan else [] + + # Нормализуем: LLM иногда возвращает строки вместо объектов + all_files = [ + f if isinstance(f, dict) else {"filename": f, "purpose": ""} + for f in raw_files + ] + + code_files = [f for f in all_files if f["filename"] not in _SERVICE_FILES] + + if not code_files: + print("[ANALYZER] ⚠️ Нет кодовых файлов в плане — добавляю solution.py") + code_files = [{"filename": "solution.py", "purpose": "основное решение задания"}] + + # Страховка: добавляем файлы, явно упомянутые в условии но пропущенные LLM + mentioned = re.findall(r'\b[\w][\w.-]*\.(?:py|js|ts|sh|sql|html|css)\b', task_description) + plan_fnames = {f["filename"] for f in code_files} + for mf in dict.fromkeys(mentioned): + if mf not in plan_fnames and mf not in _SERVICE_FILES: + print(f"[ANALYZER] ➕ Добавляю файл из условия: {mf}") + code_files.append({"filename": mf, "purpose": "файл упомянутый в условии задания"}) + plan_fnames.add(mf) + + print(f"[ANALYZER] 📁 Итого файлов: {[f['filename'] for f in code_files]}") + return code_files diff --git a/src/subagents/coder.py b/src/subagents/coder.py new file mode 100644 index 0000000..15eb7bf --- /dev/null +++ b/src/subagents/coder.py @@ -0,0 +1,58 @@ +""" +Субагент-кодер: пишет или редактирует один файл. +""" + +from langchain_core.messages import SystemMessage, HumanMessage +from ..utils import strip_md + +WRITE_PROMPT = """Ты — опытный Python-разработчик. Напиши ОДИН файл решения. + +Правила: +- Реализуй ВСЁ что требует условие для ЭТОГО файла +- Код запускается без ошибок +- Комментарии на русском языке +- Без заглушек (pass, TODO, raise NotImplementedError) +- Используй именно тот стек что указан в условии (FastMCP, rich, Qdrant, LangChain и т.д.) +- Верни ТОЛЬКО чистый код без markdown-обёртки и пояснений""" + +EDIT_PROMPT = """Ты — опытный Python-разработчик. Тебе дан файл с кодом и инструкция по его доработке. + +Правила: +- Внеси ТОЛЬКО те изменения, которые описаны в инструкции. Не трогай остальной код. +- Код должен оставаться рабочим после правок. +- Комментарии на русском языке. +- Без заглушек (pass, TODO, raise NotImplementedError). +- Верни ПОЛНОЕ содержимое файла после правок — без markdown-обёртки и пояснений.""" + + +def write_file(llm, task_title: str, task_description: str, filename: str, purpose: str) -> str: + """Пишет содержимое одного файла по условию задания.""" + print(f"[CODER] ✍️ Пишу {filename} ({purpose})...") + r = llm.invoke([ + SystemMessage(content=WRITE_PROMPT), + HumanMessage(content=( + f"Задание: {task_title}\n\n" + f"Условие:\n{task_description}\n\n" + f"Сейчас пиши ТОЛЬКО файл: {filename}\n" + f"Назначение этого файла: {purpose}" + )), + ]) + content = strip_md(r.content) + print(f"[CODER] ✅ {filename}: {len(content)} символов") + return content + + +def edit_file_content(llm, filename: str, current_content: str, instruction: str) -> str: + """Вносит правки в существующий файл по инструкции пользователя.""" + print(f"[CODER] ✍️ Редактирую {filename}: {instruction[:60]}...") + r = llm.invoke([ + SystemMessage(content=EDIT_PROMPT), + HumanMessage(content=( + f"Файл: {filename}\n\n" + f"Текущий код:\n{current_content}\n\n" + f"Инструкция по правке:\n{instruction}" + )), + ]) + content = strip_md(r.content) + print(f"[CODER] ✅ {filename} отредактирован: {len(content)} символов") + return content diff --git a/src/subagents/documenter.py b/src/subagents/documenter.py new file mode 100644 index 0000000..54cca66 --- /dev/null +++ b/src/subagents/documenter.py @@ -0,0 +1,47 @@ +""" +Субагент-документатор: генерирует requirements.txt и README.md. +""" + +from langchain_core.messages import SystemMessage, HumanMessage +from ..utils import strip_md + +REQUIREMENTS_PROMPT = """Посмотри на эти Python-файлы и напиши минимальный requirements.txt. +Верни ТОЛЬКО содержимое файла — одна библиотека на строку, без комментариев. +Если нужны конкретные версии — укажи. Никакого лишнего текста.""" + +README_PROMPT = """Напиши README.md для этого проекта. +Включи: описание, установка зависимостей, запуск каждого файла, пример использования. +Верни ТОЛЬКО markdown.""" + + +def write_requirements(llm, code_files: dict[str, str]) -> str: + """Генерирует requirements.txt на основе написанного кода.""" + print("[DOCUMENTER] 📦 Пишу requirements.txt...") + preview = "\n\n# ---\n\n".join( + f"# {fn}\n{code[:1500]}" for fn, code in code_files.items() + ) + r = llm.invoke([ + SystemMessage(content=REQUIREMENTS_PROMPT), + HumanMessage(content=preview), + ]) + return strip_md(r.content) + + +def write_readme(llm, task_title: str, task_description: str, code_files: dict[str, str]) -> str: + """Генерирует README.md для проекта.""" + print("[DOCUMENTER] 📄 Пишу README.md...") + # Передаём фрагменты ВСЕХ файлов, а не только первого + code_previews = "\n\n".join( + f"### {fn}\n```python\n{code[:800]}\n```" + for fn, code in code_files.items() + ) + r = llm.invoke([ + SystemMessage(content=README_PROMPT), + HumanMessage(content=( + f"Проект: {task_title}\n\n" + f"Файлы проекта: {', '.join(code_files.keys())}\n\n" + f"Условие задания:\n{task_description[:1500]}\n\n" + f"Фрагменты кода:\n{code_previews}" + )), + ]) + return strip_md(r.content) diff --git a/src/subagents/validator.py b/src/subagents/validator.py new file mode 100644 index 0000000..f863499 --- /dev/null +++ b/src/subagents/validator.py @@ -0,0 +1,72 @@ +""" +Субагент-валидатор: проверяет обоснованность замечания проверяющего. +""" + +from langchain_core.messages import SystemMessage, HumanMessage +from ..utils import parse_json, strip_md + +PROMPT = """Ты — эксперт по проверке кода. Тебе дано условие задания, написанный код и замечание проверяющего. + +Твоя задача: определить, ВЕРНО ли замечание проверяющего. + +Замечание ВЕРНО — если код действительно не выполняет требования задания (ошибка в логике, отсутствует функционал, неверный стек и т.д.). +Замечание НЕВЕРНО — если код корректно выполняет все требования задания, а замечание ошибочно или придирчиво. + +Будь объективен. Если код правильный — не соглашайся с замечанием. + +Верни ТОЛЬКО валидный JSON без markdown: +{ + "valid": true, + "explanation": "Краткое объяснение почему замечание верно или неверно", + "files_to_fix": ["server.py"], + "fix_instruction": "Что конкретно исправить в коде (только если valid=true, иначе null)" +}""" + + +def validate_remark( + llm, + task_description: str, + code_files: dict[str, str], + reviewer_comment: str, +) -> dict: + """ + Проверяет обоснованность замечания проверяющего. + + Returns: + { + "valid": bool, — замечание обоснованно? + "explanation": str, — аргументация + "files_to_fix": list[str], — файлы для правки (если valid=True) + "fix_instruction": str, — что именно исправить (если valid=True) + } + """ + print(f"[VALIDATOR] 🔍 Проверяю замечание: «{reviewer_comment[:80]}»") + + code_block = "\n\n".join( + f"### {fn}\n```\n{code[:2000]}\n```" + for fn, code in code_files.items() + ) + + r = llm.invoke([ + SystemMessage(content=PROMPT), + HumanMessage(content=( + f"## Условие задания\n{task_description}\n\n" + f"## Написанный код\n{code_block}\n\n" + f"## Замечание проверяющего\n{reviewer_comment}" + )), + ]) + + result = parse_json(r.content) + if not result or "valid" not in result: + # Если LLM не вернул JSON — считаем замечание обоснованным (безопасный fallback) + print("[VALIDATOR] ⚠️ Не удалось распарсить ответ, считаю замечание обоснованным") + return { + "valid": True, + "explanation": "Не удалось автоматически проверить замечание", + "files_to_fix": list(code_files.keys()), + "fix_instruction": reviewer_comment, + } + + verdict = "обоснованно" if result["valid"] else "НЕ обоснованно" + print(f"[VALIDATOR] {'✅' if result['valid'] else '❌'} Замечание {verdict}: {result.get('explanation', '')[:100]}") + return result