From 58497365d95ba14b445cc8000c076af9dc2a81f8 Mon Sep 17 00:00:00 2001 From: Teraonious Date: Sat, 11 Apr 2026 08:48:42 +0500 Subject: [PATCH] iFirst! --- .gitignore | 1 + agents.py | 160 +++++++++++++++++++++++++ config.py | 14 +++ main.py | 301 +++++++++++++++++++++++++++++++++++++++++++++++ models.py | 38 ++++++ orchestrator.py | 246 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 8 ++ tasks.json | 7 ++ 8 files changed, 775 insertions(+) create mode 100644 .gitignore create mode 100644 agents.py create mode 100644 config.py create mode 100644 main.py create mode 100644 models.py create mode 100644 orchestrator.py create mode 100644 requirements.txt create mode 100644 tasks.json 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