Files
ZNJJ-api-server/src/core/config.py
T

162 lines
7.0 KiB
Python
Raw Normal View History

2025-04-29 15:03:02 +08:00
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
PROJECT_NAME = "Flexible Employment Analysis API"
API_V1_STR = "/api/v1"
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key")
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
2026-01-08 16:22:25 +08:00
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t")
2026-07-28 10:38:01 +08:00
# FastGPT Configuration
FASTGPT_API_KEY = os.getenv("ANALYSIS_AUTH_TOKEN")
FASTGPT_BASE_URL = os.getenv("ANALYSIS_SERVICE_URL")
FASTGPT_APP_ID = os.getenv("APP_ID")
2026-01-08 16:22:25 +08:00
# Chat backend switch(作业 2)
# "fastgpt" 保持现有行为;"langgraph" 使用内存图 fake 回复。
AGENT_BACKEND = os.getenv("AGENT_BACKEND", "fastgpt")
# 命名 LLM 提供商列表(均为 OpenAI 兼容接口),两种写法二选一:
# ① JSON 字典(推荐,单变量管理):
# LLM_PROVIDERS={"deepseek": {"base_url": "https://...", "api_key": "...",
# "model": "deepseek-v4.1-flash"},
# "kimi": {"base_url": "http://127.0.0.1:3425/v1",
# "api_key": "magpie", "model": "kimi-code-cn/kimi-for-coding",
# "extra_body": {"reasoning_effort": "none"}}}
# 每组 base_url/api_key/model 必填;timeout_seconds/max_retries/
# temperature 可选;extra_body 为对象时直接合并进请求体(思考开关等)。
# ② 逗号分隔名单 + 分散变量:
# LLM_PROVIDERS=deepseek,kimi + LLM_DEEPSEEK_BASE_URL/_API_KEY/_MODEL 等。
# 运行时选择:LLM_PROVIDER=<名>;缺省或 fake = 离线脚本模型。
LLM_PROVIDERS = os.getenv("LLM_PROVIDERS", "")
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "")
# LLM 客户端是否信任系统代理(Windows 注册表)。缺省 False:
# 本地网关(127.0.0.1)必须直连;外部网关若需代理设为 true。
LLM_HTTP_TRUST_ENV = os.getenv("LLM_HTTP_TRUST_ENV", "false").lower() in (
"true", "1", "t",
)
@classmethod
def _declared_provider_names(cls) -> list[str]:
"""LLM_PROVIDERS 中声明的提供商名(JSON 字典取键,名单取逗号项)。"""
raw = cls.LLM_PROVIDERS.strip()
if not raw:
return []
if raw.startswith("{"):
import json
data = json.loads(raw)
return [str(k).strip().lower() for k in data]
return [n.strip().lower() for n in raw.split(",") if n.strip()]
@classmethod
def llm_profiles(cls) -> dict[str, dict]:
"""解析命名提供商列表为统一结构;声明了但缺必填项的组不收录
(由 validate() 统一报错)。extra_body 统一为 JSON 字符串。"""
raw = cls.LLM_PROVIDERS.strip()
if not raw:
return {}
if raw.startswith("{"):
return cls._profiles_from_json(raw)
return cls._profiles_from_prefixed(raw)
@classmethod
def _profiles_from_json(cls, raw: str) -> dict[str, dict]:
import json
try:
data = json.loads(raw)
except ValueError as exc:
raise ValueError(f"LLM_PROVIDERS is not valid JSON: {exc}") from exc
if not isinstance(data, dict):
raise ValueError("LLM_PROVIDERS JSON must be an object keyed by provider name")
profiles: dict[str, dict] = {}
for key, spec in data.items():
name = str(key).strip().lower()
if not name or not isinstance(spec, dict):
continue
base_url = str(spec.get("base_url", ""))
api_key = str(spec.get("api_key", ""))
model = str(spec.get("model", ""))
if not (base_url and api_key and model):
continue
extra_body = spec.get("extra_body", "")
if isinstance(extra_body, dict):
extra_body = json.dumps(extra_body, ensure_ascii=False)
temperature = spec.get("temperature")
profiles[name] = {
"base_url": base_url,
"api_key": api_key,
"model": model,
"timeout_seconds": float(spec.get("timeout_seconds", 30)),
"max_retries": int(spec.get("max_retries", 2)),
"temperature": float(temperature) if temperature is not None else None,
"extra_body": str(extra_body),
}
return profiles
@classmethod
def _profiles_from_prefixed(cls, raw: str) -> dict[str, dict]:
profiles: dict[str, dict] = {}
for name in cls._declared_provider_names():
prefix = f"LLM_{name.upper()}_"
base_url = os.getenv(prefix + "BASE_URL", "")
api_key = os.getenv(prefix + "API_KEY", "")
model = os.getenv(prefix + "MODEL", "")
if not (base_url and api_key and model):
continue
temperature = os.getenv(prefix + "TEMPERATURE")
profiles[name] = {
"base_url": base_url,
"api_key": api_key,
"model": model,
"timeout_seconds": float(os.getenv(prefix + "TIMEOUT_SECONDS", "30")),
"max_retries": int(os.getenv(prefix + "MAX_RETRIES", "2")),
"temperature": float(temperature) if temperature else None,
"extra_body": os.getenv(prefix + "EXTRA_BODY", ""),
}
return profiles
@classmethod
def llm_profile(cls, name: str) -> dict | None:
return cls.llm_profiles().get(name.strip().lower())
2026-01-08 16:22:25 +08:00
@classmethod
def validate(cls):
"""Validate required configuration."""
# TODO(作业 2): 语音管线仍依赖 FastGPT。当前仅按文本后端开关校验;
# 接入语音开关后,需要单独校验语音路径所需配置。
if cls.AGENT_BACKEND not in ("fastgpt", "langgraph"):
raise ValueError(
f"AGENT_BACKEND must be 'fastgpt' or 'langgraph', got {cls.AGENT_BACKEND!r}"
)
profiles = cls.llm_profiles() # JSON 非法时在此抛出明确错误
incomplete = [n for n in cls._declared_provider_names() if n not in profiles]
if incomplete:
raise ValueError(
"LLM providers missing BASE_URL/API_KEY/MODEL: "
+ ", ".join(incomplete)
)
if (
cls.LLM_PROVIDER
and cls.LLM_PROVIDER != "fake"
and cls.LLM_PROVIDER.strip().lower() not in profiles
):
raise ValueError(
f"Unknown LLM_PROVIDER {cls.LLM_PROVIDER!r}; "
f"configured: {sorted(profiles)}"
)
if cls.AGENT_BACKEND != "fastgpt":
return
2026-01-08 16:22:25 +08:00
required = [
2026-07-28 10:38:01 +08:00
("FASTGPT_BASE_URL (ANALYSIS_SERVICE_URL)", cls.FASTGPT_BASE_URL),
("FASTGPT_API_KEY (ANALYSIS_AUTH_TOKEN)", cls.FASTGPT_API_KEY),
("FASTGPT_APP_ID (APP_ID)", cls.FASTGPT_APP_ID),
2026-01-08 16:22:25 +08:00
]
missing = [name for name, value in required if not value]
if missing:
2026-07-28 08:41:34 +08:00
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")