Переделываю систему под агента-планировщика
This commit is contained in:
@@ -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
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
|
||||
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()
|
||||
|
||||
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:
|
||||
"""Узел разработчика"""
|
||||
@@ -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())
|
||||
|
||||
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
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