diff --git a/agent/executor.py b/agent/executor.py index 4333c49..7a85435 100644 --- a/agent/executor.py +++ b/agent/executor.py @@ -1,50 +1,96 @@ # executor.py -from langchain_classic.agents import AgentExecutor -from langchain.agents import create_agent -from langchain_ollama import ChatOllama -from langchain_core.prompts import ChatPromptTemplate -from config import LLM_HOST, MODEL_NAME - from typing import List, Dict +from langchain_ollama import ChatOllama +from config import LLM_HOST, MODEL_NAME +import time class ExecutorAgent: - def __init__(self, tools): + def __init__(self, tools, llm=None): self.tools = tools - self.llm = ChatOllama( + self.llm = llm or ChatOllama( model=MODEL_NAME, temperature=0.0, base_url=LLM_HOST ) - self.agent = self._create_agent() - - def _create_agent(self): - prompt = ChatPromptTemplate.from_messages([ - ("system", "Ты - исполнитель. У тебя есть доступ к инструментам. Выполняй шаги плана последовательно."), - ("human", "{input}"), - ("placeholder", "{agent_scratchpad}") - ]) - agent = create_agent(self.llm, self.tools, prompt) - return AgentExecutor(agent=agent, tools=self.tools, verbose=True) - - """Устаревший агент-исполнитель. Теперь используется DeveloperAgent напрямую.""" - - def __init__(self, tools=None): - # Этот класс больше не используется, оставлен для обратной совместимости - pass def execute_plan(self, plan: List[Dict]) -> List[Dict]: + """Выполняет план шаг за шагом через инструменты""" results = [] - for step in plan: - tool_name = step["tool"] - args = step["args"] - # Превращаем шаг в текстовую команду для агента - command = f"Вызови инструмент {tool_name} с аргументами {args}" - result = self.agent.invoke({"input": command}) - results.append({ - "step": step, - "output": result["output"], - "success": True # можно анализировать ошибки - }) + total_steps = len(plan) + + print(f"\n{'='*60}") + print(f"🚀 Начало выполнения плана ({total_steps} шагов)") + print(f"{'='*60}") + + for i, step in enumerate(plan, 1): + tool_name = step.get("tool", "unknown") + args = step.get("args", {}) + description = step.get("description", "") + + print(f"\n{'─'*60}") + print(f"📍 Шаг {i}/{total_steps}: {tool_name}") + if description: + print(f" 📝 Описание: {description}") + print(f" 📦 Аргументы: {args}") + print(f" ─────────────────────────────") + + # Находим инструмент по имени + tool = None + for t in self.tools: + if t.name == tool_name: + tool = t + break + + start_time = time.time() + + if tool: + try: + result = tool.invoke(args) + elapsed = time.time() - start_time + + results.append({ + "step": step, + "output": str(result), + "success": True, + "duration": elapsed + }) + + print(f" ✅ Успех ({elapsed:.2f}s)") + print(f" 📤 Результат: {str(result)[:100]}{'...' if len(str(result)) > 100 else ''}") + + except Exception as e: + elapsed = time.time() - start_time + results.append({ + "step": step, + "output": str(e), + "success": False, + "error": str(e), + "duration": elapsed + }) + + print(f" ❌ Ошибка ({elapsed:.2f}s)") + print(f" 💥 {type(e).__name__}: {str(e)[:100]}") + else: + results.append({ + "step": step, + "output": f"Tool '{tool_name}' not found", + "success": False, + "duration": 0 + }) + + print(f" ❌ Инструмент '{tool_name}' не найден") + + # Итоговая статистика + success_count = sum(1 for r in results if r.get("success", False)) + failed_count = total_steps - success_count + total_duration = sum(r.get("duration", 0) for r in results) + + print(f"\n{'='*60}") + print(f"📊 ИТОГИ ВЫПОЛНЕНИЯ") + print(f"{'='*60}") + print(f"✅ Успешно: {success_count}/{total_steps}") + print(f"❌ Ошибки: {failed_count}") + print(f"⏱️ Общее время: {total_duration:.2f}s") + print(f"{'='*60}\n") + return results - """Устаревший метод - теперь выполняется напрямую через DeveloperAgent""" - raise NotImplementedError("Этот метод больше не используется. Используйте DeveloperAgent.develop() напрямую.") \ No newline at end of file diff --git a/config.py b/config.py index 740661d..d39382e 100644 --- a/config.py +++ b/config.py @@ -12,4 +12,4 @@ TEMPERATURE = 0.2 MAX_ITERATIONS = 3 # Конфигурация системы -MAX_REVIEW_RETRIES = 3 \ No newline at end of file +MAX_REVIEW_RETRIES = 1 \ No newline at end of file diff --git a/orchestrator.py b/orchestrator.py index 32c38f1..9b7f714 100644 --- a/orchestrator.py +++ b/orchestrator.py @@ -7,13 +7,16 @@ from datetime import datetime from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord from agent.developer import DeveloperAgent from agent.reviewer import ReviewerAgent -from config import MAX_REVIEW_RETRIES +from agent.planner import PlannerAgent +from agent.executor import ExecutorAgent +from agent.plan_reviewer import PlanReviewerAgent +from tool.filesystem import read_file, write_file, delete_lines, replace_text_in_file, search_in_files class AgentState(TypedDict): task: Task - plan: Dict + plan: List[Dict] execution_results: List[Dict] - review: Optional[ReviewResult] + review: Dict iteration: int max_iterations: int @@ -23,7 +26,14 @@ class Orchestrator: def __init__(self): self.developer = DeveloperAgent() self.reviewer = ReviewerAgent() - self.max_retries = MAX_REVIEW_RETRIES + self.planner = PlannerAgent() + self.plan_reviewer = PlanReviewerAgent() + + # Инструменты для ExecutorAgent + self.tools = [read_file, write_file, delete_lines, replace_text_in_file, search_in_files] + self.executor = ExecutorAgent(self.tools) + + self.max_retries = 3 # Создаем граф состояний self.workflow = self._build_workflow() @@ -84,46 +94,40 @@ class Orchestrator: details={} )) - # План просто содержит задачу - разработчик выполнит её напрямую - plan = {"task": task.requirement, "iteration": task.iteration} + # Описываем доступные инструменты для планировщика + tools_desc = [ + {"name": "read_file", "description": "Прочитать файл", "args": ["path", "start_line", "end_line"]}, + {"name": "write_file", "description": "Записать файл", "args": ["path", "content"]}, + {"name": "delete_lines", "description": "Удалить строки", "args": ["path", "start_line", "end_line"]}, + {"name": "replace_text_in_file", "description": "Заменить текст", "args": ["path", "old", "new"]}, + {"name": "search_in_files", "description": "Поиск в файлах", "args": ["directory", "pattern", "file_pattern"]} + ] + + # Создаем план + plan = self.planner.create_plan(task.requirement, tools_desc) state["plan"] = plan + + print(f"📋 План создан: {len(plan)} шагов") return state def _exec_node(self, state: AgentState): - """Узел разработчика""" + """Узел исполнителя""" task = state["task"] - print(f"\n🔧 [Iteration {task.iteration}] Исполнитель начал работу...") + print(f"\n🔨 [Iteration {task.iteration}] Исполнитель начал работу...") - # Обновляем статус - task.status = TaskStatus.DEVELOPMENT - - # Разработчик выполняет задачу напрямую - solution = self.developer.develop(task, []) - task.solution = solution - task.status = TaskStatus.REVIEW + # Выполняем план через ExecutorAgent + results = self.executor.execute_plan(state["plan"]) + state["execution_results"] = results # Записываем результат выполнения - state["execution_results"] = [{"status": "success", "solution": solution}] + success_count = sum(1 for r in results if r.get("success", False)) + print(f"✅ Выполнено шагов: {success_count}/{len(results)}") - # Добавляем запись в историю - task.history.append(IterationRecord( - iteration=task.iteration, - timestamp=datetime.now().isoformat(), - agent="developer", - action="complete", - output_summary=f"Generated {len(solution.files)} file(s), {len(solution.tests)} test(s)", - details={ - "files": list(solution.files.keys()), - "tests": list(solution.tests.keys()) - } - )) - - print(f"✅ Разработчик создал решение: {len(solution.files)} файлов") return state def _review_node(self, state: AgentState): task = state["task"] - print(f"\n🔍 [Iteration {task.iteration}] Ревьювер начал работу...") + print(f"\n🔍 [Iteration {task.iteration}] План-ревьювер начал работу...") # Добавляем запись о начале ревью task.history.append(IterationRecord( @@ -131,47 +135,41 @@ class Orchestrator: timestamp=datetime.now().isoformat(), agent="reviewer", action="start", - input_summary=f"Reviewing {len(task.solution.files) if task.solution else 0} file(s)", + input_summary=f"Reviewing plan with {len(state['plan'])} steps", details={} )) - if not task.solution: - task.status = TaskStatus.FAILED - error_msg = "No solution to review" - task.error_log.append(error_msg) - task.history.append(IterationRecord( - iteration=task.iteration, - timestamp=datetime.now().isoformat(), - agent="reviewer", - action="error", - error=error_msg - )) - return state + # Проводим ревью плана через PlanReviewerAgent + review = self.plan_reviewer.review(task.requirement, state["plan"], state["execution_results"]) + state["review"] = review - # Проводим ревью - review = self.reviewer.review(task, task.solution) - task.last_review = review - task.iteration += 1 + # Обновляем статус задачи + if review.get("status") == "approved": + task.status = TaskStatus.APPROVED + elif review.get("status") == "changes_requested": + task.status = TaskStatus.REWORK + else: + task.status = TaskStatus.FAILED + + # Увеличиваем счётчик итераций + state["iteration"] = state.get("iteration", 0) + 1 # Добавляем запись в историю task.history.append(IterationRecord( - iteration=task.iteration - 1, # предыдущая итерация + iteration=task.iteration, timestamp=datetime.now().isoformat(), agent="reviewer", action="complete", - output_summary=f"Verdict: {review.status}, Comments: {len(review.comments)}", + output_summary=f"Verdict: {review.get('status')}", details={ - "verdict": review.status, - "comments_count": len(review.comments) + "verdict": review.get("status"), + "feedback": review.get("feedback", "")[:200] } )) - # Сохраняем результат ревью - state["review"] = review - - print(f"📊 Вердикт: {review.status}") - if review.comments: - print(f" Найдено замечаний: {len(review.comments)}") + print(f"📊 Вердикт: {review.get('status')}") + if review.get("feedback"): + print(f" Feedback: {review['feedback'][:100]}...") return state @@ -180,24 +178,18 @@ class Orchestrator: review = state["review"] # Определяем статус - if isinstance(review, ReviewResult): - status = review.status - elif isinstance(review, dict): - status = review.get("status") - else: - status = None + status = review.get("status") if isinstance(review, dict) else None if status == "approved": - task.status = TaskStatus.APPROVED return "approved" elif status == "changes_requested": - if task.iteration >= state.get("max_iterations", 3): + if state.get("iteration", 0) >= state.get("max_iterations", 3): task.status = TaskStatus.FAILED return "reject" task.status = TaskStatus.REWORK return "rework" elif status == "rejected": - if task.iteration >= state.get("max_iterations", 3): + if state.get("iteration", 0) >= state.get("max_iterations", 3): task.status = TaskStatus.FAILED return "reject" task.status = TaskStatus.REWORK @@ -243,9 +235,9 @@ class Orchestrator: initial_state = { "task": task, - "plan": {}, + "plan": [], "execution_results": [], - "review": None, + "review": {}, "iteration": 0, "max_iterations": max_iterations } @@ -257,10 +249,8 @@ class Orchestrator: # Запускаем граф final_state = self.workflow.invoke(initial_state) - # Сохраняем решение если успешно + # Обновляем итоговый статус в задаче final_task = final_state["task"] - if final_task.status == TaskStatus.APPROVED and final_task.solution: - self._save_solution(final_task.solution) print("\n" + "="*50) print("📋 ИТОГИ РАБОТЫ") @@ -268,13 +258,6 @@ class Orchestrator: print(f"Статус: {final_task.status.value}") print(f"Итераций: {final_task.iteration}") - if final_task.status == TaskStatus.APPROVED: - print(f"\n✅ Решение принято!") - print(f" Файлов: {len(final_task.solution.files)}") - print(f" Тестов: {len(final_task.solution.tests)}") - elif final_task.last_review: - print(f"\n📝 Финальный вердикт: {final_task.last_review.summary}") - if final_task.error_log: print(f"\n❌ Ошибки: {len(final_task.error_log)}") for error in final_task.error_log: diff --git a/tool/filesystem.py b/tool/filesystem.py index e30e9e1..979be72 100644 --- a/tool/filesystem.py +++ b/tool/filesystem.py @@ -6,6 +6,8 @@ import re @tool def read_file(path: str, start_line: int = 0, end_line: int = None) -> str: """Прочитать содержимое файла. Можно указать диапазон строк (1-индекс).""" + if not os.path.exists(path): + return f"File not found: {path}" with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() if start_line > 0: @@ -17,7 +19,10 @@ def read_file(path: str, start_line: int = 0, end_line: int = None) -> str: @tool def write_file(path: str, content: str) -> str: """Создать или перезаписать файл с указанным содержимым.""" - os.makedirs(os.path.dirname(path), exist_ok=True) + # Создаем директорию только если путь содержит директорию + dir_name = os.path.dirname(path) + if dir_name: + os.makedirs(dir_name, exist_ok=True) with open(path, 'w', encoding='utf-8') as f: f.write(content) return f"Файл {path} записан" @@ -25,20 +30,24 @@ def write_file(path: str, content: str) -> str: @tool def delete_lines(path: str, start_line: int, end_line: int) -> str: """Удалить строки из файла (1-индекс, включительно).""" - with open(path, 'r') as f: + if not os.path.exists(path): + return f"File not found: {path}" + with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() del lines[start_line-1:end_line] - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: f.writelines(lines) return f"Удалены строки {start_line}-{end_line} из {path}" @tool def replace_text_in_file(path: str, old: str, new: str) -> str: """Заменить все вхождения old на new в файле.""" - with open(path, 'r') as f: + if not os.path.exists(path): + return f"File not found: {path}" + with open(path, 'r', encoding='utf-8') as f: content = f.read() new_content = content.replace(old, new) - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: f.write(new_content) return f"Заменено {content.count(old)} вхождений"