69a9884db5
- Загрузка архива (§4.1): POST /ingest распаковывает ZIP, дораспознаёт и догружает прайсы в базу (append). Виджет в админке. Кэш эмбеддингов справочника (dict_emb.npy) — догрузка считает только новый файл, эмбеддинги best-effort с откатом на fuzzy при сбое Gemini. - Поиск по name_norm (lowercase): «УЗИ» и «узи» дают одинаковый результат — SQLite LIKE не сворачивает регистр для кириллицы. - Фильтр названий: код-мнемоники («МОЗО.15») больше не попадают в очередь. - /stats считается из базы — цифры на дашборде обновляются после загрузки. - Деплой через Dockerfile (зависимости в образе, код монтируется томом). - ruff: ядро и весь репозиторий проходят проверку. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""Общие модели данных — контракт между агентами (Pydantic v2).
|
|
|
|
`RawRow` и `ParsedDocument` — выход любого парсера (граница агентов B → C).
|
|
`MatchResult` — выход сопоставления (агент C). `*Out`-модели — выдача API (агент D),
|
|
её же потребляет фронт (агент E). Любая правка этих типов проходит через оркестратора.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
# Карта тарифов: ключ — тип цены, значение — сумма в KZT.
|
|
# Возможные ключи: resident, nonresident, cis, far, insurance, partner.
|
|
PriceTiers = dict[str, float]
|
|
|
|
|
|
class RawRow(BaseModel):
|
|
"""Одна сырая строка прайса — единый выход любого парсера."""
|
|
|
|
service_name_raw: str
|
|
service_code_source: str | None = None
|
|
prices: PriceTiers = Field(default_factory=dict)
|
|
unit: str | None = None
|
|
section: str | None = None # заголовок секции — контекст специальности
|
|
|
|
|
|
class ParsedDocument(BaseModel):
|
|
"""Результат разбора одного файла прайса."""
|
|
|
|
file_name: str
|
|
file_format: str # pdf / scan_pdf / docx / xlsx / xls
|
|
partner_name: str | None = None
|
|
city: str | None = None
|
|
effective_date: date | None = None
|
|
rows: list[RawRow] = Field(default_factory=list)
|
|
raw_content: str = ""
|
|
parse_log: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class MatchResult(BaseModel):
|
|
"""Результат сопоставления строки со справочником."""
|
|
|
|
service_id: str | None = None
|
|
method: str | None = None # code / exact / embedding / fuzzy / manual
|
|
confidence: float = 0.0
|
|
# Лучший кандидат для очереди верификации, даже если ниже порога автосопоставления.
|
|
suggested_service_id: str | None = None
|
|
suggested_score: float = 0.0
|
|
|
|
|
|
# --- выдача API ---
|
|
class ServiceOut(BaseModel):
|
|
service_id: str
|
|
specialty: str
|
|
name_ru: str
|
|
tarificator_code: str | None = None
|
|
|
|
|
|
class PartnerOut(BaseModel):
|
|
partner_id: str
|
|
name: str
|
|
city: str | None = None
|
|
is_active: bool = True
|
|
|
|
|
|
class PriceOut(BaseModel):
|
|
partner_id: str
|
|
partner_name: str
|
|
price_resident_kzt: float | None = None
|
|
price_nonresident_kzt: float | None = None
|
|
prices: PriceTiers = Field(default_factory=dict)
|
|
effective_date: date | None = None
|
|
|
|
|
|
class Stats(BaseModel):
|
|
"""Сводка для дашборда и отчёта о качестве."""
|
|
|
|
documents_total: int = 0
|
|
documents_done: int = 0
|
|
items_total: int = 0
|
|
auto_matched_pct: float = 0.0
|
|
unmatched_total: int = 0
|