Редактировать react.py
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user