Вынес агентов в отдельные файлы

This commit is contained in:
ki.sagidullin
2026-04-12 08:51:33 +05:00
parent f110f52e91
commit 324097988b
9 changed files with 203 additions and 100 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
__pycache__ __pycache__
report_*.json

4
agent/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from .developer import DeveloperAgent
from .reviewer import ReviewerAgent
__all__ = ["DeveloperAgent", "ReviewerAgent"]

View File

@@ -1,6 +1,4 @@
# agents.py
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.chat_models import ChatOllama from langchain_community.chat_models import ChatOllama
import json import json
@@ -15,7 +13,8 @@ class DeveloperAgent:
self.llm = ChatOllama( self.llm = ChatOllama(
model=MODEL_NAME, model=MODEL_NAME,
temperature=TEMPERATURE, temperature=TEMPERATURE,
base_url=LLM_HOST base_url=LLM_HOST,
num_predict=8192
) )
def develop(self, task: Task, previous_comments: list = None) -> CodeSolution: def develop(self, task: Task, previous_comments: list = None) -> CodeSolution:
@@ -136,86 +135,3 @@ class DeveloperAgent:
except: except:
pass pass
return None return None
class ReviewerAgent:
"""Агент-ревьювер: проверяет качество кода"""
def __init__(self):
self.llm = ChatOllama(
model=MODEL_NAME,
temperature=0.1,
base_url=LLM_HOST
)
def review(self, task: Task, solution: CodeSolution) -> ReviewResult:
"""Проверяет код и возвращает замечания"""
system_prompt = """Ты - строгий ревьювер кода. Твоя задача:
1. Проверить соответствие требованиям
2. Найти логические ошибки и баги
3. Оценить читаемость и стиль кода
4. Проверить покрытие тестами
5. Найти проблемы безопасности
6. Предложить улучшения
Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки.
Верни JSON с полями:
- status: "approved", "changes_requested", или "rejected"
- comments: массив объектов с полями: file, line, severity, text
- summary: краткое резюме ревью
"""
# Формируем представление кода для ревью
code_for_review = ""
for filename, content in solution.files.items():
code_for_review += f"\n--- {filename} ---\n{content}\n"
if solution.tests:
code_for_review += "\n--- TESTS ---\n"
for testname, test_content in solution.tests.items():
code_for_review += f"\n--- {testname} ---\n{test_content}\n"
user_prompt = f"""
Требование: {task.requirement}
Код на ревью:
{code_for_review}
Описание решения разработчика: {solution.description}
Проведи ревью. Будь строг, но справедлив.
"""
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_prompt)
]
response = self.llm.invoke(messages)
# Парсим JSON ответ
try:
content = response.content
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
data = json.loads(content.strip())
comments = []
for comment_data in data.get("comments", []):
comments.append(ReviewComment(
line=comment_data.get("line"),
file=comment_data.get("file"),
severity=comment_data.get("severity", "major"),
text=comment_data.get("text", "")
))
return ReviewResult(
status=data.get("status", "changes_requested"),
comments=comments,
summary=data.get("summary", "")
)
except Exception as e:
raise ValueError(f"Failed to parse reviewer response: {e}\nResponse: {response.content}")

100
agent/reviewer.py Normal file
View File

@@ -0,0 +1,100 @@
from typing import Optional, Dict, Any
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.chat_models import ChatOllama
import json
from config import MODEL_NAME, TEMPERATURE, LLM_HOST
from models import CodeSolution, ReviewResult, ReviewComment, Task
import re
class ReviewerAgent:
"""Агент-ревьювер: проверяет качество кода"""
def __init__(self):
self.llm = ChatOllama(
model=MODEL_NAME,
temperature=0.1,
base_url=LLM_HOST,
num_predict=2048
)
def review(self, task: Task, solution: CodeSolution) -> ReviewResult:
"""Проверяет код и возвращает замечания"""
system_prompt = """Ты - строгий ревьювер кода. Твоя задача:
1. Проверить соответствие требованиям
2. Найти логические ошибки и баги
3. Оценить читаемость и стиль кода
4. Проверить покрытие тестами
5. Найти проблемы безопасности
6. Предложить улучшения
Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки.
Допустимые уровни severity (от критичного к информационному):
- critical: блокирующие ошибки (безопасность, падения)
- major: серьёзные логические ошибки, неверная работа
- minor: стиль, читаемость, мелкие баги
- warning: потенциальные проблемы, улучшения, edge cases
- info: предложения, рекомендации, необязательные улучшения
- suggestion: альтернативные подходы
Верни JSON с полями:
- status: "approved", "changes_requested", или "rejected"
- comments: массив объектов с полями: file, line, severity, text
- summary: краткое резюме ревью
"""
# Формируем представление кода для ревью
code_for_review = ""
for filename, content in solution.files.items():
code_for_review += f"\n--- {filename} ---\n{content}\n"
if solution.tests:
code_for_review += "\n--- TESTS ---\n"
for testname, test_content in solution.tests.items():
code_for_review += f"\n--- {testname} ---\n{test_content}\n"
user_prompt = f"""
Требование: {task.requirement}
Код на ревью:
{code_for_review}
Описание решения разработчика: {solution.description}
Проведи ревью. Будь строг, но справедлив.
"""
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_prompt)
]
response = self.llm.invoke(messages)
# Парсим JSON ответ
try:
content = response.content
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
data = json.loads(content.strip())
comments = []
for comment_data in data.get("comments", []):
comments.append(ReviewComment(
line=comment_data.get("line"),
file=comment_data.get("file"),
severity=comment_data.get("severity", "major"),
text=comment_data.get("text", "")
))
return ReviewResult(
status=data.get("status", "changes_requested"),
comments=comments,
summary=data.get("summary", "")
)
except Exception as e:
raise ValueError(f"Failed to parse reviewer response: {e}\nResponse: {response.content}")

