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 |