Compare commits

...

6 Commits

Author SHA1 Message Date
ki.sagidullin
14bc0b906b Итерация 3, финальная 2026-04-12 12:03:19 +05:00
ki.sagidullin
f5677013cc вторая итерация 2026-04-12 11:48:37 +05:00
ki.sagidullin
8f16a6d669 Переделываю систему под агента-планировщика 2026-04-12 09:27:07 +05:00
ki.sagidullin
ee6d12c5fb переход на langchain-ollama 2026-04-12 09:03:37 +05:00
ki.sagidullin
324097988b Вынес агентов в отдельные файлы 2026-04-12 08:51:33 +05:00
ki.sagidullin
f110f52e91 Рабочие агенты, но пока требуют улучшений 2026-04-11 10:20:47 +05:00
14 changed files with 645 additions and 264 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
__pycache__ __pycache__
report_*.json

7
agent/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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

View File

@@ -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:
@@ -98,12 +31,19 @@ class ReviewerAgent:
Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки. Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки.
Допустимые уровни 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():

View File

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

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

View File

@@ -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
@@ -36,3 +48,4 @@ class Task(BaseModel):
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) # <-- новая история

View File

@@ -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 _build_workflow(self) -> StateGraph: 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):
"""Строит граф процесса разработки""" """Строит граф процесса разработки"""
# Определяем состояния # Определяем состояния
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 _plan_node(self, state: AgentState):
def _developer_node(self, state: Dict) -> Dict:
"""Узел разработчика"""
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={}
))
# Описываем доступные инструменты для планировщика
tools_desc = [
{"name": "read_file", "description": "Прочитать файл", "args": ["path", "start_line", "end_line"]},
{"name": "write_file", "description": "Записать файл", "args": ["path", "content"]},
{"name": "delete_lines", "description": "Удалить строки", "args": ["path", "start_line", "end_line"]},
{"name": "replace_text_in_file", "description": "Заменить текст", "args": ["path", "old", "new"]},
{"name": "search_in_files", "description": "Поиск в файлах", "args": ["directory", "pattern", "file_pattern"]}
] ]
try: # Создаем план
# Генерируем код plan = self.planner.create_plan(task.requirement, tools_desc)
solution = self.developer.develop(task, previous_comments) state["plan"] = plan
task.solution = solution
task.status = TaskStatus.REVIEW
print(f"✅ Разработчик создал решение: {len(solution.files)} файлов") print(f"📋 План создан: {len(plan)} шагов")
for filename in solution.files: return state
print(f" - {filename}")
except Exception as e: def _exec_node(self, state: AgentState):
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"] 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}") return state
if review.comments:
print(f" Найдено замечаний: {len(review.comments)}")
for comment in review.comments[:3]: # Показываем первые 3
print(f" - [{comment.severity}] {comment.text[:100]}")
# Обновляем статус def _review_node(self, state: AgentState):
if review.status == "approved": task = state["task"]
print(f"\n🔍 [Iteration {task.iteration}] План-ревьювер начал работу...")
# Добавляем запись о начале ревью
task.history.append(IterationRecord(
iteration=task.iteration,
timestamp=datetime.now().isoformat(),
agent="reviewer",
action="start",
input_summary=f"Reviewing plan with {len(state['plan'])} steps",
details={}
))
# Проводим ревью плана через 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 task.status = TaskStatus.APPROVED
print("✅ Код одобрен!") elif review.get("status") == "changes_requested":
elif review.status == "rejected":
task.status = TaskStatus.REJECTED
print("❌ Код отклонен архитектурно")
else:
task.status = TaskStatus.REWORK 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: 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 print("\n" + "="*50)
def main(): print("📋 ИТОГИ РАБОТЫ")
"""Пример использования""" print("="*50)
print(f"Статус: {final_task.status.value}")
print(f"Итераций: {final_task.iteration}")
# Создаем оркестратора if final_task.error_log:
orchestrator = Orchestrator() print(f"\n❌ Ошибки: {len(final_task.error_log)}")
for error in final_task.error_log:
print(f" - {error}")
# Пример задачи return {"task": final_task}
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()

View File

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

82
tool/filesystem.py Normal file
View 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}"