release: bump version to 0.51.0

This commit is contained in:
rayd1o
2026-05-11 09:49:08 +08:00
parent 455b8360d0
commit 1cb51b1172
52 changed files with 4440 additions and 279 deletions

View File

@@ -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):

View File

@@ -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,
}