116 lines
6.3 KiB
Python
116 lines
6.3 KiB
Python
class CoderAgent:
|
||||
|
|
"""Агент-исполнитель: выполняет подзадачу, генерирует код и применяет изменения."""
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def execute(project: Project, subtask: str, files: List[str], error_feedback: str = "") -> bool:
|
|||
|
|
print(f"💻 Кодер выполняет: {subtask}")
|
|||
|
|
|
|||
|
|
# 1. Начинаем с переданных файлов
|
|||
|
|
context_files = set(files)
|
|||
|
|
|
|||
|
|
# 2. Извлекаем пути из error_feedback (если есть)
|
|||
|
|
if error_feedback:
|
|||
|
|
import re
|
|||
|
|
# Ищем пути к файлам (с расширениями .ts, .tsx, .js, .jsx, .html, .css, .py)
|
|||
|
|
# Паттерн ищет строки, содержащие путь с расширением, например "src/App.tsx"
|
|||
|
|
path_pattern = r'[^\s"\']+\.(tsx?|jsx?|html|css|py)\b'
|
|||
|
|
found_paths = re.findall(path_pattern, error_feedback, re.IGNORECASE)
|
|||
|
|
# Также ищем полные URL типа file:// или http:// с путем
|
|||
|
|
url_pattern = r'(?:file://|https?://[^\s]+)(/[^\s"\']+)'
|
|||
|
|
url_matches = re.findall(url_pattern, error_feedback)
|
|||
|
|
for match in url_matches:
|
|||
|
|
# обрезаем до относительного пути относительно корня проекта?
|
|||
|
|
# Пока просто добавляем как есть, но лучше нормализовать
|
|||
|
|
found_paths.append(match)
|
|||
|
|
|
|||
|
|
# Добавляем найденные пути, если они не в списке
|
|||
|
|
for p in found_paths:
|
|||
|
|
# Удаляем лишние символы, например, :line:col или суффиксы
|
|||
|
|
# Часто путь может быть вида "file:///home/project/src/App.tsx:12:34"
|
|||
|
|
# или просто "src/App.tsx:12:34"
|
|||
|
|
p = p.split(':')[0] # отбрасываем строку/колонку, если есть
|
|||
|
|
p = p.strip()
|
|||
|
|
# Если путь начинается с /, то это абсолютный путь, пытаемся сделать относительным
|
|||
|
|
if p.startswith('/'):
|
|||
|
|
p = p.lstrip('/')
|
|||
|
|
# Если это абсолютный путь внутри проекта, то можно обрезать корень
|
|||
|
|
# Упрощённо: ищем вхождение папки проекта
|
|||
|
|
proj_root = str(project.path.resolve())
|
|||
|
|
if p.startswith(proj_root):
|
|||
|
|
p = os.path.relpath(p, proj_root)
|
|||
|
|
else:
|
|||
|
|
# иначе оставляем как есть (может не сработать)
|
|||
|
|
pass
|
|||
|
|
if p and p not in context_files:
|
|||
|
|
context_files.add(p)
|
|||
|
|
|
|||
|
|
# 3. Читаем содержимое всех файлов
|
|||
|
|
file_context = []
|
|||
|
|
for rel_path in context_files:
|
|||
|
|
full_path = project.path / rel_path
|
|||
|
|
if not full_path.exists():
|
|||
|
|
print(f" ⚠️ Файл не найден: {rel_path}")
|
|||
|
|
continue
|
|||
|
|
content = project.read_file_content(full_path)
|
|||
|
|
if content:
|
|||
|
|
file_context.append(f"=== {rel_path} ===\n{content}")
|
|||
|
|
else:
|
|||
|
|
print(f" ⚠️ Не удалось прочитать {rel_path}")
|
|||
|
|
|
|||
|
|
context = "\n".join(file_context) if file_context else "(нет файлов для контекста)"
|
|||
|
|
|
|||
|
|
# 4. Формируем промпт
|
|||
|
|
system_prompt = "Ты — опытный разработчик. Отвечай только кодом, без лишнего текста. Если нужно изменить несколько файлов, выдавай блоки вида: [filename]\n```language\nкод\n```"
|
|||
|
|
user_prompt = f"""
|
|||
|
|
Текущая задача: {subtask}
|
|||
|
|
{f"При предыдущей попытке возникли ошибки: {error_feedback}" if error_feedback else ""}
|
|||
|
|
|
|||
|
|
Текущий код указанных файлов:
|
|||
|
|
{context}
|
|||
|
|
|
|||
|
|
Внеси изменения в соответствующие файлы. Если создаёшь новый файл — укажи его путь относительно корня проекта.
|
|||
|
|
Выдай изменения в формате:
|
|||
|
|
[путь/к/файлу1]
|
|||
|
|
```язык
|
|||
|
|
код
|
|||
|
|
```
|
|||
|
|
[путь/к/файлу2]
|
|||
|
|
```язык
|
|||
|
|
код
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
messages = [{"role": "system", "content": system_prompt},
|
|||
|
|
{"role": "user", "content": user_prompt}]
|
|||
|
|
response = project.ollama_client.chat(messages, temperature=0.2)
|
|||
|
|
if not response:
|
|||
|
|
print("⚠️ Ответ от Ollama пустой.")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
pattern = r'\[(.*?)\]\s*```(?:\w+)?\s*(.*?)```'
|
|||
|
|
matches = re.findall(pattern, response, re.DOTALL)
|
|||
|
|
if not matches:
|
|||
|
|
print("⚠️ Не удалось распарсить ответ, пытаемся сохранить как один файл")
|
|||
|
|
if context_files:
|
|||
|
|
first_file = next(iter(context_files))
|
|||
|
|
full_path = project.path / first_file
|
|||
|
|
project.write_file(full_path, response)
|
|||
|
|
print(f" Записан {first_file} (весь ответ)")
|
|||
|
|
return True
|
|||
|
|
if files:
|
|||
|
|
first_file = files[0]
|
|||
|
|
full_path = project.path / first_file
|
|||
|
|
project.write_file(full_path, response)
|
|||
|
|
print(f" Записан {first_file} (весь ответ)")
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
for file_path_str, content in matches:
|
|||
|
|
file_path_str = file_path_str.strip().strip('"').strip("'")
|
|||
|
|
target_path = project.path / file_path_str
|
|||
|
|
project.write_file(target_path, content.strip())
|
|||
|
|
print(f" Обновлён {target_path.relative_to(project.path)}")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" Ошибка при парсинге ответа кодера: {e}")
|
|||
|
|
return False
|