Update 3 files

- /rag/intent_router.py
- /rag/query_processor.py
- /rag/history_manager.py
This commit is contained in:
Markov Andrey
2026-06-30 16:15:00 +00:00
parent 9d351d037b
commit fbfe178067
3 changed files with 118 additions and 118 deletions

View File

@@ -12,7 +12,7 @@ import logging
import re
import asyncio
import os
from typing import Optional, Dict, List, Any
from typing import Optional, Dict, List, Any, Union, Tuple
from core.services.giga_client import GigaClient
from core.services.kb_service import KBService
@@ -21,6 +21,7 @@ from core.functions.extract_metrics import extract_metrics
from core.functions.summarize_document import summarize_document
from core.functions.check_consistency import check_consistency
from core.functions.check_spelling import check_spelling
from core.config_models import AppConfig # предположим, что путь такой
logger = logging.getLogger(__name__)
@@ -35,24 +36,17 @@ class IntentRouter:
giga: GigaClient,
kb: KBService,
files: FileService,
config,
config: AppConfig,
default_prompts: Optional[Dict[str, str]] = None
):
) -> None:
"""
Инициализация маршрутизатора.
Аргументы:
giga: клиент GigaChat
kb: сервис базы знаний
files: сервис файлов
config: объект конфигурации
default_prompts: словарь промптов по умолчанию
"""
self.giga = giga
self.kb = kb
self.files = files
self.config = config
self.default_prompts = default_prompts or {}
self.giga: GigaClient = giga
self.kb: KBService = kb
self.files: FileService = files
self.config: AppConfig = config
self.default_prompts: Dict[str, str] = default_prompts or {}
# ------------------------------------------------------------------
# Основной метод маршрутизации
@@ -133,23 +127,23 @@ class IntentRouter:
prompts: Dict[str, str]
) -> Dict[str, Any]:
"""Обработка METRICS: извлечение KPI из базы знаний."""
context = await self.kb.find_relevant_info(
context: str = await self.kb.find_relevant_info(
query, user_jid, room_jid,
top_k=getattr(self.config, 'rag_metrics_top_k', 30)
)
if not context:
return {"answer": "Не найдено данных для извлечения метрик.", "context": "", "sources": []}
metrics_prompt = prompts.get('metrics', '')
metrics = await extract_metrics(
metrics_prompt: str = prompts.get('metrics', '')
metrics: List[Dict] = await extract_metrics(
giga=self.giga,
context=context,
prompt_text=metrics_prompt,
bot_config=self.config
)
if metrics:
lines = [f"- {m.get('metric_name')}: {m.get('value')} {m.get('unit', '')}" for m in metrics[:10]]
answer = "📊 **Извлечённые метрики:**\n" + "\n".join(lines)
lines: List[str] = [f"- {m.get('metric_name')}: {m.get('value')} {m.get('unit', '')}" for m in metrics[:10]]
answer: str = "📊 **Извлечённые метрики:**\n" + "\n".join(lines)
else:
answer = "Не удалось извлечь метрики."
@@ -165,8 +159,8 @@ class IntentRouter:
if not last_file_text:
return {"answer": "Нет документа для суммаризации.", "context": "", "sources": []}
summary_prompt = prompts.get('summary', '')
answer = await summarize_document(
summary_prompt: str = prompts.get('summary', '')
answer: str = await summarize_document(
giga=self.giga,
text=last_file_text,
title="Ваш документ",
@@ -183,19 +177,19 @@ class IntentRouter:
prompts: Dict[str, str]
) -> Dict[str, Any]:
"""Обработка CONTRADICTION: проверка противоречий в базе знаний."""
context = await self.kb.find_relevant_info(
context: str = await self.kb.find_relevant_info(
query, user_jid, room_jid,
top_k=getattr(self.config, 'rag_contradiction_top_k', 10)
)
if not context:
return {"answer": "Недостаточно данных для проверки противоречий.", "context": "", "sources": []}
chunks = [c.strip() for c in context.split("\n\n") if c.strip()]
chunks: List[str] = [c.strip() for c in context.split("\n\n") if c.strip()]
if len(chunks) < 2:
return {"answer": "Недостаточно фрагментов для проверки противоречий.", "context": context, "sources": []}
consistency_prompt = prompts.get('consistency', '')
consistency = await check_consistency(
consistency_prompt: str = prompts.get('consistency', '')
consistency: str = await check_consistency(
giga=self.giga,
chunks=chunks,
query=query,
@@ -203,7 +197,7 @@ class IntentRouter:
bot_config=self.config
)
if "[CONFLICT]" in consistency:
answer = f"⚠️ **Обнаружены противоречия:**\n{consistency}"
answer: str = f"⚠️ **Обнаружены противоречия:**\n{consistency}"
else:
answer = "✅ Противоречий не обнаружено."
@@ -219,10 +213,9 @@ class IntentRouter:
last_file_text: Optional[str]
) -> Dict[str, Any]:
"""Обработка TEMPLATE_FILL: заполнение шаблона документа."""
# Получаем текст шаблона
template_text = last_file_text or ""
template_text: str = last_file_text or ""
if not template_text and last_file_path and os.path.exists(last_file_path):
result = await asyncio.to_thread(self.files.process_any_file, last_file_path)
result: Any = await asyncio.to_thread(self.files.process_any_file, last_file_path)
if isinstance(result, tuple) and len(result) == 2:
template_text = result[0]
else:
@@ -231,16 +224,15 @@ class IntentRouter:
if not template_text:
return {"answer": "❌ Нет шаблона документа для заполнения. Загрузите файл .docx.", "context": "", "sources": []}
# Обрезаем шаблон для экономии токенов
truncated_template = template_text[:5000]
search_query = f"{query}\n{truncated_template}"
context = await self.kb.find_relevant_info(search_query, user_jid, room_jid, top_k=15)
truncated_template: str = template_text[:5000]
search_query: str = f"{query}\n{truncated_template}"
context: str = await self.kb.find_relevant_info(search_query, user_jid, room_jid, top_k=15)
fill_prompt = prompts.get('generate_document', '')
fill_prompt: str = prompts.get('generate_document', '')
if not fill_prompt:
fill_prompt = "Заполни плейсхолдеры, используя базу знаний."
full_prompt = (
full_prompt: str = (
f"Перед тобой шаблон документа. Заполни плейсхолдеры, используя ТОЛЬКО базу знаний.\n\n"
f"[ТЕКСТ ШАБЛОНА]:\n{truncated_template}\n\n"
f"[ДАННЫЕ ИЗ БАЗЫ ЗНАНИЙ]:\n{context}\n\n"
@@ -248,7 +240,7 @@ class IntentRouter:
f"Формат ответа: [SURGICAL_REPLACE]\nинструкция_из_шаблона ||| текст_из_БЗ\n[/SURGICAL_REPLACE]"
)
answer = await self.giga.chat(
answer: str = await self.giga.chat(
history=[],
query=full_prompt,
system_prompt=None,
@@ -266,42 +258,39 @@ class IntentRouter:
if not last_file_path:
return {"answer": "❌ Нет загруженного документа для замены.", "context": "", "sources": []}
# Парсим запрос: "замени слово на слово"
q = re.sub(r'\s+', ' ', query.lower()).strip()
m = re.search(r'замени\s+(.+?)\s+на\s+(.+)', q)
q: str = re.sub(r'\s+', ' ', query.lower()).strip()
m: Optional[re.Match] = re.search(r'замени\s+(.+?)\s+на\s+(.+)', q)
if not m:
m = re.search(r'заменить\s+(.+?)\s+на\s+(.+)', q)
if not m:
return {"answer": "Не удалось распознать, что на что заменять. Используйте формат: замени слово на слово", "context": "", "sources": []}
old_word = m.group(1).strip()
new_word = m.group(2).strip()
old_word: str = m.group(1).strip()
new_word: str = m.group(2).strip()
try:
from mawo_pymorphy3 import create_analyzer
morph = create_analyzer()
# Генерируем все формы старого слова
old_forms = set()
old_forms: set = set()
parsed_old = morph.parse(old_word)[0]
old_forms.add(old_word)
old_forms.add(parsed_old.normal_form)
cases = ['nomn', 'gent', 'datv', 'accs', 'ablt', 'loct']
numbers = ['sing', 'plur']
cases: List[str] = ['nomn', 'gent', 'datv', 'accs', 'ablt', 'loct']
numbers: List[str] = ['sing', 'plur']
for number in numbers:
for case in cases:
inflected = parsed_old.inflect({case, number})
if inflected:
old_forms.add(inflected.word)
# Кэш для разбора слов
parse_cache = {}
def get_new_form(new_word, old_form_text):
parse_cache: Dict[str, Any] = {}
def get_new_form(new_word: str, old_form_text: str) -> str:
if old_form_text in parse_cache:
parsed_old = parse_cache[old_form_text]
else:
parsed_old = morph.parse(old_form_text)[0]
parse_cache[old_form_text] = parsed_old
tags = set()
tags: set = set()
if parsed_old.tag.case:
tags.add(parsed_old.tag.case)
if parsed_old.tag.number:
@@ -312,14 +301,13 @@ class IntentRouter:
inflected = parsed_new.inflect(tags)
return inflected.word if inflected else new_word
replacements = {}
replacements: Dict[str, str] = {}
for old_form in old_forms:
replacements[old_form] = get_new_form(new_word, old_form)
# Выполняем замену
new_path = await asyncio.to_thread(self.files.surgical_replace, last_file_path, replacements)
new_path: Optional[str] = await asyncio.to_thread(self.files.surgical_replace, last_file_path, replacements)
if new_path:
answer = f"Замена '{old_word}''{new_word}' выполнена. Файл сохранён: {new_path}"
answer: str = f"Замена '{old_word}''{new_word}' выполнена. Файл сохранён: {new_path}"
else:
answer = f"❌ Ошибка при замене '{old_word}''{new_word}'."
return {"answer": answer, "context": "", "sources": []}
@@ -348,7 +336,7 @@ class IntentRouter:
if not last_file_path.lower().endswith('.docx'):
return {"answer": "❌ Проверка орфографии поддерживается только для файлов .docx.", "context": "", "sources": []}
spellcheck_prompt = prompts.get('spellcheck', '')
spellcheck_prompt: str = prompts.get('spellcheck', '')
if not spellcheck_prompt:
return {"answer": "❌ Промпт проверки орфографии не загружен.", "context": "", "sources": []}
@@ -360,7 +348,7 @@ class IntentRouter:
config=self.config
)
if changes:
answer = "📝 **Исправления:**\n" + "\n".join(changes)
answer: str = "📝 **Исправления:**\n" + "\n".join(changes)
else:
answer = "✅ Ошибок не найдено."
return {"answer": answer, "context": "", "sources": []}
@@ -376,14 +364,13 @@ class IntentRouter:
prompts: Dict[str, str]
) -> Dict[str, Any]:
"""Обработка GREETING: простой ответ через GigaChat без поиска."""
# Используем synthesis промпт, если есть, или просто передаём запрос
synthesis_template = prompts.get('synthesis', '')
synthesis_template: str = prompts.get('synthesis', '')
if synthesis_template:
full_query = synthesis_template.format(context="", query=query)
full_query: str = synthesis_template.format(context="", query=query)
else:
full_query = query
answer = await self.giga.chat(
answer: str = await self.giga.chat(
history=history or [],
query=full_query,
system_prompt=system_prompt,