Update 3 files
- /rag/intent_router.py - /rag/query_processor.py - /rag/history_manager.py
This commit is contained in:
@@ -11,13 +11,13 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Optional, Any
|
from typing import List, Dict, Optional, Any, Tuple
|
||||||
|
|
||||||
from rag.services.postgres_service import PostgresService
|
from core.services.postgres_service import PostgresService
|
||||||
from rag.services.giga_client import GigaClient
|
from core.services.giga_client import GigaClient
|
||||||
from rag.utils.text_utils import count_tokens
|
from core.utils.text_utils import count_tokens
|
||||||
from rag.functions.hierarchical_summarize import hierarchical_summarize
|
from core.functions.hierarchical_summarize import hierarchical_summarize
|
||||||
from rag.config_models import AppConfig
|
from core.config_models import AppConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class HistoryManager:
|
|||||||
giga: GigaClient,
|
giga: GigaClient,
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
default_prompts: Optional[Dict[str, str]] = None
|
default_prompts: Optional[Dict[str, str]] = None
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Инициализация менеджера истории.
|
Инициализация менеджера истории.
|
||||||
|
|
||||||
@@ -43,10 +43,14 @@ class HistoryManager:
|
|||||||
config: объект конфигурации (AppConfig)
|
config: объект конфигурации (AppConfig)
|
||||||
default_prompts: словарь промптов по умолчанию (нужен для hierarchical_summary)
|
default_prompts: словарь промптов по умолчанию (нужен для hierarchical_summary)
|
||||||
"""
|
"""
|
||||||
self.db = db
|
self.db: PostgresService = db
|
||||||
self.giga = giga
|
self.giga: GigaClient = giga
|
||||||
self.config = config
|
self.config: AppConfig = config
|
||||||
self.default_prompts = default_prompts or {}
|
self.default_prompts: Dict[str, str] = default_prompts or {}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Получение и форматирование истории
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def get_history(
|
async def get_history(
|
||||||
self,
|
self,
|
||||||
@@ -65,7 +69,7 @@ class HistoryManager:
|
|||||||
Возвращает:
|
Возвращает:
|
||||||
List[Dict[str, str]]: список сообщений с ключами 'role' и 'content'
|
List[Dict[str, str]]: список сообщений с ключами 'role' и 'content'
|
||||||
"""
|
"""
|
||||||
raw_history = await self.db.get_history(user_jid, room_jid, limit=limit)
|
raw_history: List[Dict[str, Any]] = await self.db.get_history(user_jid, room_jid, limit=limit)
|
||||||
# Преобразуем в формат, ожидаемый LLM
|
# Преобразуем в формат, ожидаемый LLM
|
||||||
return [
|
return [
|
||||||
{"role": rec['role'], "content": rec['content']}
|
{"role": rec['role'], "content": rec['content']}
|
||||||
@@ -95,8 +99,8 @@ class HistoryManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# Подсчитываем общее количество токенов в истории
|
# Подсчитываем общее количество токенов в истории
|
||||||
history_text = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history])
|
history_text: str = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history])
|
||||||
total_tokens = count_tokens(history_text)
|
total_tokens: int = count_tokens(history_text)
|
||||||
|
|
||||||
if total_tokens <= max_tokens:
|
if total_tokens <= max_tokens:
|
||||||
logger.debug(f"История укладывается в {max_tokens} токенов (фактически {total_tokens})")
|
logger.debug(f"История укладывается в {max_tokens} токенов (фактически {total_tokens})")
|
||||||
@@ -127,14 +131,14 @@ class HistoryManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Параметры сжатия из конфига
|
# Параметры сжатия из конфига
|
||||||
summarization_config = self.config.summarization
|
summarization_config: Dict[str, Any] = getattr(self.config, 'summarization', {})
|
||||||
target_tokens = summarization_config.target_tokens_after_summary
|
target_tokens: int = summarization_config.get('target_tokens_after_summary', max_tokens)
|
||||||
chunk_size = summarization_config.chunk_size_tokens
|
chunk_size: int = summarization_config.get('chunk_size_tokens', 500)
|
||||||
max_depth = summarization_config.max_depth
|
max_depth: int = summarization_config.get('max_depth', 2)
|
||||||
temperature = self.config.ai.temperature
|
temperature: float = getattr(self.config, 'ai_temperature', 0.1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
compressed_text = await hierarchical_summarize(
|
compressed_text: str = await hierarchical_summarize(
|
||||||
text=history_text,
|
text=history_text,
|
||||||
giga=self.giga,
|
giga=self.giga,
|
||||||
prompt_template=prompt_template,
|
prompt_template=prompt_template,
|
||||||
@@ -153,10 +157,10 @@ class HistoryManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при сжатии истории: {e}, выполняем простую обрезку")
|
logger.error(f"Ошибка при сжатии истории: {e}, выполняем простую обрезку")
|
||||||
# Fallback: простая обрезка по токенам
|
# Fallback: простая обрезка по токенам
|
||||||
truncated_history = []
|
truncated_history: List[Dict[str, str]] = []
|
||||||
total = 0
|
total: int = 0
|
||||||
for msg in reversed(history):
|
for msg in reversed(history):
|
||||||
tokens = count_tokens(msg['content'])
|
tokens: int = count_tokens(msg['content'])
|
||||||
if total + tokens <= max_tokens:
|
if total + tokens <= max_tokens:
|
||||||
truncated_history.append(msg)
|
truncated_history.append(msg)
|
||||||
total += tokens
|
total += tokens
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
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.giga_client import GigaClient
|
||||||
from core.services.kb_service import KBService
|
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.summarize_document import summarize_document
|
||||||
from core.functions.check_consistency import check_consistency
|
from core.functions.check_consistency import check_consistency
|
||||||
from core.functions.check_spelling import check_spelling
|
from core.functions.check_spelling import check_spelling
|
||||||
|
from core.config_models import AppConfig # предположим, что путь такой
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -35,24 +36,17 @@ class IntentRouter:
|
|||||||
giga: GigaClient,
|
giga: GigaClient,
|
||||||
kb: KBService,
|
kb: KBService,
|
||||||
files: FileService,
|
files: FileService,
|
||||||
config,
|
config: AppConfig,
|
||||||
default_prompts: Optional[Dict[str, str]] = None
|
default_prompts: Optional[Dict[str, str]] = None
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Инициализация маршрутизатора.
|
Инициализация маршрутизатора.
|
||||||
|
|
||||||
Аргументы:
|
|
||||||
giga: клиент GigaChat
|
|
||||||
kb: сервис базы знаний
|
|
||||||
files: сервис файлов
|
|
||||||
config: объект конфигурации
|
|
||||||
default_prompts: словарь промптов по умолчанию
|
|
||||||
"""
|
"""
|
||||||
self.giga = giga
|
self.giga: GigaClient = giga
|
||||||
self.kb = kb
|
self.kb: KBService = kb
|
||||||
self.files = files
|
self.files: FileService = files
|
||||||
self.config = config
|
self.config: AppConfig = config
|
||||||
self.default_prompts = default_prompts or {}
|
self.default_prompts: Dict[str, str] = default_prompts or {}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Основной метод маршрутизации
|
# Основной метод маршрутизации
|
||||||
@@ -133,23 +127,23 @@ class IntentRouter:
|
|||||||
prompts: Dict[str, str]
|
prompts: Dict[str, str]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Обработка METRICS: извлечение KPI из базы знаний."""
|
"""Обработка METRICS: извлечение KPI из базы знаний."""
|
||||||
context = await self.kb.find_relevant_info(
|
context: str = await self.kb.find_relevant_info(
|
||||||
query, user_jid, room_jid,
|
query, user_jid, room_jid,
|
||||||
top_k=getattr(self.config, 'rag_metrics_top_k', 30)
|
top_k=getattr(self.config, 'rag_metrics_top_k', 30)
|
||||||
)
|
)
|
||||||
if not context:
|
if not context:
|
||||||
return {"answer": "Не найдено данных для извлечения метрик.", "context": "", "sources": []}
|
return {"answer": "Не найдено данных для извлечения метрик.", "context": "", "sources": []}
|
||||||
|
|
||||||
metrics_prompt = prompts.get('metrics', '')
|
metrics_prompt: str = prompts.get('metrics', '')
|
||||||
metrics = await extract_metrics(
|
metrics: List[Dict] = await extract_metrics(
|
||||||
giga=self.giga,
|
giga=self.giga,
|
||||||
context=context,
|
context=context,
|
||||||
prompt_text=metrics_prompt,
|
prompt_text=metrics_prompt,
|
||||||
bot_config=self.config
|
bot_config=self.config
|
||||||
)
|
)
|
||||||
if metrics:
|
if metrics:
|
||||||
lines = [f"- {m.get('metric_name')}: {m.get('value')} {m.get('unit', '')}" for m in metrics[:10]]
|
lines: List[str] = [f"- {m.get('metric_name')}: {m.get('value')} {m.get('unit', '')}" for m in metrics[:10]]
|
||||||
answer = "📊 **Извлечённые метрики:**\n" + "\n".join(lines)
|
answer: str = "📊 **Извлечённые метрики:**\n" + "\n".join(lines)
|
||||||
else:
|
else:
|
||||||
answer = "Не удалось извлечь метрики."
|
answer = "Не удалось извлечь метрики."
|
||||||
|
|
||||||
@@ -165,8 +159,8 @@ class IntentRouter:
|
|||||||
if not last_file_text:
|
if not last_file_text:
|
||||||
return {"answer": "Нет документа для суммаризации.", "context": "", "sources": []}
|
return {"answer": "Нет документа для суммаризации.", "context": "", "sources": []}
|
||||||
|
|
||||||
summary_prompt = prompts.get('summary', '')
|
summary_prompt: str = prompts.get('summary', '')
|
||||||
answer = await summarize_document(
|
answer: str = await summarize_document(
|
||||||
giga=self.giga,
|
giga=self.giga,
|
||||||
text=last_file_text,
|
text=last_file_text,
|
||||||
title="Ваш документ",
|
title="Ваш документ",
|
||||||
@@ -183,19 +177,19 @@ class IntentRouter:
|
|||||||
prompts: Dict[str, str]
|
prompts: Dict[str, str]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Обработка CONTRADICTION: проверка противоречий в базе знаний."""
|
"""Обработка CONTRADICTION: проверка противоречий в базе знаний."""
|
||||||
context = await self.kb.find_relevant_info(
|
context: str = await self.kb.find_relevant_info(
|
||||||
query, user_jid, room_jid,
|
query, user_jid, room_jid,
|
||||||
top_k=getattr(self.config, 'rag_contradiction_top_k', 10)
|
top_k=getattr(self.config, 'rag_contradiction_top_k', 10)
|
||||||
)
|
)
|
||||||
if not context:
|
if not context:
|
||||||
return {"answer": "Недостаточно данных для проверки противоречий.", "context": "", "sources": []}
|
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:
|
if len(chunks) < 2:
|
||||||
return {"answer": "Недостаточно фрагментов для проверки противоречий.", "context": context, "sources": []}
|
return {"answer": "Недостаточно фрагментов для проверки противоречий.", "context": context, "sources": []}
|
||||||
|
|
||||||
consistency_prompt = prompts.get('consistency', '')
|
consistency_prompt: str = prompts.get('consistency', '')
|
||||||
consistency = await check_consistency(
|
consistency: str = await check_consistency(
|
||||||
giga=self.giga,
|
giga=self.giga,
|
||||||
chunks=chunks,
|
chunks=chunks,
|
||||||
query=query,
|
query=query,
|
||||||
@@ -203,7 +197,7 @@ class IntentRouter:
|
|||||||
bot_config=self.config
|
bot_config=self.config
|
||||||
)
|
)
|
||||||
if "[CONFLICT]" in consistency:
|
if "[CONFLICT]" in consistency:
|
||||||
answer = f"⚠️ **Обнаружены противоречия:**\n{consistency}"
|
answer: str = f"⚠️ **Обнаружены противоречия:**\n{consistency}"
|
||||||
else:
|
else:
|
||||||
answer = "✅ Противоречий не обнаружено."
|
answer = "✅ Противоречий не обнаружено."
|
||||||
|
|
||||||
@@ -219,10 +213,9 @@ class IntentRouter:
|
|||||||
last_file_text: Optional[str]
|
last_file_text: Optional[str]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Обработка TEMPLATE_FILL: заполнение шаблона документа."""
|
"""Обработка TEMPLATE_FILL: заполнение шаблона документа."""
|
||||||
# Получаем текст шаблона
|
template_text: str = last_file_text or ""
|
||||||
template_text = last_file_text or ""
|
|
||||||
if not template_text and last_file_path and os.path.exists(last_file_path):
|
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:
|
if isinstance(result, tuple) and len(result) == 2:
|
||||||
template_text = result[0]
|
template_text = result[0]
|
||||||
else:
|
else:
|
||||||
@@ -231,16 +224,15 @@ class IntentRouter:
|
|||||||
if not template_text:
|
if not template_text:
|
||||||
return {"answer": "❌ Нет шаблона документа для заполнения. Загрузите файл .docx.", "context": "", "sources": []}
|
return {"answer": "❌ Нет шаблона документа для заполнения. Загрузите файл .docx.", "context": "", "sources": []}
|
||||||
|
|
||||||
# Обрезаем шаблон для экономии токенов
|
truncated_template: str = template_text[:5000]
|
||||||
truncated_template = template_text[:5000]
|
search_query: str = f"{query}\n{truncated_template}"
|
||||||
search_query = f"{query}\n{truncated_template}"
|
context: str = await self.kb.find_relevant_info(search_query, user_jid, room_jid, top_k=15)
|
||||||
context = 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:
|
if not fill_prompt:
|
||||||
fill_prompt = "Заполни плейсхолдеры, используя базу знаний."
|
fill_prompt = "Заполни плейсхолдеры, используя базу знаний."
|
||||||
|
|
||||||
full_prompt = (
|
full_prompt: str = (
|
||||||
f"Перед тобой шаблон документа. Заполни плейсхолдеры, используя ТОЛЬКО базу знаний.\n\n"
|
f"Перед тобой шаблон документа. Заполни плейсхолдеры, используя ТОЛЬКО базу знаний.\n\n"
|
||||||
f"[ТЕКСТ ШАБЛОНА]:\n{truncated_template}\n\n"
|
f"[ТЕКСТ ШАБЛОНА]:\n{truncated_template}\n\n"
|
||||||
f"[ДАННЫЕ ИЗ БАЗЫ ЗНАНИЙ]:\n{context}\n\n"
|
f"[ДАННЫЕ ИЗ БАЗЫ ЗНАНИЙ]:\n{context}\n\n"
|
||||||
@@ -248,7 +240,7 @@ class IntentRouter:
|
|||||||
f"Формат ответа: [SURGICAL_REPLACE]\nинструкция_из_шаблона ||| текст_из_БЗ\n[/SURGICAL_REPLACE]"
|
f"Формат ответа: [SURGICAL_REPLACE]\nинструкция_из_шаблона ||| текст_из_БЗ\n[/SURGICAL_REPLACE]"
|
||||||
)
|
)
|
||||||
|
|
||||||
answer = await self.giga.chat(
|
answer: str = await self.giga.chat(
|
||||||
history=[],
|
history=[],
|
||||||
query=full_prompt,
|
query=full_prompt,
|
||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
@@ -266,42 +258,39 @@ class IntentRouter:
|
|||||||
if not last_file_path:
|
if not last_file_path:
|
||||||
return {"answer": "❌ Нет загруженного документа для замены.", "context": "", "sources": []}
|
return {"answer": "❌ Нет загруженного документа для замены.", "context": "", "sources": []}
|
||||||
|
|
||||||
# Парсим запрос: "замени слово на слово"
|
q: str = re.sub(r'\s+', ' ', query.lower()).strip()
|
||||||
q = re.sub(r'\s+', ' ', query.lower()).strip()
|
m: Optional[re.Match] = re.search(r'замени\s+(.+?)\s+на\s+(.+)', q)
|
||||||
m = re.search(r'замени\s+(.+?)\s+на\s+(.+)', q)
|
|
||||||
if not m:
|
if not m:
|
||||||
m = re.search(r'заменить\s+(.+?)\s+на\s+(.+)', q)
|
m = re.search(r'заменить\s+(.+?)\s+на\s+(.+)', q)
|
||||||
if not m:
|
if not m:
|
||||||
return {"answer": "❌ Не удалось распознать, что на что заменять. Используйте формат: замени слово на слово", "context": "", "sources": []}
|
return {"answer": "❌ Не удалось распознать, что на что заменять. Используйте формат: замени слово на слово", "context": "", "sources": []}
|
||||||
|
|
||||||
old_word = m.group(1).strip()
|
old_word: str = m.group(1).strip()
|
||||||
new_word = m.group(2).strip()
|
new_word: str = m.group(2).strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mawo_pymorphy3 import create_analyzer
|
from mawo_pymorphy3 import create_analyzer
|
||||||
morph = create_analyzer()
|
morph = create_analyzer()
|
||||||
# Генерируем все формы старого слова
|
old_forms: set = set()
|
||||||
old_forms = set()
|
|
||||||
parsed_old = morph.parse(old_word)[0]
|
parsed_old = morph.parse(old_word)[0]
|
||||||
old_forms.add(old_word)
|
old_forms.add(old_word)
|
||||||
old_forms.add(parsed_old.normal_form)
|
old_forms.add(parsed_old.normal_form)
|
||||||
cases = ['nomn', 'gent', 'datv', 'accs', 'ablt', 'loct']
|
cases: List[str] = ['nomn', 'gent', 'datv', 'accs', 'ablt', 'loct']
|
||||||
numbers = ['sing', 'plur']
|
numbers: List[str] = ['sing', 'plur']
|
||||||
for number in numbers:
|
for number in numbers:
|
||||||
for case in cases:
|
for case in cases:
|
||||||
inflected = parsed_old.inflect({case, number})
|
inflected = parsed_old.inflect({case, number})
|
||||||
if inflected:
|
if inflected:
|
||||||
old_forms.add(inflected.word)
|
old_forms.add(inflected.word)
|
||||||
|
|
||||||
# Кэш для разбора слов
|
parse_cache: Dict[str, Any] = {}
|
||||||
parse_cache = {}
|
def get_new_form(new_word: str, old_form_text: str) -> str:
|
||||||
def get_new_form(new_word, old_form_text):
|
|
||||||
if old_form_text in parse_cache:
|
if old_form_text in parse_cache:
|
||||||
parsed_old = parse_cache[old_form_text]
|
parsed_old = parse_cache[old_form_text]
|
||||||
else:
|
else:
|
||||||
parsed_old = morph.parse(old_form_text)[0]
|
parsed_old = morph.parse(old_form_text)[0]
|
||||||
parse_cache[old_form_text] = parsed_old
|
parse_cache[old_form_text] = parsed_old
|
||||||
tags = set()
|
tags: set = set()
|
||||||
if parsed_old.tag.case:
|
if parsed_old.tag.case:
|
||||||
tags.add(parsed_old.tag.case)
|
tags.add(parsed_old.tag.case)
|
||||||
if parsed_old.tag.number:
|
if parsed_old.tag.number:
|
||||||
@@ -312,14 +301,13 @@ class IntentRouter:
|
|||||||
inflected = parsed_new.inflect(tags)
|
inflected = parsed_new.inflect(tags)
|
||||||
return inflected.word if inflected else new_word
|
return inflected.word if inflected else new_word
|
||||||
|
|
||||||
replacements = {}
|
replacements: Dict[str, str] = {}
|
||||||
for old_form in old_forms:
|
for old_form in old_forms:
|
||||||
replacements[old_form] = get_new_form(new_word, old_form)
|
replacements[old_form] = get_new_form(new_word, old_form)
|
||||||
|
|
||||||
# Выполняем замену
|
new_path: Optional[str] = await asyncio.to_thread(self.files.surgical_replace, last_file_path, replacements)
|
||||||
new_path = await asyncio.to_thread(self.files.surgical_replace, last_file_path, replacements)
|
|
||||||
if new_path:
|
if new_path:
|
||||||
answer = f"Замена '{old_word}' → '{new_word}' выполнена. Файл сохранён: {new_path}"
|
answer: str = f"Замена '{old_word}' → '{new_word}' выполнена. Файл сохранён: {new_path}"
|
||||||
else:
|
else:
|
||||||
answer = f"❌ Ошибка при замене '{old_word}' → '{new_word}'."
|
answer = f"❌ Ошибка при замене '{old_word}' → '{new_word}'."
|
||||||
return {"answer": answer, "context": "", "sources": []}
|
return {"answer": answer, "context": "", "sources": []}
|
||||||
@@ -348,7 +336,7 @@ class IntentRouter:
|
|||||||
if not last_file_path.lower().endswith('.docx'):
|
if not last_file_path.lower().endswith('.docx'):
|
||||||
return {"answer": "❌ Проверка орфографии поддерживается только для файлов .docx.", "context": "", "sources": []}
|
return {"answer": "❌ Проверка орфографии поддерживается только для файлов .docx.", "context": "", "sources": []}
|
||||||
|
|
||||||
spellcheck_prompt = prompts.get('spellcheck', '')
|
spellcheck_prompt: str = prompts.get('spellcheck', '')
|
||||||
if not spellcheck_prompt:
|
if not spellcheck_prompt:
|
||||||
return {"answer": "❌ Промпт проверки орфографии не загружен.", "context": "", "sources": []}
|
return {"answer": "❌ Промпт проверки орфографии не загружен.", "context": "", "sources": []}
|
||||||
|
|
||||||
@@ -360,7 +348,7 @@ class IntentRouter:
|
|||||||
config=self.config
|
config=self.config
|
||||||
)
|
)
|
||||||
if changes:
|
if changes:
|
||||||
answer = "📝 **Исправления:**\n" + "\n".join(changes)
|
answer: str = "📝 **Исправления:**\n" + "\n".join(changes)
|
||||||
else:
|
else:
|
||||||
answer = "✅ Ошибок не найдено."
|
answer = "✅ Ошибок не найдено."
|
||||||
return {"answer": answer, "context": "", "sources": []}
|
return {"answer": answer, "context": "", "sources": []}
|
||||||
@@ -376,14 +364,13 @@ class IntentRouter:
|
|||||||
prompts: Dict[str, str]
|
prompts: Dict[str, str]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Обработка GREETING: простой ответ через GigaChat без поиска."""
|
"""Обработка GREETING: простой ответ через GigaChat без поиска."""
|
||||||
# Используем synthesis промпт, если есть, или просто передаём запрос
|
synthesis_template: str = prompts.get('synthesis', '')
|
||||||
synthesis_template = prompts.get('synthesis', '')
|
|
||||||
if synthesis_template:
|
if synthesis_template:
|
||||||
full_query = synthesis_template.format(context="", query=query)
|
full_query: str = synthesis_template.format(context="", query=query)
|
||||||
else:
|
else:
|
||||||
full_query = query
|
full_query = query
|
||||||
|
|
||||||
answer = await self.giga.chat(
|
answer: str = await self.giga.chat(
|
||||||
history=history or [],
|
history=history or [],
|
||||||
query=full_query,
|
query=full_query,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from typing import Optional, Dict, List, Any
|
from typing import Optional, Dict, List, Any
|
||||||
|
|
||||||
from .services.giga_client import GigaClient
|
from core.services.giga_client import GigaClient
|
||||||
from .services.kb_service import KBService
|
from core.services.kb_service import KBService
|
||||||
from .functions.expand_query import expand_query
|
from core.functions.expand_query import expand_query
|
||||||
from .functions.rerank_context import rerank_context
|
from core.functions.rerank_context import rerank_context
|
||||||
from .functions.critique_answer import critique_answer
|
from core.functions.critique_answer import critique_answer
|
||||||
from .utils.text_utils import count_tokens
|
from core.utils.text_utils import count_tokens
|
||||||
from .prompt_builder import PromptBuilder
|
from core.prompt_builder import PromptBuilder
|
||||||
from .config_models import AppConfig
|
from core.config_models import AppConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -32,12 +32,12 @@ class QueryProcessor:
|
|||||||
kb: KBService,
|
kb: KBService,
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
default_prompts: Optional[Dict[str, str]] = None
|
default_prompts: Optional[Dict[str, str]] = None
|
||||||
):
|
) -> None:
|
||||||
self.giga = giga
|
self.giga: GigaClient = giga
|
||||||
self.kb = kb
|
self.kb: KBService = kb
|
||||||
self.config = config
|
self.config: AppConfig = config
|
||||||
self.default_prompts = default_prompts or {}
|
self.default_prompts: Dict[str, str] = default_prompts or {}
|
||||||
self.prompt_builder = PromptBuilder(config)
|
self.prompt_builder: PromptBuilder = PromptBuilder(config)
|
||||||
|
|
||||||
async def process(
|
async def process(
|
||||||
self,
|
self,
|
||||||
@@ -52,18 +52,31 @@ class QueryProcessor:
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Выполняет полный RAG-пайплайн для обычного запроса.
|
Выполняет полный RAG-пайплайн для обычного запроса.
|
||||||
|
|
||||||
|
Аргументы:
|
||||||
|
query: текст запроса пользователя
|
||||||
|
user_jid: JID пользователя
|
||||||
|
room_jid: JID комнаты (None для личного чата)
|
||||||
|
prompts: словарь промптов (expand, synthesis, critique, ...)
|
||||||
|
intent: код намерения (для выбора стратегии)
|
||||||
|
history: история диалога (уже сжатая, если нужно)
|
||||||
|
system_prompt: системный промпт
|
||||||
|
available_tokens_for_context: сколько токенов доступно для контекста
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
Словарь с ключами 'answer', 'context', 'sources', 'confidence'
|
||||||
"""
|
"""
|
||||||
# 1. Расширение запроса
|
# 1. Расширение запроса
|
||||||
expanded = await expand_query(
|
expanded: str = await expand_query(
|
||||||
giga=self.giga,
|
giga=self.giga,
|
||||||
query=query,
|
query=query,
|
||||||
prompt_text=prompts.get('expand', ''),
|
prompt_text=prompts.get('expand', ''),
|
||||||
bot_config=self.config
|
bot_config=self.config
|
||||||
)
|
)
|
||||||
search_query = expanded if expanded and expanded != query else query
|
search_query: str = expanded if expanded and expanded != query else query
|
||||||
|
|
||||||
# 2. Поиск релевантного контекста в базе знаний
|
# 2. Поиск релевантного контекста в базе знаний
|
||||||
context = 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)
|
||||||
)
|
)
|
||||||
@@ -71,20 +84,20 @@ class QueryProcessor:
|
|||||||
|
|
||||||
# 3. Обрезка контекста по токенам
|
# 3. Обрезка контекста по токенам
|
||||||
if context:
|
if context:
|
||||||
context_tokens = count_tokens(context)
|
context_tokens: int = count_tokens(context)
|
||||||
if context_tokens > available_tokens_for_context:
|
if context_tokens > available_tokens_for_context:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Контекст слишком длинный ({context_tokens} токенов), "
|
f"Контекст слишком длинный ({context_tokens} токенов), "
|
||||||
f"обрезаем до {available_tokens_for_context}"
|
f"обрезаем до {available_tokens_for_context}"
|
||||||
)
|
)
|
||||||
max_context_chars = int(available_tokens_for_context * 3.5)
|
max_context_chars: int = int(available_tokens_for_context * 3.5)
|
||||||
if max_context_chars > 0:
|
if max_context_chars > 0:
|
||||||
context = context[:max_context_chars]
|
context = context[:max_context_chars]
|
||||||
else:
|
else:
|
||||||
context = ""
|
context = ""
|
||||||
|
|
||||||
# 4. Переранжирование контекста (если включено и контекст достаточно длинный)
|
# 4. Переранжирование контекста (если включено и контекст достаточно длинный)
|
||||||
rerank_min_length = 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(
|
||||||
bot=None,
|
bot=None,
|
||||||
@@ -94,8 +107,8 @@ class QueryProcessor:
|
|||||||
bot_config=self.config
|
bot_config=self.config
|
||||||
)
|
)
|
||||||
|
|
||||||
# 5. Формирование промта с помощью PromptBuilder (динамические few-shot, сэндвич)
|
# 5. Формирование промта с помощью PromptBuilder
|
||||||
extra_instructions = ""
|
extra_instructions: str = ""
|
||||||
if intent in ("CALCULATION", "PROCEDURE"):
|
if intent in ("CALCULATION", "PROCEDURE"):
|
||||||
extra_instructions = (
|
extra_instructions = (
|
||||||
"Пожалуйста, покажи пошаговое решение перед итоговым ответом. "
|
"Пожалуйста, покажи пошаговое решение перед итоговым ответом. "
|
||||||
@@ -103,13 +116,11 @@ class QueryProcessor:
|
|||||||
"После всех шагов дай итоговый ответ."
|
"После всех шагов дай итоговый ответ."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Если synthesis_template задан, добавим его в extra_instructions (но PromptBuilder уже использует стандартные инструкции)
|
synthesis_template: str = prompts.get('synthesis', '')
|
||||||
synthesis_template = prompts.get('synthesis', '')
|
|
||||||
if synthesis_template and not extra_instructions:
|
if synthesis_template and not extra_instructions:
|
||||||
# Если есть кастомный шаблон, используем его как дополнительную инструкцию
|
|
||||||
extra_instructions = synthesis_template.format(context=context, query=query) if '{context}' in synthesis_template else synthesis_template
|
extra_instructions = synthesis_template.format(context=context, query=query) if '{context}' in synthesis_template else synthesis_template
|
||||||
|
|
||||||
prompt = self.prompt_builder.build_prompt(
|
prompt: str = self.prompt_builder.build_prompt(
|
||||||
query=query,
|
query=query,
|
||||||
intent=intent,
|
intent=intent,
|
||||||
context=context,
|
context=context,
|
||||||
@@ -119,21 +130,21 @@ class QueryProcessor:
|
|||||||
)
|
)
|
||||||
logger.debug(f"Сформированный промт (первые 500 символов): {prompt[:500]}")
|
logger.debug(f"Сформированный промт (первые 500 символов): {prompt[:500]}")
|
||||||
|
|
||||||
# 6. Генерация ответа (без отдельной передачи истории, она уже в промте)
|
# 6. Генерация ответа
|
||||||
answer = await self.giga.chat(
|
answer: str = await self.giga.chat(
|
||||||
history=[], # история уже в промте
|
history=[],
|
||||||
query=prompt,
|
query=prompt,
|
||||||
system_prompt=None, # системный промпт тоже в промте
|
system_prompt=None,
|
||||||
file_id=None,
|
file_id=None,
|
||||||
temperature=getattr(self.config, 'ai_temperature', 0.1)
|
temperature=getattr(self.config, 'ai_temperature', 0.1)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 7. Самокритика (если включена)
|
# 7. Самокритика (если включена)
|
||||||
if getattr(self.config, 'enable_self_critique', False) and context:
|
if getattr(self.config, 'enable_self_critique', False) and context:
|
||||||
critique_prompt = prompts.get('critique', '')
|
critique_prompt: str = prompts.get('critique', '')
|
||||||
if critique_prompt:
|
if critique_prompt:
|
||||||
logger.debug("Запуск самокритики")
|
logger.debug("Запуск самокритики")
|
||||||
is_ok = await critique_answer(
|
is_ok: bool = await critique_answer(
|
||||||
giga=self.giga,
|
giga=self.giga,
|
||||||
query=query,
|
query=query,
|
||||||
context=context,
|
context=context,
|
||||||
@@ -143,7 +154,6 @@ class QueryProcessor:
|
|||||||
)
|
)
|
||||||
if not is_ok:
|
if not is_ok:
|
||||||
logger.warning("Ответ не прошёл самокритику, перегенерация")
|
logger.warning("Ответ не прошёл самокритику, перегенерация")
|
||||||
# Перегенерируем с тем же промтом
|
|
||||||
answer = await self.giga.chat(
|
answer = await self.giga.chat(
|
||||||
history=[],
|
history=[],
|
||||||
query=prompt,
|
query=prompt,
|
||||||
@@ -151,7 +161,6 @@ class QueryProcessor:
|
|||||||
file_id=None,
|
file_id=None,
|
||||||
temperature=getattr(self.config, 'ai_temperature', 0.1)
|
temperature=getattr(self.config, 'ai_temperature', 0.1)
|
||||||
)
|
)
|
||||||
# Повторная проверка после перегенерации
|
|
||||||
if not await critique_answer(
|
if not await critique_answer(
|
||||||
giga=self.giga,
|
giga=self.giga,
|
||||||
query=query,
|
query=query,
|
||||||
@@ -163,7 +172,7 @@ class QueryProcessor:
|
|||||||
answer = "⚠️ Извините, я не уверен в точности ответа. Проверьте данные."
|
answer = "⚠️ Извините, я не уверен в точности ответа. Проверьте данные."
|
||||||
|
|
||||||
# 8. Извлечение источников
|
# 8. Извлечение источников
|
||||||
sources = []
|
sources: List[str] = []
|
||||||
if context:
|
if context:
|
||||||
for match in re.finditer(r'\[источник:\s*([^\]]+)\]', context):
|
for match in re.finditer(r'\[источник:\s*([^\]]+)\]', context):
|
||||||
sources.append(match.group(1))
|
sources.append(match.group(1))
|
||||||
|
|||||||
Reference in New Issue
Block a user