вторая итерация

This commit is contained in:
ki.sagidullin
2026-04-12 11:48:37 +05:00
parent 8f16a6d669
commit f5677013cc
7 changed files with 190 additions and 234 deletions

View File

@@ -1,5 +1,7 @@
from .developer import DeveloperAgent from .developer import DeveloperAgent
from .reviewer import ReviewerAgent from .reviewer import ReviewerAgent
from .planner import PlannerAgent from .planner import PlannerAgent
from .executor import ExecutorAgent
from .plan_reviewer import PlanReviewerAgent
__all__ = ["DeveloperAgent", "ReviewerAgent", "PlannerAgent"] __all__ = ["DeveloperAgent", "ReviewerAgent", "PlannerAgent", "ExecutorAgent", "PlanReviewerAgent"]

View File

@@ -1,14 +1,19 @@
# executor.py # executor.py
from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_classic.agents import AgentExecutor
from langchain.agents import create_agent
from langchain_ollama import ChatOllama from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate from langchain_core.prompts import ChatPromptTemplate
from config import LLM_HOST, MODEL_NAME
class ToolExecutor: from typing import List, Dict
class ExecutorAgent:
def __init__(self, tools): def __init__(self, tools):
self.tools = tools self.tools = tools
self.llm = llm or ChatOllama( self.llm = ChatOllama(
model=MODEL_NAME, model=MODEL_NAME,
temperature=0.0 temperature=0.0,
base_url=LLM_HOST
) )
self.agent = self._create_agent() self.agent = self._create_agent()
@@ -18,9 +23,15 @@ class ToolExecutor:
("human", "{input}"), ("human", "{input}"),
("placeholder", "{agent_scratchpad}") ("placeholder", "{agent_scratchpad}")
]) ])
agent = create_tool_calling_agent(self.llm, self.tools, prompt) agent = create_agent(self.llm, self.tools, prompt)
return AgentExecutor(agent=agent, tools=self.tools, verbose=True) 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: for step in plan:
@@ -35,3 +46,5 @@ class ToolExecutor:
"success": True # можно анализировать ошибки "success": True # можно анализировать ошибки
}) })
return results return results
"""Устаревший метод - теперь выполняется напрямую через DeveloperAgent"""
raise NotImplementedError("Этот метод больше не используется. Используйте DeveloperAgent.develop() напрямую.")

View File

@@ -1,12 +1,16 @@
# reviewer.py # reviewer.py
from langchain_ollama import ChatOllama from langchain_ollama import ChatOllama
from langchain_core.messages import SystemMessage, HumanMessage from langchain_core.messages import SystemMessage, HumanMessage
from typing import List, Dict
from config import MODEL_NAME, LLM_HOST
import json
class PlanReviewerAgent: class PlanReviewerAgent:
def __init__(self, llm=None): def __init__(self, llm=None):
self.llm = llm or ChatOllama( self.llm = llm or ChatOllama(
model=MODEL_NAME, model=MODEL_NAME,
temperature=0.0 temperature=0.0,
base_url=LLM_HOST
) )
def review(self, original_task: str, plan: List[Dict], execution_results: List[Dict]) -> dict: def review(self, original_task: str, plan: List[Dict], execution_results: List[Dict]) -> dict:

View File

@@ -3,13 +3,14 @@ from langchain_ollama import ChatOllama
from langchain_core.messages import SystemMessage, HumanMessage from langchain_core.messages import SystemMessage, HumanMessage
import json import json
from typing import List, Dict from typing import List, Dict
from config import MODEL_NAME from config import MODEL_NAME, LLM_HOST
class PlannerAgent: class PlannerAgent:
def __init__(self, llm=None): def __init__(self, llm=None):
self.llm = llm or ChatOllama( self.llm = llm or ChatOllama(
model=MODEL_NAME, model=MODEL_NAME,
temperature=0.0 temperature=0.0,
base_url=LLM_HOST
) )
def create_plan(self, task: str, tools_description: List[Dict]) -> List[Dict]: def create_plan(self, task: str, tools_description: List[Dict]) -> List[Dict]:

View File

