Переделываю систему под агента-планировщика
This commit is contained in:
@@ -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
37
agent/executor.py
Normal 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
26
agent/plan_reviewer.py
Normal 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
40
agent/planner.py
Normal 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
|
||||||
109
orchestrator.py
109
orchestrator.py
@@ -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
0
tool/__init__.py
Normal file
73
tool/filesystem.py
Normal file
73
tool/filesystem.py
Normal 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}"
|
||||||
Reference in New Issue
Block a user