Редактировать query_processor.py

This commit is contained in:
Markov Andrey
2026-06-30 22:27:55 +00:00
parent 517618ba00
commit f0c7b92613

View File

@@ -3,10 +3,17 @@
Обработчик обычных RAG-запросов (FACT, PROCEDURE, COMPARISON, CALCULATION, GENERAL). Обработчик обычных RAG-запросов (FACT, PROCEDURE, COMPARISON, CALCULATION, GENERAL).
Выполняет расширение запроса, поиск в БЗ, переранжирование, синтез ответа, Выполняет расширение запроса, поиск в БЗ, переранжирование, синтез ответа,
самокритику и перегенерацию при необходимости. самокритику и перегенерацию при необходимости.
ДОБАВЛЕНО:
- Интеграция SOMA-анализа (LLM-as-judge)
- Интеграция ReAct (вызов внешних инструментов)
- Интеграция планирования действий (generate_plan)
- Интеграция многоагентности (AgentCoordinator)
""" """
import logging import logging
import re import re
import json
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
from core.services.giga_client import GigaClient from core.services.giga_client import GigaClient
@@ -18,12 +25,19 @@ from core.utils.text_utils import count_tokens
from core.prompt_builder import PromptBuilder from core.prompt_builder import PromptBuilder
from core.config_models import AppConfig from core.config_models import AppConfig
# Импорты новых модулей (интеграция)
from .functions.soma_evaluate import soma_evaluate
from .functions.react import react_loop
from .functions.plan_generation import generate_plan
from .agents.coordinator import AgentCoordinator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class QueryProcessor: class QueryProcessor:
""" """
Обрабатывает обычные RAG-запросы (не специализированные намерения). Обрабатывает обычные RAG-запросы (не специализированные намерения).
Интегрирует SOMA, ReAct, планирование и агентов.
""" """
def __init__( def __init__(
@@ -31,13 +45,56 @@ class QueryProcessor:
giga: GigaClient, giga: GigaClient,
kb: KBService, kb: KBService,
config: AppConfig, config: AppConfig,
default_prompts: Optional[Dict[str, str]] = None default_prompts: Optional[Dict[str, str]] = None,
agent_coordinator: Optional[AgentCoordinator] = None,
) -> None: ) -> None:
"""
Инициализация QueryProcessor.
Аргументы:
giga: клиент GigaChat.
kb: сервис базы знаний.
config: конфигурация приложения.
default_prompts: словарь промптов по умолчанию.
agent_coordinator: координатор агентов (если включена многоагентность).
"""
self.giga: GigaClient = giga self.giga: GigaClient = giga
self.kb: KBService = kb self.kb: KBService = kb
self.config: AppConfig = config self.config: AppConfig = config
self.default_prompts: Dict[str, str] = default_prompts or {} self.default_prompts: Dict[str, str] = default_prompts or {}
self.prompt_builder: PromptBuilder = PromptBuilder(config) self.prompt_builder: PromptBuilder = PromptBuilder(config)
self.agent_coordinator: Optional[AgentCoordinator] = agent_coordinator
def _needs_external_data(self, query: str) -> bool:
"""
Определяет, требует ли запрос вызова внешних инструментов.
Используется для активации ReAct.
"""
keywords = ["найди", "поищи", "забронируй", "купи", "узнай курс", "какая погода",
"погода", "билеты", "отель", "рейс", "тариф", "курс"]
return any(kw in query.lower() for kw in keywords)
def _get_available_tools(self) -> List[Dict[str, Any]]:
"""
Возвращает список доступных инструментов для ReAct.
В реальности инструменты определяются в конфиге и регистрируются в приложении.
Здесь возвращаем заглушку, которая должна быть переопределена в рантайме.
"""
# В будущем можно загружать из конфига: getattr(self.config, 'react_tools', [])
return [
{
"name": "search",
"description": "Поиск информации в интернете",
"parameters": {"query": "string"},
"handler": None # будет передан извне (например, через регистрацию)
},
{
"name": "get_weather",
"description": "Получить текущую погоду в городе",
"parameters": {"city": "string"},
"handler": None
}
]
async def process( async def process(
self, self,
@@ -51,7 +108,7 @@ class QueryProcessor:
available_tokens_for_context: int, available_tokens_for_context: int,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Выполняет полный RAG-пайплайн для обычного запроса. Выполняет полный RAG-пайплайн для обычного запроса с интеграцией новых функций.
Аргументы: Аргументы:
query: текст запроса пользователя query: текст запроса пользователя
@@ -66,7 +123,31 @@ class QueryProcessor:
Возвращает: Возвращает:
Словарь с ключами 'answer', 'context', 'sources', 'confidence' Словарь с ключами 'answer', 'context', 'sources', 'confidence'
""" """
# 1. Расширение запроса
# ---- 1. Использование агентов (если включено) ----
if (getattr(self.config, 'enable_agents', False) and
self.agent_coordinator is not None):
logger.info("Запрос передан в AgentCoordinator")
try:
# Пытаемся обработать запрос через агентов
agent_result = await self.agent_coordinator.process(
query=query,
context="", # агенты сами ищут контекст (или используют свой)
history=history,
tools=None # можно передать общие инструменты
)
if agent_result:
logger.info("AgentCoordinator вернул ответ")
return {
"answer": agent_result,
"context": "",
"sources": [],
"confidence": None
}
except Exception as e:
logger.error(f"Ошибка при работе AgentCoordinator: {e}, переходим к обычному RAG")
# ---- 2. Расширение запроса ----
expanded: str = await expand_query( expanded: str = await expand_query(
giga=self.giga, giga=self.giga,
query=query, query=query,
@@ -75,14 +156,14 @@ class QueryProcessor:
) )
search_query: str = expanded if expanded and expanded != query else query search_query: str = expanded if expanded and expanded != query else query
# 2. Поиск релевантного контекста в базе знаний # ---- 3. Поиск релевантного контекста в базе знаний ----
context: str = await self.kb.find_relevant_info( context: str = await self.kb.find_relevant_info(
search_query, user_jid, room_jid, search_query, user_jid, room_jid,
top_k=getattr(self.config, 'rag_default_top_k', 30) top_k=getattr(self.config, 'rag_default_top_k', 30)
) )
logger.info(f"Найден контекст длиной {len(context)} символов (room={room_jid})") logger.info(f"Найден контекст длиной {len(context)} символов (room={room_jid})")
# 3. Обрезка контекста по токенам # ---- 4. Обрезка контекста по токенам ----
if context: if context:
context_tokens: int = count_tokens(context) context_tokens: int = count_tokens(context)
if context_tokens > available_tokens_for_context: if context_tokens > available_tokens_for_context:
@@ -96,7 +177,7 @@ class QueryProcessor:
else: else:
context = "" context = ""
# 4. Переранжирование контекста (если включено и контекст достаточно длинный) # ---- 5. Переранжирование контекста ----
rerank_min_length: int = getattr(self.config, 'rerank_min_length', 5000) rerank_min_length: int = getattr(self.config, 'rerank_min_length', 5000)
if intent != "FACT" and len(context) > rerank_min_length: if intent != "FACT" and len(context) > rerank_min_length:
context = await rerank_context( context = await rerank_context(
@@ -107,9 +188,23 @@ class QueryProcessor:
bot_config=self.config bot_config=self.config
) )
# 5. Формирование промта с помощью PromptBuilder # ---- 6. Планирование (если включено) ----
plan = None
extra_instructions: str = "" extra_instructions: str = ""
if intent in ("CALCULATION", "PROCEDURE"): if getattr(self.config, 'enable_planning', False) and intent in ("CALCULATION", "PROCEDURE"):
plan = await generate_plan(
query=query,
history=history,
config=self.config,
giga=self.giga,
prompts=self.default_prompts
)
if plan:
logger.info(f"Сгенерирован план из {len(plan)} шагов")
extra_instructions = f"Выполни следующие шаги по порядку: {json.dumps(plan, ensure_ascii=False)}"
# ---- 7. Формирование промта (с учётом возможного плана) ----
if intent in ("CALCULATION", "PROCEDURE") and not extra_instructions:
extra_instructions = ( extra_instructions = (
"Пожалуйста, покажи пошаговое решение перед итоговым ответом. " "Пожалуйста, покажи пошаговое решение перед итоговым ответом. "
"Опиши каждый шаг вычислений или действий в логической последовательности. " "Опиши каждый шаг вычислений или действий в логической последовательности. "
@@ -130,7 +225,7 @@ class QueryProcessor:
) )
logger.debug(f"Сформированный промт (первые 500 символов): {prompt[:500]}") logger.debug(f"Сформированный промт (первые 500 символов): {prompt[:500]}")
# 6. Генерация ответа # ---- 8. Генерация ответа ----
answer: str = await self.giga.chat( answer: str = await self.giga.chat(
history=[], history=[],
query=prompt, query=prompt,
@@ -139,7 +234,31 @@ class QueryProcessor:
temperature=getattr(self.config, 'ai_temperature', 0.1) temperature=getattr(self.config, 'ai_temperature', 0.1)
) )
# 7. Самокритика (если включена) # ---- 9. ReAct (если требуется внешний вызов) ----
if (getattr(self.config, 'enable_react', False) and
self._needs_external_data(query)):
logger.info("Запрос требует внешних данных, запускаем ReAct цикл")
try:
tools = self._get_available_tools()
# В реальности нужно передать реальные обработчики инструментов.
# Здесь они могут быть зарегистрированы в конфиге.
# Для демонстрации мы пытаемся вызвать react_loop.
react_answer = await react_loop(
query=query,
history=history,
tools=tools,
config=self.config,
giga=self.giga,
prompts=self.default_prompts,
max_iterations=getattr(self.config, 'react_max_iterations', 5)
)
if react_answer:
answer = react_answer
logger.info("ReAct цикл завершён, ответ получен")
except Exception as e:
logger.error(f"ReAct цикл не удался: {e}, используем обычный ответ")
# ---- 10. Самокритика ----
if getattr(self.config, 'enable_self_critique', False) and context: if getattr(self.config, 'enable_self_critique', False) and context:
critique_prompt: str = prompts.get('critique', '') critique_prompt: str = prompts.get('critique', '')
if critique_prompt: if critique_prompt:
@@ -171,7 +290,47 @@ class QueryProcessor:
): ):
answer = "⚠️ Извините, я не уверен в точности ответа. Проверьте данные." answer = "⚠️ Извините, я не уверен в точности ответа. Проверьте данные."
# 8. Извлечение источников # ---- 11. SOMA-анализ (если включен) ----
if getattr(self.config, 'enable_soma', False):
logger.debug("Запуск SOMA-оценки")
try:
evaluation = await soma_evaluate(
query=query,
context=context,
answer=answer,
config=self.config,
giga=self.giga,
prompts=self.default_prompts
)
if evaluation.get('verdict') == 'fail':
threshold = getattr(self.config, 'soma_threshold', 3.5)
if evaluation.get('overall_score', 0) < threshold:
logger.warning(f"SOMA оценка низкая ({evaluation['overall_score']}), перегенерация")
# Перегенерация с повышенной температурой для разнообразия
answer = await self.giga.chat(
history=[],
query=prompt,
system_prompt=None,
file_id=None,
temperature=0.3
)
# Вторая оценка (опционально)
evaluation2 = await soma_evaluate(
query=query,
context=context,
answer=answer,
config=self.config,
giga=self.giga,
prompts=self.default_prompts
)
if evaluation2.get('verdict') == 'fail':
answer = "⚠️ Извините, я не уверен в точности ответа. Проверьте данные."
else:
logger.info("SOMA оценка после перегенерации успешна")
except Exception as e:
logger.error(f"Ошибка в SOMA-оценке: {e}, пропускаем")
# ---- 12. Извлечение источников ----
sources: List[str] = [] sources: List[str] = []
if context: if context:
for match in re.finditer(r'\[источник:\s*([^\]]+)\]', context): for match in re.finditer(r'\[источник:\s*([^\]]+)\]', context):