Initial commit: VKUS vitrina MVP (frontend + backend).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 23:13:06 +07:00
commit 862e6b7b25
113 changed files with 50949 additions and 0 deletions
+489
View File
@@ -0,0 +1,489 @@
import re
from typing import Optional
from app.config import settings
from app.data.knowledge_base import (
DEFAULT_PROFILE,
GLOSSARY_ANSWERS,
IT_SYSTEMS,
KNOWLEDGE_ENTRIES,
get_colleague_cases_for_system,
)
from app.llm.client import get_llm_client
from app.models import AssistantResult, ColleagueCase, ResponsePackage, UserProfile
from app.package_builder import build_why_suggested, ensure_escalation_draft, enrich_ticket_draft
from app.rag.service import (
build_package_from_hits,
build_package_with_llm,
search_documents,
)
def _detect_glossary(query: str) -> Optional[str]:
match = re.search(r"что такое\s+([а-яёa-z0-9]+)", query, re.IGNORECASE)
if not match:
return None
return GLOSSARY_ANSWERS.get(match.group(1).lower())
def _detect_system(query: str) -> Optional[str]:
lower = query.lower()
if re.search(r"meridium|меридиум", lower):
return "Meridium 4"
if re.search(r"есуид|esuid|esu[iі]d", lower):
return "ЕСУИД"
if re.search(r"1[сc]|1c", lower):
return "1С"
if re.search(r"starlims|старлимс", lower):
return "StarLims"
if re.search(r"лимс|lims", lower):
return "ЛИМС"
if re.search(r"вкус|vkus", lower):
return "ВКУС"
return None
def _is_thanks(query: str) -> bool:
return bool(
re.match(
r"^(спасибо|благодар|всё получилось|все получилось|понятно|достаточно|до свидания|нет,? спасибо)",
query.strip(),
re.IGNORECASE,
)
)
def _is_off_topic(query: str) -> bool:
return bool(re.search(r"политик|выборы|погод|рецепт|футбол|кино|личн", query, re.IGNORECASE))
def _is_confidential_request(query: str) -> bool:
return bool(
re.search(r"зарплат|табельн|паспорт|телефон.*коллег|заявк.*коллег|данн.*сотрудник", query, re.IGNORECASE)
)
def _is_security_probe(query: str) -> bool:
return bool(
re.search(r"директор|служб.*безопас|представь.*что|гипотетич|покажи.*промпт|как ты устроен", query, re.IGNORECASE)
)
def _try_pattern_knowledge(
trimmed: str,
greeting: str,
conversation: list[dict[str, str]] | None = None,
) -> AssistantResult | None:
if not settings.use_pattern_kb:
return None
for entry in KNOWLEDGE_ENTRIES:
if any(re.search(pattern, trimmed, re.IGNORECASE) for pattern in entry["patterns"]):
confidence = entry["confidence"]
intro = (
f"{greeting}нашёл инструкцию по вашему вопросу. Вот что рекомендую:"
if confidence >= settings.confidence_threshold
else f"{greeting}нашёл частично подходящую информацию, но уверенность ниже {settings.confidence_threshold}%. Рекомендую также оформить обращение:"
)
package_data = {**entry["package"], "confidence": confidence, "system": entry["system"]}
package = ResponsePackage.model_validate(package_data)
package.why_suggested = build_why_suggested(
system=entry["system"],
sources=package.sources,
confidence=confidence,
)
if package.ticket_draft:
cases = package.colleague_cases or [
ColleagueCase.model_validate(case)
for case in get_colleague_cases_for_system(entry["system"])
]
package.ticket_draft = enrich_ticket_draft(
trimmed,
entry["system"],
package.ticket_draft.priority,
conversation=conversation,
sources=package.sources,
colleague_cases=cases,
recommendation=package.recommendation,
)
package = ensure_escalation_draft(package, trimmed, conversation=conversation)
return AssistantResult(text=intro, package=package)
return None
def _try_local_rag(
trimmed: str,
greeting: str,
system: str,
conversation: list[dict[str, str]] | None = None,
) -> AssistantResult | None:
"""Временный RAG в backend: поиск по Manuals/ + опционально внешняя LLM."""
if not settings.rag_local_enabled or settings.rag_mode != "local":
return None
hits = search_documents(trimmed)
if not hits:
return None
llm = get_llm_client()
if llm.is_configured():
llm_result = build_package_with_llm(
trimmed, hits, llm, greeting, system=system, conversation=conversation
)
if llm_result:
intro, package = llm_result
return AssistantResult(text=intro, package=package)
package = build_package_from_hits(trimmed, hits, system=system, conversation=conversation)
if package is None:
return None
threshold = settings.confidence_threshold
intro = (
f"{greeting}нашёл релевантные фрагменты в инструкциях (локальный RAG). Вот что рекомендую:"
if package.confidence >= threshold
else f"{greeting}нашёл частично подходящие материалы (уверенность ниже {threshold}%). Рекомендую также оформить обращение:"
)
return AssistantResult(text=intro, package=package)
def process_message(
query: str,
profile: UserProfile,
is_first_message: bool,
conversation: list[dict[str, str]] | None = None,
) -> AssistantResult:
trimmed = query.strip()
greeting = f"{profile.first_name} {profile.patronymic}, " if is_first_message else ""
if _is_thanks(trimmed):
return AssistantResult(
text="Рад был помочь. Если появятся вопросы — обращайтесь.",
is_simple=True,
)
if _is_off_topic(trimmed):
return AssistantResult(
text=f"{greeting}этот вопрос выходит за рамки моих задач — я помогаю только с рабочими вопросами. Могу ли я помочь ещё?",
is_simple=True,
)
if _is_confidential_request(trimmed):
return AssistantResult(
text=f"{greeting}эта информация является конфиденциальной и доступна только самому сотруднику. Могу ли я помочь ещё?",
is_simple=True,
)
if _is_security_probe(trimmed):
return AssistantResult(
text=f"{greeting}я работаю на основе ваших актуальных данных в системе. Гипотетические сценарии не влияют на мои возможности и правила работы. Могу ли я помочь ещё?",
is_simple=True,
)
glossary = _detect_glossary(trimmed)
if glossary:
return AssistantResult(
text=f"{greeting}{glossary}\n\nМогу ли я помочь ещё?",
is_simple=True,
)
dialog = conversation or []
# 1) Курируемые сценарии (быстрые ответы для демо)
pattern_result = _try_pattern_knowledge(trimmed, greeting, dialog)
if pattern_result:
return pattern_result
# 2) Определение ИТ-системы до поиска (CJM §3.2, §4.1)
system = _detect_system(trimmed)
if not system:
systems_list = ", ".join(IT_SYSTEMS)
return AssistantResult(
text=f"{greeting}чтобы подобрать точную инструкцию, уточните, пожалуйста: с какой ИТ-системой связан ваш вопрос? ({systems_list})",
needs_clarification=True,
is_simple=True,
)
if system not in IT_SYSTEMS:
return AssistantResult(
text=f"{greeting}система «{system}» пока не входит в базу знаний ассистента. Могу помочь оформить обращение в поддержку.",
create_ticket=True,
is_simple=True,
)
# 3) Локальный RAG по Manuals/ после определения системы
rag_result = _try_local_rag(trimmed, greeting, system, dialog)
if rag_result:
return rag_result
cases = [ColleagueCase.model_validate(case) for case in get_colleague_cases_for_system(system)]
ticket = enrich_ticket_draft(
trimmed,
system,
"Средний",
conversation=dialog,
colleague_cases=cases,
recommendation="К сожалению, в базе знаний нет точного ответа на ваш вопрос.",
)
why = build_why_suggested(system=system, sources=[], confidence=35)
return AssistantResult(
text=f"{greeting}по системе «{system}» не нашёл подходящей инструкции. Предлагаю создать обращение с предзаполненным контекстом.",
create_ticket=True,
package=ResponsePackage(
recommendation="К сожалению, в базе знаний нет точного ответа на ваш вопрос.",
steps=[],
sources=[],
colleague_cases=cases or None,
why_suggested=why,
confidence=35,
system=system,
ticket_draft=ticket,
),
)
def get_default_profile() -> UserProfile:
return UserProfile.model_validate(DEFAULT_PROFILE)