"""Runtime composition shared by Gateway routes and the read-only agent tool.""" from __future__ import annotations import asyncio import httpx from deerflow.config import get_app_config from deerflow.config.app_config import AppConfig from deerflow.config.llmwiki_config import ResolvedLlmWikiRuntime, resolve_llmwiki_runtime from deerflow.config.system_settings import load_system_settings from deerflow.integrations.weknora.client import WeKnoraClient from deerflow.persistence.llmwiki import LlmWikiStore _default_store: LlmWikiStore | None = None def register_llmwiki_store(store: LlmWikiStore | None) -> None: global _default_store _default_store = store def get_llmwiki_store() -> LlmWikiStore: if _default_store is None: raise RuntimeError("LLMWiki store is not initialized") return _default_store def get_resolved_llmwiki_runtime(config: AppConfig | None = None) -> ResolvedLlmWikiRuntime: current = config or get_app_config() return resolve_llmwiki_runtime(current.llmwiki, load_system_settings().llmwiki) def build_weknora_client( runtime: ResolvedLlmWikiRuntime, *, http_client: httpx.AsyncClient | None = None, ) -> WeKnoraClient: if not runtime.weknora_enabled: raise RuntimeError("WeKnora mode is not enabled") if http_client is None: return _shared_weknora_client( runtime.api_base_url, runtime.admin_email, runtime.admin_password, ) return WeKnoraClient( base_url=runtime.api_base_url, http_client=http_client, admin_email=runtime.admin_email, admin_password=runtime.admin_password, ) def _shared_weknora_client( base_url: str, admin_email: str, admin_password: str, ) -> WeKnoraClient: """Reuse auth state only inside the current event loop. ``WeKnoraClient`` owns asyncio primitives (an authentication lock and a cached directory task). A process-wide ``lru_cache`` therefore lets a client first used by the Gateway loop escape into a LangGraph/tool loop. Once either primitive has been awaited, reusing it from the other loop can fail with ``... is bound to a different event loop``. Smart retrieval used to normalize that internal exception to ``PROVIDER_UNAVAILABLE``, so an otherwise healthy selected knowledge base appeared empty. Store the small configuration cache on the loop itself. The loop owns the clients for exactly its lifetime, equivalent configurations still share their bearer token and knowledge-base directory within that loop, and no async primitive crosses loop boundaries. Synchronous callers have no safe loop ownership yet, so they receive an uncached client. """ try: loop = asyncio.get_running_loop() except RuntimeError: return WeKnoraClient( base_url=base_url, admin_email=admin_email, admin_password=admin_password, ) cache_name = "_deerflow_weknora_clients" cache = getattr(loop, cache_name, None) if not isinstance(cache, dict): cache = {} setattr(loop, cache_name, cache) key = (base_url, admin_email, admin_password) client = cache.get(key) if client is None: client = WeKnoraClient( base_url=base_url, admin_email=admin_email, admin_password=admin_password, ) cache[key] = client return client