Import Vector lab project

This commit is contained in:
root
2026-06-06 18:31:55 +00:00
commit fcc22063ed
105 changed files with 31639 additions and 0 deletions
+347
View File
@@ -0,0 +1,347 @@
"""
Analysis API endpoints for full case analysis pipeline.
Pipeline: distance → geo → scoring → psychotype → claude → merged result
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from uuid import UUID
from datetime import datetime
import time
from database import get_db
from models import Case, AnalysisLog, User
from api.v1.auth import get_current_user
# Import services
from services.distance_service import calculate_max_distance
from services.geo_service import build_search_zones
from services.scoring_service import WeightedScorer
from services.psychotype_service import (
detect_psychotype,
get_psychotype_modifiers,
get_search_recommendations
)
from services.claude_service import analyze_case as claude_analyze
router = APIRouter()
class AnalysisRequest(BaseModel):
"""Request for full case analysis"""
case_id: UUID = Field(..., description="ID случая для анализа")
class ZoneResult(BaseModel):
"""Search zone with score and recommendations"""
priority: int
name: str
direction: str
distance_km: float
score: float
reasoning: str
forest_pct: Optional[float] = None
road_density: Optional[float] = None
water_distance_km: Optional[float] = None
class AnalysisResponse(BaseModel):
"""Full analysis result"""
case_id: UUID
analyzed_at: datetime
# Distance calculation
max_distance_km: float
# Psychotype (if available)
psychotype: Optional[str] = None
psychotype_modifiers: Optional[Dict[str, Any]] = None
psychotype_recommendations: Optional[Dict[str, Any]] = None
# Zones
zones: List[ZoneResult]
# Claude analysis (if available)
urgency: Optional[str] = None
key_locations: Optional[List[str]] = None
immediate_actions: Optional[List[str]] = None
behavioral_prediction: Optional[str] = None
summary: Optional[str] = None
# Meta
execution_time_ms: float
services_used: List[str]
class SavedAnalysisResponse(BaseModel):
"""Saved analysis result from database"""
case_id: UUID
analysis_log: Dict[str, Any]
created_at: datetime
@router.post("/analyze", response_model=AnalysisResponse, status_code=200)
async def analyze_full_case(
request: AnalysisRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Запустить полный анализ случая.
Пайплайн:
1. Distance service - расчет максимальной дистанции
2. Geo service - построение зон поиска
3. Scoring service - оценка и ранжирование зон
4. Psychotype service - определение психотипа (если есть данные)
5. Claude service - интеллектуальный анализ (опционально)
6. Merge results - объединение результатов
Требуется аутентификация (operator, field, admin).
"""
start_time = time.time()
services_used = []
# 1. Получить случай из БД
case = db.query(Case).filter(Case.id == request.case_id).first()
if not case:
raise HTTPException(status_code=404, detail=f"Case {request.case_id} not found")
# Подготовить данные для анализа
case_data = {
'age': case.age_years,
'gender': case.gender,
'elapsed_hours': case.elapsed_hours or 1.0,
'terrain_primary': case.terrain[0] if case.terrain else 'лес',
'season': case.season or 'лето',
'temperature_c': case.temperature_c or 20.0,
'has_transport': case.has_transport,
'has_diagnosis': case.has_diagnosis,
'diagnosis_type': case.diagnosis_type or [],
'tnp_lat': case.tnp_lat,
'tnp_lon': case.tnp_lon,
}
# 2. Distance service - расчет максимальной дистанции
try:
max_distance = calculate_max_distance(case_data)
services_used.append('distance')
except Exception as e:
raise HTTPException(status_code=500, detail=f"Distance calculation failed: {str(e)}")
# 3. Geo service - построение зон поиска (если есть координаты)
zones_data = []
if case.tnp_lat and case.tnp_lon:
try:
zones_data = await build_search_zones(
lat=case.tnp_lat,
lon=case.tnp_lon,
case_data=case_data,
max_distance_km=max_distance
)
services_used.append('geo')
except Exception as e:
# Geo service опционален, продолжаем без него
print(f"Geo service failed: {e}")
# 4. Scoring service - оценка и ранжирование зон
scored_zones = []
if zones_data:
try:
scorer = WeightedScorer()
for zone in zones_data:
zone_dict = zone.model_dump() if hasattr(zone, 'model_dump') else zone
score = scorer.score_zone(zone_dict, case_data, max_distance_km=max_distance)
zone_dict['score'] = score
zone_dict['reasoning'] = f"Оценка на основе {len(case_data)} факторов"
scored_zones.append(zone_dict)
# Сортировать по score
scored_zones.sort(key=lambda z: z.get('score', 0), reverse=True)
services_used.append('scoring')
except Exception as e:
print(f"Scoring service failed: {e}")
scored_zones = _create_fallback_zones(max_distance)
else:
# Если geo не работает, создаем базовые зоны
scored_zones = _create_fallback_zones(max_distance)
# 5. Psychotype service - определение психотипа
psychotype = None
psychotype_modifiers = None
psychotype_recommendations = None
if case.psychotype_answers:
try:
psychotype = detect_psychotype(case.psychotype_answers)
psychotype_modifiers = get_psychotype_modifiers(psychotype)
psychotype_recommendations = get_search_recommendations(psychotype)
services_used.append('psychotype')
# Применить модификаторы психотипа к зонам
scored_zones = _apply_psychotype_modifiers(scored_zones, psychotype_modifiers)
except Exception as e:
print(f"Psychotype service failed: {e}")
# 6. Claude service - интеллектуальный анализ (опционально)
urgency = None
key_locations = None
immediate_actions = None
behavioral_prediction = None
summary = None
try:
claude_result = await claude_analyze(case_data)
urgency = claude_result.urgency
key_locations = claude_result.key_locations
immediate_actions = claude_result.immediate_actions
behavioral_prediction = claude_result.behavioral_prediction
summary = claude_result.summary
services_used.append('claude')
except Exception as e:
# Claude опционален, продолжаем без него
print(f"Claude service failed: {e}")
# 7. Формируем результат
zones_result = [
ZoneResult(
priority=i + 1,
name=zone.get('name', f"Зона {zone.get('direction', 'N')}"),
direction=zone.get('direction', 'N'),
distance_km=zone.get('distance_km', 0),
score=zone.get('score', 0),
reasoning=zone.get('reasoning', 'Автоматическая оценка'),
forest_pct=zone.get('forest_pct'),
road_density=zone.get('road_density'),
water_distance_km=zone.get('water_distance_km')
)
for i, zone in enumerate(scored_zones[:10]) # Топ-10 зон
]
execution_time = (time.time() - start_time) * 1000
# 8. Сохранить результат в analysis_log
analysis_result = {
'max_distance_km': max_distance,
'psychotype': psychotype,
'psychotype_modifiers': psychotype_modifiers,
'zones': [z.model_dump() for z in zones_result],
'urgency': urgency,
'key_locations': key_locations,
'immediate_actions': immediate_actions,
'summary': summary,
'services_used': services_used,
'execution_time_ms': execution_time
}
# Обновить case.analysis_log
case.analysis_log = analysis_result
db.commit()
# Создать запись в AnalysisLog
log_entry = AnalysisLog(
case_id=case.id,
user_id=current_user.id,
analysis_type='full_pipeline',
input_data={'case_id': str(case.id)},
output_data=analysis_result,
execution_time=execution_time / 1000,
status='success'
)
db.add(log_entry)
db.commit()
return AnalysisResponse(
case_id=case.id,
analyzed_at=datetime.utcnow(),
max_distance_km=max_distance,
psychotype=psychotype,
psychotype_modifiers=psychotype_modifiers,
psychotype_recommendations=psychotype_recommendations,
zones=zones_result,
urgency=urgency,
key_locations=key_locations,
immediate_actions=immediate_actions,
behavioral_prediction=behavioral_prediction,
summary=summary,
execution_time_ms=execution_time,
services_used=services_used
)
@router.get("/analyze/{case_id}", response_model=SavedAnalysisResponse)
async def get_saved_analysis(
case_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Получить сохранённый результат анализа.
Возвращает последний analysis_log из таблицы cases.
Требуется аутентификация (operator, field, admin).
"""
case = db.query(Case).filter(Case.id == case_id).first()
if not case:
raise HTTPException(status_code=404, detail=f"Case {case_id} not found")
if not case.analysis_log:
raise HTTPException(
status_code=404,
detail=f"No analysis found for case {case_id}. Run POST /analyze first."
)
return SavedAnalysisResponse(
case_id=case.id,
analysis_log=case.analysis_log,
created_at=case.created_at
)
def _create_fallback_zones(max_distance: float) -> List[Dict[str, Any]]:
"""Create basic zones when geo/scoring services fail"""
directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
zones = []
for i, direction in enumerate(directions):
zones.append({
'direction': direction,
'distance_km': max_distance * 0.8,
'score': 100 - (i * 10),
'name': f"Сектор {direction}",
'reasoning': 'Базовая оценка (сервисы недоступны)'
})
return zones
def _apply_psychotype_modifiers(zones: List[Dict], modifiers: Dict) -> List[Dict]:
"""Apply psychotype modifiers to zone scores"""
if not modifiers:
return zones
# Применяем модификаторы зон из психотипа
for zone in zones:
distance = zone.get('distance_km', 0)
# Определяем зону дистанции
if distance < 0.5:
modifier = modifiers.get('zone_0_500', 1.0)
elif distance < 1.5:
modifier = modifiers.get('zone_500_1500', 1.0)
elif distance < 2.5:
modifier = modifiers.get('zone_1500_2500', 1.0)
else:
modifier = modifiers.get('zone_2500plus', 1.0)
# Применяем модификатор к score
zone['score'] = zone.get('score', 0) * modifier
zone['reasoning'] += f" (психотип: ×{modifier:.1f})"
# Пересортировать по score
zones.sort(key=lambda z: z.get('score', 0), reverse=True)
return zones