192 lines
11 KiB
Python
192 lines
11 KiB
Python
class PlannerAgent:
|
|||
|
|
"""Агент-планировщик: анализирует проект и создаёт план подзадач."""
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def plan(project: Project, user_task: str, errors: List[str] = None) -> List[Dict[str, str]]:
|
||
|
|
# Получаем суммари из состояния
|
||
|
|
summaries = project.state.get("file_summaries", {})
|
||
|
|
if not summaries:
|
||
|
|
print("⚠️ Нет описаний файлов. Сначала сгенерируйте их.")
|
||
|
|
return [{"type": "error", "description": "Нет описаний файлов"}]
|
||
|
|
|
||
|
|
# Формируем текстовое представление суммари
|
||
|
|
items = []
|
||
|
|
for file_path, desc in summaries.items():
|
||
|
|
items.append(f"- {file_path}: {desc}")
|
||
|
|
summaries_text = "\n".join(items)
|
||
|
|
|
||
|
|
# Берём последние ошибки из состояния (если есть)
|
||
|
|
errors_context = ""
|
||
|
|
if errors:
|
||
|
|
errors_context = "Актуальные ошибки в браузере:\n" + "\n".join(errors[:5])
|
||
|
|
else:
|
||
|
|
errors_context = "Ошибок в браузере не зафиксировано."
|
||
|
|
# last_errors = project.state.get("last_errors", [])
|
||
|
|
# errors_context = ""
|
||
|
|
# if last_errors:
|
||
|
|
# errors_context = "\n".join(last_errors[:3])
|
||
|
|
|
||
|
|
prompt = f"""
|
||
|
|
Ты — главный архитектор. Получена задача: {user_task}
|
||
|
|
|
||
|
|
Вот краткое описание каждого важного файла проекта:
|
||
|
|
{summaries_text}
|
||
|
|
|
||
|
|
{errors_context}
|
||
|
|
|
||
|
|
Твоя задача — составить план действий. План должен быть списком задач.
|
||
|
|
Для каждой задачи укажи тип агента:
|
||
|
|
- "test" — запустить браузер и проверить консоль на ошибки
|
||
|
|
- "code" — написать или изменить код. Для этой задачи обязательно укажи поле "files" — массив относительных путей к файлам, которые нужно передать кодеру для контекста.
|
||
|
|
- "browser" — открыть браузер для визуальной проверки (не обязательно)
|
||
|
|
|
||
|
|
План должен быть логичным: сначала обычно проверяют текущее состояние (test), затем исправляют ошибки (code), снова проверяют (test) и т.д.
|
||
|
|
Но ты можешь предложить любую последовательность, которая, по твоему мнению, решит задачу.
|
||
|
|
|
||
|
|
Ответ дай строго в формате JSON-списка объектов:
|
||
|
|
[
|
||
|
|
{{"type": "test", "description": "описание задачи для тестировщика"}},
|
||
|
|
{{"type": "code", "description": "описание задачи для кодера", "files": ["путь/к/файлу1", "путь/к/файлу2"]}}
|
||
|
|
]
|
||
|
|
Никаких пояснений, только JSON.
|
||
|
|
"""
|
||
|
|
messages = [{"role": "system", "content": "Ты — архитектор, выдаёшь только JSON."},
|
||
|
|
{"role": "user", "content": prompt}]
|
||
|
|
response = project.ollama_client.chat(messages, temperature=0.3)
|
||
|
|
try:
|
||
|
|
start = response.find('[')
|
||
|
|
end = response.rfind(']') + 1
|
||
|
|
if start != -1 and end != -1:
|
||
|
|
json_str = response[start:end]
|
||
|
|
plan = json.loads(json_str)
|
||
|
|
if isinstance(plan, list) and all(
|
||
|
|
isinstance(item, dict) and "type" in item and "description" in item for item in plan):
|
||
|
|
for item in plan:
|
||
|
|
if item["type"] == "code" and "files" not in item:
|
||
|
|
item["files"] = [] # если не указано, ставим пустой массив
|
||
|
|
return plan
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Ошибка парсинга плана: {e}")
|
||
|
|
# fallback
|
||
|
|
return [{"type": "code", "description": "Не удалось создать план, попробуйте вручную.", "files": []}]
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def replan(project: Project, user_task: str, errors: List[str]) -> List[Dict[str, str]]:
|
||
|
|
"""Анализирует ошибки и возвращает список новых задач для их исправления."""
|
||
|
|
summaries = project.state.get("file_summaries", {})
|
||
|
|
summaries_text = "\n".join([f"- {p}: {d}" for p, d in summaries.items()])
|
||
|
|
errors_text = "\n".join(errors) # ограничим для контекста
|
||
|
|
|
||
|
|
prompt = f"""
|
||
|
|
Ты — главный архитектор. Получена задача: {user_task}
|
||
|
|
|
||
|
|
Вот описание файлов проекта:
|
||
|
|
{summaries_text}
|
||
|
|
|
||
|
|
При выполнении теста были обнаружены следующие ошибки:
|
||
|
|
{errors_text}
|
||
|
|
|
||
|
|
Проанализируй ошибки. Определи, какие изменения в коде нужно внести, чтобы их исправить.
|
||
|
|
Составь список задач для кодера (тип "code") с конкретными инструкциями по исправлению.
|
||
|
|
Если ошибки связаны с отсутствием файлов или неправильными путями, добавь задачу по созданию или перемещению.
|
||
|
|
Ответ дай строго в формате JSON-списка объектов: [{{"type": "code", "description": "описание"}}]
|
||
|
|
Никаких пояснений, только JSON.
|
||
|
|
"""
|
||
|
|
messages = [{"role": "system", "content": "Ты — архитектор, выдаёшь только JSON."},
|
||
|
|
{"role": "user", "content": prompt}]
|
||
|
|
response = project.ollama_client.chat(messages, temperature=0.3)
|
||
|
|
try:
|
||
|
|
start = response.find('[')
|
||
|
|
end = response.rfind(']') + 1
|
||
|
|
if start != -1 and end != -1:
|
||
|
|
json_str = response[start:end]
|
||
|
|
tasks = json.loads(json_str)
|
||
|
|
if isinstance(tasks, list) and all(
|
||
|
|
isinstance(item, dict) and "type" in item and "description" in item for item in tasks):
|
||
|
|
return tasks
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return [{"type": "code", "description": "Исправить ошибки в коде (ручное вмешательство)"}]
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def next_task(project: Project, user_task: str) -> dict:
|
||
|
|
summaries = project.state.get("file_summaries", {})
|
||
|
|
if not summaries:
|
||
|
|
return {"type": "error", "description": "Нет описаний файлов"}
|
||
|
|
|
||
|
|
completed = project.state.get("completed", [])
|
||
|
|
last_errors = project.state.get("last_errors", [])
|
||
|
|
|
||
|
|
errors_text = ""
|
||
|
|
if last_errors:
|
||
|
|
for err in last_errors[:5]:
|
||
|
|
msg = err.get("message", "")
|
||
|
|
loc = err.get("location")
|
||
|
|
if loc:
|
||
|
|
file_name = loc.get("url", "неизвестный файл")
|
||
|
|
line = loc.get("line", "?")
|
||
|
|
col = loc.get("column", "?")
|
||
|
|
errors_text += f"- {msg} (в {file_name}:{line}:{col})\n"
|
||
|
|
else:
|
||
|
|
errors_text += f"- {msg}\n"
|
||
|
|
if err.get("stack"):
|
||
|
|
errors_text += f" Стек: {err['stack']}\n"
|
||
|
|
else:
|
||
|
|
errors_text = "Ошибок не зафиксировано."
|
||
|
|
|
||
|
|
summaries_text = "\n".join([f"- {path}: {desc}" for path, desc in list(summaries.items())[:10]])
|
||
|
|
completed_text = "\n".join([f"- {task['type']}: {task['description']}" for task in completed]) or "нет"
|
||
|
|
|
||
|
|
prompt = f"""
|
||
|
|
Ты — главный архитектор. Получена задача: {user_task}
|
||
|
|
|
||
|
|
Описание файлов проекта:
|
||
|
|
{summaries_text}
|
||
|
|
|
||
|
|
Уже выполненные задачи:
|
||
|
|
{completed_text}
|
||
|
|
|
||
|
|
Актуальные ошибки (с указанием файлов и строк):
|
||
|
|
{errors_text}
|
||
|
|
|
||
|
|
Твоя задача — решить, что делать дальше.
|
||
|
|
ВНИМАНИЕ: НЕ возвращай finish, пока не будут выполнены как минимум 2 задачи и не будет ошибок.
|
||
|
|
Если ошибки есть — ты должен предложить задачу типа "code" для их исправления.
|
||
|
|
Если ошибок нет и уже было выполнено хотя бы 2 задачи, тогда можешь вернуть finish.
|
||
|
|
Иначе верни одну задачу для следующего шага. Типы задач:
|
||
|
|
- "test" — запустить браузер и проверить консоль на ошибки (это нужно делать, если неизвестно состояние)
|
||
|
|
- "code" — написать или изменить код. Укажи поле "files" — массив относительных путей к файлам, которые нужно передать кодеру. Старайся указывать файлы на основе информации об ошибках.
|
||
|
|
- "browser" — открыть браузер для визуальной проверки
|
||
|
|
|
||
|
|
Ответ дай строго в формате JSON-объекта:
|
||
|
|
{{"type": "test", "description": "описание"}}
|
||
|
|
или
|
||
|
|
{{"type": "code", "description": "описание", "files": ["путь/к/файлу"]}}
|
||
|
|
Никаких пояснений, только JSON.
|
||
|
|
"""
|
||
|
|
messages = [{"role": "system", "content": "Ты — архитектор, выдаёшь только JSON."},
|
||
|
|
{"role": "user", "content": prompt}]
|
||
|
|
response = project.ollama_client.chat(messages, temperature=0.3)
|
||
|
|
print(f"Ответ планировщика: {response}") # отладка
|
||
|
|
|
||
|
|
if not response:
|
||
|
|
return {"type": "test", "description": "Проверить состояние"}
|
||
|
|
|
||
|
|
try:
|
||
|
|
start = response.find('{')
|
||
|
|
end = response.rfind('}') + 1
|
||
|
|
if start != -1 and end != -1:
|
||
|
|
json_str = response[start:end]
|
||
|
|
task = json.loads(json_str)
|
||
|
|
if "type" in task and "description" in task:
|
||
|
|
if task["type"] == "code" and "files" not in task:
|
||
|
|
task["files"] = []
|
||
|
|
# Принудительно не даём finish, если мало задач выполнено
|
||
|
|
if task.get("type") == "finish" and len(completed) < 2:
|
||
|
|
return {"type": "test", "description": "Проверить состояние"}
|
||
|
|
return task
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Парсинг JSON не удался: {e}")
|
||
|
|
|
||
|
|
# Если не удалось распарсить, возвращаем тест по умолчанию
|
||
|
|
return {"type": "test", "description": "Проверить состояние"}
|