Files
vector/backend/schemas.py
T
Sadmin 3e38aae2be feat: завершённые поиски — ручной ввод + детерминированный прогон модели (без ИИ)
Бэкенд:
- schemas.py: ClosedCaseCreate/Response — форма ~25 полей (субъект, среда,
  ресурсы, исход) для внесения завершённых поисков вручную
- routers/closed_cases.py: CRUD + POST {id}/reanalyze. _run_model() —
  тот же скоринг, что /analyze (WeightedScorer + distance_service +
  психотип), НО БЕЗ claude_service: urgency/behavior/actions по правилам
- _accuracy_note(): прогнозный радиус vs факт (в радиусе / мимо)
- main.py: роутер подключён

Фронтенд:
- pages/ClosedCases.jsx + .css: форма внесения (VCL-density, чипы диагнозов/
  рельефа), блок 'Прогноз vs факт', таблица архива (в радиусе/мимо)
- AdminDashboard.jsx: вкладки Кейсы | Завершённые поиски

Тесты: test_closed_cases.py — 6 passed. Существующие 15 failed
(test_distance_service) не связаны с изменениями — падали до них.
2026-09-09 10:41:33 +03:00

204 lines
6.8 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
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