B2: tolerant JSON extraction from Claude responses + graceful fallback
_extract_json_payload handles a json/JSON/bare fence, raw JSON and JSON embedded in prose; any unparseable response or contract violation now degrades to the deterministic scoring_service instead of raising. Also guards the response envelope itself (content[0].text). B3: single home for recommendation scoring services/recommendation_service.py holds the rules; routers/stats.py and backend/services/stats_service.py both delegate to it. Unified rules are the union of the two old copies: same weights/threshold, substring matching (superset of the old exact match), tolerant key aliases, health_flags rule kept. Endpoint response contract unchanged. Plus: Overpass circuit breaker and concurrent zone queries in geo_service - 128 sequential calls per analysis no longer each burn a connect timeout when the host has no outbound network. Tests: 152 -> 194 passed.
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from backend.database import db
|
||||
except ImportError: # pragma: no cover - compatibility for direct service imports
|
||||
from database import db
|
||||
|
||||
# Scoring rules live in services/recommendation_service.py (B3) — this module
|
||||
# only re-exports them so existing `stats_service.get_statistical_recommendation`
|
||||
# callers keep working.
|
||||
from services.recommendation_service import get_statistical_recommendation
|
||||
|
||||
__all__ = ['summary', 'heatmap', 'get_statistical_recommendation']
|
||||
|
||||
|
||||
def summary() -> dict:
|
||||
return db.stats()
|
||||
@@ -14,26 +19,3 @@ def summary() -> dict:
|
||||
|
||||
def heatmap() -> list[dict]:
|
||||
return db.heatmap()
|
||||
|
||||
|
||||
def get_statistical_recommendation(case_data: dict[str, Any]) -> dict[str, Any]:
|
||||
score = 0
|
||||
age = case_data.get('age') or case_data.get('age_years')
|
||||
elapsed = case_data.get('elapsed_hours')
|
||||
terrain = str(case_data.get('terrain') or case_data.get('terrain_primary') or '').lower()
|
||||
weather = str(case_data.get('weather') or case_data.get('precipitation') or '').lower()
|
||||
|
||||
if age is not None and age < 12:
|
||||
score += 20
|
||||
if elapsed is not None and elapsed >= 12:
|
||||
score += 20
|
||||
if any(token in terrain for token in ('лес', 'болото', 'вода')):
|
||||
score += 15
|
||||
if any(token in weather for token in ('дождь', 'туман', 'снег', 'ночь')):
|
||||
score += 15
|
||||
|
||||
return {
|
||||
'score': score,
|
||||
'priority': 'high' if score >= 40 else 'normal',
|
||||
'recommendation': 'Высокий приоритет на прочёс и дрон' if score >= 40 else 'Стандартный приоритет поиска',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user