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

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

1
.gitignore vendored
View File

@@ -1 +1,2 @@
__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 langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.chat_models import ChatOllama
import json
@@ -15,7 +13,8 @@ class DeveloperAgent:
self.llm = ChatOllama(
model=MODEL_NAME,
temperature=TEMPERATURE,
base_url=LLM_HOST
base_url=LLM_HOST,
num_predict=8192
)
def develop(self, task: Task, previous_comments: list = None) -> CodeSolution:
@@ -136,86 +135,3 @@ class DeveloperAgent:
except:
pass
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")
LLM_HOST = "http://192.168.212.100:11434"
MODEL_NAME = "qwen3.5-16k:latest" # или "gpt-3.5-turbo" для тестов
TEMPERATURE = 0.3
TEMPERATURE = 0.2
MAX_ITERATIONS = 3
# Конфигурация системы

21
main.py
View File

@@ -15,7 +15,7 @@ def load_requirement_from_file(filepath: str) -> str:
return f.read()
def save_report(task, output_file: Optional[str] = None):
"""Сохраняет детальный отчет о выполнении"""
"""Сохраняет детальный отчет с историей всех итераций"""
report = {
"task_id": task.id,
"timestamp": datetime.now().isoformat(),
@@ -24,9 +24,23 @@ def save_report(task, output_file: Optional[str] = None):
"iterations": task.iteration,
"final_review": None,
"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:
report["final_review"] = {
"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["description"] = task.solution.description
# Сохраняем в файл
if not output_file:
output_file = f"report_{task.id}.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print(f"\n📄 Отчет сохранен: {output_file}")
print(f"\n📄 Report saved: {output_file}")
return report
def interactive_mode():

View File

@@ -15,7 +15,7 @@ class TaskStatus(str, Enum):
class ReviewComment(BaseModel):
line: Optional[int] = None
file: Optional[str] = None
severity: Literal["critical", "major", "minor", "suggestion"] = "major"
severity: Literal["critical", "major", "minor", "suggestion", "warning", "info"] = "major"
text: str
class ReviewResult(BaseModel):
@@ -28,6 +28,17 @@ class CodeSolution(BaseModel):
description: str
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):
id: str
requirement: str
@@ -36,3 +47,4 @@ class Task(BaseModel):
solution: Optional[CodeSolution] = None
last_review: Optional[ReviewResult] = None
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
import uuid
from datetime import datetime
from models import Task, TaskStatus, CodeSolution, ReviewResult
from agents import DeveloperAgent, ReviewerAgent
from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
from agent import DeveloperAgent, ReviewerAgent
from config import MAX_REVIEW_RETRIES
class Orchestrator:
@@ -21,6 +21,22 @@ class Orchestrator:
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,
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:
"""Строит граф процесса разработки"""
@@ -57,6 +73,13 @@ class Orchestrator:
task = state["task"]
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 = []
if task.last_review and task.last_review.comments:
@@ -71,6 +94,13 @@ class Orchestrator:
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}")
@@ -78,6 +108,13 @@ class Orchestrator:
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}
@@ -87,9 +124,18 @@ class Orchestrator:
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:
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}
try:
@@ -98,6 +144,17 @@ class Orchestrator:
task.last_review = review
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}")
if review.comments:
print(f" Найдено замечаний: {len(review.comments)}")
@@ -117,8 +174,9 @@ class Orchestrator:
except Exception as e:
task.status = TaskStatus.FAILED
task.error_log.append(f"Reviewer error: {str(e)}")
print(f"❌ Ошибка ревьювера: {e}")
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}

View File

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