Files

102 lines
3.4 KiB
Python

"""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