diff --git a/rag/functions/react.py b/rag/functions/react.py index d2444ac..783b37f 100644 --- a/rag/functions/react.py +++ b/rag/functions/react.py @@ -1,11 +1,17 @@ +# rag/functions/react.py """ Цикл ReAct (Reasoning + Acting) для вызова внешних инструментов. Модель может генерировать мысль и действие, вызывать инструмент, получать результат и продолжать, пока не будет готов финальный ответ. + +Улучшен fallback-парсинг JSON: +- Если модель возвращает невалидный JSON, пытается извлечь JSON-объект из текста. +- Если JSON не найден, ищет поля "final_answer" или "action" через регулярные выражения. """ import json import logging +import re from typing import List, Dict, Any, Callable, Awaitable, Optional from rag.services.giga_client import GigaClient @@ -14,6 +20,22 @@ from rag.config_models import AppConfig logger = logging.getLogger(__name__) +def _extract_json_from_text(text: str) -> Optional[Dict[str, Any]]: + """ + Пытается извлечь JSON-объект из текста, используя регулярное выражение для поиска + фигурных скобок. Если найден валидный JSON, возвращает его, иначе None. + """ + # Ищем блок текста между первой открывающей и последней закрывающей фигурной скобкой + # Жадный поиск, чтобы захватить весь объект + match = re.search(r'(\{.*\})', text, re.DOTALL) + if match: + try: + return json.loads(match.group(1)) + except json.JSONDecodeError: + pass + return None + + async def react_loop( query: str, history: List[Dict[str, str]], @@ -78,20 +100,49 @@ async def react_loop( temperature=getattr(config, 'react_temperature', 0.3), ) + data = None + + # ---- Попытка 1: стандартный парсинг JSON ---- try: data = json.loads(response.strip()) except json.JSONDecodeError: - logger.error(f"Невалидный JSON от модели: {response[:200]}") - 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]}") + logger.debug("Невалидный JSON, пробуем извлечь JSON из текста") + # ---- Попытка 2: извлечь JSON из текста ---- + data = _extract_json_from_text(response) + # ---- Если JSON всё ещё не получен, пробуем регулярки для ключевых полей ---- + if data is None: + logger.debug("Не удалось извлечь JSON, пытаемся найти ключевые поля через regex") + final_answer_match = re.search(r'"final_answer"\s*:\s*"([^"]+)"', response) + if final_answer_match: + return final_answer_match.group(1) + action_match = re.search(r'"action"\s*:\s*"([^"]+)"', response) + action_input_match = re.search(r'"action_input"\s*:\s*({[^}]*})', response) + if action_match and action_input_match: + try: + action_input = json.loads(action_input_match.group(1)) + data = {"action": action_match.group(1), "action_input": action_input} + except json.JSONDecodeError: + # Если action_input невалидный JSON, попробуем извлечь как строку + action_input_str_match = re.search(r'"action_input"\s*:\s*"([^"]+)"', response) + if action_input_str_match: + data = {"action": action_match.group(1), "action_input": action_input_str_match.group(1)} + elif action_match: + # Если только action, без action_input, пробуем как есть + data = {"action": action_match.group(1), "action_input": {}} + + # Если данные всё ещё не получены — ошибка + if data is None: + logger.error(f"Не удалось распарсить ответ модели: {response[:200]}") + internal_history.append({"role": "assistant", "content": response}) + internal_history.append({"role": "user", "content": "Не удалось распознать действие или финальный ответ. Пожалуйста, выведи JSON с 'action' или 'final_answer'."}) + continue + + # ---- Проверяем наличие финального ответа ---- if "final_answer" in data: return data["final_answer"] + # ---- Проверяем наличие действия ---- action = data.get("action") action_input = data.get("action_input", {}) @@ -100,6 +151,7 @@ async def react_loop( internal_history.append({"role": "user", "content": "Не удалось распознать действие или финальный ответ. Пожалуйста, выведи JSON с 'action' или 'final_answer'."}) continue + # ---- Находим обработчик инструмента ---- handler = None for tool in tools: if tool["name"] == action: @@ -113,6 +165,7 @@ async def react_loop( internal_history.append({"role": "user", "content": f"Ошибка: {error_msg}. Попробуй другой инструмент или дай финальный ответ."}) continue + # ---- Вызываем обработчик ---- try: result = await handler(action_input) except Exception as e: