- 8 个阶段镜像中仅 bioflow-gf 内置 /bioflow-kernel,其余 7 个 (rnaseq/wgs/gfpy/rnar/rde/pan/interproscan) 执行时 No module named cli - 服务端统一 -v kernel_dir:/bioflow-kernel:ro + -w /bioflow-kernel, 全部镜像可用,免重建 7 个镜像 - config 新增 kernel_dir/container_kernel_dir(env 可覆盖) - 337 用例全绿 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
167 lines
6.9 KiB
Python
167 lines
6.9 KiB
Python
"""config.py —— 生信服务(bioflow-service 8098)环境变量驱动配置(pydantic-settings)。
|
||
|
||
全部键可用环境变量覆盖(前缀 BIOFLOW_;LLM 沿用 docgen 的 DOCGEN_LLM_* 直连模式)。
|
||
零硬编码纪律:不出现任何写死宿主路径;仓库内相对路径按 __file__ 推导 repo 根。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
||
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent # bioflow_service/.. = 仓库根
|
||
|
||
|
||
def _resolve_root(v: str) -> str:
|
||
"""仓库内相对路径按 __file__ 推导 repo 根解析为绝对路径。"""
|
||
p = Path(v).expanduser()
|
||
if not p.is_absolute():
|
||
p = _REPO_ROOT / p
|
||
return str(p)
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
model_config = SettingsConfigDict(
|
||
env_file=".env",
|
||
env_file_encoding="utf-8",
|
||
extra="ignore",
|
||
env_prefix="BIOFLOW_",
|
||
)
|
||
|
||
# ---- 应用 ----
|
||
app_name: str = "plantai-bioflow-service"
|
||
version: str = "0.1.0"
|
||
|
||
# ---- 阶段→镜像映射(A1 交付物,仓库内 bioflow_kernel/stage_image_map.json)----
|
||
image_map_path: str = "bioflow_kernel/stage_image_map.json"
|
||
# 内核树:运行时只读挂载进执行容器(-v kernel:/bioflow-kernel:ro + -w)
|
||
# 除 bioflow-gf 外其余镜像未内置内核,挂载后全部阶段镜像统一可用
|
||
kernel_dir: str = "bioflow_kernel"
|
||
container_kernel_dir: str = "/bioflow-kernel"
|
||
|
||
# ---- 数据与任务目录 ----
|
||
data_root: str = "bioflow_data" # 生信数据根(hmm/rag_db 等,只读挂载进容器)
|
||
jobs_dir: str = "bioflow_data/jobs" # 任务目录根(input/workspace/events)
|
||
db_path: str = "bioflow_data/bioflow.db" # 独立 SQLite(与 BFF 完全隔离,FR-BIO-028)
|
||
archives_dir: str = "bioflow_data/archives"
|
||
|
||
# ---- 执行 ----
|
||
concurrency: int = 3 # 全局并发信号量上限(FR-BIO-031,可配)
|
||
stage_timeout_sec: float = 36000.0 # 单阶段 docker 硬超时(秒,10h)
|
||
docker_image_tag_suffix: str = "dev" # 镜像 tag 后缀(P0 交付物为 :dev)
|
||
host_uid_env: str = "BIOFLOW_HOST_UID" # 宿主机 uid 环境变量名;未设置时回退服务进程 uid
|
||
container_data_dir: str = "/bioflow-data"
|
||
container_input_dir: str = "/job/input"
|
||
container_workspace_dir: str = "/job/workspace"
|
||
default_cpus: str = "4"
|
||
default_memory: str = "32g"
|
||
log_tail_lines: int = 80 # manifest 接口日志尾行数(≤80)
|
||
max_upload_gb: float = 100.0 # 上传大小上限(GB;超限 413)
|
||
upload_ext_allow: str = ".fasta,.fa,.faa,.fna,.pep,.cds,.gz,.gff,.gff3,.gtf,.txt,.tsv,.csv,.xlsx,.xls,.bed,.vcf,.vcf.gz,.bam,.cram,.sam,.hmm,.nwk,.newick,.treefile,.json,.yaml,.yml,.pdf,.zip,.py,.r,.sh"
|
||
|
||
# ---- 服务器路径登记(FR-BIO-007;用户数据可在任意位置)----
|
||
max_register_files_per_job: int = 200 # 目录展开单任务文件数上限(防误填大目录)
|
||
# 敏感路径黑名单:命中前缀即 403(白名单根校验已按用户要求移除)
|
||
sensitive_path_prefixes: tuple[str, ...] = (
|
||
"/etc", "/root", "/proc", "/sys", "/dev", "/run", "/boot",
|
||
"/bin", "/sbin", "/lib", "/lib64", "/usr/bin", "/usr/sbin",
|
||
"/usr/lib", "/usr/lib64", "/usr/local/bin", "/var/lib", "/var/log",
|
||
"/opt/neo4j", "/var/lib/docker", "/srv/plantai/install",
|
||
)
|
||
# 敏感文件名(正则,re.match 整名);.env 等含密钥配置、密钥文件不可登记
|
||
sensitive_filename_patterns: tuple[str, ...] = (
|
||
r"\.env.*", r".*\.key$", r".*\.pem$", r"id_rsa.*", r"id_ed25519.*",
|
||
r".*\.p12$", r"\.gitconfig.*",
|
||
)
|
||
|
||
# ---- 用户自定义扩展分析(并行链路;存储根 + AI 转换 LLM 键 + 修复上限)----
|
||
extensions_dir: str = "bioflow_data/extensions" # 扩展存储根({ext_id}/extension.json...)
|
||
extension_max_repair_rounds: int = 20 # AI 修复循环上限(契约 §6 可配)
|
||
# 扩展转换 LLM 键:DOCGEN_LLM_* 直接复用(docgen 直连模式),
|
||
# BIOFLOW_EXTENSION_* 为可选别名(extension_llm.llm_config 兜底读取)。
|
||
|
||
# ---- 规划(planner)----
|
||
llm_plan_timeout: float = 60.0 # 规划 LLM 单次调用超时(秒)
|
||
plan_candidates: int = 3 # 候选路径条数(2~3,FR-BIO-002)
|
||
plan_max_rounds: int = 2 # LLM 输出硬校验失败的最多重试轮数
|
||
|
||
# ---- 知识库(vstore/flow.py 本地实例;无 vstore 时自动降级 llm_only)----
|
||
flow_docs_dir: str = "vstore/data/flow_docs"
|
||
|
||
# ---- LLM(沿用 docgen deepseek 直连模式,DOCGEN_LLM_* 键)----
|
||
docgen_llm_base_url: str = "https://api.deepseek.com/v1"
|
||
docgen_llm_api_key: str = ""
|
||
docgen_llm_model: str = "deepseek-chat"
|
||
docgen_llm_timeout: float = 300.0
|
||
|
||
# ---- 报告登记(BFF 反代端点;空=未配置,登记降级为仅落表)----
|
||
service_url: str = "http://127.0.0.1:8098"
|
||
|
||
# ---- 容器环境变量(BBA_*;零硬编码:路径全部从本配置推导)----
|
||
@property
|
||
def bba_env(self) -> dict[str, str]:
|
||
"""docker run 注入的 BBA_* 环境(值全部来自配置推导,无写死宿主路径)。"""
|
||
return {
|
||
"BBA_PFAM_DIR": f"{self.container_data_dir}/hmm",
|
||
"BBA_RAG_MAPPING_FILE": f"{self.container_data_dir}/rag_db/pfam_mapping.json",
|
||
"BBA_DISABLE_RAG_PFAM_LOOKUP": "0",
|
||
}
|
||
|
||
@property
|
||
def host_uid(self) -> int:
|
||
"""--user 注入 uid:BIOFLOW_HOST_UID 环境变量优先,否则服务进程 uid。"""
|
||
import os
|
||
|
||
raw = os.environ.get(self.host_uid_env, "").strip()
|
||
if raw:
|
||
try:
|
||
return int(raw)
|
||
except ValueError:
|
||
pass
|
||
return os.getuid()
|
||
|
||
@property
|
||
def image_map_abs(self) -> str:
|
||
return _resolve_root(self.image_map_path)
|
||
|
||
@property
|
||
def data_root_abs(self) -> str:
|
||
return _resolve_root(self.data_root)
|
||
|
||
@property
|
||
def kernel_dir_abs(self) -> str:
|
||
return _resolve_root(self.kernel_dir)
|
||
|
||
@property
|
||
def jobs_dir_abs(self) -> str:
|
||
return _resolve_root(self.jobs_dir)
|
||
|
||
@property
|
||
def db_path_abs(self) -> str:
|
||
return _resolve_root(self.db_path)
|
||
|
||
@property
|
||
def archives_dir_abs(self) -> str:
|
||
return _resolve_root(self.archives_dir)
|
||
|
||
@property
|
||
def flow_docs_dir_abs(self) -> str:
|
||
return _resolve_root(self.flow_docs_dir)
|
||
|
||
@property
|
||
def extensions_dir_abs(self) -> str:
|
||
return _resolve_root(self.extensions_dir)
|
||
|
||
@property
|
||
def upload_ext_set(self) -> set[str]:
|
||
return {ext.strip().lower() for ext in self.upload_ext_allow.split(",") if ext.strip()}
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def get_settings() -> Settings:
|
||
return Settings()
|
||
|
||
|
||
settings = get_settings()
|