Редактировать react.py

This commit is contained in:
Markov Andrey
2026-06-30 22:41:50 +00:00
parent 674efe8fc8
commit dbc807f2e9

View File

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