- Removed legacy agent profile settings from the .env.example and README, streamlining the configuration process. - Introduced a new local YAML configuration adapter for assistant settings, allowing for easier management of assistant profiles. - Updated backend integration documentation to clarify the behavior of assistant config sourcing based on backend URL settings. - Adjusted various service implementations to directly utilize API keys from the new configuration structure. - Enhanced test coverage for the new local YAML adapter and its integration with backend services.
662 lines
23 KiB
Python
662 lines
23 KiB
Python
"""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,
|
|
)
|