hermes-hub/src/antigravity_provider/router/ui/model_catalog.py

106 lines
4.4 KiB
Python

"""Non-blocking UI adapter for the backend model-discovery cache.
The UI never discovers models synchronously and never invents model IDs. The
adapter deliberately tolerates the small naming differences used by backend
revisions so the presentation layer can be merged independently from A9.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable, Optional
@dataclass(frozen=True)
class CachedModels:
provider: str
models: tuple[str, ...] = ()
fetched_at: str = ""
is_stale: bool = True
unavailable_reason: str = "Список моделей ещё не получен"
def _service() -> Any:
try:
from antigravity_provider.router.model_discovery import ModelDiscoveryService
getter = getattr(ModelDiscoveryService, "get", None)
return getter() if callable(getter) else ModelDiscoveryService()
except (ImportError, AttributeError, TypeError):
pass
try:
from antigravity_provider.router.model_discovery_service import ModelDiscoveryService
getter = getattr(ModelDiscoveryService, "get", None)
return getter() if callable(getter) else ModelDiscoveryService()
except (ImportError, AttributeError, TypeError):
return None
def _normalise(provider: str, raw: Any) -> CachedModels:
if raw is None:
return CachedModels(provider=provider)
if isinstance(raw, dict):
models = raw.get("models") or raw.get("discovered_models") or []
fetched_at = raw.get("fetched_at") or raw.get("discovered_at") or raw.get("updated_at") or raw.get("last_refresh_at") or ""
stale = raw.get("is_stale", raw.get("stale", False))
reason = raw.get("unavailable_reason") or raw.get("error") or ""
else:
models = getattr(raw, "models", None) or getattr(raw, "discovered_models", None) or []
fetched_at = (
getattr(raw, "fetched_at", None)
or getattr(raw, "updated_at", None)
or getattr(raw, "last_refresh_at", None)
or ""
)
stale = getattr(raw, "is_stale", getattr(raw, "stale", False))
reason = getattr(raw, "unavailable_reason", None) or getattr(raw, "error", None) or ""
clean = tuple(dict.fromkeys(str(model).strip() for model in models if str(model).strip()))
return CachedModels(
provider=provider,
models=clean,
fetched_at=str(fetched_at),
is_stale=bool(stale),
unavailable_reason=str(reason or ("" if clean else "Список моделей ещё не получен")),
)
def get_cached_models(provider: str) -> CachedModels:
"""Return cached models immediately; never performs provider I/O."""
service = _service()
if service is None:
return CachedModels(provider=provider, unavailable_reason="Служба обнаружения моделей ещё не подключена")
for name in ("get_cached", "get_cached_models", "get_snapshot", "get_provider_models"):
method = getattr(service, name, None)
if not callable(method):
continue
try:
return _normalise(provider, method(provider))
except Exception as exc:
return CachedModels(provider=provider, unavailable_reason=f"Кэш моделей недоступен: {exc}")
return CachedModels(provider=provider, unavailable_reason="Служба не предоставляет чтение кэша моделей")
def refresh_models_async(provider: str, on_complete: Callable[[CachedModels], None]) -> bool:
"""Request a backend background refresh without blocking the Tk thread."""
service = _service()
if service is None:
on_complete(get_cached_models(provider))
return False
for name in ("refresh_models_async", "refresh_models", "refresh_provider_async", "refresh_async", "discover_async"):
method = getattr(service, name, None)
if not callable(method):
continue
try:
result: Optional[Any] = method(provider, on_complete=lambda *_args: on_complete(get_cached_models(provider)))
return result is not False
except TypeError:
try:
result = method(provider)
return result is not False
except Exception:
break
except Exception:
break
on_complete(get_cached_models(provider))
return False