210 lines
9.2 KiB
Python
210 lines
9.2 KiB
Python
import os
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
|
||
from dotenv import load_dotenv
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||
|
||
load_dotenv(PROJECT_ROOT / ".env")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Settings:
|
||
# LLM 配置
|
||
llm_api_key: str = field(
|
||
default_factory=lambda: os.getenv("LLM_API_KEY", ""),
|
||
repr=False,
|
||
)
|
||
llm_model: str = field(default_factory=lambda: os.getenv("LLM_MODEL", ""))
|
||
llm_base_url: str | None = field(
|
||
default_factory=lambda: os.getenv("LLM_BASE_URL") or None
|
||
)
|
||
# 统计 5000 多次调用:正常的都在 30 秒内(含上千 token 的长输出),
|
||
# 卡住的请求会一直等到超时才重试,超时越长,卡一次多等越久
|
||
llm_timeout: float = field(
|
||
default_factory=lambda: float(os.getenv("LLM_TIMEOUT", "30"))
|
||
)
|
||
llm_max_retries: int = 2
|
||
# 推理 token 占了单次调用耗时的大头;步骤都很简单,不需要深想
|
||
llm_reasoning_effort: str | None = field(
|
||
default_factory=lambda: os.getenv("LLM_REASONING_EFFORT", "low") or None
|
||
)
|
||
|
||
# Jev
|
||
typesafe_api_key: str = field(
|
||
default_factory=lambda: os.getenv("TYPESAFE_API_KEY", ""), repr=False
|
||
)
|
||
typesafe_model: str = field(
|
||
# 固定版本号:jev-latest 别名会随新版本移动,意图判断的阈值是按 1.13.0 调的
|
||
default_factory=lambda: os.getenv("TYPESAFE_DEFAULT_MODEL") or "jev-1.13.0"
|
||
)
|
||
typesafe_base_url: str = field(
|
||
default_factory=lambda: (
|
||
os.getenv("TYPESAFE_BASE_URL") or "https://api.typesafe.ai"
|
||
)
|
||
)
|
||
typesafe_timeout: float = field(
|
||
default_factory=lambda: float(os.getenv("TYPESAFE_TIMEOUT", "30"))
|
||
)
|
||
typesafe_max_retries: int = field(
|
||
default_factory=lambda: int(os.getenv("TYPESAFE_MAX_RETRIES", "2"))
|
||
)
|
||
|
||
# 意图门控:Jev 选中 qa 且概率达到阈值才走问答,其余都交给 screen agent;阈值按真实语料校准
|
||
intent_qa_min_probability: float = field(
|
||
default_factory=lambda: float(os.getenv("INTENT_QA_MIN_PROBABILITY", "0.5"))
|
||
)
|
||
|
||
# screen agent 单次运行的模型调用上限,包括读取 skill、查询物料、提交修正和最终回复;
|
||
# 用尽或没有给出结论时节点再收尾 1 次,所以一次请求最多 MAX_AGENT_STEPS + 1 次
|
||
max_agent_steps: int = field(
|
||
default_factory=lambda: int(os.getenv("MAX_AGENT_STEPS", "12"))
|
||
)
|
||
# 问答 agent 单次运行的模型调用上限;查询数据时每次查询多一次调用
|
||
max_qa_steps: int = field(
|
||
default_factory=lambda: int(os.getenv("MAX_QA_STEPS", "8"))
|
||
)
|
||
# 单次 MCP 工具调用的超时(秒),服务列表见 mcp_servers.json
|
||
mcp_tool_timeout: float = field(
|
||
default_factory=lambda: float(os.getenv("MCP_TOOL_TIMEOUT", "5"))
|
||
)
|
||
|
||
# 没有任何对话的空会话保留时长(秒),超时后视为不存在
|
||
conversation_empty_ttl: int = field(
|
||
default_factory=lambda: int(os.getenv("CONVERSATION_EMPTY_TTL", "7200"))
|
||
)
|
||
# 每次请求从会话记录读取的最近轮数上限,实际带入多少由 history_max_tokens 决定
|
||
conversation_history_turns: int = field(
|
||
default_factory=lambda: int(os.getenv("CONVERSATION_HISTORY_TURNS", "20"))
|
||
)
|
||
# 每次请求带入的会话历史 token 预算(o200k 计数),对话消息和轮次记录合计,从最近一轮往前保留完整的轮次
|
||
history_max_tokens: int = field(
|
||
default_factory=lambda: int(os.getenv("HISTORY_MAX_TOKENS", "10000"))
|
||
)
|
||
|
||
# MongoDB 连接串,未配置时会话记录和检查点回退为进程内存实现
|
||
mongodb_uri: str | None = field(
|
||
default_factory=lambda: os.getenv("MONGODB_URI") or None, repr=False
|
||
)
|
||
mongodb_db: str = field(
|
||
default_factory=lambda: os.getenv("MONGODB_DB") or "screen_agent"
|
||
)
|
||
# 检查点保留时长(秒),超时由 MongoDB TTL 索引清理,0 表示永久保留
|
||
mongodb_checkpoint_ttl: int = field(
|
||
default_factory=lambda: int(os.getenv("MONGODB_CHECKPOINT_TTL", "604800"))
|
||
)
|
||
|
||
# 上传的数据文件:原文件和解析出的 Parquet 放这个目录,相对路径相对项目根目录;容器部署时挂载出来
|
||
data_file_dir: Path = field(
|
||
default_factory=lambda: (
|
||
PROJECT_ROOT / (os.getenv("DATA_FILE_DIR") or "data/files")
|
||
)
|
||
)
|
||
# 单个数据文件的大小上限(MB)
|
||
data_file_max_mb: int = field(
|
||
default_factory=lambda: int(os.getenv("DATA_FILE_MAX_MB", "500"))
|
||
)
|
||
# 数据文件保留时长(秒),从上传时算起,默认 7 天
|
||
data_file_ttl: int = field(
|
||
default_factory=lambda: int(os.getenv("DATA_FILE_TTL", "604800"))
|
||
)
|
||
|
||
# 上传后用 LLM 分析文件(一句话说明、图表推荐、列的含义);关闭后文件照常可用,只是没有这些信息
|
||
data_file_insights: bool = field(
|
||
default_factory=lambda: (
|
||
os.getenv("DATA_FILE_INSIGHTS", "true").strip().lower()
|
||
not in ("false", "0", "no", "off")
|
||
)
|
||
)
|
||
|
||
# 大屏保存与发布:单次请求体上限(MB);图表绑定数据文件时数据快照嵌在页面里,一屏很容易上 MB
|
||
screen_max_mb: int = field(
|
||
default_factory=lambda: int(os.getenv("SCREEN_MAX_MB", "10"))
|
||
)
|
||
# 回收站保留天数:软删的大屏超过这个时间由后台任务彻底删除(连同发布历史和封面)
|
||
trash_retention_days: int = field(
|
||
default_factory=lambda: int(os.getenv("TRASH_RETENTION_DAYS", "30"))
|
||
)
|
||
# 大屏封面图存放目录,相对路径相对项目根目录;和数据文件一样放本地磁盘
|
||
thumbnail_dir: Path = field(
|
||
default_factory=lambda: (
|
||
PROJECT_ROOT / (os.getenv("THUMBNAIL_DIR") or "data/thumbnails")
|
||
)
|
||
)
|
||
|
||
# 演示账号的用户 ID:未登录访问时展示这个账号下的大屏,不配置则没有演示数据
|
||
demo_owner_id: int | None = field(
|
||
default_factory=lambda: (
|
||
int(value) if (value := os.getenv("DEMO_OWNER_ID", "").strip()) else None
|
||
)
|
||
)
|
||
|
||
# 模型写的单条数据查询的超时(秒)
|
||
data_query_timeout: float = field(
|
||
default_factory=lambda: float(os.getenv("DATA_QUERY_TIMEOUT", "10"))
|
||
)
|
||
|
||
# apply_actions 里一个 $query 写进组件的最多行数,超过时报错,让模型在 SQL 里聚合
|
||
data_embed_max_rows: int = field(
|
||
default_factory=lambda: int(os.getenv("DATA_EMBED_MAX_ROWS", "1000"))
|
||
)
|
||
|
||
# 用户中心:请求携带的 token 交给它校验,换回 user_id
|
||
user_center_url: str = field(
|
||
default_factory=lambda: (
|
||
os.getenv("USER_CENTER_URL") or "http://127.0.0.1:8100"
|
||
).rstrip("/")
|
||
)
|
||
user_center_timeout_seconds: float = field(
|
||
default_factory=lambda: float(os.getenv("USER_CENTER_TIMEOUT_SECONDS", "5"))
|
||
)
|
||
# 关闭后不校验 token,所有请求视为同一个本地调试用户,仅用于开发
|
||
auth_enabled: bool = field(
|
||
default_factory=lambda: (
|
||
os.getenv("AUTH_ENABLED", "true").strip().lower()
|
||
not in ("false", "0", "no", "off")
|
||
)
|
||
)
|
||
# token 校验结果缓存时长(秒),登出或禁用后最多延迟这么久失效,0 表示不缓存
|
||
auth_cache_ttl_seconds: int = field(
|
||
default_factory=lambda: int(os.getenv("AUTH_CACHE_TTL_SECONDS", "30"))
|
||
)
|
||
|
||
def __post_init__(self):
|
||
if self.user_center_timeout_seconds <= 0:
|
||
raise ValueError("USER_CENTER_TIMEOUT_SECONDS 必须是正数")
|
||
if self.auth_cache_ttl_seconds < 0:
|
||
raise ValueError("AUTH_CACHE_TTL_SECONDS 必须是非负整数")
|
||
if self.mongodb_checkpoint_ttl < 0:
|
||
raise ValueError("MONGODB_CHECKPOINT_TTL 必须是非负整数")
|
||
if self.conversation_empty_ttl <= 0:
|
||
raise ValueError("CONVERSATION_EMPTY_TTL 必须是正整数")
|
||
if self.conversation_history_turns < 0:
|
||
raise ValueError("CONVERSATION_HISTORY_TURNS 必须是非负整数")
|
||
if self.history_max_tokens < 1:
|
||
raise ValueError("HISTORY_MAX_TOKENS 必须是正整数")
|
||
if self.max_agent_steps < 1:
|
||
raise ValueError("MAX_AGENT_STEPS 必须是正整数")
|
||
if self.max_qa_steps < 1:
|
||
raise ValueError("MAX_QA_STEPS 必须是正整数")
|
||
if self.mcp_tool_timeout <= 0:
|
||
raise ValueError("MCP_TOOL_TIMEOUT 必须是正数")
|
||
if self.data_file_max_mb < 1:
|
||
raise ValueError("DATA_FILE_MAX_MB 必须是正整数")
|
||
if self.data_file_ttl < 1:
|
||
raise ValueError("DATA_FILE_TTL 必须是正整数")
|
||
if self.trash_retention_days < 1:
|
||
raise ValueError("TRASH_RETENTION_DAYS 必须是正整数")
|
||
if self.screen_max_mb < 1:
|
||
raise ValueError("SCREEN_MAX_MB 必须是正整数")
|
||
if self.data_query_timeout <= 0:
|
||
raise ValueError("DATA_QUERY_TIMEOUT 必须是正数")
|
||
if self.data_embed_max_rows < 1:
|
||
raise ValueError("DATA_EMBED_MAX_ROWS 必须是正整数")
|
||
if not 0 <= self.intent_qa_min_probability <= 1:
|
||
raise ValueError("INTENT_QA_MIN_PROBABILITY 必须在 0 到 1 之间")
|
||
|
||
|
||
settings = Settings()
|