from __future__ import annotations from dataclasses import asdict, dataclass from typing import Any from router.query_classifier import RouteDecisionFlags from router.store_sufficiency import StoreSufficiencyResult ALL_ROUTES = [ "live_mcp_drilldown", "store_canonical", "store_feature_risk", "hybrid_store_plus_live", "batch_refresh_then_store", ] @dataclass class RouteSelectionResult: chosen_route: str candidate_routes: list[str] rejected_routes: dict[str, str] def to_dict(self) -> dict[str, Any]: return asdict(self) def choose_route( flags: RouteDecisionFlags, suff: StoreSufficiencyResult, *, parsed_as_trend_or_risk: bool, ) -> RouteSelectionResult: rejected: dict[str, str] = {} if flags.needs_exact_object_trace: rejected["store_canonical"] = "exact_trace_requires_live" rejected["store_feature_risk"] = "exact_trace_requires_live" rejected["hybrid_store_plus_live"] = "exact_trace_prefers_direct_live" rejected["batch_refresh_then_store"] = "exact_trace_not_batch" return RouteSelectionResult( chosen_route="live_mcp_drilldown", candidate_routes=list(ALL_ROUTES), rejected_routes=rejected, ) heavy_shape = ( flags.needs_full_period_aggregation or flags.needs_ranking or (flags.needs_anomaly_summary and not parsed_as_trend_or_risk) ) if heavy_shape: aggregate_ok = ( flags.precomputed_aggregate_available and suff.freshness_ok and suff.aggregate_level_ok and (not flags.needs_ranking or suff.ranking_ready) ) if not aggregate_ok: rejected["store_feature_risk"] = "aggregate_not_sufficient" rejected["store_canonical"] = "wrong_query_shape" rejected["live_mcp_drilldown"] = "heavy_query_not_live" return RouteSelectionResult( chosen_route="batch_refresh_then_store", candidate_routes=list(ALL_ROUTES), rejected_routes=rejected, ) if flags.needs_cross_entity_join and flags.needs_causal_chain: if not suff.explanation_ready: rejected["store_canonical"] = "cross_entity_causal_needs_live_stitching" return RouteSelectionResult( chosen_route="hybrid_store_plus_live", candidate_routes=list(ALL_ROUTES), rejected_routes=rejected, ) if parsed_as_trend_or_risk: if suff.feature_sufficient and not flags.needs_runtime_truth: rejected["store_canonical"] = "trend_risk_query_prefers_feature" return RouteSelectionResult( chosen_route="store_feature_risk", candidate_routes=list(ALL_ROUTES), rejected_routes=rejected, ) rejected["store_feature_risk"] = "feature_or_freshness_not_sufficient" if ( suff.canonical_sufficient and not flags.needs_causal_chain and not flags.ambiguous_object_scope and not flags.needs_runtime_truth ): if flags.needs_cross_entity_join and not suff.explanation_ready: rejected["store_canonical"] = "cross_entity_needs_explanation_stitching" else: return RouteSelectionResult( chosen_route="store_canonical", candidate_routes=list(ALL_ROUTES), rejected_routes=rejected, ) if not suff.canonical_sufficient: rejected["store_canonical"] = "canonical_not_sufficient" if flags.ambiguous_object_scope: rejected["store_canonical"] = "ambiguous_scope_requires_hybrid_or_feature" return RouteSelectionResult( chosen_route="hybrid_store_plus_live", candidate_routes=list(ALL_ROUTES), rejected_routes=rejected, )