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