Files
fckbot/rag/functions/react.py
Markov Andrey 0d6f224270 Add new file
2026-06-30 20:48:48 +00:00

146 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Цикл 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}) без получения финального ответа.")