a1d33a2b28
- Вкладка «Операции»: список 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).
206 lines
6.9 KiB
Python
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
|