commit 58497365d95ba14b445cc8000c076af9dc2a81f8 Author: Teraonious Date: Sat Apr 11 08:48:42 2026 +0500 iFirst! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/agents.py b/agents.py new file mode 100644 index 0000000..c01bba1 --- /dev/null +++ b/agents.py @@ -0,0 +1,160 @@ +# agents.py +from typing import Dict, Any +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_community.chat_models import ChatOllama +import json +from config import MODEL_NAME, TEMPERATURE +from models import CodeSolution, ReviewResult, ReviewComment, Task + +class DeveloperAgent: + """Агент-разработчик: генерирует код по требованию""" + + def __init__(self): + self.llm = ChatOllama( + model=MODEL_NAME, + temperature=TEMPERATURE, + base_url="http://localhost:11434" + ) + + def develop(self, task: Task, previous_comments: list = None) -> CodeSolution: + """Генерирует код на основе требования и замечаний""" + + system_prompt = """Ты - опытный разработчик. Твоя задача - написать качественный код. + Ты должен: + 1. Писать чистый, читаемый код с комментариями + 2. Добавлять обработку ошибок + 3. Писать юнит-тесты (pytest/unittest) + 4. Следовать PEP8 (для Python) или стандартам языка + 5. Возвращать решение в строго определённом JSON формате + + В ответе должен быть JSON с полями: + - files: dict (имя_файла -> содержимое) + - description: str (описание решения) + - tests: dict (имя_теста -> содержимое теста) + """ + + user_prompt = f""" + Требование: {task.requirement} + + Текущая итерация: {task.iteration} + """ + + if previous_comments: + user_prompt += f""" + + Замечания с предыдущего ревью: + {json.dumps(previous_comments, indent=2, ensure_ascii=False)} + + Пожалуйста, исправь код согласно замечаниям. + """ + + messages = [ + SystemMessage(content=system_prompt), + HumanMessage(content=user_prompt) + ] + + response = self.llm.invoke(messages) + + # Парсим JSON ответ + try: + # Извлекаем JSON из ответа + content = response.content + # Находим JSON в тексте (между ```json и ``` или просто сам JSON) + 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()) + return CodeSolution( + files=data.get("files", {}), + description=data.get("description", ""), + tests=data.get("tests", {}) + ) + except Exception as e: + raise ValueError(f"Failed to parse developer response: {e}\nResponse: {response.content}") + +class ReviewerAgent: + """Агент-ревьювер: проверяет качество кода""" + + def __init__(self): + self.llm = ChatOllama( + model=MODEL_NAME, + temperature=0.1, + base_url="http://localhost:11434" + ) + + 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/config.py b/config.py new file mode 100644 index 0000000..e8951f9 --- /dev/null +++ b/config.py @@ -0,0 +1,14 @@ +# config.py +import os +from dotenv import load_dotenv + +load_dotenv() + +# Конфигурация модели +# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +MODEL_NAME = "qwen3.5:latest" # или "gpt-3.5-turbo" для тестов +TEMPERATURE = 0.3 +MAX_ITERATIONS = 3 + +# Конфигурация системы +MAX_REVIEW_RETRIES = 3 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..331a591 --- /dev/null +++ b/main.py @@ -0,0 +1,301 @@ +# main.py +import sys +import argparse +import json +from pathlib import Path +from datetime import datetime +from typing import Optional + +from orchestrator import Orchestrator +from models import TaskStatus + +def load_requirement_from_file(filepath: str) -> str: + """Загружает требование из файла""" + with open(filepath, 'r', encoding='utf-8') as f: + return f.read() + +def save_report(task, output_file: Optional[str] = None): + """Сохраняет детальный отчет о выполнении""" + report = { + "task_id": task.id, + "timestamp": datetime.now().isoformat(), + "requirement": task.requirement, + "status": task.status.value, + "iterations": task.iteration, + "final_review": None, + "error_log": task.error_log, + "files_generated": [] + } + + if task.last_review: + report["final_review"] = { + "status": task.last_review.status, + "summary": task.last_review.summary, + "comments_count": len(task.last_review.comments), + "comments": [ + { + "file": c.file, + "line": c.line, + "severity": c.severity, + "text": c.text + } + for c in task.last_review.comments + ] + } + + if task.solution: + report["files_generated"] = list(task.solution.files.keys()) + 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}") + return report + +def interactive_mode(): + """Интерактивный режим для ввода задач""" + print("\n" + "="*60) + print("🤖 Мультиагентная система разработки") + print("="*60) + print("\nВведите задачу для разработки (для завершения введите 'exit'):") + print("-"*60) + + tasks = [] + orchestrator = Orchestrator() + + while True: + print("\n📝 Описание задачи (Ctrl+D или 'exit' для завершения):") + lines = [] + try: + while True: + line = input() + if line.lower() == 'exit': + break + lines.append(line) + except EOFError: + break + + if not lines or lines[0].lower() == 'exit': + break + + requirement = "\n".join(lines) + + if requirement.strip(): + print("\n🚀 Запускаем разработку...") + result = orchestrator.run(requirement) + task = result["task"] + save_report(task) + tasks.append(task) + + # Показываем результат + if task.status == TaskStatus.APPROVED: + print("\n✨ Код успешно создан и прошел ревью!") + if task.solution: + print(f"📁 Сгенерировано файлов: {len(task.solution.files)}") + print(f"🧪 Сгенерировано тестов: {len(task.solution.tests)}") + else: + print(f"\n❌ Неудача. Статус: {task.status}") + if task.last_review: + print(f"💬 {task.last_review.summary}") + + print("\n" + "-"*60) + print("Введите следующую задачу или 'exit' для выхода:") + + print(f"\n📊 Всего выполнено задач: {len(tasks)}") + print(f"✅ Успешно: {sum(1 for t in tasks if t.status == TaskStatus.APPROVED)}") + print(f"❌ Провалено: {sum(1 for t in tasks if t.status != TaskStatus.APPROVED)}") + +def batch_mode(tasks_file: str): + """Пакетный режим для выполнения задач из JSON файла""" + + with open(tasks_file, 'r', encoding='utf-8') as f: + tasks_config = json.load(f) + + requirements = tasks_config.get('tasks', []) + orchestrator = Orchestrator() + results = [] + + print(f"\n📦 Запуск пакетной обработки: {len(requirements)} задач") + + for i, req in enumerate(requirements, 1): + print(f"\n{'='*60}") + print(f"Задача {i}/{len(requirements)}") + print(f"{'='*60}") + + result = orchestrator.run(req) + task = result["task"] + save_report(task, f"report_task_{i}_{task.id}.json") + results.append(task) + + # Небольшая задержка между задачами + import time + time.sleep(1) + + # Итоговый отчет + print("\n" + "="*60) + print("📊 ИТОГОВАЯ СТАТИСТИКА") + print("="*60) + print(f"Всего задач: {len(results)}") + print(f"✅ Approved: {sum(1 for t in results if t.status == TaskStatus.APPROVED)}") + print(f"🔄 Rework (лимит): {sum(1 for t in results if t.status == TaskStatus.REWORK)}") + print(f"❌ Rejected: {sum(1 for t in results if t.status == TaskStatus.REJECTED)}") + print(f"💀 Failed: {sum(1 for t in results if t.status == TaskStatus.FAILED)}") + + avg_iterations = sum(t.iteration for t in results) / len(results) + print(f"📈 Среднее количество итераций: {avg_iterations:.2f}") + +def preview_mode(): + """Режим предпросмотра - показывает примеры задач""" + + examples = [ + { + "name": "Калькулятор", + "task": """ +Реализуй класс Calculator с методами: +- add(a, b) -> сумма +- subtract(a, b) -> разность +- multiply(a, b) -> произведение +- divide(a, b) -> частное (обработать деление на ноль) + +Требования: +- Все методы должны возвращать float +- Добавь обработку исключений +- Напиши юнит-тесты для всех методов +- Добавь аннотации типов +""" + }, + { + "name": "Кеш с TTL", + "task": """ +Реализуй класс TTLCache с поддержкой: +- set(key, value, ttl_seconds) - установка значения с TTL +- get(key) - получение значения (возвращает None, если истекло) +- delete(key) - удаление ключа +- clear() - очистка всего кеша + +Особенности: +- Автоматическая очистка просроченных записей +- Потокобезопасность +- Эффективная память (удалять просроченные при get) + +Напиши тесты, проверяющие TTL и конкурентный доступ. +""" + } + ] + + print("\n📚 ПРИМЕРЫ ЗАДАЧ") + print("="*60) + + for i, example in enumerate(examples, 1): + print(f"\n{i}. {example['name']}:") + print("-"*40) + print(example['task'].strip()) + print("-"*40) + + print("\n💡 Используйте эти задачи для тестирования системы") + print("Запустите интерактивный режим и скопируйте одну из задач") + +def main(): + parser = argparse.ArgumentParser( + description="Мультиагентная система разработки с оркестратором, разработчиком и ревьювером", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + '--mode', '-m', + choices=['interactive', 'batch', 'preview', 'single'], + default='interactive', + help='Режим работы: interactive (по умолч.), batch, preview, single' + ) + + parser.add_argument( + '--task', '-t', + type=str, + help='Одиночная задача (текст или путь к файлу)' + ) + + parser.add_argument( + '--tasks-file', '-f', + type=str, + help='JSON файл с задачами для пакетного режима' + ) + + parser.add_argument( + '--output', '-o', + type=str, + help='Файл для сохранения отчета' + ) + + args = parser.parse_args() + + if args.mode == 'preview': + preview_mode() + return + + if args.mode == 'single': + if not args.task: + print("❌ Ошибка: для режима 'single' укажите задачу через --task") + sys.exit(1) + + # Проверяем, является ли аргумент файлом + if Path(args.task).exists(): + requirement = load_requirement_from_file(args.task) + else: + requirement = args.task + + orchestrator = Orchestrator() + result = orchestrator.run(requirement) + task = result["task"] + save_report(task, args.output) + + # Показываем результат + if task.status == TaskStatus.APPROVED: + print("\n✅ Успех! Код сгенерирован и прошел ревью.") + if task.solution: + print(f"📁 Файлы сохранены в папке 'generated_code/'") + else: + print(f"\n❌ Неудача. Статус: {task.status}") + if task.last_review: + print(f"Причина: {task.last_review.summary}") + + return + + if args.mode == 'batch': + if not args.tasks_file: + print("❌ Ошибка: для пакетного режима укажите файл с задачами через --tasks-file") + print("Пример: python main.py --mode batch --tasks-file tasks.json") + sys.exit(1) + + if not Path(args.tasks_file).exists(): + print(f"❌ Ошибка: файл {args.tasks_file} не найден") + sys.exit(1) + + batch_mode(args.tasks_file) + return + + # Режим по умолчанию - интерактивный + interactive_mode() + +# tasks.json - пример файла для пакетного режима +EXAMPLE_TASKS_JSON = { + "tasks": [ + "Реализуй функцию is_palindrome(s) для проверки палиндрома", + "Создай класс Stack с методами push, pop, peek, is_empty", + "Напиши декоратор @timer, который замеряет время выполнения функции" + ] +} + +if __name__ == "__main__": + # Создаем пример конфига, если его нет + if not Path("tasks.json").exists(): + with open("tasks.json", "w", encoding='utf-8') as f: + json.dump(EXAMPLE_TASKS_JSON, f, indent=2, ensure_ascii=False) + print("📝 Создан пример файла tasks.json с тестовыми задачами") + + main() \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..e96d2ec --- /dev/null +++ b/models.py @@ -0,0 +1,38 @@ +# models.py +from typing import List, Dict, Any, Optional, Literal +from pydantic import BaseModel, Field +from enum import Enum + +class TaskStatus(str, Enum): + PENDING = "pending" + CODING = "coding" + REVIEW = "review" + REWORK = "rework" + APPROVED = "approved" + REJECTED = "rejected" + FAILED = "failed" + +class ReviewComment(BaseModel): + line: Optional[int] = None + file: Optional[str] = None + severity: Literal["critical", "major", "minor", "suggestion"] = "major" + text: str + +class ReviewResult(BaseModel): + status: Literal["approved", "changes_requested", "rejected"] + comments: List[ReviewComment] = Field(default_factory=list) + summary: str + +class CodeSolution(BaseModel): + files: Dict[str, str] = Field(default_factory=dict) # filename -> content + description: str + tests: Dict[str, str] = Field(default_factory=dict) # test_filename -> content + +class Task(BaseModel): + id: str + requirement: str + status: TaskStatus = TaskStatus.PENDING + 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 diff --git a/orchestrator.py b/orchestrator.py new file mode 100644 index 0000000..2bfb77e --- /dev/null +++ b/orchestrator.py @@ -0,0 +1,246 @@ +# orchestrator.py +from typing import Dict, Any, Literal +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 config import MAX_REVIEW_RETRIES + +class Orchestrator: + """Оркестратор - управляет процессом разработки""" + + def __init__(self): + self.developer = DeveloperAgent() + self.reviewer = ReviewerAgent() + self.max_retries = MAX_REVIEW_RETRIES + + # Создаем граф состояний + self.workflow = self._build_workflow() + self.app = self.workflow.compile(checkpointer=MemorySaver()) + + def _build_workflow(self) -> StateGraph: + """Строит граф процесса разработки""" + + # Определяем состояния + workflow = StateGraph(Dict) + + # Добавляем узлы + workflow.add_node("developer", self._developer_node) + workflow.add_node("reviewer", self._reviewer_node) + workflow.add_node("finalize", self._finalize_node) + + # Определяем переходы + workflow.set_entry_point("developer") + workflow.add_edge("developer", "reviewer") + + # Условные переходы после ревью + workflow.add_conditional_edges( + "reviewer", + self._decide_next_step, + { + "approved": "finalize", + "rework": "developer", + "rejected": "finalize", + "failed": "finalize" + } + ) + + workflow.add_edge("finalize", END) + + return workflow + + def _developer_node(self, state: Dict) -> Dict: + """Узел разработчика""" + task = state["task"] + print(f"\n🔧 [Iteration {task.iteration}] Разработчик начал работу...") + + # Получаем замечания с предыдущего ревью + 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 + + 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)}") + print(f"❌ Ошибка разработчика: {e}") + + return {"task": task} + + def _reviewer_node(self, state: Dict) -> Dict: + """Узел ревьювера""" + task = state["task"] + print(f"\n🔍 [Iteration {task.iteration}] Ревьювер проверяет код...") + + if not task.solution: + task.status = TaskStatus.FAILED + task.error_log.append("No solution to review") + return {"task": task} + + try: + # Проводим ревью + review = self.reviewer.review(task, task.solution) + task.last_review = review + task.iteration += 1 + + print(f"📊 Вердикт: {review.status}") + if review.comments: + print(f" Найдено замечаний: {len(review.comments)}") + for comment in review.comments[:3]: # Показываем первые 3 + print(f" - [{comment.severity}] {comment.text[:100]}") + + # Обновляем статус + if review.status == "approved": + task.status = TaskStatus.APPROVED + print("✅ Код одобрен!") + elif review.status == "rejected": + task.status = TaskStatus.REJECTED + print("❌ Код отклонен архитектурно") + else: + task.status = TaskStatus.REWORK + print("🔄 Требуются доработки") + + except Exception as e: + task.status = TaskStatus.FAILED + task.error_log.append(f"Reviewer error: {str(e)}") + print(f"❌ Ошибка ревьювера: {e}") + + return {"task": task} + + def _decide_next_step(self, state: Dict) -> Literal["approved", "rework", "rejected", "failed"]: + """Решает, что делать дальше""" + task = state["task"] + + if task.status == TaskStatus.APPROVED: + return "approved" + elif task.status == TaskStatus.REJECTED: + return "rejected" + elif task.status == TaskStatus.FAILED: + return "failed" + elif task.iteration >= self.max_retries: + print(f"\n⚠️ Достигнут лимит итераций ({self.max_retries}). Останавливаемся.") + task.status = TaskStatus.FAILED + return "failed" + else: + return "rework" + + def _finalize_node(self, state: Dict) -> Dict: + """Финальный узел - подведение итогов""" + task = state["task"] + + print("\n" + "="*50) + print("📋 ИТОГИ РАБОТЫ") + print("="*50) + print(f"Задача: {task.requirement[:100]}...") + 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): + """Сохраняет решение в файлы""" + import os + output_dir = "generated_code" + os.makedirs(output_dir, exist_ok=True) + + for filename, content in solution.files.items(): + filepath = os.path.join(output_dir, filename) + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + print(f" 💾 Сохранен файл: {filepath}") + + for testname, test_content in solution.tests.items(): + testpath = os.path.join(output_dir, testname) + with open(testpath, "w", encoding="utf-8") as f: + f.write(test_content) + print(f" 💾 Сохранен тест: {testpath}") + + def run(self, requirement: str, task_id: str = None) -> Dict: + """Запускает процесс разработки""" + + if not task_id: + task_id = str(uuid.uuid4()) + + initial_task = Task( + id=task_id, + requirement=requirement, + status=TaskStatus.PENDING, + iteration=0 + ) + + print(f"\n🚀 Запуск мультиагентной системы") + print(f"📝 Задача: {requirement}") + print(f"🆔 ID: {task_id}") + + # Запускаем граф + initial_state = {"task": initial_task} + final_state = self.app.invoke(initial_state) + + return final_state + +# main.py +def main(): + """Пример использования""" + + # Создаем оркестратора + orchestrator = Orchestrator() + + # Пример задачи + requirement = """ + Реализуй класс BankAccount с методами: + - deposit(amount): пополнение счета + - withdraw(amount): снятие средств (нельзя снять больше, чем есть) + - get_balance(): получение баланса + - add_interest(rate): добавление процентов (rate в процентах) + + Требования: + - Нельзя создать счет с отрицательным балансом + - Все операции должны быть потокобезопасными (используй 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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..70a9a75 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +# requirements.txt + +langgraph>=0.2.0 +langchain>=0.2.0 +langchain-openai>=0.1.0 +langchain-community>=0.0.16 +python-dotenv>=1.0.0 +pydantic>=2.0.0 diff --git a/tasks.json b/tasks.json new file mode 100644 index 0000000..4b2106f --- /dev/null +++ b/tasks.json @@ -0,0 +1,7 @@ +{ + "tasks": [ + "Реализуй функцию is_palindrome(s) для проверки палиндрома", + "Создай класс Stack с методами push, pop, peek, is_empty", + "Напиши декоратор @timer, который замеряет время выполнения функции" + ] +} \ No newline at end of file