Загрузить файлы в «src/subagents»
This commit is contained in:
Binary file not shown.
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user