Редактировать 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) для вызова внешних инструментов.
Модель может генерировать мысль и действие, вызывать инструмент,
получать результат и продолжать, пока не будет готов финальный ответ.
Улучшен 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: