From 0d6f224270cc17218f2e74adfcb0bf6e53e617f0 Mon Sep 17 00:00:00 2001 From: Markov Andrey Date: Tue, 30 Jun 2026 20:48:48 +0000 Subject: [PATCH] Add new file --- rag/functions/react.py | 146 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 rag/functions/react.py diff --git a/rag/functions/react.py b/rag/functions/react.py new file mode 100644 index 0000000..f906752 --- /dev/null +++ b/rag/functions/react.py @@ -0,0 +1,146 @@ +""" +Цикл 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}) без получения финального ответа.") \ No newline at end of file