""" Цикл ReAct (Reasoning + Acting) для вызова внешних инструментов. Модель может генерировать мысль и действие, вызывать инструмент, получать результат и продолжать, пока не будет готов финальный ответ. """ import json import logging from typing import List, Dict, Any, Callable, Awaitable, Optional from rag.services.giga_client import GigaClient from rag.config_models import AppConfig logger = logging.getLogger(__name__) async def react_loop( query: str, history: List[Dict[str, str]], tools: List[Dict[str, Any]], config: AppConfig, giga: GigaClient, prompts: Dict[str, str], max_iterations: int = 5, ) -> str: """ Выполняет ReAct-цикл для генерации ответа с использованием внешних инструментов. Аргументы: query: текущий запрос пользователя. history: история диалога (список сообщений с role и content). tools: список описаний инструментов. Каждый элемент: { "name": str, "description": str, "parameters": dict, # JSON-схема параметров "handler": Callable[..., Awaitable[str]], # асинхронная функция } config: конфигурация. giga: клиент GigaChat. prompts: словарь с промптами (ключ 'react_system'). max_iterations: максимальное количество итераций. Возвращает: str: финальный ответ. Исключения: RuntimeError: если превышен лимит итераций или возникла критическая ошибка. """ # Формируем системный промпт с описанием инструментов system_prompt_template = prompts.get("react_system") if not system_prompt_template: raise ValueError("Промпт react_system не найден") # Описание инструментов для модели tools_desc = [] for tool in tools: tools_desc.append(f"- {tool['name']}: {tool['description']} (параметры: {tool.get('parameters', {})})") tools_description = "\n".join(tools_desc) system_prompt = system_prompt_template.format(tools_description=tools_description) # Создаём внутреннюю историю (копируем переданную, но не модифицируем) internal_history = history.copy() if history else [] # Добавляем текущий запрос как сообщение пользователя (если его нет в конце) if not internal_history or internal_history[-1].get("role") != "user" or internal_history[-1].get("content") != query: internal_history.append({"role": "user", "content": query}) iteration = 0 while iteration < max_iterations: iteration += 1 logger.debug(f"ReAct итерация {iteration}") # Формируем промпт (можно использовать стандартный билдер, но здесь упрощённо) messages = [{"role": "system", "content": system_prompt}] + internal_history # Добавляем указание формата вывода messages.append({"role": "user", "content": "Выведи JSON с действием или финальным ответом."}) # Вызываем GigaChat response = await giga.chat( history=messages, # history передаётся как список сообщений query="", # не нужно, так как мы уже включили в history system_prompt=None, file_id=None, temperature=getattr(config, 'react_temperature', 0.3), ) # Парсим JSON try: data = json.loads(response.strip()) except json.JSONDecodeError: logger.error(f"Невалидный JSON от модели: {response[:200]}") # В качестве fallback пробуем извлечь final_answer или action вручную # Если не удаётся, считаем, что это финальный ответ if "final_answer" in response.lower(): # грубое извлечение import re match = re.search(r'"final_answer"\s*:\s*"([^"]+)"', response) if match: return match.group(1) # Если ничего не вышло, выбрасываем ошибку raise RuntimeError(f"Модель вернула невалидный JSON на итерации {iteration}: {response[:200]}") # Проверяем наличие финального ответа if "final_answer" in data: return data["final_answer"] # Проверяем наличие действия action = data.get("action") action_input = data.get("action_input", {}) if not action: # Если нет ни final_answer, ни action, считаем, что модель ошиблась # Добавляем сообщение об ошибке в историю и продолжаем internal_history.append({"role": "assistant", "content": response}) internal_history.append({"role": "user", "content": "Не удалось распознать действие или финальный ответ. Пожалуйста, выведи JSON с 'action' или 'final_answer'."}) continue # Находим обработчик инструмента handler = None for tool in tools: if tool["name"] == action: handler = tool.get("handler") break if handler is None: error_msg = f"Инструмент '{action}' не найден." logger.warning(error_msg) internal_history.append({"role": "assistant", "content": response}) internal_history.append({"role": "user", "content": f"Ошибка: {error_msg}. Попробуй другой инструмент или дай финальный ответ."}) continue # Вызываем обработчик try: result = await handler(action_input) except Exception as e: result = f"Ошибка при вызове инструмента '{action}': {str(e)}" logger.exception(result) # Добавляем результат в историю internal_history.append({"role": "assistant", "content": response}) internal_history.append({"role": "tool", "content": result}) # Если цикл завершился без ответа, генерируем исключение raise RuntimeError(f"Превышено максимальное число итераций ({max_iterations}) без получения финального ответа.")