Compare commits
6 Commits
58497365d9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14bc0b906b | ||
|
|
f5677013cc | ||
|
|
8f16a6d669 | ||
|
|
ee6d12c5fb | ||
|
|
324097988b | ||
|
|
f110f52e91 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
report_*.json
|
||||||
7
agent/__init__.py
Normal file
7
agent/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from .developer import DeveloperAgent
|
||||||
|
from .reviewer import ReviewerAgent
|
||||||
|
from .planner import PlannerAgent
|
||||||
|
from .executor import ExecutorAgent
|
||||||
|
from .plan_reviewer import PlanReviewerAgent
|
||||||
|
|
||||||
|
__all__ = ["DeveloperAgent", "ReviewerAgent", "PlannerAgent", "ExecutorAgent", "PlanReviewerAgent"]
|
||||||
137
agent/developer.py
Normal file
137
agent/developer.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
|
from langchain_ollama import ChatOllama
|
||||||
|
import json
|
||||||
|
from config import MODEL_NAME, TEMPERATURE, LLM_HOST
|
||||||
|
from models import CodeSolution, ReviewResult, ReviewComment, Task
|
||||||
|
import re
|
||||||
|
|
||||||
|
class DeveloperAgent:
|
||||||
|
"""Агент-разработчик: генерирует код по требованию"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.llm = ChatOllama(
|
||||||
|
model=MODEL_NAME,
|
||||||
|
temperature=TEMPERATURE,
|
||||||
|
base_url=LLM_HOST,
|
||||||
|
num_predict=8192
|
||||||
|
)
|
||||||
|
|
||||||
|
def develop(self, task: Task, previous_comments: list = None) -> CodeSolution:
|
||||||
|
"""Генерирует код с улучшенным парсингом JSON"""
|
||||||
|
|
||||||
|
system_prompt = """Ты - опытный разработчик. Твоя задача - написать качественный код.
|
||||||
|
Ты должен:
|
||||||
|
1. Писать чистый, читаемый код с комментариями
|
||||||
|
2. Добавлять обработку ошибок
|
||||||
|
3. Писать юнит-тесты (pytest/unittest)
|
||||||
|
4. Следовать PEP8 (для Python) или стандартам языка
|
||||||
|
|
||||||
|
ВАЖНО: Возвращай ТОЛЬКО валидный JSON без лишних символов.
|
||||||
|
ЗАПРЕЩЕНЫ trailing commas (запятые перед закрывающей скобкой).
|
||||||
|
|
||||||
|
Формат ответа (пример):
|
||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"main.py": "print('hello')"
|
||||||
|
},
|
||||||
|
"description": "Краткое описание",
|
||||||
|
"tests": {
|
||||||
|
"test_main.py": "def test_hello(): assert True"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Не добавляй пояснений вне JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
raw_content = response.content
|
||||||
|
|
||||||
|
# Пытаемся извлечь JSON из разных форматов
|
||||||
|
parsed_data = self._extract_json(raw_content)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
# Если не удалось, пробуем повторно с explicit инструкцией
|
||||||
|
print("⚠️ Не удалось распарсить JSON, повторный запрос с исправлением...")
|
||||||
|
correction_prompt = f"""
|
||||||
|
Предыдущий ответ содержал невалидный JSON (возможно, trailing commas).
|
||||||
|
Пожалуйста, исправь и верни ТОЛЬКО валидный JSON в формате:
|
||||||
|
{{
|
||||||
|
"files": {{"filename": "content"}},
|
||||||
|
"description": "text",
|
||||||
|
"tests": {{"testfile": "test_content"}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
Твоя предыдущая попытка:
|
||||||
|
{raw_content[:1000]}
|
||||||
|
|
||||||
|
Требование: {task.requirement}
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages2 = [
|
||||||
|
SystemMessage(content="Ты - разработчик. Верни только валидный JSON без лишних символов."),
|
||||||
|
HumanMessage(content=correction_prompt)
|
||||||
|
]
|
||||||
|
response2 = self.llm.invoke(messages2)
|
||||||
|
parsed_data = self._extract_json(response2.content)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
raise ValueError(f"Failed to parse JSON after retry. Raw: {response2.content[:500]}")
|
||||||
|
|
||||||
|
return CodeSolution(
|
||||||
|
files=parsed_data.get("files", {}),
|
||||||
|
description=parsed_data.get("description", ""),
|
||||||
|
tests=parsed_data.get("tests", {})
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_json(self, text: str) -> Optional[dict]:
|
||||||
|
"""Извлекает и очищает JSON из текста ответа LLM"""
|
||||||
|
|
||||||
|
# 1. Извлечь блок между ```json и ``` или просто ```
|
||||||
|
if "```json" in text:
|
||||||
|
text = text.split("```json")[1].split("```")[0]
|
||||||
|
elif "```" in text:
|
||||||
|
text = text.split("```")[1].split("```")[0]
|
||||||
|
|
||||||
|
text = text.strip()
|
||||||
|
|
||||||
|
# 2. Удалить trailing commas в объектах и массивах
|
||||||
|
# Удаляем запятую перед } или ]
|
||||||
|
text = re.sub(r',\s*}', '}', text)
|
||||||
|
text = re.sub(r',\s*]', ']', text)
|
||||||
|
|
||||||
|
# 3. Удалить комментарии (// ...) если есть
|
||||||
|
text = re.sub(r'//.*?(\n|$)', '', text)
|
||||||
|
|
||||||
|
# 4. Попробовать распарсить
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"JSON decode error: {e}")
|
||||||
|
# 5. Еще одна попытка: найти любой JSON объект в строке
|
||||||
|
match = re.search(r'\{.*\}|\[.*\]', text, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
return json.loads(match.group())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
96
agent/executor.py
Normal file
96
agent/executor.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# executor.py
|
||||||
|
from typing import List, Dict
|
||||||
|
from langchain_ollama import ChatOllama
|
||||||
|
from config import LLM_HOST, MODEL_NAME
|
||||||
|
import time
|
||||||
|
|
||||||
|
class ExecutorAgent:
|
||||||
|
def __init__(self, tools, llm=None):
|
||||||
|
self.tools = tools
|
||||||
|
self.llm = llm or ChatOllama(
|
||||||
|
model=MODEL_NAME,
|
||||||
|
temperature=0.0,
|
||||||
|
base_url=LLM_HOST
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_plan(self, plan: List[Dict]) -> List[Dict]:
|
||||||
|
"""Выполняет план шаг за шагом через инструменты"""
|
||||||
|
results = []
|
||||||
|
total_steps = len(plan)
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"🚀 Начало выполнения плана ({total_steps} шагов)")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
for i, step in enumerate(plan, 1):
|
||||||
|
tool_name = step.get("tool", "unknown")
|
||||||
|
args = step.get("args", {})
|
||||||
|
description = step.get("description", "")
|
||||||
|
|
||||||
|
print(f"\n{'─'*60}")
|
||||||
|
print(f"📍 Шаг {i}/{total_steps}: {tool_name}")
|
||||||
|
if description:
|
||||||
|
print(f" 📝 Описание: {description}")
|
||||||
|
print(f" 📦 Аргументы: {args}")
|
||||||
|
print(f" ─────────────────────────────")
|
||||||
|
|
||||||
|
# Находим инструмент по имени
|
||||||
|
tool = None
|
||||||
|
for t in self.tools:
|
||||||
|
if t.name == tool_name:
|
||||||
|
tool = t
|
||||||
|
break
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
if tool:
|
||||||
|
try:
|
||||||
|
result = tool.invoke(args)
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"step": step,
|
||||||
|
"output": str(result),
|
||||||
|
"success": True,
|
||||||
|
"duration": elapsed
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" ✅ Успех ({elapsed:.2f}s)")
|
||||||
|
print(f" 📤 Результат: {str(result)[:100]}{'...' if len(str(result)) > 100 else ''}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
results.append({
|
||||||
|
"step": step,
|
||||||
|
"output": str(e),
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"duration": elapsed
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" ❌ Ошибка ({elapsed:.2f}s)")
|
||||||
|
print(f" 💥 {type(e).__name__}: {str(e)[:100]}")
|
||||||
|
else:
|
||||||
|
results.append({
|
||||||
|
"step": step,
|
||||||
|
"output": f"Tool '{tool_name}' not found",
|
||||||
|
"success": False,
|
||||||
|
"duration": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" ❌ Инструмент '{tool_name}' не найден")
|
||||||
|
|
||||||
|
# Итоговая статистика
|
||||||
|
success_count = sum(1 for r in results if r.get("success", False))
|
||||||
|
failed_count = total_steps - success_count
|
||||||
|
total_duration = sum(r.get("duration", 0) for r in results)
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"📊 ИТОГИ ВЫПОЛНЕНИЯ")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f"✅ Успешно: {success_count}/{total_steps}")
|
||||||
|
print(f"❌ Ошибки: {failed_count}")
|
||||||
|
print(f"⏱️ Общее время: {total_duration:.2f}s")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
return results
|
||||||
30
agent/plan_reviewer.py
Normal file
30
agent/plan_reviewer.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# reviewer.py
|
||||||
|
from langchain_ollama import ChatOllama
|
||||||
|
from langchain_core.messages import SystemMessage, HumanMessage
|
||||||
|
from typing import List, Dict
|
||||||
|
from config import MODEL_NAME, LLM_HOST
|
||||||
|
import json
|
||||||
|
|
||||||
|
class PlanReviewerAgent:
|
||||||
|
def __init__(self, llm=None):
|
||||||
|
self.llm = llm or ChatOllama(
|
||||||
|
model=MODEL_NAME,
|
||||||
|
temperature=0.0,
|
||||||
|
base_url=LLM_HOST
|
||||||
|
)
|
||||||
|
|
||||||
|
def review(self, original_task: str, plan: List[Dict], execution_results: List[Dict]) -> dict:
|
||||||
|
system_prompt = """Ты - ревьювер. Оцени, достигнута ли цель задачи на основе выполненного плана.
|
||||||
|
Верни JSON с полями:
|
||||||
|
- "status": "approved" / "changes_requested" / "rejected"
|
||||||
|
- "feedback": пояснение
|
||||||
|
- "suggested_plan_correction": если нужно изменить план - предложи новый план (массив шагов)
|
||||||
|
"""
|
||||||
|
user_prompt = f"""
|
||||||
|
Задача: {original_task}
|
||||||
|
План: {json.dumps(plan, indent=2)}
|
||||||
|
Результаты выполнения: {json.dumps(execution_results, indent=2)}
|
||||||
|
Оцени результат.
|
||||||
|
"""
|
||||||
|
response = self.llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=user_prompt)])
|
||||||
|
return json.loads(response.content)
|
||||||
41
agent/planner.py
Normal file
41
agent/planner.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# planner.py
|
||||||
|
from langchain_ollama import ChatOllama
|
||||||
|
from langchain_core.messages import SystemMessage, HumanMessage
|
||||||
|
import json
|
||||||
|
from typing import List, Dict
|
||||||
|
from config import MODEL_NAME, LLM_HOST
|
||||||
|
|
||||||
|
class PlannerAgent:
|
||||||
|
def __init__(self, llm=None):
|
||||||
|
self.llm = llm or ChatOllama(
|
||||||
|
model=MODEL_NAME,
|
||||||
|
temperature=0.0,
|
||||||
|
base_url=LLM_HOST
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_plan(self, task: str, tools_description: List[Dict]) -> List[Dict]:
|
||||||
|
system_prompt = """Ты - планировщик. Твоя задача - разбить запрос пользователя на последовательность шагов.
|
||||||
|
Каждый шаг - это вызов одного из доступных инструментов с конкретными аргументами.
|
||||||
|
Верни JSON массив шагов, где каждый шаг содержит:
|
||||||
|
- "tool": имя инструмента (строго из списка)
|
||||||
|
- "args": словарь аргументов для этого инструмента
|
||||||
|
- "description": краткое пояснение, зачем этот шаг
|
||||||
|
|
||||||
|
Инструменты и их аргументы:
|
||||||
|
{tools_description}
|
||||||
|
|
||||||
|
План должен быть линейным (шаг за шагом). Если нужны условия или циклы - разворачивай их в последовательность.
|
||||||
|
После выполнения всех шагов задача должна быть решена.
|
||||||
|
Верни ТОЛЬКО JSON массив, без лишнего текста."""
|
||||||
|
|
||||||
|
user_prompt = f"Задача: {task}"
|
||||||
|
tools_str = json.dumps(tools_description, indent=2)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
SystemMessage(content=system_prompt.format(tools_description=tools_str)),
|
||||||
|
HumanMessage(content=user_prompt)
|
||||||
|
]
|
||||||
|
response = self.llm.invoke(messages)
|
||||||
|
# парсим JSON
|
||||||
|
plan = json.loads(response.content)
|
||||||
|
return plan
|
||||||
@@ -1,79 +1,11 @@
|
|||||||
# agents.py
|
from typing import Optional, Dict, Any
|
||||||
from typing import Dict, Any
|
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langchain_core.messages import HumanMessage, SystemMessage
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
from langchain_community.chat_models import ChatOllama
|
from langchain_ollama import ChatOllama
|
||||||
import json
|
import json
|
||||||
from config import MODEL_NAME, TEMPERATURE
|
from config import MODEL_NAME, TEMPERATURE, LLM_HOST
|
||||||
from models import CodeSolution, ReviewResult, ReviewComment, Task
|
from models import CodeSolution, ReviewResult, ReviewComment, Task
|
||||||
|
import re
|
||||||
|
|
||||||
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:
|
class ReviewerAgent:
|
||||||
"""Агент-ревьювер: проверяет качество кода"""
|
"""Агент-ревьювер: проверяет качество кода"""
|
||||||
@@ -82,7 +14,8 @@ class ReviewerAgent:
|
|||||||
self.llm = ChatOllama(
|
self.llm = ChatOllama(
|
||||||
model=MODEL_NAME,
|
model=MODEL_NAME,
|
||||||
temperature=0.1,
|
temperature=0.1,
|
||||||
base_url="http://localhost:11434"
|
base_url=LLM_HOST,
|
||||||
|
num_predict=2048
|
||||||
)
|
)
|
||||||
|
|
||||||
def review(self, task: Task, solution: CodeSolution) -> ReviewResult:
|
def review(self, task: Task, solution: CodeSolution) -> ReviewResult:
|
||||||
@@ -95,15 +28,22 @@ class ReviewerAgent:
|
|||||||
4. Проверить покрытие тестами
|
4. Проверить покрытие тестами
|
||||||
5. Найти проблемы безопасности
|
5. Найти проблемы безопасности
|
||||||
6. Предложить улучшения
|
6. Предложить улучшения
|
||||||
|
|
||||||
Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки.
|
Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки.
|
||||||
|
|
||||||
|
Допустимые уровни severity (от критичного к информационному):
|
||||||
|
- critical: блокирующие ошибки (безопасность, падения)
|
||||||
|
- major: серьёзные логические ошибки, неверная работа
|
||||||
|
- minor: стиль, читаемость, мелкие баги
|
||||||
|
- warning: потенциальные проблемы, улучшения, edge cases
|
||||||
|
- info: предложения, рекомендации, необязательные улучшения
|
||||||
|
- suggestion: альтернативные подходы
|
||||||
|
|
||||||
Верни JSON с полями:
|
Верни JSON с полями:
|
||||||
- status: "approved", "changes_requested", или "rejected"
|
- status: "approved", "changes_requested", или "rejected"
|
||||||
- comments: массив объектов с полями: file, line, severity, text
|
- comments: массив объектов с полями: file, line, severity, text
|
||||||
- summary: краткое резюме ревью
|
- summary: краткое резюме ревью
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Формируем представление кода для ревью
|
# Формируем представление кода для ревью
|
||||||
code_for_review = ""
|
code_for_review = ""
|
||||||
for filename, content in solution.files.items():
|
for filename, content in solution.files.items():
|
||||||
@@ -6,9 +6,10 @@ load_dotenv()
|
|||||||
|
|
||||||
# Конфигурация модели
|
# Конфигурация модели
|
||||||
# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
MODEL_NAME = "qwen3.5:latest" # или "gpt-3.5-turbo" для тестов
|
LLM_HOST = "http://192.168.212.100:11434"
|
||||||
TEMPERATURE = 0.3
|
MODEL_NAME = "qwen3.5-16k:latest" # или "gpt-3.5-turbo" для тестов
|
||||||
|
TEMPERATURE = 0.2
|
||||||
MAX_ITERATIONS = 3
|
MAX_ITERATIONS = 3
|
||||||
|
|
||||||
# Конфигурация системы
|
# Конфигурация системы
|
||||||
MAX_REVIEW_RETRIES = 3
|
MAX_REVIEW_RETRIES = 1
|
||||||
21
main.py
21
main.py
@@ -15,7 +15,7 @@ def load_requirement_from_file(filepath: str) -> str:
|
|||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
def save_report(task, output_file: Optional[str] = None):
|
def save_report(task, output_file: Optional[str] = None):
|
||||||
"""Сохраняет детальный отчет о выполнении"""
|
"""Сохраняет детальный отчет с историей всех итераций"""
|
||||||
report = {
|
report = {
|
||||||
"task_id": task.id,
|
"task_id": task.id,
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
@@ -24,9 +24,23 @@ def save_report(task, output_file: Optional[str] = None):
|
|||||||
"iterations": task.iteration,
|
"iterations": task.iteration,
|
||||||
"final_review": None,
|
"final_review": None,
|
||||||
"error_log": task.error_log,
|
"error_log": task.error_log,
|
||||||
"files_generated": []
|
"files_generated": [],
|
||||||
|
"history": [] # <-- добавляем историю
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Добавляем историю итераций
|
||||||
|
for record in task.history:
|
||||||
|
report["history"].append({
|
||||||
|
"iteration": record.iteration,
|
||||||
|
"timestamp": record.timestamp,
|
||||||
|
"agent": record.agent,
|
||||||
|
"action": record.action,
|
||||||
|
"input_summary": record.input_summary,
|
||||||
|
"output_summary": record.output_summary,
|
||||||
|
"details": record.details,
|
||||||
|
"error": record.error
|
||||||
|
})
|
||||||
|
|
||||||
if task.last_review:
|
if task.last_review:
|
||||||
report["final_review"] = {
|
report["final_review"] = {
|
||||||
"status": task.last_review.status,
|
"status": task.last_review.status,
|
||||||
@@ -48,14 +62,13 @@ def save_report(task, output_file: Optional[str] = None):
|
|||||||
report["tests_generated"] = list(task.solution.tests.keys())
|
report["tests_generated"] = list(task.solution.tests.keys())
|
||||||
report["description"] = task.solution.description
|
report["description"] = task.solution.description
|
||||||
|
|
||||||
# Сохраняем в файл
|
|
||||||
if not output_file:
|
if not output_file:
|
||||||
output_file = f"report_{task.id}.json"
|
output_file = f"report_{task.id}.json"
|
||||||
|
|
||||||
with open(output_file, 'w', encoding='utf-8') as f:
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
json.dump(report, f, indent=2, ensure_ascii=False)
|
json.dump(report, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
print(f"\n📄 Отчет сохранен: {output_file}")
|
print(f"\n📄 Report saved: {output_file}")
|
||||||
return report
|
return report
|
||||||
|
|
||||||
def interactive_mode():
|
def interactive_mode():
|
||||||
|
|||||||
17
models.py
17
models.py
@@ -11,11 +11,12 @@ class TaskStatus(str, Enum):
|
|||||||
APPROVED = "approved"
|
APPROVED = "approved"
|
||||||
REJECTED = "rejected"
|
REJECTED = "rejected"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
DEVELOPMENT = "development"
|
||||||
|
|
||||||
class ReviewComment(BaseModel):
|
class ReviewComment(BaseModel):
|
||||||
line: Optional[int] = None
|
line: Optional[int] = None
|
||||||
file: Optional[str] = None
|
file: Optional[str] = None
|
||||||
severity: Literal["critical", "major", "minor", "suggestion"] = "major"
|
severity: Literal["critical", "major", "minor", "suggestion", "warning", "info"] = "major"
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
class ReviewResult(BaseModel):
|
class ReviewResult(BaseModel):
|
||||||
@@ -28,6 +29,17 @@ class CodeSolution(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
tests: Dict[str, str] = Field(default_factory=dict) # test_filename -> content
|
tests: Dict[str, str] = Field(default_factory=dict) # test_filename -> content
|
||||||
|
|
||||||
|
class IterationRecord(BaseModel):
|
||||||
|
"""Запись одной итерации"""
|
||||||
|
iteration: int
|
||||||
|
timestamp: str
|
||||||
|
agent: Literal["developer", "reviewer"]
|
||||||
|
action: str # "start", "complete", "error"
|
||||||
|
input_summary: Optional[str] = None
|
||||||
|
output_summary: Optional[str] = None
|
||||||
|
details: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
class Task(BaseModel):
|
class Task(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
requirement: str
|
requirement: str
|
||||||
@@ -35,4 +47,5 @@ class Task(BaseModel):
|
|||||||
iteration: int = 0
|
iteration: int = 0
|
||||||
solution: Optional[CodeSolution] = None
|
solution: Optional[CodeSolution] = None
|
||||||
last_review: Optional[ReviewResult] = None
|
last_review: Optional[ReviewResult] = None
|
||||||
error_log: List[str] = Field(default_factory=list)
|
error_log: List[str] = Field(default_factory=list)
|
||||||
|
history: List[IterationRecord] = Field(default_factory=list) # <-- новая история
|
||||||
370
orchestrator.py
370
orchestrator.py
@@ -1,12 +1,24 @@
|
|||||||
# orchestrator.py
|
# orchestrator.py
|
||||||
from typing import Dict, Any, Literal
|
from typing import Dict, Any, Literal, TypedDict, List, Optional
|
||||||
from langgraph.graph import StateGraph, END
|
from langgraph.graph import StateGraph, END
|
||||||
from langgraph.checkpoint.memory import MemorySaver
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from models import Task, TaskStatus, CodeSolution, ReviewResult
|
from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
|
||||||
from agents import DeveloperAgent, ReviewerAgent
|
from agent.developer import DeveloperAgent
|
||||||
from config import MAX_REVIEW_RETRIES
|
from agent.reviewer import ReviewerAgent
|
||||||
|
from agent.planner import PlannerAgent
|
||||||
|
from agent.executor import ExecutorAgent
|
||||||
|
from agent.plan_reviewer import PlanReviewerAgent
|
||||||
|
from tool.filesystem import read_file, write_file, delete_lines, replace_text_in_file, search_in_files
|
||||||
|
|
||||||
|
class AgentState(TypedDict):
|
||||||
|
task: Task
|
||||||
|
plan: List[Dict]
|
||||||
|
execution_results: List[Dict]
|
||||||
|
review: Dict
|
||||||
|
iteration: int
|
||||||
|
max_iterations: int
|
||||||
|
|
||||||
class Orchestrator:
|
class Orchestrator:
|
||||||
"""Оркестратор - управляет процессом разработки"""
|
"""Оркестратор - управляет процессом разработки"""
|
||||||
@@ -14,158 +26,177 @@ class Orchestrator:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.developer = DeveloperAgent()
|
self.developer = DeveloperAgent()
|
||||||
self.reviewer = ReviewerAgent()
|
self.reviewer = ReviewerAgent()
|
||||||
self.max_retries = MAX_REVIEW_RETRIES
|
self.planner = PlannerAgent()
|
||||||
|
self.plan_reviewer = PlanReviewerAgent()
|
||||||
|
|
||||||
|
# Инструменты для ExecutorAgent
|
||||||
|
self.tools = [read_file, write_file, delete_lines, replace_text_in_file, search_in_files]
|
||||||
|
self.executor = ExecutorAgent(self.tools)
|
||||||
|
|
||||||
|
self.max_retries = 3
|
||||||
|
|
||||||
# Создаем граф состояний
|
# Создаем граф состояний
|
||||||
self.workflow = self._build_workflow()
|
self.workflow = self._build_workflow()
|
||||||
self.app = self.workflow.compile(checkpointer=MemorySaver())
|
self.checkpointer = MemorySaver()
|
||||||
|
|
||||||
|
def _add_history(self, task: Task, agent: str, action: str,
|
||||||
|
input_summary: str = None, output_summary: str = None,
|
||||||
|
details: Dict = None, error: str = None):
|
||||||
|
"""Добавляет запись в историю задачи"""
|
||||||
|
record = IterationRecord(
|
||||||
|
iteration=task.iteration,
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
agent=agent,
|
||||||
|
action=action,
|
||||||
|
input_summary=input_summary,
|
||||||
|
output_summary=output_summary,
|
||||||
|
details=details or {},
|
||||||
|
error=error
|
||||||
|
)
|
||||||
|
task.history.append(record)
|
||||||
|
|
||||||
def _build_workflow(self) -> StateGraph:
|
def _build_workflow(self):
|
||||||
"""Строит граф процесса разработки"""
|
"""Строит граф процесса разработки"""
|
||||||
|
|
||||||
# Определяем состояния
|
# Определяем состояния
|
||||||
workflow = StateGraph(Dict)
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
# Добавляем узлы
|
# Добавляем узлы
|
||||||
workflow.add_node("developer", self._developer_node)
|
workflow.add_node("planner", self._plan_node)
|
||||||
workflow.add_node("reviewer", self._reviewer_node)
|
workflow.add_node("executor", self._exec_node)
|
||||||
workflow.add_node("finalize", self._finalize_node)
|
workflow.add_node("reviewer", self._review_node)
|
||||||
|
|
||||||
# Определяем переходы
|
# Определяем переходы
|
||||||
workflow.set_entry_point("developer")
|
workflow.set_entry_point("planner")
|
||||||
workflow.add_edge("developer", "reviewer")
|
workflow.add_edge("planner", "executor")
|
||||||
|
workflow.add_edge("executor", "reviewer")
|
||||||
|
|
||||||
# Условные переходы после ревью
|
workflow.add_conditional_edges("reviewer", self._after_review, {
|
||||||
workflow.add_conditional_edges(
|
"approved": END,
|
||||||
"reviewer",
|
"rework": "executor", # повторить выполнение с тем же планом? или перепланировать?
|
||||||
self._decide_next_step,
|
"replan": "planner",
|
||||||
{
|
"reject": END
|
||||||
"approved": "finalize",
|
})
|
||||||
"rework": "developer",
|
|
||||||
"rejected": "finalize",
|
|
||||||
"failed": "finalize"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
workflow.add_edge("finalize", END)
|
return workflow.compile()
|
||||||
|
|
||||||
return workflow
|
|
||||||
|
|
||||||
def _developer_node(self, state: Dict) -> Dict:
|
def _plan_node(self, state: AgentState):
|
||||||
"""Узел разработчика"""
|
|
||||||
task = state["task"]
|
task = state["task"]
|
||||||
print(f"\n🔧 [Iteration {task.iteration}] Разработчик начал работу...")
|
print(f"\n🔧 [Iteration {task.iteration}] Планировщик начал работу...")
|
||||||
|
|
||||||
# Получаем замечания с предыдущего ревью
|
# Создаем запись в истории
|
||||||
previous_comments = []
|
task.history.append(IterationRecord(
|
||||||
if task.last_review and task.last_review.comments:
|
iteration=task.iteration,
|
||||||
previous_comments = [
|
timestamp=datetime.now().isoformat(),
|
||||||
{"file": c.file, "line": c.line, "text": c.text}
|
agent="developer",
|
||||||
for c in task.last_review.comments
|
action="start",
|
||||||
]
|
input_summary=f"Requirement: {task.requirement[:100]}...",
|
||||||
|
details={}
|
||||||
|
))
|
||||||
|
|
||||||
try:
|
# Описываем доступные инструменты для планировщика
|
||||||
# Генерируем код
|
tools_desc = [
|
||||||
solution = self.developer.develop(task, previous_comments)
|
{"name": "read_file", "description": "Прочитать файл", "args": ["path", "start_line", "end_line"]},
|
||||||
task.solution = solution
|
{"name": "write_file", "description": "Записать файл", "args": ["path", "content"]},
|
||||||
task.status = TaskStatus.REVIEW
|
{"name": "delete_lines", "description": "Удалить строки", "args": ["path", "start_line", "end_line"]},
|
||||||
|
{"name": "replace_text_in_file", "description": "Заменить текст", "args": ["path", "old", "new"]},
|
||||||
print(f"✅ Разработчик создал решение: {len(solution.files)} файлов")
|
{"name": "search_in_files", "description": "Поиск в файлах", "args": ["directory", "pattern", "file_pattern"]}
|
||||||
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}
|
# Создаем план
|
||||||
|
plan = self.planner.create_plan(task.requirement, tools_desc)
|
||||||
def _reviewer_node(self, state: Dict) -> Dict:
|
state["plan"] = plan
|
||||||
"""Узел ревьювера"""
|
|
||||||
|
print(f"📋 План создан: {len(plan)} шагов")
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _exec_node(self, state: AgentState):
|
||||||
|
"""Узел исполнителя"""
|
||||||
task = state["task"]
|
task = state["task"]
|
||||||
print(f"\n🔍 [Iteration {task.iteration}] Ревьювер проверяет код...")
|
print(f"\n🔨 [Iteration {task.iteration}] Исполнитель начал работу...")
|
||||||
|
|
||||||
if not task.solution:
|
# Выполняем план через ExecutorAgent
|
||||||
task.status = TaskStatus.FAILED
|
results = self.executor.execute_plan(state["plan"])
|
||||||
task.error_log.append("No solution to review")
|
state["execution_results"] = results
|
||||||
return {"task": task}
|
|
||||||
|
|
||||||
try:
|
# Записываем результат выполнения
|
||||||
# Проводим ревью
|
success_count = sum(1 for r in results if r.get("success", False))
|
||||||
review = self.reviewer.review(task, task.solution)
|
print(f"✅ Выполнено шагов: {success_count}/{len(results)}")
|
||||||
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}
|
return state
|
||||||
|
|
||||||
def _decide_next_step(self, state: Dict) -> Literal["approved", "rework", "rejected", "failed"]:
|
def _review_node(self, state: AgentState):
|
||||||
"""Решает, что делать дальше"""
|
|
||||||
task = state["task"]
|
task = state["task"]
|
||||||
|
print(f"\n🔍 [Iteration {task.iteration}] План-ревьювер начал работу...")
|
||||||
|
|
||||||
if task.status == TaskStatus.APPROVED:
|
# Добавляем запись о начале ревью
|
||||||
return "approved"
|
task.history.append(IterationRecord(
|
||||||
elif task.status == TaskStatus.REJECTED:
|
iteration=task.iteration,
|
||||||
return "rejected"
|
timestamp=datetime.now().isoformat(),
|
||||||
elif task.status == TaskStatus.FAILED:
|
agent="reviewer",
|
||||||
return "failed"
|
action="start",
|
||||||
elif task.iteration >= self.max_retries:
|
input_summary=f"Reviewing plan with {len(state['plan'])} steps",
|
||||||
print(f"\n⚠️ Достигнут лимит итераций ({self.max_retries}). Останавливаемся.")
|
details={}
|
||||||
task.status = TaskStatus.FAILED
|
))
|
||||||
return "failed"
|
|
||||||
|
# Проводим ревью плана через PlanReviewerAgent
|
||||||
|
review = self.plan_reviewer.review(task.requirement, state["plan"], state["execution_results"])
|
||||||
|
state["review"] = review
|
||||||
|
|
||||||
|
# Обновляем статус задачи
|
||||||
|
if review.get("status") == "approved":
|
||||||
|
task.status = TaskStatus.APPROVED
|
||||||
|
elif review.get("status") == "changes_requested":
|
||||||
|
task.status = TaskStatus.REWORK
|
||||||
else:
|
else:
|
||||||
return "rework"
|
task.status = TaskStatus.FAILED
|
||||||
|
|
||||||
def _finalize_node(self, state: Dict) -> Dict:
|
# Увеличиваем счётчик итераций
|
||||||
"""Финальный узел - подведение итогов"""
|
state["iteration"] = state.get("iteration", 0) + 1
|
||||||
|
|
||||||
|
# Добавляем запись в историю
|
||||||
|
task.history.append(IterationRecord(
|
||||||
|
iteration=task.iteration,
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
agent="reviewer",
|
||||||
|
action="complete",
|
||||||
|
output_summary=f"Verdict: {review.get('status')}",
|
||||||
|
details={
|
||||||
|
"verdict": review.get("status"),
|
||||||
|
"feedback": review.get("feedback", "")[:200]
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
print(f"📊 Вердикт: {review.get('status')}")
|
||||||
|
if review.get("feedback"):
|
||||||
|
print(f" Feedback: {review['feedback'][:100]}...")
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _after_review(self, state: AgentState):
|
||||||
task = state["task"]
|
task = state["task"]
|
||||||
|
review = state["review"]
|
||||||
|
|
||||||
print("\n" + "="*50)
|
# Определяем статус
|
||||||
print("📋 ИТОГИ РАБОТЫ")
|
status = review.get("status") if isinstance(review, dict) else None
|
||||||
print("="*50)
|
|
||||||
print(f"Задача: {task.requirement[:100]}...")
|
|
||||||
print(f"Статус: {task.status}")
|
|
||||||
print(f"Итераций: {task.iteration}")
|
|
||||||
|
|
||||||
if task.status == TaskStatus.APPROVED and task.solution:
|
if status == "approved":
|
||||||
print(f"\n✅ Решение принято!")
|
return "approved"
|
||||||
print(f" Файлов: {len(task.solution.files)}")
|
elif status == "changes_requested":
|
||||||
print(f" Тестов: {len(task.solution.tests)}")
|
if state.get("iteration", 0) >= state.get("max_iterations", 3):
|
||||||
|
task.status = TaskStatus.FAILED
|
||||||
# Сохраняем решение в файлы
|
return "reject"
|
||||||
self._save_solution(task.solution)
|
task.status = TaskStatus.REWORK
|
||||||
|
return "rework"
|
||||||
elif task.last_review:
|
elif status == "rejected":
|
||||||
print(f"\n📝 Финальный вердикт: {task.last_review.summary}")
|
if state.get("iteration", 0) >= state.get("max_iterations", 3):
|
||||||
|
task.status = TaskStatus.FAILED
|
||||||
if task.error_log:
|
return "reject"
|
||||||
print(f"\n❌ Ошибки: {len(task.error_log)}")
|
task.status = TaskStatus.REWORK
|
||||||
for error in task.error_log:
|
return "replan"
|
||||||
print(f" - {error}")
|
else:
|
||||||
|
task.status = TaskStatus.FAILED
|
||||||
return {"task": task, "finalized": True}
|
return "reject"
|
||||||
|
|
||||||
def _save_solution(self, solution: CodeSolution):
|
def _save_solution(self, solution: CodeSolution):
|
||||||
"""Сохраняет решение в файлы"""
|
"""Сохраняет решение в файлы"""
|
||||||
@@ -185,62 +216,51 @@ class Orchestrator:
|
|||||||
f.write(test_content)
|
f.write(test_content)
|
||||||
print(f" 💾 Сохранен тест: {testpath}")
|
print(f" 💾 Сохранен тест: {testpath}")
|
||||||
|
|
||||||
def run(self, requirement: str, task_id: str = None) -> Dict:
|
def run(self, task_text: str, max_iterations: int = 3) -> Dict:
|
||||||
"""Запускает процесс разработки"""
|
"""Запускает процесс разработки"""
|
||||||
|
|
||||||
if not task_id:
|
task_id = str(uuid.uuid4())
|
||||||
task_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
initial_task = Task(
|
# Создаем объект Task
|
||||||
|
task = Task(
|
||||||
id=task_id,
|
id=task_id,
|
||||||
requirement=requirement,
|
requirement=task_text,
|
||||||
status=TaskStatus.PENDING,
|
status=TaskStatus.PENDING,
|
||||||
iteration=0
|
iteration=0,
|
||||||
|
solution=None,
|
||||||
|
last_review=None,
|
||||||
|
error_log=[],
|
||||||
|
history=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
initial_state = {
|
||||||
|
"task": task,
|
||||||
|
"plan": [],
|
||||||
|
"execution_results": [],
|
||||||
|
"review": {},
|
||||||
|
"iteration": 0,
|
||||||
|
"max_iterations": max_iterations
|
||||||
|
}
|
||||||
|
|
||||||
print(f"\n🚀 Запуск мультиагентной системы")
|
print(f"\n🚀 Запуск мультиагентной системы")
|
||||||
print(f"📝 Задача: {requirement}")
|
print(f"📝 Задача: {task_text[:100]}...")
|
||||||
print(f"🆔 ID: {task_id}")
|
print(f"🆔 ID: {task_id}")
|
||||||
|
|
||||||
# Запускаем граф
|
# Запускаем граф
|
||||||
initial_state = {"task": initial_task}
|
final_state = self.workflow.invoke(initial_state)
|
||||||
final_state = self.app.invoke(initial_state)
|
|
||||||
|
|
||||||
return final_state
|
# Обновляем итоговый статус в задаче
|
||||||
|
final_task = final_state["task"]
|
||||||
# main.py
|
|
||||||
def main():
|
print("\n" + "="*50)
|
||||||
"""Пример использования"""
|
print("📋 ИТОГИ РАБОТЫ")
|
||||||
|
print("="*50)
|
||||||
# Создаем оркестратора
|
print(f"Статус: {final_task.status.value}")
|
||||||
orchestrator = Orchestrator()
|
print(f"Итераций: {final_task.iteration}")
|
||||||
|
|
||||||
# Пример задачи
|
if final_task.error_log:
|
||||||
requirement = """
|
print(f"\n❌ Ошибки: {len(final_task.error_log)}")
|
||||||
Реализуй класс BankAccount с методами:
|
for error in final_task.error_log:
|
||||||
- deposit(amount): пополнение счета
|
print(f" - {error}")
|
||||||
- withdraw(amount): снятие средств (нельзя снять больше, чем есть)
|
|
||||||
- get_balance(): получение баланса
|
return {"task": final_task}
|
||||||
- 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()
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# requirements.txt
|
# requirements.txt
|
||||||
|
|
||||||
langgraph>=0.2.0
|
langgraph>=0.3.0
|
||||||
langchain>=0.2.0
|
langchain>=0.3.0
|
||||||
langchain-openai>=0.1.0
|
|
||||||
langchain-community>=0.0.16
|
langchain-community>=0.0.16
|
||||||
|
langchain-ollama
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
pydantic>=2.0.0
|
pydantic>=2.0.0
|
||||||
|
|||||||
0
tool/__init__.py
Normal file
0
tool/__init__.py
Normal file
82
tool/filesystem.py
Normal file
82
tool/filesystem.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from langchain.tools import tool
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def read_file(path: str, start_line: int = 0, end_line: int = None) -> str:
|
||||||
|
"""Прочитать содержимое файла. Можно указать диапазон строк (1-индекс)."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return f"File not found: {path}"
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
if start_line > 0:
|
||||||
|
lines = lines[start_line-1:]
|
||||||
|
if end_line:
|
||||||
|
lines = lines[:end_line-start_line+1]
|
||||||
|
return ''.join(lines)
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def write_file(path: str, content: str) -> str:
|
||||||
|
"""Создать или перезаписать файл с указанным содержимым."""
|
||||||
|
# Создаем директорию только если путь содержит директорию
|
||||||
|
dir_name = os.path.dirname(path)
|
||||||
|
if dir_name:
|
||||||
|
os.makedirs(dir_name, exist_ok=True)
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
return f"Файл {path} записан"
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def delete_lines(path: str, start_line: int, end_line: int) -> str:
|
||||||
|
"""Удалить строки из файла (1-индекс, включительно)."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return f"File not found: {path}"
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
del lines[start_line-1:end_line]
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
return f"Удалены строки {start_line}-{end_line} из {path}"
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def replace_text_in_file(path: str, old: str, new: str) -> str:
|
||||||
|
"""Заменить все вхождения old на new в файле."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return f"File not found: {path}"
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
new_content = content.replace(old, new)
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
return f"Заменено {content.count(old)} вхождений"
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search_in_files(directory: str, pattern: str, file_pattern: str = "*.py") -> str:
|
||||||
|
"""Найти все файлы, содержащие pattern (grep)."""
|
||||||
|
cmd = f"grep -l --include='{file_pattern}' -r '{pattern}' {directory}"
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
# @tool
|
||||||
|
# def run_command(command: str) -> str:
|
||||||
|
# """Выполнить системную команду и вернуть вывод."""
|
||||||
|
# result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||||
|
# return f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
||||||
|
|
||||||
|
# @tool
|
||||||
|
# def find_class_definition(file_path: str, class_name: str) -> str:
|
||||||
|
# """Найти в файле определение класса (простой поиск по regex)."""
|
||||||
|
# with open(file_path, 'r') as f:
|
||||||
|
# content = f.read()
|
||||||
|
# pattern = rf'^\s*class\s+{class_name}\s*[:\(]'
|
||||||
|
# match = re.search(pattern, content, re.MULTILINE)
|
||||||
|
# if match:
|
||||||
|
# # найти конец класса по отступам (упрощённо)
|
||||||
|
# lines = content.splitlines()
|
||||||
|
# start_line = content[:match.start()].count('\n') + 1
|
||||||
|
# # грубо ищем до следующего class или def на том же уровне
|
||||||
|
# # для простоты вернём 20 строк после
|
||||||
|
# end_line = start_line + 20
|
||||||
|
# return f"Класс {class_name} найден в {file_path} (строки {start_line}-{end_line})"
|
||||||
|
# return f"Класс {class_name} не найден в {file_path}"
|
||||||
Reference in New Issue
Block a user