Add new file
This commit is contained in:
146
rag/functions/react.py
Normal file
146
rag/functions/react.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
Цикл 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}) без получения финального ответа.")
|
||||||
Reference in New Issue
Block a user