Files
vector/backend/schemas.py
T
Sadmin a1d33a2b28 Реорганизация /admin: операции, честные метрики, синхронизация закрытия
- Вкладка «Операции»: список search_operations (статус/подразделение/обновлена,
  фильтр, переход в анализ кейса), unit_name в DTO.
- Метрики: + активные/завершённые операции (stats из search_operations);
  «Активность 7 дней» теперь честная (created_at >= now-7d, была = total).
- Синхронизация: закрытие кейса через /admin PATCH → активная операция
  автоматически completed + closed_at (аудит в логе).
- ФИКС legacy-бага PATCH /cases: default_factory-списки затирали
  terrain/diagnosis_type пустыми массивами — выкидываются пустые списки.
- Редактор исхода: «Кто нашёл» → select WHO_FOUND_TYPES.
- Кнопка «Пользователи · Аудит» → /users.
- Тесты: автозакрытие операции, метрики (232 passed).
2026-09-09 22:41:14 +03:00

206 lines
6.9 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
class CaseBase(BaseModel):
model_config = ConfigDict(extra='allow')
age: int | None = None
age_years: int | None = None
gender: str | None = None
child_name: str | None = None
height_build: str | None = None
clothes_upper: str | None = None
clothes_lower: str | None = None
clothes_shoes: str | None = None
shoes: str | None = None
clothes_description: str | None = None
special_marks: str | None = None
phone_status: str | None = None
health_flags: list[str] = Field(default_factory=list)
has_diagnosis: bool | None = None
diagnosis_type: list[str] = Field(default_factory=list)
fitness_level: str | None = None
has_transport: str | None = None
cant_swim: bool | None = None
psychotype: str | None = None
psychotype_answers: dict[str, Any] | None = None
elapsed_hours: float | None = None
last_known_place: str | None = None
direction: str | None = None
reason: str | None = None
loss_reason: str | None = None
loss_time: datetime | None = None
last_seen_direction: str | None = None
last_seen_reliability: str | None = None
last_seen_description: str | None = None
behavior_description: str | None = None
familiar_places: str | None = None
lost_before: str | None = None
terrain_primary: str | None = None
terrain: list[str] = Field(default_factory=list)
weather: str | None = None
season: str | None = None
temperature_c: float | None = None
precipitation: str | None = None
visibility: str | None = None
wind: str | None = None
gps: dict[str, float] | None = None
gps_lat: float | None = None
gps_lon: float | None = None
tnp_lat: float | None = None
tnp_lon: float | None = None
tnp_address: str | None = None
resources: list[str] = Field(default_factory=list)
teams_count: int | None = None
team_size: int | None = None
has_dog: bool | None = None
extra_resources: list[str] = Field(default_factory=list)
notes: str | None = None
note: str | None = None
status: str | None = None
found_alive: bool | None = None
found_distance_km: float | None = None
found_direction: str | None = None
found_location_type: str | None = None
found_lat: float | None = None
found_lon: float | None = None
search_duration_hours: float | None = None
who_found: str | None = None
raw_text: str | None = None
analysis_log: dict[str, Any] | None = None
mobile: bool = False
class CaseCreate(CaseBase):
status: str | None = 'new'
class CaseUpdate(CaseBase):
pass
class CaseResponse(CaseBase):
id: str
created_at: datetime
updated_at: datetime
status: str
health_flags: list[str] = Field(default_factory=list)
diagnosis_type: list[str] = Field(default_factory=list)
terrain: list[str] = Field(default_factory=list)
resources: list[str] = Field(default_factory=list)
extra_resources: list[str] = Field(default_factory=list)
result: dict[str, Any] | None = None
class CaseListResponse(BaseModel):
items: list[CaseResponse]
total: int
page: int
page_size: int
class DashboardResponse(BaseModel):
total_cases: int
found_alive_count: int
found_deceased_count: int
unknown_outcome_count: int
median_found_distance_km: float | None
recent_activity_count: int
active_operations: int = 0
completed_operations: int = 0
class RecommendationRequest(BaseModel):
age: int | None = None
gender: str | None = None
health_flags: list[str] = Field(default_factory=list)
elapsed_hours: int | None = None
terrain_primary: str | None = None
weather: str | None = None
resources: list[str] = Field(default_factory=list)
class AnalyzeResponse(BaseModel):
urgency: str
primary_zones: list[dict[str, Any]]
immediate_actions: list[str]
behavioral_prediction: str
psychotype_recommendations: dict[str, Any]
key_locations: list[str]
case_id: str
class ParseDocResponse(BaseModel):
filename: str
parsed: bool
preview: dict[str, Any]
raw_text: str | None = None
# ================= ЗАВЕРШЁННЫЕ ПОИСКИ (ручной ввод, без ИИ) =================
# Оператор руками вносит данные ЗАВЕРШЁННОГО поиска: параметры субъекта/среды
# (вход матмодели) + реальный исход. Цель — датасет для калибровки модели
# (сравнение прогноза ВЕКТОРА с фактом) и архив 14 лет без LLM.
class ClosedCaseCreate(BaseModel):
# --- Субъект (вход модели) ---
age_years: int
gender: str = 'м' # 'м' | 'ж'
diagnosis_type: list[str] = Field(default_factory=list) # РАС, эпилепсия, СДВГ, ЗПР...
has_transport: str = 'none' # none|bike|scooter|other
cant_swim: bool = False
fitness_level: str | None = None # низкий|средний|высокий
# --- Обстоятельства (вход матмодели) ---
elapsed_hours: float
loss_time: datetime | None = None # время потери (для time_of_day)
terrain: list[str] = Field(default_factory=list) # лес, болото, поле...
weather: str | None = None
season: str | None = None
temperature_c: float | None = None
tnp_lat: float | None = None
tnp_lon: float | None = None
tnp_address: str | None = None
# --- Психотип (опционально, вход скоринга) ---
psychotype: str | None = None
psychotype_answers: dict[str, Any] | None = None
# --- Ресурсы ---
teams_count: int | None = None
team_size: int | None = None
has_dog: bool = False
# --- Исход (факт, для сравнения с прогнозом) ---
found_alive: bool
found_distance_km: float | None = None
found_direction: str | None = None
found_location_type: str | None = None
found_lat: float | None = None
found_lon: float | None = None
search_duration_hours: float | None = None
who_found: str | None = None
# --- Мета ---
notes: str | None = None
source: str | None = None # откуда: спецдонесение 2015, бумажный журнал...
class ClosedCaseResponse(ClosedCaseCreate):
id: str
created_at: datetime
# Сохранённый прогноз модели на момент внесения (для калибровки)
model_prediction: dict[str, Any] | None = None
# Насколько прогноз совпал с фактом
accuracy_note: str | None = None
class ClosedCaseListResponse(BaseModel):
items: list[ClosedCaseResponse]
total: int