Итерация 3, финальная

This commit is contained in:
ki.sagidullin
2026-04-12 12:03:19 +05:00
parent f5677013cc
commit 14bc0b906b
4 changed files with 160 additions and 122 deletions

View File

@@ -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() напрямую.")

View File

@@ -12,4 +12,4 @@ TEMPERATURE = 0.2
MAX_ITERATIONS = 3 MAX_ITERATIONS = 3
# Конфигурация системы # Конфигурация системы
MAX_REVIEW_RETRIES = 3 MAX_REVIEW_RETRIES = 1

View File

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

View File

@@ -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)} вхождений"