Переделываю систему под агента-планировщика

This commit is contained in:
ki.sagidullin
2026-04-12 09:27:07 +05:00
parent ee6d12c5fb
commit 8f16a6d669
7 changed files with 255 additions and 35 deletions

View File

@@ -1,4 +1,5 @@
from .developer import DeveloperAgent from .developer import DeveloperAgent
from .reviewer import ReviewerAgent from .reviewer import ReviewerAgent
from .planner import PlannerAgent
__all__ = ["DeveloperAgent", "ReviewerAgent"] __all__ = ["DeveloperAgent", "ReviewerAgent", "PlannerAgent"]

37
agent/executor.py Normal file
View File

@@ -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

26
agent/plan_reviewer.py Normal file
View File

@@ -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)

40
agent/planner.py Normal file
View File

@@ -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

View File

@@ -1,5 +1,5 @@
# orchestrator.py # orchestrator.py
from typing import Dict, Any, Literal from typing import Dict, Any, Literal, TypedDict, List
from langgraph.graph import StateGraph, END from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.memory import MemorySaver
import uuid import uuid
@@ -8,6 +8,14 @@ from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
from agent import DeveloperAgent, ReviewerAgent from agent import DeveloperAgent, ReviewerAgent
from config import MAX_REVIEW_RETRIES 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: class Orchestrator:
"""Оркестратор - управляет процессом разработки""" """Оркестратор - управляет процессом разработки"""
@@ -19,7 +27,6 @@ class Orchestrator:
# Создаем граф состояний # Создаем граф состояний
self.workflow = self._build_workflow() self.workflow = self._build_workflow()
self.checkpointer = MemorySaver() self.checkpointer = MemorySaver()
self.app = self.workflow.compile()
def _add_history(self, task: Task, agent: str, action: str, def _add_history(self, task: Task, agent: str, action: str,
input_summary: str = None, output_summary: str = None, input_summary: str = None, output_summary: str = None,
@@ -37,36 +44,72 @@ class Orchestrator:
) )
task.history.append(record) 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("planner", self._plan_node)
workflow.add_node("reviewer", self._reviewer_node) workflow.add_node("executor", self._exec_node)
workflow.add_node("finalize", self._finalize_node) workflow.add_node("reviewer", self._review_node)
# Определяем переходы # Определяем переходы
workflow.set_entry_point("developer") workflow.set_entry_point("planner")
workflow.add_edge("developer", "reviewer") workflow.add_edge("planner", "executor")
workflow.add_edge("executor", "reviewer")
# Условные переходы после ревью workflow.add_conditional_edges("reviewer", self._after_review, {
workflow.add_conditional_edges( "approved": END,
"reviewer", "rework": "executor", # повторить выполнение с тем же планом? или перепланировать?
self._decide_next_step, "replan": "planner",
{ "reject": END
"approved": "finalize", })
"rework": "developer",
"rejected": "finalize",
"failed": "finalize"
}
)
workflow.add_edge("finalize", END) return workflow.compile()
return workflow 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)}] Исполнитель начал работу...")
# Логируем начало работы разработчика
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: def _developer_node(self, state: Dict) -> Dict:
"""Узел разработчика""" """Узел разработчика"""
@@ -244,26 +287,26 @@ class Orchestrator:
f.write(test_content) f.write(test_content)
print(f" 💾 Сохранен тест: {testpath}") 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( initial_state = {
id=task_id, "task": task,
requirement=requirement, "plan": [],
status=TaskStatus.PENDING, "execution_results": [],
iteration=0 "review": {},
) "iteration": 0,
"max_iterations": max_iterations
}
print(f"\n🚀 Запуск мультиагентной системы") print(f"\n🚀 Запуск мультиагентной системы")
print(f"📝 Задача: {requirement}") print(f"📝 Задача: {task}")
print(f"🆔 ID: {task_id}") print(f"🆔 ID: {task_id}")
# Запускаем граф # Запускаем граф
initial_state = {"task": initial_task} final_state = self.workflow.invoke(initial_state)
final_state = self.app.invoke(initial_state)
return final_state return final_state

0
tool/__init__.py Normal file
View File

73
tool/filesystem.py Normal file
View File

@@ -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}"