From 8f16a6d6695d124935dada1a59498b1d32fdc183 Mon Sep 17 00:00:00 2001 From: "ki.sagidullin" Date: Sun, 12 Apr 2026 09:27:07 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=8E=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D1=83=20=D0=BF=D0=BE=D0=B4=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0-=D0=BF=D0=BB=D0=B0=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D1=89?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent/__init__.py | 3 +- agent/executor.py | 37 ++++++++++++++ agent/plan_reviewer.py | 26 ++++++++++ agent/planner.py | 40 +++++++++++++++ orchestrator.py | 111 ++++++++++++++++++++++++++++------------- tool/__init__.py | 0 tool/filesystem.py | 73 +++++++++++++++++++++++++++ 7 files changed, 255 insertions(+), 35 deletions(-) create mode 100644 agent/executor.py create mode 100644 agent/plan_reviewer.py create mode 100644 agent/planner.py create mode 100644 tool/__init__.py create mode 100644 tool/filesystem.py diff --git a/agent/__init__.py b/agent/__init__.py index cb26b80..d698911 100644 --- a/agent/__init__.py +++ b/agent/__init__.py @@ -1,4 +1,5 @@ from .developer import DeveloperAgent from .reviewer import ReviewerAgent +from .planner import PlannerAgent -__all__ = ["DeveloperAgent", "ReviewerAgent"] \ No newline at end of file +__all__ = ["DeveloperAgent", "ReviewerAgent", "PlannerAgent"] \ No newline at end of file diff --git a/agent/executor.py b/agent/executor.py new file mode 100644 index 0000000..737d0ae --- /dev/null +++ b/agent/executor.py @@ -0,0 +1,37 @@ +# executor.py +from langchain.agents import AgentExecutor, create_tool_calling_agent +from langchain_ollama import ChatOllama +from langchain_core.prompts import ChatPromptTemplate + +class ToolExecutor: + def __init__(self, tools): + self.tools = tools + self.llm = llm or ChatOllama( + model=MODEL_NAME, + temperature=0.0 + ) + self.agent = self._create_agent() + + def _create_agent(self): + prompt = ChatPromptTemplate.from_messages([ + ("system", "Ты - исполнитель. У тебя есть доступ к инструментам. Выполняй шаги плана последовательно."), + ("human", "{input}"), + ("placeholder", "{agent_scratchpad}") + ]) + agent = create_tool_calling_agent(self.llm, self.tools, prompt) + return AgentExecutor(agent=agent, tools=self.tools, verbose=True) + + 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 # можно анализировать ошибки + }) + return results \ No newline at end of file diff --git a/agent/plan_reviewer.py b/agent/plan_reviewer.py new file mode 100644 index 0000000..cfedf8e --- /dev/null +++ b/agent/plan_reviewer.py @@ -0,0 +1,26 @@ +# reviewer.py +from langchain_ollama import ChatOllama +from langchain_core.messages import SystemMessage, HumanMessage + +class PlanReviewerAgent: + def __init__(self, llm=None): + self.llm = llm or ChatOllama( + model=MODEL_NAME, + temperature=0.0 + ) + + def review(self, original_task: str, plan: List[Dict], execution_results: List[Dict]) -> dict: + system_prompt = """Ты - ревьювер. Оцени, достигнута ли цель задачи на основе выполненного плана. +Верни JSON с полями: + - "status": "approved" / "changes_requested" / "rejected" + - "feedback": пояснение + - "suggested_plan_correction": если нужно изменить план - предложи новый план (массив шагов) +""" + user_prompt = f""" +Задача: {original_task} +План: {json.dumps(plan, indent=2)} +Результаты выполнения: {json.dumps(execution_results, indent=2)} +Оцени результат. +""" + response = self.llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=user_prompt)]) + return json.loads(response.content) \ No newline at end of file diff --git a/agent/planner.py b/agent/planner.py new file mode 100644 index 0000000..916e41c --- /dev/null +++ b/agent/planner.py @@ -0,0 +1,40 @@ +# planner.py +from langchain_ollama import ChatOllama +from langchain_core.messages import SystemMessage, HumanMessage +import json +from typing import List, Dict +from config import MODEL_NAME + +class PlannerAgent: + def __init__(self, llm=None): + self.llm = llm or ChatOllama( + model=MODEL_NAME, + temperature=0.0 + ) + + def create_plan(self, task: str, tools_description: List[Dict]) -> List[Dict]: + system_prompt = """Ты - планировщик. Твоя задача - разбить запрос пользователя на последовательность шагов. +Каждый шаг - это вызов одного из доступных инструментов с конкретными аргументами. +Верни JSON массив шагов, где каждый шаг содержит: + - "tool": имя инструмента (строго из списка) + - "args": словарь аргументов для этого инструмента + - "description": краткое пояснение, зачем этот шаг + +Инструменты и их аргументы: +{tools_description} + +План должен быть линейным (шаг за шагом). Если нужны условия или циклы - разворачивай их в последовательность. +После выполнения всех шагов задача должна быть решена. +Верни ТОЛЬКО JSON массив, без лишнего текста.""" + + user_prompt = f"Задача: {task}" + tools_str = json.dumps(tools_description, indent=2) + + messages = [ + SystemMessage(content=system_prompt.format(tools_description=tools_str)), + HumanMessage(content=user_prompt) + ] + response = self.llm.invoke(messages) + # парсим JSON + plan = json.loads(response.content) + return plan \ No newline at end of file diff --git a/orchestrator.py b/orchestrator.py index d596c2d..8155e79 100644 --- a/orchestrator.py +++ b/orchestrator.py @@ -1,5 +1,5 @@ # orchestrator.py -from typing import Dict, Any, Literal +from typing import Dict, Any, Literal, TypedDict, List from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver import uuid @@ -8,6 +8,14 @@ from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord from agent import DeveloperAgent, ReviewerAgent from config import MAX_REVIEW_RETRIES +class AgentState(TypedDict): + task: str + plan: List[Dict] + execution_results: List[Dict] + review: Dict + iteration: int + max_iterations: int + class Orchestrator: """Оркестратор - управляет процессом разработки""" @@ -19,7 +27,6 @@ class Orchestrator: # Создаем граф состояний self.workflow = self._build_workflow() self.checkpointer = MemorySaver() - self.app = self.workflow.compile() def _add_history(self, task: Task, agent: str, action: str, input_summary: str = None, output_summary: str = None, @@ -37,36 +44,72 @@ class Orchestrator: ) task.history.append(record) - def _build_workflow(self) -> StateGraph: + def _build_workflow(self): """Строит граф процесса разработки""" # Определяем состояния - workflow = StateGraph(Dict) + workflow = StateGraph(AgentState) # Добавляем узлы - workflow.add_node("developer", self._developer_node) - workflow.add_node("reviewer", self._reviewer_node) - workflow.add_node("finalize", self._finalize_node) + workflow.add_node("planner", self._plan_node) + workflow.add_node("executor", self._exec_node) + workflow.add_node("reviewer", self._review_node) # Определяем переходы - workflow.set_entry_point("developer") - workflow.add_edge("developer", "reviewer") + workflow.set_entry_point("planner") + workflow.add_edge("planner", "executor") + workflow.add_edge("executor", "reviewer") - # Условные переходы после ревью - workflow.add_conditional_edges( - "reviewer", - self._decide_next_step, - { - "approved": "finalize", - "rework": "developer", - "rejected": "finalize", - "failed": "finalize" - } - ) + workflow.add_conditional_edges("reviewer", self._after_review, { + "approved": END, + "rework": "executor", # повторить выполнение с тем же планом? или перепланировать? + "replan": "planner", + "reject": END + }) - workflow.add_edge("finalize", END) + return workflow.compile() + + def _plan_node(self, state: AgentState): + print(f"\n🔧 [Iteration {state.get('iteration', 0)}] Планировщик начал работу...") + # Сначала получим описания инструментов для планировщика + tools_desc = [{"name": t.name, "description": t.description, "args": t.args} for t in self.tools] + plan = self.planner.create_plan(state["task"], tools_desc) + state["plan"] = plan + return state + + def _exec_node(self, state: AgentState): + """Узел разработчика""" + print(f"\n🔧 [Iteration {state.get('iteration', 0)}] Исполнитель начал работу...") - return workflow + # Логируем начало работы разработчика + results = self.executor.execute_plan(state["plan"]) + state["execution_results"] = results + return state + + def _review_node(self, state: AgentState): + print(f"\n🔧 [Iteration {state.get('iteration', 0)}] Ревьювер начал работу...") + + review = self.reviewer.review(state["task"], state["plan"], state["execution_results"]) + state["review"] = review + state["iteration"] = state.get("iteration", 0) + 1 + return state + + def _after_review(self, state: AgentState): + status = state["review"].get("status") + if status == "approved": + return "approved" + elif status == "changes_requested": + if state["iteration"] >= state.get("max_iterations", 3): + return "reject" + # пробуем перевыполнить те же шаги (может, ошибка временная) + return "rework" + elif status == "rejected": + if state["iteration"] >= state.get("max_iterations", 3): + return "reject" + # нужен новый план + return "replan" + else: + return "reject" def _developer_node(self, state: Dict) -> Dict: """Узел разработчика""" @@ -244,26 +287,26 @@ class Orchestrator: f.write(test_content) print(f" 💾 Сохранен тест: {testpath}") - def run(self, requirement: str, task_id: str = None) -> Dict: + def run(self, task: str, max_iterations:int = None) -> Dict: """Запускает процесс разработки""" - if not task_id: - task_id = str(uuid.uuid4()) + task_id = str(uuid.uuid4()) - initial_task = Task( - id=task_id, - requirement=requirement, - status=TaskStatus.PENDING, - iteration=0 - ) + initial_state = { + "task": task, + "plan": [], + "execution_results": [], + "review": {}, + "iteration": 0, + "max_iterations": max_iterations + } print(f"\n🚀 Запуск мультиагентной системы") - print(f"📝 Задача: {requirement}") + print(f"📝 Задача: {task}") print(f"🆔 ID: {task_id}") # Запускаем граф - initial_state = {"task": initial_task} - final_state = self.app.invoke(initial_state) + final_state = self.workflow.invoke(initial_state) return final_state diff --git a/tool/__init__.py b/tool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tool/filesystem.py b/tool/filesystem.py new file mode 100644 index 0000000..e30e9e1 --- /dev/null +++ b/tool/filesystem.py @@ -0,0 +1,73 @@ +from langchain.tools import tool +import os +import subprocess +import re + +@tool +def read_file(path: str, start_line: int = 0, end_line: int = None) -> str: + """Прочитать содержимое файла. Можно указать диапазон строк (1-индекс).""" + with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + if start_line > 0: + lines = lines[start_line-1:] + if end_line: + lines = lines[:end_line-start_line+1] + return ''.join(lines) + +@tool +def write_file(path: str, content: str) -> str: + """Создать или перезаписать файл с указанным содержимым.""" + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w', encoding='utf-8') as f: + f.write(content) + return f"Файл {path} записан" + +@tool +def delete_lines(path: str, start_line: int, end_line: int) -> str: + """Удалить строки из файла (1-индекс, включительно).""" + with open(path, 'r') as f: + lines = f.readlines() + del lines[start_line-1:end_line] + with open(path, 'w') 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: + content = f.read() + new_content = content.replace(old, new) + with open(path, 'w') as f: + f.write(new_content) + return f"Заменено {content.count(old)} вхождений" + +@tool +def search_in_files(directory: str, pattern: str, file_pattern: str = "*.py") -> str: + """Найти все файлы, содержащие pattern (grep).""" + cmd = f"grep -l --include='{file_pattern}' -r '{pattern}' {directory}" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.stdout.strip() + +# @tool +# def run_command(command: str) -> str: +# """Выполнить системную команду и вернуть вывод.""" +# result = subprocess.run(command, shell=True, capture_output=True, text=True) +# return f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + +# @tool +# def find_class_definition(file_path: str, class_name: str) -> str: +# """Найти в файле определение класса (простой поиск по regex).""" +# with open(file_path, 'r') as f: +# content = f.read() +# pattern = rf'^\s*class\s+{class_name}\s*[:\(]' +# match = re.search(pattern, content, re.MULTILINE) +# if match: +# # найти конец класса по отступам (упрощённо) +# lines = content.splitlines() +# start_line = content[:match.start()].count('\n') + 1 +# # грубо ищем до следующего class или def на том же уровне +# # для простоты вернём 20 строк после +# end_line = start_line + 20 +# return f"Класс {class_name} найден в {file_path} (строки {start_line}-{end_line})" +# return f"Класс {class_name} не найден в {file_path}" \ No newline at end of file