From 324097988b9b10f5bdc3adf5a0f92bc9d8358c54 Mon Sep 17 00:00:00 2001 From: "ki.sagidullin" Date: Sun, 12 Apr 2026 08:51:33 +0500 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=20=D0=B0=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B2=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- agent/__init__.py | 4 ++ agents.py => agent/developer.py | 88 +--------------------------- agent/reviewer.py | 100 ++++++++++++++++++++++++++++++++ config.py | 2 +- main.py | 21 +++++-- models.py | 16 ++++- orchestrator.py | 68 ++++++++++++++++++++-- requirements.txt | 1 - 9 files changed, 203 insertions(+), 100 deletions(-) create mode 100644 agent/__init__.py rename agents.py => agent/developer.py (61%) create mode 100644 agent/reviewer.py diff --git a/.gitignore b/.gitignore index ed8ebf5..9005ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__ \ No newline at end of file +__pycache__ +report_*.json \ No newline at end of file diff --git a/agent/__init__.py b/agent/__init__.py new file mode 100644 index 0000000..cb26b80 --- /dev/null +++ b/agent/__init__.py @@ -0,0 +1,4 @@ +from .developer import DeveloperAgent +from .reviewer import ReviewerAgent + +__all__ = ["DeveloperAgent", "ReviewerAgent"] \ No newline at end of file diff --git a/agents.py b/agent/developer.py similarity index 61% rename from agents.py rename to agent/developer.py index 508c33a..5b4b081 100644 --- a/agents.py +++ b/agent/developer.py @@ -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}") \ No newline at end of file diff --git a/agent/reviewer.py b/agent/reviewer.py new file mode 100644 index 0000000..9d9abda --- /dev/null +++ b/agent/reviewer.py @@ -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}") \ No newline at end of file diff --git a/config.py b/config.py index 3b1ea97..740661d 100644 --- a/config.py +++ b/config.py @@ -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 # Конфигурация системы diff --git a/main.py b/main.py index 331a591..1580ac3 100644 --- a/main.py +++ b/main.py @@ -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(): diff --git a/models.py b/models.py index e96d2ec..7d87b14 100644 --- a/models.py +++ b/models.py @@ -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 @@ -35,4 +46,5 @@ class Task(BaseModel): iteration: int = 0 solution: Optional[CodeSolution] = None last_review: Optional[ReviewResult] = None - error_log: List[str] = Field(default_factory=list) \ No newline at end of file + error_log: List[str] = Field(default_factory=list) + history: List[IterationRecord] = Field(default_factory=list) # <-- новая история \ No newline at end of file diff --git a/orchestrator.py b/orchestrator.py index 73575e8..d596c2d 100644 --- a/orchestrator.py +++ b/orchestrator.py @@ -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: @@ -20,6 +20,22 @@ class Orchestrator: self.workflow = self._build_workflow() 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} diff --git a/requirements.txt b/requirements.txt index b45a989..e17e3b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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