Итерация 3, финальная
This commit is contained in:
@@ -1,50 +1,96 @@
|
|||||||
# executor.py
|
# executor.py
|
||||||
from langchain_classic.agents import AgentExecutor
|
|
||||||
from langchain.agents import create_agent
|
|
||||||
from langchain_ollama import ChatOllama
|
|
||||||
from langchain_core.prompts import ChatPromptTemplate
|
|
||||||
from config import LLM_HOST, MODEL_NAME
|
|
||||||
|
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
from langchain_ollama import ChatOllama
|
||||||
|
from config import LLM_HOST, MODEL_NAME
|
||||||
|
import time
|
||||||
|
|
||||||
class ExecutorAgent:
|
class ExecutorAgent:
|
||||||
def __init__(self, tools):
|
def __init__(self, tools, llm=None):
|
||||||
self.tools = tools
|
self.tools = tools
|
||||||
self.llm = ChatOllama(
|
self.llm = llm or ChatOllama(
|
||||||
model=MODEL_NAME,
|
model=MODEL_NAME,
|
||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
base_url=LLM_HOST
|
base_url=LLM_HOST
|
||||||
)
|
)
|
||||||
self.agent = self._create_agent()
|
|
||||||
|
|
||||||
def _create_agent(self):
|
|
||||||
prompt = ChatPromptTemplate.from_messages([
|
|
||||||
("system", "Ты - исполнитель. У тебя есть доступ к инструментам. Выполняй шаги плана последовательно."),
|
|
||||||
("human", "{input}"),
|
|
||||||
("placeholder", "{agent_scratchpad}")
|
|
||||||
])
|
|
||||||
agent = create_agent(self.llm, self.tools, prompt)
|
|
||||||
return AgentExecutor(agent=agent, tools=self.tools, verbose=True)
|
|
||||||
|
|
||||||
"""Устаревший агент-исполнитель. Теперь используется DeveloperAgent напрямую."""
|
|
||||||
|
|
||||||
def __init__(self, tools=None):
|
|
||||||
# Этот класс больше не используется, оставлен для обратной совместимости
|
|
||||||
pass
|
|
||||||
|
|
||||||
def execute_plan(self, plan: List[Dict]) -> List[Dict]:
|
def execute_plan(self, plan: List[Dict]) -> List[Dict]:
|
||||||
|
"""Выполняет план шаг за шагом через инструменты"""
|
||||||
results = []
|
results = []
|
||||||
for step in plan:
|
total_steps = len(plan)
|
||||||
tool_name = step["tool"]
|
|
||||||
args = step["args"]
|
print(f"\n{'='*60}")
|
||||||
# Превращаем шаг в текстовую команду для агента
|
print(f"🚀 Начало выполнения плана ({total_steps} шагов)")
|
||||||
command = f"Вызови инструмент {tool_name} с аргументами {args}"
|
print(f"{'='*60}")
|
||||||
result = self.agent.invoke({"input": command})
|
|
||||||
results.append({
|
for i, step in enumerate(plan, 1):
|
||||||
"step": step,
|
tool_name = step.get("tool", "unknown")
|
||||||
"output": result["output"],
|
args = step.get("args", {})
|
||||||
"success": True # можно анализировать ошибки
|
description = step.get("description", "")
|
||||||
})
|
|
||||||
|
print(f"\n{'─'*60}")
|
||||||
|
print(f"📍 Шаг {i}/{total_steps}: {tool_name}")
|
||||||
|
if description:
|
||||||
|
print(f" 📝 Описание: {description}")
|
||||||
|
print(f" 📦 Аргументы: {args}")
|
||||||
|
print(f" ─────────────────────────────")
|
||||||
|
|
||||||
|
# Находим инструмент по имени
|
||||||
|
tool = None
|
||||||
|
for t in self.tools:
|
||||||
|
if t.name == tool_name:
|
||||||
|
tool = t
|
||||||
|
break
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
if tool:
|
||||||
|
try:
|
||||||
|
result = tool.invoke(args)
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"step": step,
|
||||||
|
"output": str(result),
|
||||||
|
"success": True,
|
||||||
|
"duration": elapsed
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" ✅ Успех ({elapsed:.2f}s)")
|
||||||
|
print(f" 📤 Результат: {str(result)[:100]}{'...' if len(str(result)) > 100 else ''}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
results.append({
|
||||||
|
"step": step,
|
||||||
|
"output": str(e),
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"duration": elapsed
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" ❌ Ошибка ({elapsed:.2f}s)")
|
||||||
|
print(f" 💥 {type(e).__name__}: {str(e)[:100]}")
|
||||||
|
else:
|
||||||
|
results.append({
|
||||||
|
"step": step,
|
||||||
|
"output": f"Tool '{tool_name}' not found",
|
||||||
|
"success": False,
|
||||||
|
"duration": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" ❌ Инструмент '{tool_name}' не найден")
|
||||||
|
|
||||||
|
# Итоговая статистика
|
||||||
|
success_count = sum(1 for r in results if r.get("success", False))
|
||||||
|
failed_count = total_steps - success_count
|
||||||
|
total_duration = sum(r.get("duration", 0) for r in results)
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"📊 ИТОГИ ВЫПОЛНЕНИЯ")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f"✅ Успешно: {success_count}/{total_steps}")
|
||||||
|
print(f"❌ Ошибки: {failed_count}")
|
||||||
|
print(f"⏱️ Общее время: {total_duration:.2f}s")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
"""Устаревший метод - теперь выполняется напрямую через DeveloperAgent"""
|
|
||||||
raise NotImplementedError("Этот метод больше не используется. Используйте DeveloperAgent.develop() напрямую.")
|
|
||||||
@@ -12,4 +12,4 @@ TEMPERATURE = 0.2
|
|||||||
MAX_ITERATIONS = 3
|
MAX_ITERATIONS = 3
|
||||||
|
|
||||||
# Конфигурация системы
|
# Конфигурация системы
|
||||||
MAX_REVIEW_RETRIES = 3
|
MAX_REVIEW_RETRIES = 1
|
||||||
141
orchestrator.py
141
orchestrator.py
@@ -7,13 +7,16 @@ from datetime import datetime
|
|||||||
from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
|
from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
|
||||||
from agent.developer import DeveloperAgent
|
from agent.developer import DeveloperAgent
|
||||||
from agent.reviewer import ReviewerAgent
|
from agent.reviewer import ReviewerAgent
|
||||||
from config import MAX_REVIEW_RETRIES
|
from agent.planner import PlannerAgent
|
||||||
|
from agent.executor import ExecutorAgent
|
||||||
|
from agent.plan_reviewer import PlanReviewerAgent
|
||||||
|
from tool.filesystem import read_file, write_file, delete_lines, replace_text_in_file, search_in_files
|
||||||
|
|
||||||
class AgentState(TypedDict):
|
class AgentState(TypedDict):
|
||||||
task: Task
|
task: Task
|
||||||
plan: Dict
|
plan: List[Dict]
|
||||||
execution_results: List[Dict]
|
execution_results: List[Dict]
|
||||||
review: Optional[ReviewResult]
|
review: Dict
|
||||||
iteration: int
|
iteration: int
|
||||||
max_iterations: int
|
max_iterations: int
|
||||||
|
|
||||||
@@ -23,7 +26,14 @@ class Orchestrator:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.developer = DeveloperAgent()
|
self.developer = DeveloperAgent()
|
||||||
self.reviewer = ReviewerAgent()
|
self.reviewer = ReviewerAgent()
|
||||||
self.max_retries = MAX_REVIEW_RETRIES
|
self.planner = PlannerAgent()
|
||||||
|
self.plan_reviewer = PlanReviewerAgent()
|
||||||
|
|
||||||
|
# Инструменты для ExecutorAgent
|
||||||
|
self.tools = [read_file, write_file, delete_lines, replace_text_in_file, search_in_files]
|
||||||
|
self.executor = ExecutorAgent(self.tools)
|
||||||
|
|
||||||
|
self.max_retries = 3
|
||||||
|
|
||||||
# Создаем граф состояний
|
# Создаем граф состояний
|
||||||
self.workflow = self._build_workflow()
|
self.workflow = self._build_workflow()
|
||||||
@@ -84,46 +94,40 @@ class Orchestrator:
|
|||||||
details={}
|
details={}
|
||||||
))
|
))
|
||||||
|
|
||||||
# План просто содержит задачу - разработчик выполнит её напрямую
|
# Описываем доступные инструменты для планировщика
|
||||||
plan = {"task": task.requirement, "iteration": task.iteration}
|
tools_desc = [
|
||||||
|
{"name": "read_file", "description": "Прочитать файл", "args": ["path", "start_line", "end_line"]},
|
||||||
|
{"name": "write_file", "description": "Записать файл", "args": ["path", "content"]},
|
||||||
|
{"name": "delete_lines", "description": "Удалить строки", "args": ["path", "start_line", "end_line"]},
|
||||||
|
{"name": "replace_text_in_file", "description": "Заменить текст", "args": ["path", "old", "new"]},
|
||||||
|
{"name": "search_in_files", "description": "Поиск в файлах", "args": ["directory", "pattern", "file_pattern"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Создаем план
|
||||||
|
plan = self.planner.create_plan(task.requirement, tools_desc)
|
||||||
state["plan"] = plan
|
state["plan"] = plan
|
||||||
|
|
||||||
|
print(f"📋 План создан: {len(plan)} шагов")
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def _exec_node(self, state: AgentState):
|
def _exec_node(self, state: AgentState):
|
||||||
"""Узел разработчика"""
|
"""Узел исполнителя"""
|
||||||
task = state["task"]
|
task = state["task"]
|
||||||
print(f"\n🔧 [Iteration {task.iteration}] Исполнитель начал работу...")
|
print(f"\n🔨 [Iteration {task.iteration}] Исполнитель начал работу...")
|
||||||
|
|
||||||
# Обновляем статус
|
# Выполняем план через ExecutorAgent
|
||||||
task.status = TaskStatus.DEVELOPMENT
|
results = self.executor.execute_plan(state["plan"])
|
||||||
|
state["execution_results"] = results
|
||||||
# Разработчик выполняет задачу напрямую
|
|
||||||
solution = self.developer.develop(task, [])
|
|
||||||
task.solution = solution
|
|
||||||
task.status = TaskStatus.REVIEW
|
|
||||||
|
|
||||||
# Записываем результат выполнения
|
# Записываем результат выполнения
|
||||||
state["execution_results"] = [{"status": "success", "solution": solution}]
|
success_count = sum(1 for r in results if r.get("success", False))
|
||||||
|
print(f"✅ Выполнено шагов: {success_count}/{len(results)}")
|
||||||
|
|
||||||
# Добавляем запись в историю
|
|
||||||
task.history.append(IterationRecord(
|
|
||||||
iteration=task.iteration,
|
|
||||||
timestamp=datetime.now().isoformat(),
|
|
||||||
agent="developer",
|
|
||||||
action="complete",
|
|
||||||
output_summary=f"Generated {len(solution.files)} file(s), {len(solution.tests)} test(s)",
|
|
||||||
details={
|
|
||||||
"files": list(solution.files.keys()),
|
|
||||||
"tests": list(solution.tests.keys())
|
|
||||||
}
|
|
||||||
))
|
|
||||||
|
|
||||||
print(f"✅ Разработчик создал решение: {len(solution.files)} файлов")
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def _review_node(self, state: AgentState):
|
def _review_node(self, state: AgentState):
|
||||||
task = state["task"]
|
task = state["task"]
|
||||||
print(f"\n🔍 [Iteration {task.iteration}] Ревьювер начал работу...")
|
print(f"\n🔍 [Iteration {task.iteration}] План-ревьювер начал работу...")
|
||||||
|
|
||||||
# Добавляем запись о начале ревью
|
# Добавляем запись о начале ревью
|
||||||
task.history.append(IterationRecord(
|
task.history.append(IterationRecord(
|
||||||
@@ -131,47 +135,41 @@ class Orchestrator:
|
|||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
agent="reviewer",
|
agent="reviewer",
|
||||||
action="start",
|
action="start",
|
||||||
input_summary=f"Reviewing {len(task.solution.files) if task.solution else 0} file(s)",
|
input_summary=f"Reviewing plan with {len(state['plan'])} steps",
|
||||||
details={}
|
details={}
|
||||||
))
|
))
|
||||||
|
|
||||||
if not task.solution:
|
# Проводим ревью плана через PlanReviewerAgent
|
||||||
task.status = TaskStatus.FAILED
|
review = self.plan_reviewer.review(task.requirement, state["plan"], state["execution_results"])
|
||||||
error_msg = "No solution to review"
|
state["review"] = review
|
||||||
task.error_log.append(error_msg)
|
|
||||||
task.history.append(IterationRecord(
|
|
||||||
iteration=task.iteration,
|
|
||||||
timestamp=datetime.now().isoformat(),
|
|
||||||
agent="reviewer",
|
|
||||||
action="error",
|
|
||||||
error=error_msg
|
|
||||||
))
|
|
||||||
return state
|
|
||||||
|
|
||||||
# Проводим ревью
|
# Обновляем статус задачи
|
||||||
review = self.reviewer.review(task, task.solution)
|
if review.get("status") == "approved":
|
||||||
task.last_review = review
|
task.status = TaskStatus.APPROVED
|
||||||
task.iteration += 1
|
elif review.get("status") == "changes_requested":
|
||||||
|
task.status = TaskStatus.REWORK
|
||||||
|
else:
|
||||||
|
task.status = TaskStatus.FAILED
|
||||||
|
|
||||||
|
# Увеличиваем счётчик итераций
|
||||||
|
state["iteration"] = state.get("iteration", 0) + 1
|
||||||
|
|
||||||
# Добавляем запись в историю
|
# Добавляем запись в историю
|
||||||
task.history.append(IterationRecord(
|
task.history.append(IterationRecord(
|
||||||
iteration=task.iteration - 1, # предыдущая итерация
|
iteration=task.iteration,
|
||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
agent="reviewer",
|
agent="reviewer",
|
||||||
action="complete",
|
action="complete",
|
||||||
output_summary=f"Verdict: {review.status}, Comments: {len(review.comments)}",
|
output_summary=f"Verdict: {review.get('status')}",
|
||||||
details={
|
details={
|
||||||
"verdict": review.status,
|
"verdict": review.get("status"),
|
||||||
"comments_count": len(review.comments)
|
"feedback": review.get("feedback", "")[:200]
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
# Сохраняем результат ревью
|
print(f"📊 Вердикт: {review.get('status')}")
|
||||||
state["review"] = review
|
if review.get("feedback"):
|
||||||
|
print(f" Feedback: {review['feedback'][:100]}...")
|
||||||
print(f"📊 Вердикт: {review.status}")
|
|
||||||
if review.comments:
|
|
||||||
print(f" Найдено замечаний: {len(review.comments)}")
|
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@@ -180,24 +178,18 @@ class Orchestrator:
|
|||||||
review = state["review"]
|
review = state["review"]
|
||||||
|
|
||||||
# Определяем статус
|
# Определяем статус
|
||||||
if isinstance(review, ReviewResult):
|
status = review.get("status") if isinstance(review, dict) else None
|
||||||
status = review.status
|
|
||||||
elif isinstance(review, dict):
|
|
||||||
status = review.get("status")
|
|
||||||
else:
|
|
||||||
status = None
|
|
||||||
|
|
||||||
if status == "approved":
|
if status == "approved":
|
||||||
task.status = TaskStatus.APPROVED
|
|
||||||
return "approved"
|
return "approved"
|
||||||
elif status == "changes_requested":
|
elif status == "changes_requested":
|
||||||
if task.iteration >= state.get("max_iterations", 3):
|
if state.get("iteration", 0) >= state.get("max_iterations", 3):
|
||||||
task.status = TaskStatus.FAILED
|
task.status = TaskStatus.FAILED
|
||||||
return "reject"
|
return "reject"
|
||||||
task.status = TaskStatus.REWORK
|
task.status = TaskStatus.REWORK
|
||||||
return "rework"
|
return "rework"
|
||||||
elif status == "rejected":
|
elif status == "rejected":
|
||||||
if task.iteration >= state.get("max_iterations", 3):
|
if state.get("iteration", 0) >= state.get("max_iterations", 3):
|
||||||
task.status = TaskStatus.FAILED
|
task.status = TaskStatus.FAILED
|
||||||
return "reject"
|
return "reject"
|
||||||
task.status = TaskStatus.REWORK
|
task.status = TaskStatus.REWORK
|
||||||
@@ -243,9 +235,9 @@ class Orchestrator:
|
|||||||
|
|
||||||
initial_state = {
|
initial_state = {
|
||||||
"task": task,
|
"task": task,
|
||||||
"plan": {},
|
"plan": [],
|
||||||
"execution_results": [],
|
"execution_results": [],
|
||||||
"review": None,
|
"review": {},
|
||||||
"iteration": 0,
|
"iteration": 0,
|
||||||
"max_iterations": max_iterations
|
"max_iterations": max_iterations
|
||||||
}
|
}
|
||||||
@@ -257,10 +249,8 @@ class Orchestrator:
|
|||||||
# Запускаем граф
|
# Запускаем граф
|
||||||
final_state = self.workflow.invoke(initial_state)
|
final_state = self.workflow.invoke(initial_state)
|
||||||
|
|
||||||
# Сохраняем решение если успешно
|
# Обновляем итоговый статус в задаче
|
||||||
final_task = final_state["task"]
|
final_task = final_state["task"]
|
||||||
if final_task.status == TaskStatus.APPROVED and final_task.solution:
|
|
||||||
self._save_solution(final_task.solution)
|
|
||||||
|
|
||||||
print("\n" + "="*50)
|
print("\n" + "="*50)
|
||||||
print("📋 ИТОГИ РАБОТЫ")
|
print("📋 ИТОГИ РАБОТЫ")
|
||||||
@@ -268,13 +258,6 @@ class Orchestrator:
|
|||||||
print(f"Статус: {final_task.status.value}")
|
print(f"Статус: {final_task.status.value}")
|
||||||
print(f"Итераций: {final_task.iteration}")
|
print(f"Итераций: {final_task.iteration}")
|
||||||
|
|
||||||
if final_task.status == TaskStatus.APPROVED:
|
|
||||||
print(f"\n✅ Решение принято!")
|
|
||||||
print(f" Файлов: {len(final_task.solution.files)}")
|
|
||||||
print(f" Тестов: {len(final_task.solution.tests)}")
|
|
||||||
elif final_task.last_review:
|
|
||||||
print(f"\n📝 Финальный вердикт: {final_task.last_review.summary}")
|
|
||||||
|
|
||||||
if final_task.error_log:
|
if final_task.error_log:
|
||||||
print(f"\n❌ Ошибки: {len(final_task.error_log)}")
|
print(f"\n❌ Ошибки: {len(final_task.error_log)}")
|
||||||
for error in final_task.error_log:
|
for error in final_task.error_log:
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import re
|
|||||||
@tool
|
@tool
|
||||||
def read_file(path: str, start_line: int = 0, end_line: int = None) -> str:
|
def read_file(path: str, start_line: int = 0, end_line: int = None) -> str:
|
||||||
"""Прочитать содержимое файла. Можно указать диапазон строк (1-индекс)."""
|
"""Прочитать содержимое файла. Можно указать диапазон строк (1-индекс)."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return f"File not found: {path}"
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
if start_line > 0:
|
if start_line > 0:
|
||||||
@@ -17,7 +19,10 @@ def read_file(path: str, start_line: int = 0, end_line: int = None) -> str:
|
|||||||
@tool
|
@tool
|
||||||
def write_file(path: str, content: str) -> str:
|
def write_file(path: str, content: str) -> str:
|
||||||
"""Создать или перезаписать файл с указанным содержимым."""
|
"""Создать или перезаписать файл с указанным содержимым."""
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
# Создаем директорию только если путь содержит директорию
|
||||||
|
dir_name = os.path.dirname(path)
|
||||||
|
if dir_name:
|
||||||
|
os.makedirs(dir_name, exist_ok=True)
|
||||||
with open(path, 'w', encoding='utf-8') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
return f"Файл {path} записан"
|
return f"Файл {path} записан"
|
||||||
@@ -25,20 +30,24 @@ def write_file(path: str, content: str) -> str:
|
|||||||
@tool
|
@tool
|
||||||
def delete_lines(path: str, start_line: int, end_line: int) -> str:
|
def delete_lines(path: str, start_line: int, end_line: int) -> str:
|
||||||
"""Удалить строки из файла (1-индекс, включительно)."""
|
"""Удалить строки из файла (1-индекс, включительно)."""
|
||||||
with open(path, 'r') as f:
|
if not os.path.exists(path):
|
||||||
|
return f"File not found: {path}"
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
del lines[start_line-1:end_line]
|
del lines[start_line-1:end_line]
|
||||||
with open(path, 'w') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
f.writelines(lines)
|
f.writelines(lines)
|
||||||
return f"Удалены строки {start_line}-{end_line} из {path}"
|
return f"Удалены строки {start_line}-{end_line} из {path}"
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def replace_text_in_file(path: str, old: str, new: str) -> str:
|
def replace_text_in_file(path: str, old: str, new: str) -> str:
|
||||||
"""Заменить все вхождения old на new в файле."""
|
"""Заменить все вхождения old на new в файле."""
|
||||||
with open(path, 'r') as f:
|
if not os.path.exists(path):
|
||||||
|
return f"File not found: {path}"
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
new_content = content.replace(old, new)
|
new_content = content.replace(old, new)
|
||||||
with open(path, 'w') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
return f"Заменено {content.count(old)} вхождений"
|
return f"Заменено {content.count(old)} вхождений"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user