iFirst!
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
160
agents.py
Normal file
160
agents.py
Normal 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
14
config.py
Normal 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
301
main.py
Normal 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
38
models.py
Normal 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
246
orchestrator.py
Normal 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
8
requirements.txt
Normal 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
7
tasks.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tasks": [
|
||||
"Реализуй функцию is_palindrome(s) для проверки палиндрома",
|
||||
"Создай класс Stack с методами push, pop, peek, is_empty",
|
||||
"Напиши декоратор @timer, который замеряет время выполнения функции"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user