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

@@ -137,6 +137,20 @@ async def test_collect_bgp_collector_location_uses_llm_when_candidates_empty(mon
lambda **_kwargs: ([], ["Lyon, France"]),
)
from app.services.location.llm_fallback import LocationSearchEvidenceResult
async def _search_evidence(**_kwargs):
return LocationSearchEvidenceResult(
evidence=[
{
"title": "RRC source",
"url": "https://example.test/rrc",
"snippet": "rrc-mystery is in Lyon.",
}
],
attempted_queries=["web_search:bgp_collector:rrc-mystery Lyon France physical location route collector city"],
)
async def _fallback(**_kwargs):
return LocationLLMFallbackResult(
candidates=[llm_candidate],
@@ -144,6 +158,8 @@ async def test_collect_bgp_collector_location_uses_llm_when_candidates_empty(mon
)
monkeypatch.setattr(bgp_api, "get_ai_provider_client", AsyncMock(return_value=object()))
monkeypatch.setattr(bgp_api, "get_web_search_client", AsyncMock(return_value=object()))
monkeypatch.setattr(bgp_api, "collect_location_search_evidence", _search_evidence)
monkeypatch.setattr(bgp_api, "collect_llm_location_fallback_candidate", _fallback)
response = await bgp_api.collect_bgp_collector_location(
@@ -158,6 +174,7 @@ async def test_collect_bgp_collector_location_uses_llm_when_candidates_empty(mon
assert response["best_candidate"]["needs_confirmation"] is True
assert response["attempted_queries"] == [
"Lyon, France",
"web_search:bgp_collector:rrc-mystery Lyon France physical location route collector city",
"llm_factcheck:bgp_collector:rrc-mystery",
]

View File

@@ -569,7 +569,19 @@ async def test_collect_compute_center_location_uses_llm_when_candidates_empty(mo
lambda **_kwargs: ([], ["Mystery Cluster, France"]),
)
from app.services.location.llm_fallback import LocationLLMFallbackResult
from app.services.location.llm_fallback import LocationLLMFallbackResult, LocationSearchEvidenceResult
async def _search_evidence(**_kwargs):
return LocationSearchEvidenceResult(
evidence=[
{
"title": "Mystery Cluster source",
"url": "https://example.test/mystery",
"snippet": "Mystery Cluster is in Lyon.",
}
],
attempted_queries=["web_search:compute_center:Mystery Cluster France physical location"],
)
async def _fallback(**_kwargs):
return LocationLLMFallbackResult(
@@ -578,6 +590,8 @@ async def test_collect_compute_center_location_uses_llm_when_candidates_empty(mo
)
monkeypatch.setattr(visualization_api, "get_ai_provider_client", AsyncMock(return_value=object()))
monkeypatch.setattr(visualization_api, "get_web_search_client", AsyncMock(return_value=object()))
monkeypatch.setattr(visualization_api, "collect_location_search_evidence", _search_evidence)
monkeypatch.setattr(visualization_api, "collect_llm_location_fallback_candidate", _fallback)
response = await visualization_api.collect_compute_center_location(
@@ -595,6 +609,7 @@ async def test_collect_compute_center_location_uses_llm_when_candidates_empty(mo
assert response["best_candidate"]["needs_confirmation"] is True
assert response["attempted_queries"] == [
"Mystery Cluster, France",
"web_search:compute_center:Mystery Cluster France physical location",
"llm_factcheck:compute_center:Mystery Cluster",
]

View File

@@ -0,0 +1,261 @@
from types import SimpleNamespace
import pytest
from app.api.v1 import settings as settings_api
from app.api.v1.settings import (
WebSearchIntegrationUpdate,
_build_web_search_payload,
_mask_secret,
_normalize_web_search_payload,
_resolve_web_search_api_key,
)
from app.services.ai_tools.schemas import WebSearchConfig, WebSearchProviderConfig
from app.services.ai_tools.web_search import WebSearchClient
from app.services.credential_guides import generate_credential_guide
from app.services.location.llm_fallback import (
collect_llm_location_fallback_candidate,
collect_location_search_evidence,
)
from app.services.location.models import LocationQuery
@pytest.fixture(autouse=True)
def isolated_web_search_env_files(monkeypatch, tmp_path):
env_file = tmp_path / ".env"
monkeypatch.setattr(settings_api, "WEB_SEARCH_ENV_FILES", (env_file,))
return env_file
def test_normalize_web_search_payload_adds_default_provider():
payload = _normalize_web_search_payload({})
assert payload["default_provider"] == "tavily"
assert payload["providers"]["tavily"]["base_url"] == "https://api.tavily.com"
def test_web_search_key_prefers_provider_env(isolated_web_search_env_files):
isolated_web_search_env_files.write_text(
"TAVILY_API_KEY=tavily-env-key\nWEB_SEARCH_API_KEY=generic-search-key\n",
encoding="utf-8",
)
value, source = _resolve_web_search_api_key("tavily", {"api_key": ""})
assert value == "tavily-env-key"
assert source == "env_file"
def test_build_web_search_payload_keeps_saved_key_when_preview_submitted():
current = {
"web_search": {
"default_provider": "tavily",
"providers": {
"tavily": {
"provider": "tavily",
"api_key": "tvly-old-secret",
},
},
}
}
update = WebSearchIntegrationUpdate(
enabled=True,
provider="tavily",
base_url="https://api.tavily.com",
api_key=_mask_secret("tvly-old-secret")["preview"],
)
payload = _build_web_search_payload(current, update)
assert payload["enabled"] is True
assert payload["providers"]["tavily"]["api_key"] == "tvly-old-secret"
@pytest.mark.asyncio
async def test_tavily_adapter_normalizes_results(monkeypatch):
config = WebSearchConfig(
enabled=True,
default_provider="tavily",
provider="tavily",
providers={
"tavily": WebSearchProviderConfig(
provider="tavily",
base_url="https://api.tavily.com",
api_key="key",
)
},
)
client = WebSearchClient(config)
async def fake_request_json(*args, **kwargs):
return {
"query": "Alem.Cloud",
"results": [
{
"title": "Alem.Cloud official",
"url": "https://example.test/alem",
"content": "Alem.Cloud is in Astana.",
"score": 0.9,
}
],
}
monkeypatch.setattr(client, "_request_json", fake_request_json)
results = await client.search("Alem.Cloud")
assert results[0].source_provider == "tavily"
assert results[0].url == "https://example.test/alem"
assert "Astana" in results[0].snippet
@pytest.mark.asyncio
async def test_searxng_adapter_allows_empty_api_key(monkeypatch):
config = WebSearchConfig(
enabled=True,
default_provider="searxng",
provider="searxng",
providers={
"searxng": WebSearchProviderConfig(
provider="searxng",
base_url="http://localhost:8080",
api_key="",
)
},
)
client = WebSearchClient(config)
async def fake_request_json(*args, **kwargs):
return {
"results": [
{
"title": "TAIPEI-1",
"url": "https://example.test/taipei",
"content": "TAIPEI-1 is in Taipei.",
"score": 2,
"engine": "duckduckgo",
}
],
}
monkeypatch.setattr(client, "_request_json", fake_request_json)
results = await client.search("TAIPEI-1")
assert results[0].source_provider == "searxng"
assert results[0].metadata["engine"] == "duckduckgo"
@pytest.mark.asyncio
async def test_location_search_evidence_returns_failure_on_empty_results(monkeypatch):
class EmptySearchClient:
async def search(self, *args, **kwargs):
return []
result = await collect_location_search_evidence(
web_search_client=EmptySearchClient(),
query=LocationQuery(name="TAIPEI-1", country="Taiwan"),
entity_type="compute_center",
)
assert result.evidence == []
assert "no usable" in result.failure_reason
@pytest.mark.asyncio
async def test_llm_location_fallback_skips_when_search_evidence_empty():
class ExplodingAIClient:
async def analyze(self, *_args, **_kwargs):
raise AssertionError("LLM should not be called without evidence")
result = await collect_llm_location_fallback_candidate(
provider_client=ExplodingAIClient(),
query=LocationQuery(name="TAIPEI-1", country="Taiwan"),
entity_type="compute_center",
search_evidence=[],
)
assert result.candidates == []
assert "no WebSearch evidence" in result.failure_reason
@pytest.mark.asyncio
async def test_credential_guide_keeps_default_without_search_evidence():
class EmptySearchClient:
async def search(self, *args, **kwargs):
return []
class ExplodingAIClient:
async def analyze(self, *_args, **_kwargs):
raise AssertionError("AI should not be called without search evidence")
async def fake_get_store(_db):
return None, {}
import app.services.credential_guides as credential_guides
original = credential_guides._get_guide_store
credential_guides._get_guide_store = fake_get_store
try:
guide = await generate_credential_guide(
object(),
"barentswatch",
ExplodingAIClient(),
EmptySearchClient(),
)
finally:
credential_guides._get_guide_store = original
assert guide["source"] == "default"
assert guide["verification_status"] == "unverified_no_search_evidence"
@pytest.mark.asyncio
async def test_credential_guide_uses_search_evidence(monkeypatch):
class SearchClient:
async def search(self, *args, **kwargs):
from app.services.ai_tools.schemas import SearchEvidence
return [
SearchEvidence(
title="Official docs",
url="https://docs.example.test",
snippet="Create an AIS client.",
source_provider="tavily",
)
]
class AIClient:
async def analyze(self, payload):
assert payload.context["search_evidence"]
return SimpleNamespace(content="## Generated\n\nSources included.")
saved = {}
async def fake_get_store(_db):
return None, saved
async def fake_save(db, provider, title, markdown, **metadata):
return {
"provider": provider,
"title": title,
"markdown": markdown,
"source": "ai",
**metadata,
}
import app.services.credential_guides as credential_guides
monkeypatch.setattr(credential_guides, "_get_guide_store", fake_get_store)
monkeypatch.setattr(credential_guides, "save_credential_guide", fake_save)
guide = await generate_credential_guide(
object(),
"barentswatch",
AIClient(),
SearchClient(),
)
assert guide["source"] == "ai"
assert guide["verification_status"] == "verified_with_search_evidence"
assert guide["sources"][0]["url"] == "https://docs.example.test"