"""Детерминированный rules-анализ (замена Claude API, решение Виктора: ИИ из проекта убран — карточки детей не покидают контур). Логика — бывший analyze_with_fallback из claude_service (правила по возрасту, профилям, координатам) + urgency по правилам closed_cases. Никаких внешних вызовов: только geo_service (Overpass) + scoring_service + справочники. """ from __future__ import annotations import logging from typing import Any from pydantic import BaseModel, Field from services.geo_service import build_search_zones from services.scoring_service import WeightedScorer logger = logging.getLogger(__name__) class PrimaryZone(BaseModel): priority: int name: str direction: str distance: float reason: str class AnalysisResult(BaseModel): urgency: str # "критическая", "высокая", "средняя", "низкая" primary_zones: list[PrimaryZone] search_radius_km: float key_locations: list[str] behavioral_prediction: str immediate_actions: list[str] summary: str fallback_used: bool = Field(default=True) def rule_based_urgency(elapsed: float, age: int, diagnosis: list[str]) -> str: """urgency по правилам: чем меньше ребёнок и дольше поиск — тем выше срочность.""" score = 0 if elapsed >= 24: score += 2 elif elapsed >= 8: score += 1 if age <= 7: score += 2 elif age <= 12: score += 1 if diagnosis: score += 1 if score >= 4: return 'критическая' if score >= 2: return 'высокая' return 'средняя' def _age_urgency(age: int, profiles: list[str]) -> str: if age <= 4 or 'эпилепсия' in profiles or 'РАС' in profiles: return 'критическая' if age <= 7 or 'велосипед' in profiles: return 'высокая' return 'средняя' def _key_locations(age: int) -> list[str]: if age <= 7: return ['водоёмы', 'укрытия', 'густая растительность', 'ближайшие постройки'] if age <= 12: return ['водоёмы', 'дороги', 'тропы', 'лесные массивы'] return ['дороги', 'населённые пункты', 'транспортные узлы', 'водоёмы'] def _behavioral_prediction(age: int, profiles: list[str]) -> str: if 'РАС' in profiles: return 'Высокий риск движения к водоёмам и ж/д путям. Может не откликаться на имя.' if age <= 4: return 'Минимальное движение, вероятно находится близко к точке потери.' if age <= 12: return 'Умеренное движение, может следовать по тропам или дорогам.' return 'Целенаправленное движение, возможен выход к населённым пунктам.' def _immediate_actions(profiles: list[str]) -> list[str]: actions = [ 'Организовать поисковые группы', 'Проверить ближайшие водоёмы', 'Опросить свидетелей в районе последнего местоположения', ] if 'РАС' in profiles: actions.insert(0, 'КРИТИЧНО: Перекрыть все водоёмы и ж/д пути в радиусе 5 км') if 'велосипед' in profiles: actions.insert(0, 'Расширить зону поиска до 10-15 км, проверить дорожные камеры') return actions async def rules_analyze(case_data: dict[str, Any]) -> AnalysisResult: """Детерминированный анализ: scoring_service + geo_service, без LLM.""" age = int(case_data.get('age') or 10) profiles = list(case_data.get('profiles') or []) urgency = _age_urgency(age, profiles) primary_zones: list[PrimaryZone] = [] search_radius_km = 5.0 if case_data.get('lat') and case_data.get('lon'): try: zones = await build_search_zones( case_data['lat'], case_data['lon'], case_data, ) scorer = WeightedScorer() zones_dict = [ { 'direction': z.direction, 'distance_km': z.distance_km, 'forest_pct': z.forest_pct, 'road_density': z.road_density, 'water_distance_km': z.water_distance_km, 'settlement_distance_km': z.settlement_distance_km, } for z in zones ] ranked_zones = scorer.rank_zones(zones_dict, case_data) for i, zone in enumerate(ranked_zones[:5]): primary_zones.append(PrimaryZone( priority=i + 1, name=f"Зона {zone['direction']} {zone['distance_km']}км", direction=zone['direction'], distance=zone['distance_km'], reason=f"Оценка {zone['score']}/100 по scoring_service", )) search_radius_km = scorer.distance_multiplier * 2.0 except Exception as e: logger.warning(f'Geo/scoring service error: {e}') if not primary_zones: primary_zones = [ PrimaryZone(priority=1, name='Ближняя зона', direction='N', distance=0.5, reason='Базовая зона поиска'), PrimaryZone(priority=2, name='Средняя зона', direction='E', distance=1.0, reason='Расширенная зона поиска'), ] key_locations = _key_locations(age) behavioral_prediction = _behavioral_prediction(age, profiles) immediate_actions = _immediate_actions(profiles) summary = ( f'Случай классифицирован как {urgency} срочность. ' f'Рекомендуемый радиус поиска: {search_radius_km} км. ' f'Приоритет: {key_locations[0]}.' ) return AnalysisResult( urgency=urgency, primary_zones=primary_zones, search_radius_km=search_radius_km, key_locations=key_locations, behavioral_prediction=behavioral_prediction, immediate_actions=immediate_actions, summary=summary, fallback_used=True, )