""" Shared embedding provider factory. Resolves the embedding provider purely from config.json, so every caller (agent initialization, knowledge base sync, index rebuild, ...) selects the same provider instead of silently degrading to keyword-only search. Two paths: A. Default (no `embedding_provider` in config.json): Auto-init OpenAI -> LinkAI fallback. B. Explicit (`embedding_provider` is set): Initialize the requested vendor with unified dim (default per vendor). """ import os from typing import Optional from common.log import logger # Track whether the embedding model log has been printed in this process, # so we avoid spamming it once per session/caller. _embedding_logged: bool = False def create_default_embedding_provider(): """Build the embedding provider from config, or None for keyword-only mode.""" from config import conf explicit_provider = (conf().get("embedding_provider") or "").strip().lower() if not explicit_provider: return _init_legacy_provider() return _init_explicit_provider(explicit_provider) def _init_legacy_provider(): """Legacy auto-init path: OpenAI -> LinkAI.""" from agent.memory.embedding.provider import create_embedding_provider from config import conf embedding_provider = None embedding_model = None openai_api_key = conf().get("open_ai_api_key", "") openai_api_base = conf().get("open_ai_api_base", "") if openai_api_key and openai_api_key not in ["", "YOUR API KEY", "YOUR_API_KEY"]: try: model = "text-embedding-3-small" embedding_provider = create_embedding_provider( provider="openai", model=model, api_key=openai_api_key, api_base=openai_api_base or "https://api.openai.com/v1", ) embedding_model = f"openai/{model}" except Exception as e: logger.warning(f"[EmbeddingFactory] OpenAI embedding failed: {e}") if embedding_provider is None: linkai_api_key = conf().get("linkai_api_key", "") or os.environ.get("LINKAI_API_KEY", "") linkai_api_base = conf().get("linkai_api_base", "https://api.link-ai.tech") if linkai_api_key and linkai_api_key not in ["", "YOUR API KEY", "YOUR_API_KEY"]: try: model = "text-embedding-3-small" embedding_provider = create_embedding_provider( provider="linkai", model=model, api_key=linkai_api_key, api_base=f"{linkai_api_base}/v1", ) embedding_model = f"linkai/{model}" except Exception as e: logger.warning(f"[EmbeddingFactory] LinkAI embedding failed: {e}") if embedding_provider is not None and embedding_model: _log_provider_once(f"{embedding_model} (dim={embedding_provider.dimensions})") return embedding_provider def _init_explicit_provider(provider_key: str): """Explicit-provider path: build the configured vendor.""" from agent.memory.embedding.provider import EMBEDDING_VENDORS, create_embedding_provider from config import conf # Custom providers ("custom:") resolve credentials from custom_providers. resolved_provider_key = provider_key if provider_key.startswith("custom:"): resolved_provider_key = "custom" meta = EMBEDDING_VENDORS.get(resolved_provider_key) if meta is None: logger.error( f"[EmbeddingFactory] Unknown embedding_provider '{provider_key}'. " f"Supported: {sorted(EMBEDDING_VENDORS.keys())}. " f"Memory will run in keyword-only mode." ) return None api_key = _resolve_api_key(provider_key) api_base = _resolve_api_base(provider_key, meta["default_base_url"]) if not api_key: logger.error( f"[EmbeddingFactory] embedding_provider='{provider_key}' is set but its " f"API key is missing. Memory will run in keyword-only mode." ) return None model = (conf().get("embedding_model") or "").strip() # Custom providers without a model fall back to the provider's default. if not model and resolved_provider_key == "custom": from models.custom_provider import parse_custom_bot_type, get_custom_providers, _find_provider_by_id _, custom_id = parse_custom_bot_type(provider_key) if custom_id: entry = _find_provider_by_id(get_custom_providers(), custom_id) if entry and entry.get("model"): model = entry["model"] if not model and resolved_provider_key != "custom": model = meta["default_model"] try: cfg_dim = int(conf().get("embedding_dimensions") or 0) except (TypeError, ValueError): cfg_dim = 0 dim = cfg_dim if cfg_dim > 0 else meta["default_dimensions"] try: provider = create_embedding_provider( provider=resolved_provider_key, model=model, api_key=api_key, api_base=api_base, dimensions=dim, ) except Exception as e: logger.error( f"[EmbeddingFactory] Failed to init embedding provider " f"'{provider_key}/{model}': {e}" ) return None _log_provider_once(f"{provider_key}/{model} (dim={provider.dimensions})") return provider def _resolve_api_key(provider_key: str) -> str: """Pick the API key for an explicit embedding provider from config.""" from config import conf if provider_key.startswith("custom:"): from models.custom_provider import parse_custom_bot_type, get_custom_providers, _find_provider_by_id _, custom_id = parse_custom_bot_type(provider_key) if custom_id: entry = _find_provider_by_id(get_custom_providers(), custom_id) if entry: return entry.get("api_key", "") return "" key_map = { "openai": "open_ai_api_key", "linkai": "linkai_api_key", "dashscope": "dashscope_api_key", "doubao": "ark_api_key", "zhipu": "zhipu_ai_api_key", } field = key_map.get(provider_key) if not field: return "" value = conf().get(field, "") or "" if value in ["", "YOUR API KEY", "YOUR_API_KEY"]: return "" return value def _resolve_api_base(provider_key: str, default_base: str) -> str: """Pick the API base for an explicit embedding provider from config.""" from config import conf if provider_key.startswith("custom:"): from models.custom_provider import parse_custom_bot_type, get_custom_providers, _find_provider_by_id _, custom_id = parse_custom_bot_type(provider_key) if custom_id: entry = _find_provider_by_id(get_custom_providers(), custom_id) if entry and entry.get("api_base"): return entry["api_base"] return default_base base_map = { "openai": "open_ai_api_base", "linkai": "linkai_api_base", "doubao": "ark_base_url", "zhipu": "zhipu_ai_api_base", } field = base_map.get(provider_key) if not field: return default_base value = (conf().get(field) or "").strip() if not value: return default_base if provider_key == "linkai" and not value.rstrip("/").endswith("/v1"): return f"{value.rstrip('/')}/v1" return value def _log_provider_once(detail: str): global _embedding_logged if not _embedding_logged: logger.info(f"[EmbeddingFactory] Embedding model in use: {detail}") _embedding_logged = True