Files
vector/backend/routers/analyze.py
T
2026-06-08 16:21:23 +00:00

147 lines
5.4 KiB
Python

from __future__ import annotations
from datetime import datetime
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.utcnow().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'],
}