@@ -11,6 +11,7 @@ class TaskStatus(str, Enum):
APPROVED = "approved" APPROVED = "approved"
REJECTED = "rejected" REJECTED = "rejected"
FAILED = "failed" FAILED = "failed"
DEVELOPMENT = "development"
class ReviewComment(BaseModel): class ReviewComment(BaseModel):
line: Optional[int] = None line: Optional[int] = None

View File

@@ -1,18 +1,19 @@
# orchestrator.py # orchestrator.py
from typing import Dict, Any, Literal, TypedDict, List from typing import Dict, Any, Literal, TypedDict, List, Optional
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
from datetime import datetime from datetime import datetime
from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
from agent import DeveloperAgent, ReviewerAgent from agent.developer import DeveloperAgent
from agent.reviewer import ReviewerAgent
from config import MAX_REVIEW_RETRIES from config import MAX_REVIEW_RETRIES
class AgentState(TypedDict): class AgentState(TypedDict):
task: str task: Task
plan: List[Dict] plan: Dict
execution_results: List[Dict] execution_results: List[Dict]
review: Dict review: Optional[ReviewResult]
iteration: int iteration: int
max_iterations: int max_iterations: int
@@ -70,204 +71,140 @@ class Orchestrator:
return workflow.compile() return workflow.compile()
def _plan_node(self, state: AgentState): def _plan_node(self, state: AgentState):
print(f"\n🔧 [Iteration {state.get('iteration', 0)}] Планировщик начал работу...") task = state["task"]
# Сначала получим описания инструментов для планировщика print(f"\n🔧 [Iteration {task.iteration}] Планировщик начал работу...")
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) # Создаем запись в истории
task.history.append(IterationRecord(
iteration=task.iteration,
timestamp=datetime.now().isoformat(),
agent="developer",
action="start",
input_summary=f"Requirement: {task.requirement[:100]}...",
details={}
))
# План просто содержит задачу - разработчик выполнит её напрямую
plan = {"task": task.requirement, "iteration": task.iteration}
state["plan"] = plan state["plan"] = plan
return state return state
def _exec_node(self, state: AgentState): def _exec_node(self, state: AgentState):
"""Узел разработчика""" """Узел разработчика"""
print(f"\n🔧 [Iteration {state.get('iteration', 0)}] Исполнитель начал работу...") task = state["task"]
print(f"\n🔧 [Iteration {task.iteration}] Исполнитель начал работу...")
# Логируем начало работы разработчика # Обновляем статус
results = self.executor.execute_plan(state["plan"]) task.status = TaskStatus.DEVELOPMENT
state["execution_results"] = results
# Разработчик выполняет задачу напрямую
solution = self.developer.develop(task, [])
task.solution = solution
task.status = TaskStatus.REVIEW
# Записываем результат выполнения
state["execution_results"] = [{"status": "success", "solution": solution}]
# Добавляем запись в историю
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):
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:
"""Узел разработчика"""
task = state["task"] task = state["task"]
print(f"\n🔧 [Iteration {task.iteration}] Разработчик начал работу...") print(f"\n🔍 [Iteration {task.iteration}] Ревьювер начал работу...")
# Логируем начало работы разработчика # Добавляем запись о начале ревью
self._add_history( task.history.append(IterationRecord(
task, "developer", "start", iteration=task.iteration,
input_summary=f"Requirement: {task.requirement[:100]}...", timestamp=datetime.now().isoformat(),
details={"iteration": task.iteration, "previous_comments_count": len(task.last_review.comments) if task.last_review else 0} agent="reviewer",
) action="start",
input_summary=f"Reviewing {len(task.solution.files) if task.solution else 0} file(s)",
# Получаем замечания с предыдущего ревью details={}
previous_comments = [] ))
if task.last_review and task.last_review.comments:
previous_comments = [
{"file": c.file, "line": c.line, "text": c.text}
for c in task.last_review.comments
]
try:
# Генерируем код
solution = self.developer.develop(task, previous_comments)
task.solution = solution
task.status = TaskStatus.REVIEW
# Логируем успех
self._add_history(
task, "developer", "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)} файлов")
for filename in solution.files:
print(f" - {filename}")
except Exception as e:
task.status = TaskStatus.FAILED
task.error_log.append(f"Developer error: {str(e)}")
# Логируем ошибку
self._add_history(
task, "developer", "error",
error=error_msg,
details={"exception": str(e)}
)
print(f"❌ Ошибка разработчика: {e}")
return {"task": task}
def _reviewer_node(self, state: Dict) -> Dict:
"""Узел ревьювера"""
task = state["task"]
print(f"\n🔍 [Iteration {task.iteration}] Ревьювер проверяет код...")
# Логируем начало ревью
self._add_history(
task, "reviewer", "start",
input_summary=f"Reviewing {len(task.solution.files)} file(s)" if task.solution else "No solution",
details={"iteration": task.iteration}
)
if not task.solution: if not task.solution:
task.status = TaskStatus.FAILED task.status = TaskStatus.FAILED
error_msg = "No solution to review" error_msg = "No solution to review"
task.error_log.append(error_msg) task.error_log.append(error_msg)
self._add_history(task, "reviewer", "error", error=error_msg) task.history.append(IterationRecord(
return {"task": task} iteration=task.iteration,
timestamp=datetime.now().isoformat(),
agent="reviewer",
action="error",
error=error_msg
))
return state
try: # Проводим ревью
# Проводим ревью review = self.reviewer.review(task, task.solution)
review = self.reviewer.review(task, task.solution) task.last_review = review
task.last_review = review task.iteration += 1
task.iteration += 1
# Логируем результат ревью # Добавляем запись в историю
self._add_history( task.history.append(IterationRecord(
task, "reviewer", "complete", iteration=task.iteration - 1, # предыдущая итерация
output_summary=f"Verdict: {review.status}, Comments: {len(review.comments)}", timestamp=datetime.now().isoformat(),
details={ agent="reviewer",
"verdict": review.status, action="complete",
"comments_count": len(review.comments), output_summary=f"Verdict: {review.status}, Comments: {len(review.comments)}",
"summary": review.summary details={
} "verdict": review.status,
) "comments_count": len(review.comments)
}
))
print(f"📊 Вердикт: {review.status}") # Сохраняем результат ревью
if review.comments: state["review"] = review
print(f" Найдено замечаний: {len(review.comments)}")
for comment in review.comments[:3]: # Показываем первые 3
print(f" - [{comment.severity}] {comment.text[:100]}")
# Обновляем статус print(f"📊 Вердикт: {review.status}")
if review.status == "approved": if review.comments:
task.status = TaskStatus.APPROVED print(f" Найдено замечаний: {len(review.comments)}")
print("✅ Код одобрен!")
elif review.status == "rejected":
task.status = TaskStatus.REJECTED
print("❌ Код отклонен архитектурно")
else:
task.status = TaskStatus.REWORK
print("🔄 Требуются доработки")
except Exception as e: return state
task.status = TaskStatus.FAILED
error_msg = f"Reviewer error: {str(e)}"
task.error_log.append(error_msg)
self._add_history(task, "reviewer", "error", error=error_msg, details={"exception": str(e)})
return {"task": task} def _after_review(self, state: AgentState):
def _decide_next_step(self, state: Dict) -> Literal["approved", "rework", "rejected", "failed"]:
"""Решает, что делать дальше"""
task = state["task"] task = state["task"]
review = state["review"]
if task.status == TaskStatus.APPROVED: # Определяем статус
return "approved" if isinstance(review, ReviewResult):
elif task.status == TaskStatus.REJECTED: status = review.status
return "rejected" elif isinstance(review, dict):
elif task.status == TaskStatus.FAILED: status = review.get("status")
return "failed"
elif task.iteration >= self.max_retries:
print(f"\n⚠️ Достигнут лимит итераций ({self.max_retries}). Останавливаемся.")
task.status = TaskStatus.FAILED
return "failed"
else: else:
status = None
if status == "approved":
task.status = TaskStatus.APPROVED
return "approved"
elif status == "changes_requested":
if task.iteration >= state.get("max_iterations", 3):
task.status = TaskStatus.FAILED
return "reject"
task.status = TaskStatus.REWORK
return "rework" return "rework"
elif status == "rejected":
def _finalize_node(self, state: Dict) -> Dict: if task.iteration >= state.get("max_iterations", 3):
"""Финальный узел - подведение итогов""" task.status = TaskStatus.FAILED
task = state["task"] return "reject"
task.status = TaskStatus.REWORK
print("\n" + "="*50) return "replan"
print("📋 ИТОГИ РАБОТЫ") else:
print("="*50) task.status = TaskStatus.FAILED
print(f"Задача: {task.requirement[:100]}...") return "reject"
print(f"Статус: {task.status}")
print(f"Итераций: {task.iteration}")
if task.status == TaskStatus.APPROVED and task.solution:
print(f"\n✅ Решение принято!")
print(f" Файлов: {len(task.solution.files)}")
print(f" Тестов: {len(task.solution.tests)}")
# Сохраняем решение в файлы
self._save_solution(task.solution)
elif task.last_review:
print(f"\n📝 Финальный вердикт: {task.last_review.summary}")
if task.error_log:
print(f"\n❌ Ошибки: {len(task.error_log)}")
for error in task.error_log:
print(f" - {error}")
return {"task": task, "finalized": True}
def _save_solution(self, solution: CodeSolution): def _save_solution(self, solution: CodeSolution):
"""Сохраняет решение в файлы""" """Сохраняет решение в файлы"""
@@ -287,62 +224,60 @@ class Orchestrator:
f.write(test_content) f.write(test_content)
print(f" 💾 Сохранен тест: {testpath}") print(f" 💾 Сохранен тест: {testpath}")
def run(self, task: str, max_iterations:int = None) -> Dict: def run(self, task_text: str, max_iterations: int = 3) -> Dict:
"""Запускает процесс разработки""" """Запускает процесс разработки"""
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
# Создаем объект Task
task = Task(
id=task_id,
requirement=task_text,
status=TaskStatus.PENDING,
iteration=0,
solution=None,
last_review=None,
error_log=[],
history=[]
)
initial_state = { initial_state = {
"task": task, "task": task,
"plan": [], "plan": {},
"execution_results": [], "execution_results": [],
"review": {}, "review": None,
"iteration": 0, "iteration": 0,
"max_iterations": max_iterations "max_iterations": max_iterations
} }
print(f"\n🚀 Запуск мультиагентной системы") print(f"\n🚀 Запуск мультиагентной системы")
print(f"📝 Задача: {task}") print(f"📝 Задача: {task_text[:100]}...")
print(f"🆔 ID: {task_id}") print(f"🆔 ID: {task_id}")
# Запускаем граф # Запускаем граф
final_state = self.workflow.invoke(initial_state) final_state = self.workflow.invoke(initial_state)
return final_state # Сохраняем решение если успешно
final_task = final_state["task"]
if final_task.status == TaskStatus.APPROVED and final_task.solution:
self._save_solution(final_task.solution)
# main.py print("\n" + "="*50)
def main(): print("📋 ИТОГИ РАБОТЫ")
"""Пример использования""" print("="*50)
print(f"Статус: {final_task.status.value}")
print(f"Итераций: {final_task.iteration}")
# Создаем оркестратора if final_task.status == TaskStatus.APPROVED:
orchestrator = Orchestrator() 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:
requirement = """ print(f"\n❌ Ошибки: {len(final_task.error_log)}")
Реализуй класс BankAccount с методами: for error in final_task.error_log:
- deposit(amount): пополнение счета print(f" - {error}")
- withdraw(amount): снятие средств (нельзя снять больше, чем есть)
- get_balance(): получение баланса
- add_interest(rate): добавление процентов (rate в процентах)
Требования: return {"task": final_task}
- Нельзя создать счет с отрицательным балансом
- Все операции должны быть потокобезопасными (используй threading.Lock)
- Напиши юнит-тесты (pytest) для всех методов, включая граничные случаи
- Добавь docstring для всех методов
"""
# Запускаем процесс
result = orchestrator.run(requirement)
# Анализируем результат
task = result["task"]
if task.status == TaskStatus.APPROVED:
print("\n🎉 Успех! Код принят и сохранен в папке 'generated_code'")
else:
print(f"\n💔 Неудача. Статус: {task.status}")
if task.last_review:
print(f"Причина: {task.last_review.summary}")
if __name__ == "__main__":
main()

View File

@@ -1,7 +1,7 @@
# requirements.txt # requirements.txt
langgraph>=0.3.0 langgraph>=0.3.0
langchain>=0.2.0 langchain>=0.3.0
langchain-community>=0.0.16 langchain-community>=0.0.16
langchain-ollama langchain-ollama
python-dotenv>=1.0.0 python-dotenv>=1.0.0