Files
my-v0-server/app/config.py
T

202 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
default_factory=lambda: os.getenv("TYPESAFE_DEFAULT_MODEL") or "jev-latest"
)
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 预算(近似计数),从最近一轮往前保留完整的轮次
history_max_tokens: int = field(
default_factory=lambda: int(os.getenv("HISTORY_MAX_TOKENS", "4096"))
)
# 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")
)
)
# 模型写的单条数据查询的超时(秒)
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()