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

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

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