View File

@@ -8,7 +8,7 @@ load_dotenv()
# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
LLM_HOST = "http://192.168.212.100:11434" LLM_HOST = "http://192.168.212.100:11434"
MODEL_NAME = "qwen3.5-16k:latest" # или "gpt-3.5-turbo" для тестов MODEL_NAME = "qwen3.5-16k:latest" # или "gpt-3.5-turbo" для тестов
TEMPERATURE = 0.3 TEMPERATURE = 0.2
MAX_ITERATIONS = 3 MAX_ITERATIONS = 3
# Конфигурация системы # Конфигурация системы

21
main.py
View File

@@ -15,7 +15,7 @@ def load_requirement_from_file(filepath: str) -> str:
return f.read() return f.read()
def save_report(task, output_file: Optional[str] = None): def save_report(task, output_file: Optional[str] = None):
"""Сохраняет детальный отчет о выполнении""" """Сохраняет детальный отчет с историей всех итераций"""
report = { report = {
"task_id": task.id, "task_id": task.id,
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@@ -24,9 +24,23 @@ def save_report(task, output_file: Optional[str] = None):
"iterations": task.iteration, "iterations": task.iteration,
"final_review": None, "final_review": None,
"error_log": task.error_log, "error_log": task.error_log,
"files_generated": [] "files_generated": [],
"history": [] # <-- добавляем историю
} }
# Добавляем историю итераций
for record in task.history:
report["history"].append({
"iteration": record.iteration,
"timestamp": record.timestamp,
"agent": record.agent,
"action": record.action,
"input_summary": record.input_summary,
"output_summary": record.output_summary,
"details": record.details,
"error": record.error
})
if task.last_review: if task.last_review:
report["final_review"] = { report["final_review"] = {
"status": task.last_review.status, "status": task.last_review.status,
@@ -48,14 +62,13 @@ def save_report(task, output_file: Optional[str] = None):
report["tests_generated"] = list(task.solution.tests.keys()) report["tests_generated"] = list(task.solution.tests.keys())
report["description"] = task.solution.description report["description"] = task.solution.description
# Сохраняем в файл
if not output_file: if not output_file:
output_file = f"report_{task.id}.json" output_file = f"report_{task.id}.json"
with open(output_file, 'w', encoding='utf-8') as f: with open(output_file, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False) json.dump(report, f, indent=2, ensure_ascii=False)
print(f"\n📄 Отчет сохранен: {output_file}") print(f"\n📄 Report saved: {output_file}")
return report return report
def interactive_mode(): def interactive_mode():

View File

@@ -15,7 +15,7 @@ class TaskStatus(str, Enum):
class ReviewComment(BaseModel): class ReviewComment(BaseModel):
line: Optional[int] = None line: Optional[int] = None
file: Optional[str] = None file: Optional[str] = None
severity: Literal["critical", "major", "minor", "suggestion"] = "major" severity: Literal["critical", "major", "minor", "suggestion", "warning", "info"] = "major"
text: str text: str
class ReviewResult(BaseModel): class ReviewResult(BaseModel):
@@ -28,6 +28,17 @@ class CodeSolution(BaseModel):
description: str description: str
tests: Dict[str, str] = Field(default_factory=dict) # test_filename -> content tests: Dict[str, str] = Field(default_factory=dict) # test_filename -> content
class IterationRecord(BaseModel):
"""Запись одной итерации"""
iteration: int
timestamp: str
agent: Literal["developer", "reviewer"]
action: str # "start", "complete", "error"
input_summary: Optional[str] = None
output_summary: Optional[str] = None
details: Dict[str, Any] = Field(default_factory=dict)
error: Optional[str] = None
class Task(BaseModel): class Task(BaseModel):
id: str id: str
requirement: str requirement: str
@@ -35,4 +46,5 @@ class Task(BaseModel):
iteration: int = 0 iteration: int = 0
solution: Optional[CodeSolution] = None solution: Optional[CodeSolution] = None
last_review: Optional[ReviewResult] = None last_review: Optional[ReviewResult] = None
error_log: List[str] = Field(default_factory=list) error_log: List[str] = Field(default_factory=list)
history: List[IterationRecord] = Field(default_factory=list) # <-- новая история

View File

@@ -4,8 +4,8 @@ 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 from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
from agents import DeveloperAgent, ReviewerAgent from agent import DeveloperAgent, ReviewerAgent
from config import MAX_REVIEW_RETRIES from config import MAX_REVIEW_RETRIES
class Orchestrator: class Orchestrator:
@@ -20,6 +20,22 @@ class Orchestrator:
self.workflow = self._build_workflow() self.workflow = self._build_workflow()
self.checkpointer = MemorySaver() self.checkpointer = MemorySaver()
self.app = self.workflow.compile() self.app = self.workflow.compile()
def _add_history(self, task: Task, agent: str, action: str,
input_summary: str = None, output_summary: str = None,
details: Dict = None, error: str = None):
"""Добавляет запись в историю задачи"""
record = IterationRecord(
iteration=task.iteration,
timestamp=datetime.now().isoformat(),
agent=agent,
action=action,
input_summary=input_summary,
output_summary=output_summary,
details=details or {},
error=error
)
task.history.append(record)
def _build_workflow(self) -> StateGraph: def _build_workflow(self) -> StateGraph:
"""Строит граф процесса разработки""" """Строит граф процесса разработки"""
@@ -57,6 +73,13 @@ class Orchestrator:
task = state["task"] task = state["task"]
print(f"\n🔧 [Iteration {task.iteration}] Разработчик начал работу...") print(f"\n🔧 [Iteration {task.iteration}] Разработчик начал работу...")
# Логируем начало работы разработчика
self._add_history(
task, "developer", "start",
input_summary=f"Requirement: {task.requirement[:100]}...",
details={"iteration": task.iteration, "previous_comments_count": len(task.last_review.comments) if task.last_review else 0}
)
# Получаем замечания с предыдущего ревью # Получаем замечания с предыдущего ревью
previous_comments = [] previous_comments = []
if task.last_review and task.last_review.comments: if task.last_review and task.last_review.comments:
@@ -71,6 +94,13 @@ class Orchestrator:
task.solution = solution task.solution = solution
task.status = TaskStatus.REVIEW 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)} файлов") print(f"✅ Разработчик создал решение: {len(solution.files)} файлов")
for filename in solution.files: for filename in solution.files:
print(f" - {filename}") print(f" - {filename}")
@@ -78,6 +108,13 @@ class Orchestrator:
except Exception as e: except Exception as e:
task.status = TaskStatus.FAILED task.status = TaskStatus.FAILED
task.error_log.append(f"Developer error: {str(e)}") 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}") print(f"❌ Ошибка разработчика: {e}")
return {"task": task} return {"task": task}
@@ -87,9 +124,18 @@ class Orchestrator:
task = state["task"] task = state["task"]
print(f"\n🔍 [Iteration {task.iteration}] Ревьювер проверяет код...") 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
task.error_log.append("No solution to review") error_msg = "No solution to review"
task.error_log.append(error_msg)
self._add_history(task, "reviewer", "error", error=error_msg)
return {"task": task} return {"task": task}
try: try:
@@ -98,6 +144,17 @@ class Orchestrator:
task.last_review = review task.last_review = review
task.iteration += 1 task.iteration += 1
# Логируем результат ревью
self._add_history(
task, "reviewer", "complete",
output_summary=f"Verdict: {review.status}, Comments: {len(review.comments)}",
details={
"verdict": review.status,
"comments_count": len(review.comments),
"summary": review.summary
}
)
print(f"📊 Вердикт: {review.status}") print(f"📊 Вердикт: {review.status}")
if review.comments: if review.comments:
print(f" Найдено замечаний: {len(review.comments)}") print(f" Найдено замечаний: {len(review.comments)}")
@@ -117,8 +174,9 @@ class Orchestrator:
except Exception as e: except Exception as e:
task.status = TaskStatus.FAILED task.status = TaskStatus.FAILED
task.error_log.append(f"Reviewer error: {str(e)}") error_msg = f"Reviewer error: {str(e)}"
print(f"❌ Ошибка ревьювера: {e}") task.error_log.append(error_msg)
self._add_history(task, "reviewer", "error", error=error_msg, details={"exception": str(e)})
return {"task": task} return {"task": task}

View File

@@ -2,7 +2,6 @@
langgraph>=0.3.0 langgraph>=0.3.0
langchain>=0.2.0 langchain>=0.2.0
langchain-openai>=0.1.0
langchain-community>=0.0.16 langchain-community>=0.0.16
python-dotenv>=1.0.0 python-dotenv>=1.0.0
pydantic>=2.0.0 pydantic>=2.0.0