Files
fckbot/rag/functions/react.py
2026-06-30 21:01:28 +00:00

125 lines
5.4 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 с действием или финальным ответом."})
response = await giga.chat(
history=messages,
query="",
system_prompt=None,
file_id=None,
temperature=getattr(config, 'react_temperature', 0.3),
)
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]}")
if "final_answer" in data:
return data["final_answer"]
action = data.get("action")
action_input = data.get("action_input", {})
if not 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}) без получения финального ответа.")