102 lines
3.4 KiB
Python
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
|