Files
vector/backend/routers/analyze.py
T
Sadmin d8723582c9 Полный отказ от LLM: детерминированный rules-анализ
Решение Виктора: ИИ убран из проекта — карточки детей (ПД) не должны
покиать контур. Анализ полностью локальный и детерминированный.

- services/rules_analysis.py: правила вместо Claude (бывший
  analyze_with_fallback + rule_based_urgency из closed_cases):
  urgency по возрасту/профилям, зоны через geo+scoring,
  behavioral_prediction/immediate_actions/key_locations — справочники.
- search_engine: анализатор инъекцией (CaseAnalyzer, Protocol),
  импорт claude_service удалён; рантайм — rules_analyze.
- УДАЛЕНЫ: services/claude_service.py, backend/services/claude_service.py
  (устаревшая копия), test_claude_service.py; ключевые правила-кейсы
  перенесены в test_rules_analysis.py (+тест отсутствия LLM-упоминаний).
- docker-compose/.env.example: ANTHROPIC_API_KEY удалён.
- Фронт: бейдж 'ИИ-анализ' -> 'Локальный расчёт'.
- Доки: DEPLOY/SERVICES_README/ANALYZE_API переписаны, FIXES_REPORT
  помечен историческим, vector_tasks B20: zones/probability — из движка.

pytest: 187 passed (было 214; -27 удалённых LLM-тестов, +тест rules).
Контракт /analyze не изменён (фронт совместим).
2026-09-09 18:30:44 +03:00

127 lines
4.7 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.rules_analysis import rules_analyze
from services.search_engine import (
SearchInput,
build_search_model,
)
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)
has_transport: str | None = None
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)
@router.post('', dependencies=[Depends(require_roles(['operator', 'field', 'admin']))])
async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]:
# B14: роутер — тонкая обёртка. Вся расчётная логика — в
# services/search_engine.py::build_search_model (без DB/auth/HTTP).
case_data: dict[str, Any] = {
'age': payload.age,
'gender': payload.gender,
'terrain': payload.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,
'has_transport': payload.has_transport,
'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,
}
case_data = {
k: v for k, v in case_data.items()
if v is not None and not (isinstance(v, (list, dict)) and len(v) == 0)
}
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}
search_input = SearchInput(
age=case_data.get('age'),
gender=case_data.get('gender'),
terrain=case_data.get('terrain'),
terrain_primary=case_data.get('terrain_primary'),
weather=case_data.get('weather'),
elapsed_hours=case_data.get('elapsed_hours'),
last_location=case_data.get('last_location'),
circumstances=case_data.get('circumstances'),
physical_condition=case_data.get('physical_condition'),
experience=case_data.get('experience'),
season=case_data.get('season'),
diagnosis_type=case_data.get('diagnosis_type') or [],
has_transport=case_data.get('has_transport'),
cant_swim=bool(case_data.get('cant_swim')),
psychotype_answers=case_data.get('psychotype_answers') or {},
profiles=case_data.get('profiles') or [],
lat=case_data.get('lat'),
lon=case_data.get('lon'),
loss_time=case_data.get('loss_time'),
time_of_day=case_data.get('time_of_day'),
)
model = await build_search_model(search_input, analyzer=rules_analyze)
result = model.model_dump()
result['case_id'] = str(payload.case_id) if payload.case_id else None
result['analyzed_at'] = datetime.now(timezone.utc).isoformat()
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'],
}