Refactor project structure and enhance backend integration
- Expanded package inclusion in `pyproject.toml` to support new modules. - Introduced new `adapters` and `protocol` packages for better organization. - Added backend adapter implementations for control plane integration. - Updated main application imports to reflect new package structure. - Removed deprecated core components and adjusted documentation accordingly. - Enhanced architecture documentation to clarify the new runtime and integration layers.
This commit is contained in:
1
engine/adapters/__init__.py
Normal file
1
engine/adapters/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Adapters package."""
|
||||
1
engine/adapters/control_plane/__init__.py
Normal file
1
engine/adapters/control_plane/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Control-plane adapters package."""
|
||||
661
engine/adapters/control_plane/backend.py
Normal file
661
engine/adapters/control_plane/backend.py
Normal file
@@ -0,0 +1,661 @@
|
||||
"""Backend adapter implementations for engine integration ports."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
from app.config import settings
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError: # pragma: no cover - validated when local YAML source is enabled
|
||||
yaml = None
|
||||
|
||||
|
||||
_ASSISTANT_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
|
||||
|
||||
|
||||
def _assistant_error(code: str, assistant_id: str) -> Dict[str, Any]:
|
||||
return {"__error_code": code, "assistantId": str(assistant_id or "")}
|
||||
|
||||
|
||||
class NullBackendAdapter:
|
||||
"""No-op adapter for engine-only runtime without backend dependencies."""
|
||||
|
||||
async def fetch_assistant_config(self, assistant_id: str) -> Optional[Dict[str, Any]]:
|
||||
_ = assistant_id
|
||||
return None
|
||||
|
||||
async def create_call_record(
|
||||
self,
|
||||
*,
|
||||
user_id: int,
|
||||
assistant_id: Optional[str],
|
||||
source: str = "debug",
|
||||
) -> Optional[str]:
|
||||
_ = (user_id, assistant_id, source)
|
||||
return None
|
||||
|
||||
async def add_transcript(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
turn_index: int,
|
||||
speaker: str,
|
||||
content: str,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
confidence: Optional[float] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
) -> bool:
|
||||
_ = (call_id, turn_index, speaker, content, start_ms, end_ms, confidence, duration_ms)
|
||||
return False
|
||||
|
||||
async def finalize_call_record(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
status: str,
|
||||
duration_seconds: int,
|
||||
) -> bool:
|
||||
_ = (call_id, status, duration_seconds)
|
||||
return False
|
||||
|
||||
async def search_knowledge_context(
|
||||
self,
|
||||
*,
|
||||
kb_id: str,
|
||||
query: str,
|
||||
n_results: int = 5,
|
||||
) -> List[Dict[str, Any]]:
|
||||
_ = (kb_id, query, n_results)
|
||||
return []
|
||||
|
||||
async def fetch_tool_resource(self, tool_id: str) -> Optional[Dict[str, Any]]:
|
||||
_ = tool_id
|
||||
return None
|
||||
|
||||
|
||||
class HistoryDisabledBackendAdapter:
|
||||
"""Adapter wrapper that disables history writes while keeping reads available."""
|
||||
|
||||
def __init__(self, delegate: HttpBackendAdapter | NullBackendAdapter):
|
||||
self._delegate = delegate
|
||||
|
||||
async def fetch_assistant_config(self, assistant_id: str) -> Optional[Dict[str, Any]]:
|
||||
return await self._delegate.fetch_assistant_config(assistant_id)
|
||||
|
||||
async def create_call_record(
|
||||
self,
|
||||
*,
|
||||
user_id: int,
|
||||
assistant_id: Optional[str],
|
||||
source: str = "debug",
|
||||
) -> Optional[str]:
|
||||
_ = (user_id, assistant_id, source)
|
||||
return None
|
||||
|
||||
async def add_transcript(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
turn_index: int,
|
||||
speaker: str,
|
||||
content: str,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
confidence: Optional[float] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
) -> bool:
|
||||
_ = (call_id, turn_index, speaker, content, start_ms, end_ms, confidence, duration_ms)
|
||||
return False
|
||||
|
||||
async def finalize_call_record(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
status: str,
|
||||
duration_seconds: int,
|
||||
) -> bool:
|
||||
_ = (call_id, status, duration_seconds)
|
||||
return False
|
||||
|
||||
async def search_knowledge_context(
|
||||
self,
|
||||
*,
|
||||
kb_id: str,
|
||||
query: str,
|
||||
n_results: int = 5,
|
||||
) -> List[Dict[str, Any]]:
|
||||
return await self._delegate.search_knowledge_context(
|
||||
kb_id=kb_id,
|
||||
query=query,
|
||||
n_results=n_results,
|
||||
)
|
||||
|
||||
async def fetch_tool_resource(self, tool_id: str) -> Optional[Dict[str, Any]]:
|
||||
return await self._delegate.fetch_tool_resource(tool_id)
|
||||
|
||||
|
||||
class LocalYamlAssistantConfigAdapter(NullBackendAdapter):
|
||||
"""Load assistant runtime config from local YAML files."""
|
||||
|
||||
def __init__(self, config_dir: str):
|
||||
self._config_dir = self._resolve_base_dir(config_dir)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_base_dir(config_dir: str) -> Path:
|
||||
raw = Path(str(config_dir or "").strip() or "engine/config/agents")
|
||||
if raw.is_absolute():
|
||||
return raw.resolve()
|
||||
|
||||
cwd_candidate = (Path.cwd() / raw).resolve()
|
||||
if cwd_candidate.exists():
|
||||
return cwd_candidate
|
||||
|
||||
engine_dir = Path(__file__).resolve().parent.parent
|
||||
engine_candidate = (engine_dir / raw).resolve()
|
||||
if engine_candidate.exists():
|
||||
return engine_candidate
|
||||
|
||||
parts = raw.parts
|
||||
if parts and parts[0] == "engine" and len(parts) > 1:
|
||||
trimmed_candidate = (engine_dir / Path(*parts[1:])).resolve()
|
||||
if trimmed_candidate.exists():
|
||||
return trimmed_candidate
|
||||
|
||||
return cwd_candidate
|
||||
|
||||
def _resolve_config_file(self, assistant_id: str) -> Optional[Path]:
|
||||
normalized = str(assistant_id or "").strip()
|
||||
if not _ASSISTANT_ID_PATTERN.match(normalized):
|
||||
return None
|
||||
|
||||
yaml_path = self._config_dir / f"{normalized}.yaml"
|
||||
yml_path = self._config_dir / f"{normalized}.yml"
|
||||
if yaml_path.exists():
|
||||
return yaml_path
|
||||
if yml_path.exists():
|
||||
return yml_path
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _as_str(value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
@classmethod
|
||||
def _translate_agent_schema(cls, assistant_id: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Translate legacy `agent:` YAML schema into runtime assistant metadata."""
|
||||
agent = payload.get("agent")
|
||||
if not isinstance(agent, dict):
|
||||
return None
|
||||
|
||||
runtime: Dict[str, Any] = {
|
||||
"assistantId": str(assistant_id),
|
||||
"services": {},
|
||||
}
|
||||
|
||||
llm = agent.get("llm")
|
||||
if isinstance(llm, dict):
|
||||
llm_runtime: Dict[str, Any] = {}
|
||||
if cls._as_str(llm.get("provider")):
|
||||
llm_runtime["provider"] = cls._as_str(llm.get("provider"))
|
||||
if cls._as_str(llm.get("model")):
|
||||
llm_runtime["model"] = cls._as_str(llm.get("model"))
|
||||
if cls._as_str(llm.get("api_key")):
|
||||
llm_runtime["apiKey"] = cls._as_str(llm.get("api_key"))
|
||||
if cls._as_str(llm.get("api_url")):
|
||||
llm_runtime["baseUrl"] = cls._as_str(llm.get("api_url"))
|
||||
if llm_runtime:
|
||||
runtime["services"]["llm"] = llm_runtime
|
||||
|
||||
tts = agent.get("tts")
|
||||
if isinstance(tts, dict):
|
||||
tts_runtime: Dict[str, Any] = {}
|
||||
if cls._as_str(tts.get("provider")):
|
||||
tts_runtime["provider"] = cls._as_str(tts.get("provider"))
|
||||
if cls._as_str(tts.get("model")):
|
||||
tts_runtime["model"] = cls._as_str(tts.get("model"))
|
||||
if cls._as_str(tts.get("api_key")):
|
||||
tts_runtime["apiKey"] = cls._as_str(tts.get("api_key"))
|
||||
if cls._as_str(tts.get("api_url")):
|
||||
tts_runtime["baseUrl"] = cls._as_str(tts.get("api_url"))
|
||||
if cls._as_str(tts.get("voice")):
|
||||
tts_runtime["voice"] = cls._as_str(tts.get("voice"))
|
||||
if tts.get("speed") is not None:
|
||||
tts_runtime["speed"] = tts.get("speed")
|
||||
dashscope_mode = cls._as_str(tts.get("dashscope_mode")) or cls._as_str(tts.get("mode"))
|
||||
if dashscope_mode:
|
||||
tts_runtime["mode"] = dashscope_mode
|
||||
if tts_runtime:
|
||||
runtime["services"]["tts"] = tts_runtime
|
||||
|
||||
asr = agent.get("asr")
|
||||
if isinstance(asr, dict):
|
||||
asr_runtime: Dict[str, Any] = {}
|
||||
if cls._as_str(asr.get("provider")):
|
||||
asr_runtime["provider"] = cls._as_str(asr.get("provider"))
|
||||
if cls._as_str(asr.get("model")):
|
||||
asr_runtime["model"] = cls._as_str(asr.get("model"))
|
||||
if cls._as_str(asr.get("api_key")):
|
||||
asr_runtime["apiKey"] = cls._as_str(asr.get("api_key"))
|
||||
if cls._as_str(asr.get("api_url")):
|
||||
asr_runtime["baseUrl"] = cls._as_str(asr.get("api_url"))
|
||||
if asr.get("interim_interval_ms") is not None:
|
||||
asr_runtime["interimIntervalMs"] = asr.get("interim_interval_ms")
|
||||
if asr.get("min_audio_ms") is not None:
|
||||
asr_runtime["minAudioMs"] = asr.get("min_audio_ms")
|
||||
if asr_runtime:
|
||||
runtime["services"]["asr"] = asr_runtime
|
||||
|
||||
duplex = agent.get("duplex")
|
||||
if isinstance(duplex, dict):
|
||||
if cls._as_str(duplex.get("system_prompt")):
|
||||
runtime["systemPrompt"] = cls._as_str(duplex.get("system_prompt"))
|
||||
if duplex.get("greeting") is not None:
|
||||
runtime["greeting"] = duplex.get("greeting")
|
||||
|
||||
barge_in = agent.get("barge_in")
|
||||
if isinstance(barge_in, dict):
|
||||
runtime["bargeIn"] = {}
|
||||
if barge_in.get("min_duration_ms") is not None:
|
||||
runtime["bargeIn"]["minDurationMs"] = barge_in.get("min_duration_ms")
|
||||
if barge_in.get("silence_tolerance_ms") is not None:
|
||||
runtime["bargeIn"]["silenceToleranceMs"] = barge_in.get("silence_tolerance_ms")
|
||||
if not runtime["bargeIn"]:
|
||||
runtime.pop("bargeIn", None)
|
||||
|
||||
if isinstance(agent.get("tools"), list):
|
||||
runtime["tools"] = agent.get("tools")
|
||||
|
||||
if not runtime.get("services"):
|
||||
runtime.pop("services", None)
|
||||
return runtime
|
||||
|
||||
async def fetch_assistant_config(self, assistant_id: str) -> Optional[Dict[str, Any]]:
|
||||
config_file = self._resolve_config_file(assistant_id)
|
||||
if config_file is None:
|
||||
return _assistant_error("assistant.not_found", assistant_id)
|
||||
|
||||
if yaml is None:
|
||||
logger.warning(
|
||||
"Local assistant config requested but PyYAML is unavailable (assistant_id={})",
|
||||
assistant_id,
|
||||
)
|
||||
return _assistant_error("assistant.config_unavailable", assistant_id)
|
||||
|
||||
try:
|
||||
with config_file.open("r", encoding="utf-8") as handle:
|
||||
payload = yaml.safe_load(handle) or {}
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to read local assistant config {} (assistant_id={}): {}",
|
||||
config_file,
|
||||
assistant_id,
|
||||
exc,
|
||||
)
|
||||
return _assistant_error("assistant.config_unavailable", assistant_id)
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
logger.warning(
|
||||
"Local assistant config is not an object (assistant_id={}, file={})",
|
||||
assistant_id,
|
||||
config_file,
|
||||
)
|
||||
return _assistant_error("assistant.config_unavailable", assistant_id)
|
||||
|
||||
translated = self._translate_agent_schema(assistant_id, payload)
|
||||
if translated is not None:
|
||||
payload = translated
|
||||
|
||||
# Accept either backend-like payload shape or a direct assistant metadata object.
|
||||
if isinstance(payload.get("assistant"), dict) or isinstance(payload.get("sessionStartMetadata"), dict):
|
||||
normalized_payload = dict(payload)
|
||||
else:
|
||||
normalized_payload = {"assistant": dict(payload)}
|
||||
|
||||
assistant_obj = normalized_payload.get("assistant")
|
||||
if isinstance(assistant_obj, dict):
|
||||
resolved_assistant_id = assistant_obj.get("assistantId") or assistant_obj.get("id") or assistant_id
|
||||
assistant_obj["assistantId"] = str(resolved_assistant_id)
|
||||
else:
|
||||
normalized_payload["assistant"] = {"assistantId": str(assistant_id)}
|
||||
|
||||
normalized_payload.setdefault("assistantId", str(assistant_id))
|
||||
normalized_payload.setdefault("configVersionId", f"local:{config_file.name}")
|
||||
return normalized_payload
|
||||
|
||||
|
||||
class AssistantConfigSourceAdapter:
|
||||
"""Route assistant config reads by backend availability without changing other APIs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
delegate: HttpBackendAdapter | NullBackendAdapter | HistoryDisabledBackendAdapter,
|
||||
local_delegate: LocalYamlAssistantConfigAdapter,
|
||||
use_backend_assistant_config: bool,
|
||||
):
|
||||
self._delegate = delegate
|
||||
self._local_delegate = local_delegate
|
||||
self._use_backend_assistant_config = bool(use_backend_assistant_config)
|
||||
|
||||
async def fetch_assistant_config(self, assistant_id: str) -> Optional[Dict[str, Any]]:
|
||||
if self._use_backend_assistant_config:
|
||||
return await self._delegate.fetch_assistant_config(assistant_id)
|
||||
return await self._local_delegate.fetch_assistant_config(assistant_id)
|
||||
|
||||
async def create_call_record(
|
||||
self,
|
||||
*,
|
||||
user_id: int,
|
||||
assistant_id: Optional[str],
|
||||
source: str = "debug",
|
||||
) -> Optional[str]:
|
||||
return await self._delegate.create_call_record(
|
||||
user_id=user_id,
|
||||
assistant_id=assistant_id,
|
||||
source=source,
|
||||
)
|
||||
|
||||
async def add_transcript(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
turn_index: int,
|
||||
speaker: str,
|
||||
content: str,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
confidence: Optional[float] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
) -> bool:
|
||||
return await self._delegate.add_transcript(
|
||||
call_id=call_id,
|
||||
turn_index=turn_index,
|
||||
speaker=speaker,
|
||||
content=content,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
confidence=confidence,
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
|
||||
async def finalize_call_record(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
status: str,
|
||||
duration_seconds: int,
|
||||
) -> bool:
|
||||
return await self._delegate.finalize_call_record(
|
||||
call_id=call_id,
|
||||
status=status,
|
||||
duration_seconds=duration_seconds,
|
||||
)
|
||||
|
||||
async def search_knowledge_context(
|
||||
self,
|
||||
*,
|
||||
kb_id: str,
|
||||
query: str,
|
||||
n_results: int = 5,
|
||||
) -> List[Dict[str, Any]]:
|
||||
return await self._delegate.search_knowledge_context(
|
||||
kb_id=kb_id,
|
||||
query=query,
|
||||
n_results=n_results,
|
||||
)
|
||||
|
||||
async def fetch_tool_resource(self, tool_id: str) -> Optional[Dict[str, Any]]:
|
||||
return await self._delegate.fetch_tool_resource(tool_id)
|
||||
|
||||
|
||||
class HttpBackendAdapter:
|
||||
"""HTTP implementation of backend integration ports."""
|
||||
|
||||
def __init__(self, backend_url: str, timeout_sec: int = 10):
|
||||
base_url = str(backend_url or "").strip().rstrip("/")
|
||||
if not base_url:
|
||||
raise ValueError("backend_url is required for HttpBackendAdapter")
|
||||
self._base_url = base_url
|
||||
self._timeout_sec = timeout_sec
|
||||
|
||||
def _timeout(self) -> aiohttp.ClientTimeout:
|
||||
return aiohttp.ClientTimeout(total=self._timeout_sec)
|
||||
|
||||
async def fetch_assistant_config(self, assistant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch assistant config payload from backend API.
|
||||
|
||||
Expected response shape:
|
||||
{
|
||||
"assistant": {...},
|
||||
"voice": {...} | null
|
||||
}
|
||||
"""
|
||||
url = f"{self._base_url}/api/assistants/{assistant_id}/config"
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=self._timeout()) as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 404:
|
||||
logger.warning(f"Assistant config not found: {assistant_id}")
|
||||
return {"__error_code": "assistant.not_found", "assistantId": assistant_id}
|
||||
resp.raise_for_status()
|
||||
payload = await resp.json()
|
||||
if not isinstance(payload, dict):
|
||||
logger.warning("Assistant config payload is not a dict; ignoring")
|
||||
return {"__error_code": "assistant.config_unavailable", "assistantId": assistant_id}
|
||||
return payload
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to fetch assistant config ({assistant_id}): {exc}")
|
||||
return {"__error_code": "assistant.config_unavailable", "assistantId": assistant_id}
|
||||
|
||||
async def create_call_record(
|
||||
self,
|
||||
*,
|
||||
user_id: int,
|
||||
assistant_id: Optional[str],
|
||||
source: str = "debug",
|
||||
) -> Optional[str]:
|
||||
"""Create a call record via backend history API and return call_id."""
|
||||
url = f"{self._base_url}/api/history"
|
||||
payload: Dict[str, Any] = {
|
||||
"user_id": user_id,
|
||||
"assistant_id": assistant_id,
|
||||
"source": source,
|
||||
"status": "connected",
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=self._timeout()) as session:
|
||||
async with session.post(url, json=payload) as resp:
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
call_id = str((data or {}).get("id") or "")
|
||||
return call_id or None
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to create history call record: {exc}")
|
||||
return None
|
||||
|
||||
async def add_transcript(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
turn_index: int,
|
||||
speaker: str,
|
||||
content: str,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
confidence: Optional[float] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""Append a transcript segment to backend history."""
|
||||
if not call_id:
|
||||
return False
|
||||
|
||||
url = f"{self._base_url}/api/history/{call_id}/transcripts"
|
||||
payload: Dict[str, Any] = {
|
||||
"turn_index": turn_index,
|
||||
"speaker": speaker,
|
||||
"content": content,
|
||||
"confidence": confidence,
|
||||
"start_ms": start_ms,
|
||||
"end_ms": end_ms,
|
||||
"duration_ms": duration_ms,
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=self._timeout()) as session:
|
||||
async with session.post(url, json=payload) as resp:
|
||||
resp.raise_for_status()
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to append history transcript (call_id={call_id}, turn={turn_index}): {exc}")
|
||||
return False
|
||||
|
||||
async def finalize_call_record(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
status: str,
|
||||
duration_seconds: int,
|
||||
) -> bool:
|
||||
"""Finalize a call record with status and duration."""
|
||||
if not call_id:
|
||||
return False
|
||||
|
||||
url = f"{self._base_url}/api/history/{call_id}"
|
||||
payload: Dict[str, Any] = {
|
||||
"status": status,
|
||||
"duration_seconds": duration_seconds,
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=self._timeout()) as session:
|
||||
async with session.put(url, json=payload) as resp:
|
||||
resp.raise_for_status()
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to finalize history call record ({call_id}): {exc}")
|
||||
return False
|
||||
|
||||
async def search_knowledge_context(
|
||||
self,
|
||||
*,
|
||||
kb_id: str,
|
||||
query: str,
|
||||
n_results: int = 5,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Search backend knowledge base and return retrieval results."""
|
||||
if not kb_id or not query.strip():
|
||||
return []
|
||||
try:
|
||||
safe_n_results = max(1, int(n_results))
|
||||
except (TypeError, ValueError):
|
||||
safe_n_results = 5
|
||||
|
||||
url = f"{self._base_url}/api/knowledge/search"
|
||||
payload: Dict[str, Any] = {
|
||||
"kb_id": kb_id,
|
||||
"query": query,
|
||||
"nResults": safe_n_results,
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=self._timeout()) as session:
|
||||
async with session.post(url, json=payload) as resp:
|
||||
if resp.status == 404:
|
||||
logger.warning(f"Knowledge base not found for retrieval: {kb_id}")
|
||||
return []
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
if not isinstance(data, dict):
|
||||
return []
|
||||
results = data.get("results", [])
|
||||
if not isinstance(results, list):
|
||||
return []
|
||||
return [r for r in results if isinstance(r, dict)]
|
||||
except Exception as exc:
|
||||
logger.warning(f"Knowledge search failed (kb_id={kb_id}): {exc}")
|
||||
return []
|
||||
|
||||
async def fetch_tool_resource(self, tool_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch tool resource configuration from backend API."""
|
||||
if not tool_id:
|
||||
return None
|
||||
|
||||
url = f"{self._base_url}/api/tools/resources/{tool_id}"
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=self._timeout()) as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 404:
|
||||
return None
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
return data if isinstance(data, dict) else None
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to fetch tool resource ({tool_id}): {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def build_backend_adapter(
|
||||
*,
|
||||
backend_url: Optional[str],
|
||||
backend_mode: str = "auto",
|
||||
history_enabled: bool = True,
|
||||
timeout_sec: int = 10,
|
||||
assistant_local_config_dir: str = "engine/config/agents",
|
||||
) -> AssistantConfigSourceAdapter:
|
||||
"""Create backend adapter implementation based on runtime settings."""
|
||||
mode = str(backend_mode or "auto").strip().lower()
|
||||
has_url = bool(str(backend_url or "").strip())
|
||||
|
||||
base_adapter: HttpBackendAdapter | NullBackendAdapter
|
||||
using_http_backend = False
|
||||
if mode in {"disabled", "off", "none", "null", "engine_only", "engine-only"}:
|
||||
base_adapter = NullBackendAdapter()
|
||||
elif mode == "http":
|
||||
if has_url:
|
||||
base_adapter = HttpBackendAdapter(backend_url=str(backend_url), timeout_sec=timeout_sec)
|
||||
using_http_backend = True
|
||||
else:
|
||||
logger.warning("BACKEND_MODE=http but BACKEND_URL is empty; falling back to NullBackendAdapter")
|
||||
base_adapter = NullBackendAdapter()
|
||||
else:
|
||||
if has_url:
|
||||
base_adapter = HttpBackendAdapter(backend_url=str(backend_url), timeout_sec=timeout_sec)
|
||||
using_http_backend = True
|
||||
else:
|
||||
base_adapter = NullBackendAdapter()
|
||||
|
||||
runtime_adapter: HttpBackendAdapter | NullBackendAdapter | HistoryDisabledBackendAdapter
|
||||
if not history_enabled:
|
||||
runtime_adapter = HistoryDisabledBackendAdapter(base_adapter)
|
||||
else:
|
||||
runtime_adapter = base_adapter
|
||||
|
||||
return AssistantConfigSourceAdapter(
|
||||
delegate=runtime_adapter,
|
||||
local_delegate=LocalYamlAssistantConfigAdapter(assistant_local_config_dir),
|
||||
use_backend_assistant_config=using_http_backend,
|
||||
)
|
||||
|
||||
|
||||
def build_backend_adapter_from_settings() -> AssistantConfigSourceAdapter:
|
||||
"""Create backend adapter using current app settings."""
|
||||
return build_backend_adapter(
|
||||
backend_url=settings.backend_url,
|
||||
backend_mode=settings.backend_mode,
|
||||
history_enabled=settings.history_enabled,
|
||||
timeout_sec=settings.backend_timeout_sec,
|
||||
assistant_local_config_dir=settings.assistant_local_config_dir,
|
||||
)
|
||||
Reference in New Issue
Block a user