from __future__ import annotations from typing import Any from urllib.parse import urlparse from .registry import ExternalProviderRegistry from .schemas import ExternalAdapterResult, ExternalEvidenceItem, ExternalQueryExecution, ExternalQueryPlan class ExternalQueryGateway: def __init__( self, *, planner: Any, registry: ExternalProviderRegistry, ) -> None: self.planner = planner self.registry = registry async def execute( self, *, question: str, query_semantics: dict[str, Any] | None = None, ) -> ExternalQueryExecution: planned = await self.planner.build_plan( question=question, query_semantics=query_semantics, ) planner_meta: dict[str, Any] = {} if isinstance(planned, tuple) and len(planned) == 2: plan, planner_meta = planned else: plan = planned if not isinstance(plan, ExternalQueryPlan): plan = ExternalQueryPlan.model_validate(plan) adapter_attempts: list[dict[str, Any]] = [] selected = ExternalAdapterResult(adapter_name="none", status="empty", evidence=[], meta={}) selected_verifier = self._verify_evidence_bundle(plan=plan, evidence=selected.evidence) fallback_used = False for index, adapter in enumerate(self._rank_adapters(plan=plan)): adapter_name = str(getattr(adapter, "adapter_name", "") or f"adapter_{index}").strip() or f"adapter_{index}" result = await adapter.execute(plan) if not isinstance(result, ExternalAdapterResult): result = ExternalAdapterResult.model_validate(result) verifier = self._verify_evidence_bundle(plan=plan, evidence=result.evidence) adapter_attempts.append( { "adapter": adapter_name, "status": str(result.status or "").strip() or "empty", "evidence_count": len(result.evidence), "verifier_ok": bool(verifier.get("ok")), "verifier_reason": verifier.get("reason_code"), } ) if result.evidence and bool(verifier.get("ok")): selected = result selected_verifier = verifier fallback_used = index > 0 break if selected.adapter_name == "none": selected = result selected_verifier = verifier meta = dict(selected.meta or {}) meta["fallback_used"] = bool(fallback_used) meta["adapter_attempts"] = adapter_attempts return ExternalQueryExecution( plan=plan, adapter_name=selected.adapter_name, evidence=list(selected.evidence or []), verifier=selected_verifier, planner_meta=planner_meta, meta=meta, ) @staticmethod def _normalize_match_text(value: Any) -> str: raw = str(value or "").strip().lower() if not raw: return "" chars: list[str] = [] for char in raw: if char.isalnum(): chars.append(char) return "".join(chars) @classmethod def _normalize_anchor_term(cls, value: Any) -> str: return cls._normalize_match_text(value)[:48] @staticmethod def _common_prefix_size(left: str, right: str) -> int: limit = min(len(left), len(right)) idx = 0 while idx < limit and left[idx] == right[idx]: idx += 1 return idx @classmethod def _build_char_windows(cls, value: str, *, size: int = 2) -> set[str]: normalized = cls._normalize_match_text(value) if not normalized: return set() if len(normalized) <= size: return {normalized} return {normalized[idx : idx + size] for idx in range(len(normalized) - size + 1)} @classmethod def _tokenize_match_text(cls, value: Any) -> list[str]: raw = str(value or "").strip().lower() if not raw: return [] tokens: list[str] = [] current: list[str] = [] for char in raw: if char.isalnum(): current.append(char) continue if current: token = "".join(current) if token: tokens.append(token) current = [] if current: token = "".join(current) if token: tokens.append(token) normalized = cls._normalize_match_text(raw) if normalized and normalized not in tokens: tokens.append(normalized) return tokens @classmethod def _score_anchor_against_token(cls, anchor: str, token: str) -> float: if not anchor or not token: return 0.0 if anchor == token: return 1.0 if anchor in token: return max(0.82, min(0.98, len(anchor) / max(1, len(token)))) if token in anchor and len(token) >= 2: return max(0.58, len(token) / max(1, len(anchor))) prefix_size = cls._common_prefix_size(anchor, token) prefix_ratio = prefix_size / max(1, min(len(anchor), len(token))) anchor_windows = cls._build_char_windows(anchor) token_windows = cls._build_char_windows(token) window_overlap = ( len(anchor_windows & token_windows) / max(1, len(anchor_windows)) if anchor_windows and token_windows else 0.0 ) score = 0.0 if prefix_size >= 2: score = max(score, 0.42 + min(0.3, prefix_ratio * 0.4)) if window_overlap > 0: score = max(score, 0.35 + min(0.35, window_overlap * 0.5)) return round(min(score, 0.98), 4) @classmethod def _evidence_match_tokens(cls, item: ExternalEvidenceItem) -> list[str]: values = [ str(item.source_label or ""), str(item.title or ""), str(item.snippet or ""), str(item.target_url or ""), str(urlparse(str(item.target_url or "").strip()).path or ""), ] tokens: list[str] = [] seen: set[str] = set() for value in values: for token in cls._tokenize_match_text(value): if token and token not in seen: seen.add(token) tokens.append(token) return tokens @classmethod def _score_anchor_against_evidence(cls, anchor: str, evidence: list[ExternalEvidenceItem]) -> float: best = 0.0 for item in evidence: for token in cls._evidence_match_tokens(item): best = max(best, cls._score_anchor_against_token(anchor, token)) return round(best, 4) @classmethod def _adapter_support_level(cls, *, plan: ExternalQueryPlan, adapter: Any) -> int: supported = getattr(adapter, "supported_query_kinds", None) if not supported: return 1 query_kind = str(plan.query_kind or "").strip().lower() normalized = {str(item or "").strip().lower() for item in supported if str(item or "").strip()} return 0 if query_kind in normalized else 2 def _rank_adapters(self, *, plan: ExternalQueryPlan) -> list[Any]: ranked: list[tuple[int, int, Any]] = [] for index, adapter in enumerate(self._ensure_registry_items(plan=plan)): ranked.append((self._adapter_support_level(plan=plan, adapter=adapter), index, adapter)) ranked.sort(key=lambda item: (item[0], item[1])) return [item[2] for item in ranked] def _ensure_registry_items(self, *, plan: ExternalQueryPlan) -> list[Any]: _ = plan return list(self.registry.iter_adapters()) @classmethod def _build_plan_anchor_terms(cls, plan: ExternalQueryPlan) -> list[str]: terms: list[str] = [] for item in list(plan.subject_entities or []): normalized = cls._normalize_anchor_term(item) if len(normalized) >= 2 and normalized not in terms: terms.append(normalized) location_hint = cls._normalize_anchor_term(plan.location_hint) if len(location_hint) >= 2 and location_hint not in terms: terms.append(location_hint) return terms[:6] @classmethod def _verify_evidence_bundle(cls, *, plan: ExternalQueryPlan, evidence: list[ExternalEvidenceItem]) -> dict[str, Any]: if not evidence: return { "ok": False, "reason_code": "missing_external_grounding", "missing_fields": ["external_evidence"], } anchor_terms = cls._build_plan_anchor_terms(plan) anchor_hit = not anchor_terms for item in evidence: target_url = str(item.target_url or "").strip().lower() source_type = str(item.source_type or "").strip().lower() if source_type not in {"url", "mcp_search"}: return { "ok": False, "reason_code": "external_source_type_invalid", "missing_fields": ["external_source_type"], } if not target_url.startswith("http://") and not target_url.startswith("https://"): return { "ok": False, "reason_code": "external_target_url_missing", "missing_fields": ["target_url"], } anchor_scores = [cls._score_anchor_against_evidence(anchor, evidence) for anchor in anchor_terms] strong_hits = sum(1 for score in anchor_scores if score >= 0.8) medium_hits = sum(1 for score in anchor_scores if score >= 0.55) bundle_score = ( round(sum(anchor_scores) / max(1, len(anchor_scores)), 4) if anchor_scores else 1.0 ) if anchor_scores: anchor_hit = strong_hits >= 1 or medium_hits == len(anchor_scores) or bundle_score >= 0.6 if not anchor_hit: return { "ok": False, "reason_code": "external_evidence_off_topic", "missing_fields": ["plan_anchor_terms"], "bundle_score": bundle_score, "anchor_scores": anchor_scores, } return { "ok": True, "reason_code": None, "missing_fields": [], "bundle_score": bundle_score, "anchor_scores": anchor_scores, }