This commit is contained in:
2026-04-11 08:48:42 +05:00
commit 58497365d9
8 changed files with 775 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
__pycache__

160
agents.py Normal file
View File

@@ -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}")

14
config.py Normal file
View File

@@ -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

301
main.py Normal file
View File

@@ -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()

38
models.py Normal file
View File

@@ -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)

246
orchestrator.py Normal file
View File

@@ -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()

8
requirements.txt Normal file
View File

@@ -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

7
tasks.json Normal file
View File

@@ -0,0 +1,7 @@
{
"tasks": [
"Реализуй функцию is_palindrome(s) для проверки палиндрома",
"Создай класс Stack с методами push, pop, peek, is_empty",
"Напиши декоратор @timer, который замеряет время выполнения функции"
]
}