147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
|
|
from backend.database import db
|
|
from backend.routers.auth import require_roles
|
|
from services.claude_service import analyze_case as claude_analyze
|
|
from services.distance_service import calculate_max_distance
|
|
from services.psychotype_service import (
|
|
detect_psychotype,
|
|
get_psychotype_modifiers,
|
|
get_search_recommendations,
|
|
)
|
|
from services.scoring_service import WeightedScorer
|
|
|
|
router = APIRouter(prefix='/api/v1/analyze', tags=['analyze'])
|
|
|
|
|
|
class AnalysisRequest(BaseModel):
|
|
case_id: UUID | None = None
|
|
age: int | None = None
|
|
gender: str | None = None
|
|
terrain: str | list[str] | None = None
|
|
weather: str | None = None
|
|
elapsed_hours: float | None = None
|
|
last_location: str | None = None
|
|
circumstances: str | None = None
|
|
physical_condition: str | None = None
|
|
experience: str | None = None
|
|
season: str | None = None
|
|
diagnosis_type: list[str] = Field(default_factory=list)
|
|
psychotype_answers: dict[str, Any] = Field(default_factory=dict)
|
|
tnp_lat: float | None = None
|
|
tnp_lon: float | None = None
|
|
lat: float | None = None
|
|
lon: float | None = None
|
|
profiles: list[str] = Field(default_factory=list)
|
|
|
|
|
|
def _first_terrain(value: str | list[str] | None) -> str | None:
|
|
if isinstance(value, list):
|
|
return value[0] if value else None
|
|
return value
|
|
|
|
|
|
def _as_case_data(payload: AnalysisRequest) -> dict[str, Any]:
|
|
terrain = _first_terrain(payload.terrain)
|
|
case_data: dict[str, Any] = {
|
|
'age': payload.age,
|
|
'gender': payload.gender,
|
|
'terrain': terrain,
|
|
'terrain_primary': terrain,
|
|
'weather': payload.weather,
|
|
'elapsed_hours': payload.elapsed_hours,
|
|
'last_location': payload.last_location,
|
|
'circumstances': payload.circumstances,
|
|
'physical_condition': payload.physical_condition,
|
|
'experience': payload.experience,
|
|
'season': payload.season,
|
|
'diagnosis_type': payload.diagnosis_type,
|
|
'profiles': list(payload.profiles or []),
|
|
'psychotype_answers': payload.psychotype_answers,
|
|
'lat': payload.lat if payload.lat is not None else payload.tnp_lat,
|
|
'lon': payload.lon if payload.lon is not None else payload.tnp_lon,
|
|
}
|
|
return {k: v for k, v in case_data.items() if v is not None}
|
|
|
|
|
|
@router.post('', dependencies=[Depends(require_roles(['operator', 'field', 'admin']))])
|
|
async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]:
|
|
case_data = _as_case_data(payload)
|
|
|
|
if payload.case_id is not None:
|
|
case = db.get_case(str(payload.case_id))
|
|
if not case:
|
|
raise HTTPException(status_code=404, detail='Case not found')
|
|
case_data = {**case.to_detail(), **case_data}
|
|
|
|
max_distance_km = calculate_max_distance(case_data)
|
|
claude_result = await claude_analyze(case_data)
|
|
|
|
scorer = WeightedScorer()
|
|
if case_data.get('age'):
|
|
scorer.apply_age_modifiers(int(case_data['age']))
|
|
if case_data.get('season'):
|
|
scorer.apply_season_modifiers(str(case_data['season']))
|
|
if case_data.get('profiles'):
|
|
scorer.apply_profile(list(case_data['profiles']))
|
|
scorer._normalize_weights()
|
|
|
|
psychotype = None
|
|
psychotype_modifiers = None
|
|
psychotype_recommendations = None
|
|
if case_data.get('psychotype_answers'):
|
|
psychotype = detect_psychotype(case_data['psychotype_answers'])
|
|
psychotype_modifiers = get_psychotype_modifiers(psychotype)
|
|
psychotype_recommendations = get_search_recommendations(psychotype)
|
|
|
|
result = {
|
|
'case_id': str(payload.case_id) if payload.case_id else None,
|
|
'analyzed_at': datetime.now(timezone.utc).isoformat(),
|
|
'max_distance_km': max_distance_km,
|
|
'psychotype': psychotype,
|
|
'psychotype_modifiers': psychotype_modifiers,
|
|
'psychotype_recommendations': psychotype_recommendations,
|
|
'weights': scorer.weights,
|
|
'distance_multiplier': scorer.distance_multiplier,
|
|
'urgency': claude_result.urgency,
|
|
'primary_zones': [zone.model_dump() for zone in claude_result.primary_zones],
|
|
'search_radius_km': claude_result.search_radius_km,
|
|
'key_locations': claude_result.key_locations,
|
|
'behavioral_prediction': claude_result.behavioral_prediction,
|
|
'immediate_actions': claude_result.immediate_actions,
|
|
'summary': claude_result.summary,
|
|
'fallback_used': claude_result.fallback_used,
|
|
}
|
|
|
|
if payload.case_id is not None:
|
|
db.update_case(str(payload.case_id), analysis_log=result, status='analyzed')
|
|
|
|
return result
|
|
|
|
|
|
@router.post('/combined', dependencies=[Depends(require_roles(['operator', 'field', 'admin']))])
|
|
async def analyze_combined(payload: AnalysisRequest) -> dict[str, Any]:
|
|
return await analyze_case(payload)
|
|
|
|
|
|
@router.get('/{case_id}')
|
|
def get_analysis(case_id: str) -> dict[str, Any]:
|
|
case = db.get_case(case_id)
|
|
if not case:
|
|
raise HTTPException(status_code=404, detail='Case not found')
|
|
detail = case.to_detail()
|
|
if not detail.get('analysis_log'):
|
|
raise HTTPException(status_code=404, detail=f'No analysis found for case {case_id}')
|
|
return {
|
|
'case_id': case_id,
|
|
'analysis_log': detail['analysis_log'],
|
|
'created_at': detail['created_at'],
|
|
}
|