from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from typing import Optional try: from dotenv import load_dotenv except Exception: # pragma: no cover load_dotenv = None @dataclass(slots=True) class CoreAgentConfig: model: str api_key: Optional[str] base_url: Optional[str] timeout: float = 120.0 temperature: float = 0.2 max_iterations: int = 12 user_name: str = "User" agent_name: str = "MeetingAgent" def load_core_agent_env(env_dir: str | Path | None = None) -> Optional[Path]: candidates = [] if env_dir: candidates.append(Path(env_dir) / ".env") candidates.append(Path.cwd() / ".env") for path in candidates: if not path.exists(): continue if load_dotenv is not None: load_dotenv(path, override=False) else: _load_env_without_dependency(path) return path return None def apply_compat_env_aliases() -> None: if not os.getenv("OPENAI_API_KEY") and os.getenv("API_KEY"): os.environ["OPENAI_API_KEY"] = os.environ["API_KEY"] if not os.getenv("OPENAI_BASE_URL") and os.getenv("BASE_URL"): os.environ["OPENAI_BASE_URL"] = os.environ["BASE_URL"] if not os.getenv("CORE_AGENT_MODEL") and os.getenv("MODEL_NAME"): os.environ["CORE_AGENT_MODEL"] = os.environ["MODEL_NAME"] def build_core_agent_config() -> CoreAgentConfig: return CoreAgentConfig( model=os.getenv("CORE_AGENT_MODEL") or os.getenv("MODEL_NAME") or os.getenv("MODEL") or "", api_key=os.getenv("OPENAI_API_KEY") or os.getenv("API_KEY"), base_url=os.getenv("OPENAI_BASE_URL") or os.getenv("BASE_URL"), timeout=float(os.getenv("OPENAI_TIMEOUT", "120")), temperature=float(os.getenv("OPENAI_TEMPERATURE", "0.2")), max_iterations=int(os.getenv("CORE_AGENT_MAX_ITERATIONS", "12")), user_name=os.getenv("USER_NAME", "User"), agent_name=os.getenv("AGENT_NAME", "MeetingAgent"), ) def require_model_config(config: CoreAgentConfig) -> None: missing = [] if _is_missing_or_placeholder(config.api_key): missing.append("OPENAI_API_KEY") if _is_missing_or_placeholder(config.base_url): missing.append("OPENAI_BASE_URL") if _is_missing_or_placeholder(config.model): missing.append("MODEL_NAME or CORE_AGENT_MODEL") if missing: joined = ", ".join(missing) raise RuntimeError( "缺少大模型配置:" + joined + "。请复制 .env.example 为 .env,并填写 OpenAI-compatible API 配置;" "如果只想本地演示工具流程,请加 --offline。" ) def _load_env_without_dependency(path: Path) -> None: for raw_line in path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() value = value.strip().strip('"').strip("'") if key and key not in os.environ: os.environ[key] = value def _is_missing_or_placeholder(value: Optional[str]) -> bool: if value is None or not str(value).strip(): return True normalized = str(value).strip().lower() placeholders = { "your-api-key-here", "your-base-url-here", "your-model-name-here", "sk-xxx", "xxx", "changeme", } return normalized in placeholders or normalized.startswith("your-")