release: bump version to 0.51.0
This commit is contained in:
@@ -10,6 +10,8 @@ from typing import Any, Iterable
|
||||
from app.core.countries import COUNTRY_ENTRIES, normalize_country
|
||||
from app.schemas.ai import SituationalAnalysisRequest
|
||||
from app.services.ai_client import AIProviderClient
|
||||
from app.services.ai_tools.evidence_store import normalize_search_evidence
|
||||
from app.services.ai_tools.web_search import WebSearchClient, WebSearchError
|
||||
from app.services.location.models import LocationCandidate, LocationQuery
|
||||
from app.services.location.resolvers.nominatim import build_default_nominatim_geocoder
|
||||
from app.services.location.text import (
|
||||
@@ -69,6 +71,13 @@ class LocationLLMFallbackResult:
|
||||
failure_reason: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LocationSearchEvidenceResult:
|
||||
evidence: list[dict[str, Any]]
|
||||
attempted_queries: list[str]
|
||||
failure_reason: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LocationEvidenceScore:
|
||||
score: float
|
||||
@@ -751,6 +760,10 @@ def _candidate_from_payload(
|
||||
"name_location_hint": evidence_score.name_location_hint,
|
||||
},
|
||||
},
|
||||
raw_payload={
|
||||
"llm_payload": payload,
|
||||
"search_evidence": payload.get("search_evidence") or [],
|
||||
},
|
||||
), None
|
||||
|
||||
|
||||
@@ -802,6 +815,61 @@ def _observations(query: LocationQuery, attempted_queries: Iterable[str]) -> lis
|
||||
return observations
|
||||
|
||||
|
||||
def _location_search_query(query: LocationQuery, entity_type: str) -> str:
|
||||
extra = query.extra or {}
|
||||
parts = [
|
||||
coerce_str(query.name),
|
||||
coerce_str(extra.get("site")),
|
||||
coerce_str(extra.get("operator")),
|
||||
coerce_str(extra.get("organization")),
|
||||
coerce_str(query.city),
|
||||
coerce_str(query.country),
|
||||
"physical location",
|
||||
]
|
||||
if entity_type == "bgp_collector":
|
||||
parts.append("route collector city")
|
||||
elif entity_type == "compute_center":
|
||||
parts.append("datacenter supercomputer facility city")
|
||||
return " ".join(part for part in parts if part)
|
||||
|
||||
|
||||
async def collect_location_search_evidence(
|
||||
*,
|
||||
web_search_client: WebSearchClient,
|
||||
query: LocationQuery,
|
||||
entity_type: str,
|
||||
max_results: int = 5,
|
||||
) -> LocationSearchEvidenceResult:
|
||||
search_query = _location_search_query(query, entity_type)
|
||||
attempt = f"web_search:{entity_type}:{search_query}"
|
||||
try:
|
||||
evidence = await web_search_client.search(search_query, max_results=max_results)
|
||||
except WebSearchError as exc:
|
||||
return LocationSearchEvidenceResult(
|
||||
evidence=[],
|
||||
attempted_queries=[attempt],
|
||||
failure_reason=f"WebSearch location evidence failed: {exc}",
|
||||
)
|
||||
except Exception as exc:
|
||||
return LocationSearchEvidenceResult(
|
||||
evidence=[],
|
||||
attempted_queries=[attempt],
|
||||
failure_reason=f"WebSearch location evidence unavailable: {exc}",
|
||||
)
|
||||
normalized = normalize_search_evidence(evidence, limit=max_results)
|
||||
if not normalized:
|
||||
return LocationSearchEvidenceResult(
|
||||
evidence=[],
|
||||
attempted_queries=[attempt],
|
||||
failure_reason="WebSearch returned no usable location evidence.",
|
||||
)
|
||||
return LocationSearchEvidenceResult(
|
||||
evidence=normalized,
|
||||
attempted_queries=[attempt],
|
||||
failure_reason=None,
|
||||
)
|
||||
|
||||
|
||||
async def _repair_location_payload_from_text(
|
||||
*,
|
||||
provider_client: AIProviderClient,
|
||||
@@ -862,6 +930,7 @@ async def collect_llm_location_fallback_candidate(
|
||||
query: LocationQuery,
|
||||
entity_type: str,
|
||||
attempted_queries: Iterable[str] = (),
|
||||
search_evidence: list[dict[str, Any]] | None = None,
|
||||
min_confidence: float = DEFAULT_MIN_CONFIDENCE,
|
||||
) -> LocationLLMFallbackResult:
|
||||
"""Ask the configured LLM for one fact-checked location candidate.
|
||||
@@ -871,6 +940,12 @@ async def collect_llm_location_fallback_candidate(
|
||||
use this in user-triggered collection flows.
|
||||
"""
|
||||
attempt = f"llm_factcheck:{entity_type}:{coerce_str(query.name) or 'unknown'}"
|
||||
if search_evidence is not None and not search_evidence:
|
||||
return LocationLLMFallbackResult(
|
||||
candidates=[],
|
||||
attempted_queries=[attempt],
|
||||
failure_reason="LLM location factcheck skipped: no WebSearch evidence.",
|
||||
)
|
||||
request = SituationalAnalysisRequest(
|
||||
title=f"Location factcheck fallback for {entity_type}",
|
||||
objective=(
|
||||
@@ -881,6 +956,7 @@ async def collect_llm_location_fallback_candidate(
|
||||
context={
|
||||
"entity_type": entity_type,
|
||||
"location_query": _query_context(query),
|
||||
"search_evidence": search_evidence or [],
|
||||
"required_json_schema": {
|
||||
"latitude": "number",
|
||||
"longitude": "number",
|
||||
@@ -905,6 +981,7 @@ async def collect_llm_location_fallback_candidate(
|
||||
"Calibrate model confidence using this rubric: 0.85-1.0 for exact facility coordinates backed by an authoritative source; 0.70-0.84 for a confirmed facility/campus with strong public evidence; 0.55-0.69 for a confirmed city-level location backed by credible sources but without exact facility coordinates; 0.35-0.54 for weak or ambiguous city evidence; below 0.35 when the location is mostly a guess.",
|
||||
"Return evidence as objects when possible, including source, url, source_type, and entity_match.",
|
||||
"Include source names or URLs in evidence when known. The backend will recompute the final confidence from model confidence plus evidence quality.",
|
||||
"If search_evidence is provided, use only that evidence as factual support.",
|
||||
"Prefer the facility/site if known; otherwise use the best supported city.",
|
||||
],
|
||||
)
|
||||
@@ -939,6 +1016,23 @@ async def collect_llm_location_fallback_candidate(
|
||||
),
|
||||
)
|
||||
payload = _normalize_llm_payload(payload)
|
||||
if search_evidence:
|
||||
payload["search_evidence"] = search_evidence
|
||||
existing_evidence = _evidence_items(payload.get("evidence"))
|
||||
payload["evidence"] = [
|
||||
*existing_evidence,
|
||||
*[
|
||||
{
|
||||
"source": item.get("title") or item.get("url"),
|
||||
"url": item.get("url"),
|
||||
"text": item.get("snippet") or item.get("content"),
|
||||
"source_type": "web_search",
|
||||
"entity_match": True,
|
||||
}
|
||||
for item in search_evidence
|
||||
if isinstance(item, dict)
|
||||
],
|
||||
]
|
||||
latitude, longitude = _extract_llm_coordinates(payload)
|
||||
city_geocode_failure = None
|
||||
if latitude in (None, 0.0) or longitude in (None, 0.0):
|
||||
|
||||
@@ -51,6 +51,7 @@ class LocationCandidate:
|
||||
matched_location_name: str | None = None
|
||||
location_verified_at: str | None = None
|
||||
suggested_registry_entry: dict[str, Any] | None = None
|
||||
raw_payload: dict[str, Any] | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -70,6 +71,7 @@ class LocationCandidate:
|
||||
"matched_location_name": self.matched_location_name,
|
||||
"location_verified_at": self.location_verified_at,
|
||||
"suggested_registry_entry": self.suggested_registry_entry,
|
||||
"raw_payload": self.raw_payload,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user