Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и отдаёт звонящему только их. Тест прогоняет длинную серию реплик и проверяет, что ни один нераскрытый факт не прозвучал. Главная находка — матчинг по порогу близости не работает. На multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт адрес на «успокойтесь», либо не слышит «где вы находитесь?». Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько формулировок (examples), рядом общий список не-вопросов (checklists/common.yaml), и реплика засчитывается пункту, только если она ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20 вопросов, 0 из 8 ложных срабатываний. Реплика режется только по границам предложений, со знаком: «?» для e5 — сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам. Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат начнёт слышать оператора, когда голосовой контур передаст ему stt.final. Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими словами, dialog/llm.py с кэшем.
67 lines
2.6 KiB
Python
67 lines
2.6 KiB
Python
"""Сборка приложения. Роутеры подключаются по мере готовности — см. tasks/."""
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
|
|
from pathlib import Path
|
|
|
|
from app.api.http import scenarios as scenarios_api
|
|
from app.api.http import sessions
|
|
from app.api.ws import call as call_ws
|
|
from app.api.ws import control as control_ws
|
|
from app.api.ws import observe as observe_ws
|
|
from app.config import get_settings
|
|
from app.db.base import get_sessionmaker
|
|
from app.dialog.runtime import get_embedder
|
|
from app.scenarios import store
|
|
from app.session.hub import hub
|
|
from app.session.journal import DbJournal
|
|
from app.scenarios.loader import ScenarioError
|
|
|
|
|
|
LIBRARY = Path(__file__).resolve().parents[2] / "scenarios"
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Библиотека проверяется на старте целиком: сломанный сценарий, найденный
|
|
# посреди занятия, — сценарий, которого не должно случиться.
|
|
try:
|
|
loaded = store.load_from_disk(LIBRARY)
|
|
except ScenarioError as exc:
|
|
raise RuntimeError(f"библиотека сценариев не прошла проверку: {exc}") from exc
|
|
app.state.scenarios_loaded = len(loaded)
|
|
|
|
# Журнал: всё, что не записано, для оценки не существует.
|
|
hub.journal = DbJournal(get_sessionmaker())
|
|
|
|
# Эмбеддинги для слот-автомата — грузятся один раз, до первого занятия.
|
|
app.state.embeddings_ready = get_embedder() is not None
|
|
|
|
# Прогрев моделей речи — карточка lct-06.
|
|
app.state.models_ready = False
|
|
yield
|
|
|
|
await hub.shutdown()
|
|
|
|
|
|
app = FastAPI(title="Учебный симулятор занятия для системы 112", lifespan=lifespan)
|
|
app.include_router(sessions.router)
|
|
app.include_router(scenarios_api.router)
|
|
app.include_router(call_ws.router)
|
|
app.include_router(observe_ws.router)
|
|
app.include_router(control_ws.router)
|
|
|
|
|
|
@app.get("/api/health")
|
|
async def health() -> dict:
|
|
"""Готовность стенда. Фронт показывает экран «модели прогреваются»."""
|
|
settings = get_settings()
|
|
return {
|
|
"status": "ok",
|
|
"models_ready": getattr(app.state, "models_ready", False),
|
|
"scenarios_loaded": getattr(app.state, "scenarios_loaded", 0),
|
|
"embeddings_ready": getattr(app.state, "embeddings_ready", False),
|
|
"offline": settings.offline,
|
|
}
|