@dsh-bio/dsh-bio-gem 0.1.3 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/python.js CHANGED
@@ -1,27 +1,123 @@
1
1
  // python.js — dsh-bio-gem Python 子进程调用器(JSON stdin 协议)
2
2
  // bridge 契约同 dsh-bio-genie:stdout 最后一行是 JSON;stderr 含
3
3
  // "Traceback (most recent call last)" 头 = 代码级失败(恒 ok:true 时靠它判定)。
4
- import { spawn } from 'node:child_process'
4
+ import { spawn, spawnSync } from 'node:child_process'
5
5
  import { dirname, join } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import { existsSync } from 'node:fs'
8
+ import os from 'node:os'
8
9
 
9
10
  const PYTHON_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'python')
10
11
 
11
- // 运行时探测 Python:优先 miniconda(本机分析环境,cobra 已装),回退 env GEM_PYTHON / PATH
12
- function pythonExe() {
13
- const cands = [
14
- process.env.GEM_PYTHON,
15
- 'C:/Users/shuai/miniconda3/python.exe',
16
- 'python',
17
- ]
18
- for (const c of cands) {
12
+ /**
13
+ * 候选解释器,按优先级(**通用化,不写死任何本机路径**):
14
+ *
15
+ * 1. `GEM_PYTHON` — 用户显式指定,最高优先级
16
+ * 2. 宿主插件自举环境 — `$DSH_HOME/dsh-bio-genie/python-env`
17
+ * dsh-bio-genie 的环境引导把 cobra 装在**第一层依赖**里,gem 可直接复用,
18
+ * 因此用户只需安装 genie 即可获得代谢建模能力,无需自备 conda/cobra。
19
+ * (2026-09-11 实测:用该环境的 python 跑通 gem 的 ledger/validate/report op)
20
+ * 3. `CONDA_PREFIX` — 当前激活的 conda 环境(通用信号,非硬编码路径)
21
+ * 4. `python` — PATH 兜底
22
+ */
23
+ /**
24
+ * Candidate interpreters in the same priority order used by pythonExe().
25
+ *
26
+ * The integration status endpoint consumes this exported, side-effect-free
27
+ * description instead of keeping a second, drift-prone candidate list.
28
+ */
29
+ export function pythonCandidates() {
30
+ const list = []
31
+ if (process.env.GEM_PYTHON) list.push({ path: process.env.GEM_PYTHON, source: 'GEM_PYTHON' })
32
+
33
+ const dshHome = process.env.DSH_HOME ?? join(os.homedir(), '.dsh')
34
+ const hosted = join(dshHome, 'dsh-bio-genie', 'python-env')
35
+ list.push({
36
+ path: process.platform === 'win32'
37
+ ? join(hosted, 'Scripts', 'python.exe')
38
+ : join(hosted, 'bin', 'python'),
39
+ source: 'genie-hosted',
40
+ })
41
+
42
+ if (process.env.CONDA_PREFIX) {
43
+ list.push({
44
+ path: process.platform === 'win32'
45
+ ? join(process.env.CONDA_PREFIX, 'python.exe')
46
+ : join(process.env.CONDA_PREFIX, 'bin', 'python'),
47
+ source: 'CONDA_PREFIX',
48
+ })
49
+ }
50
+
51
+ list.push({ path: 'python', source: 'PATH' })
52
+ return list
53
+ }
54
+
55
+ function candidates() {
56
+ return pythonCandidates().map((candidate) => candidate.path)
57
+ }
58
+
59
+ /** cobra 是本插件除 gem_build 外全部 op 的硬依赖:探测解释器能否 import cobra。 */
60
+ function hasCobra(exe) {
61
+ try {
62
+ const r = spawnSync(exe, ['-I', '-c', 'import cobra'], {
63
+ timeout: 30_000, windowsHide: true, stdio: 'ignore',
64
+ })
65
+ return r.status === 0
66
+ } catch {
67
+ return false
68
+ }
69
+ }
70
+
71
+ let cachedExe = null
72
+
73
+ /**
74
+ * 选定解释器(进程内缓存)。
75
+ *
76
+ * 不做「路径存在即采用」的浅判断——落在一个没有 cobra 的解释器上时,
77
+ * 工具只会抛 ModuleNotFoundError 而用户无从判断该装到哪里(README 曾专门
78
+ * 警告此坑)。这里逐个探测 `import cobra`,让选择结果可解释。
79
+ *
80
+ * 注:选择依据的可视化由**宿主面板**承担(genie 设置面板的「代谢建模」分页会
81
+ * 展示选中解释器 + 来源 + cobra 版本 + 候选表)——本模块不再重复提供诊断 API,
82
+ * 也刻意不在插件加载期调用本函数(探测约 2.7s,会拖慢宿主启动)。
83
+ */
84
+ export function pythonExe() {
85
+ if (cachedExe) return cachedExe
86
+ for (const c of candidates()) {
19
87
  if (!c) continue
20
- try {
21
- if (c === 'python' || existsSync(c)) return c
22
- } catch { /* ignore */ }
88
+ if (c !== 'python' && !existsSync(c)) continue
89
+ if (hasCobra(c)) {
90
+ cachedExe = c
91
+ return c
92
+ }
93
+ }
94
+ cachedExe = 'python'
95
+ return cachedExe
96
+ }
97
+
98
+ /** op 名 → 对外工具名(多数同名;特例是 model_info 与 biomass 的两个 op)。 */
99
+ const OP_TOOL = {
100
+ model_info: 'gem_report',
101
+ biomass_inspect: 'gem_biomass',
102
+ biomass_apply: 'gem_biomass',
103
+ }
104
+
105
+ function toolNameFor(op) {
106
+ return OP_TOOL[op] ?? `gem_${op}`
107
+ }
108
+
109
+ /**
110
+ * 与 dsh-bio-genie 的溯源契约对齐:工具输出挂 `_provenance` 背书字段。
111
+ *
112
+ * genie 侧的语义化工具(bio_*)都带该字段,其计算防火墙台账据此与回复里的
113
+ * 数值声明对账;gem 的工具此前不带,两边口径不一致。统一在**唯一出口**
114
+ * (callGem)盖章,避免逐个工具遗漏。不改动已有 _provenance(幂等)。
115
+ */
116
+ export function stampProvenance(tool, value) {
117
+ if (value && typeof value === 'object' && !Array.isArray(value) && value._provenance === undefined) {
118
+ value._provenance = { tool, at: new Date().toISOString() }
23
119
  }
24
- return 'python'
120
+ return value
25
121
  }
26
122
 
27
123
  /** 调用 gem_ops.py(op 协议):{op, args} -> result;异常/代码级失败抛 Error。 */
@@ -34,7 +130,7 @@ export function callGem(op, args, opts = {}) {
34
130
  let err = ''
35
131
  cp.stdout.on('data', (d) => { out += d })
36
132
  cp.stderr.on('data', (d) => { err += d })
37
- cp.on('error', (e) => reject(new Error(`python spawn failed: ${e.message}`)))
133
+ cp.on('error', (e) => reject(new Error(`python spawn failed (${py}): ${e.message}`)))
38
134
  const timer = opts.timeoutMs
39
135
  ? setTimeout(() => { cp.kill(); reject(new Error(`gem op ${op} timeout after ${opts.timeoutMs}ms`)) }, opts.timeoutMs)
40
136
  : null
@@ -42,7 +138,7 @@ export function callGem(op, args, opts = {}) {
42
138
  if (timer) clearTimeout(timer)
43
139
  const lines = out.trim().split(/\r?\n/).filter(Boolean)
44
140
  if (!lines.length) {
45
- return reject(new Error(`gem_ops.py produced no output (op=${op}); stderr: ${err.slice(-400)}`))
141
+ return reject(new Error(`gem_ops.py produced no output (op=${op}, python=${py}); stderr: ${err.slice(-400)}`))
46
142
  }
47
143
  if (err.includes('Traceback (most recent call last)')) {
48
144
  return reject(new Error(`gem op ${op} code-level failure: ${err.slice(-400)}`))
@@ -54,11 +150,11 @@ export function callGem(op, args, opts = {}) {
54
150
  return reject(new Error(`gem op ${op} bad JSON: ${lines[lines.length - 1].slice(0, 300)}`))
55
151
  }
56
152
  if (parsed.ok === false) return reject(new Error(parsed.error || `gem op ${op} failed (ok:false)`))
57
- resolve(parsed.result)
153
+ resolve(stampProvenance(toolNameFor(op), parsed.result))
58
154
  })
59
155
  cp.stdin.write(JSON.stringify({ op, args }))
60
156
  cp.stdin.end()
61
157
  })
62
158
  }
63
159
 
64
- export { pythonExe, PYTHON_DIR }
160
+ export { PYTHON_DIR }
package/src/tools.js CHANGED
@@ -1,16 +1,14 @@
1
- // dsh-bio-gem — 工具层(defineTool 注册,20 语义化工具,2026-08-30 阶段C-C4 起)
1
+ // dsh-bio-gem — 工具层(defineTool 注册,23 语义化工具,2026-08-30 阶段C-C4 起)
2
2
  // 全部执行走 python/gem_ops.py(JSON stdin 协议)或 build.py CLI(gem_build 长任务)。
3
- // op 与工具对照:19 op(含 fluxscan/sensitivity/ledger/benchmark/secretion/double_knockout/enrichment/targets)+ build CLI;详见 docs/ARCHITECTURE.md §3。
3
+ // op 与工具对照:23 op + gem_build(build CLI 直调);详见 docs/ARCHITECTURE.md §3。
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
5
5
  import { join } from 'node:path'
6
6
  import { dirname, isAbsolute } from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
8
  import { spawn } from 'node:child_process'
9
- import { callGem, pythonExe, PYTHON_DIR } from './python.js'
9
+ import { callGem, stampProvenance } from './python.js'
10
10
  import { startBuild, jobStatus } from './jobs.js'
11
11
 
12
- const PY = pythonExe()
13
-
14
12
  /** 校验输入存在(绝对路径或用户给定路径)。 */
15
13
  function requirePath(v, label) {
16
14
  if (!v) throw new Error(`${label} required`)
@@ -46,7 +44,7 @@ function buildTool() {
46
44
  'engine=gapseq(质量档,需本机 WSL2 gapseq 环境):输入核苷酸 FASTA(*.fna),WSL 桥 gapseq doall → 模型拷回 → 目标介质验证,' +
47
45
  '约 30-60 分钟(后台进度日志旁观,不要误判超时)。' +
48
46
  '输出标准 SBML(fbc v2)+ 模型卡(sidecar JSON:引擎版本/验证结果/补洞记录)。' +
49
- '生长/通量数值为单点 FBA 值(非硬结论);条件间对比用 gem_fluxscan(区间制)。' +
47
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
50
48
  '触发词:构建代谢模型、基因组转模型、建GSMM、carveme 建模、gapseq 建模。',
51
49
  parameters: {
52
50
  input: {
@@ -86,7 +84,7 @@ function buildTool() {
86
84
  if (!st.result || st.result.ok === false || st.result.result == null) {
87
85
  throw new Error(`gem_build failed: ${st.result?.error_hint ?? st.error ?? 'result missing'}${d}`)
88
86
  }
89
- return st.result.result
87
+ return stampProvenance('gem_build', st.result.result)
90
88
  }
91
89
  }
92
90
  throw new Error('gem_build timeout (840s)')
@@ -116,7 +114,7 @@ export function registerTools(ctx) {
116
114
  'G2 内部反应元素平衡(C/N/P/S 必须为 0,H/O 单独报告)、G3 生长真实性(声明培养基上有碳源>0、无碳=0、全关=0)、' +
117
115
  'G4 底物表型对照(需 phenotype_table 路径,条件执行)、G5 必需基因抽检(需 essential_test 基因列表,条件执行)。' +
118
116
  'medium 用自然名成分(如 D-Glucose/NH3/O2),跨引擎自动解析。G2 的已知生物质方程簿记偏差(如 bio1)报 WARN 不阻塞。' +
119
- '生长/通量数值为单点 FBA 值(非硬结论);条件间对比用 gem_fluxscan(区间制)。' +
117
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
120
118
  '触发词:验证模型、质量检查、五道关卡。',
121
119
  parameters: {
122
120
  model: { type: 'string', required: true, description: 'SBML 文件绝对路径' },
@@ -136,7 +134,7 @@ export function registerTools(ctx) {
136
134
  '代谢模型缺口分级诊断:L1 缺胞外交换(培养基成分无对应 EX)、L2 缺转运(e0 代谢物无入胞出口)、' +
137
135
  'L3 内部路径(有交换+转运但 FBA 不生长)。输出分级缺口清单 + 每条是否规则可修(fixable)。' +
138
136
  '已知规律:多数「不能利用某碳源」缺口是 L1/L2 而非 L3。medium 支持自然名(跨引擎解析)。' +
139
- '生长/通量数值为单点 FBA 值(非硬结论);条件间对比用 gem_fluxscan(区间制)。' +
137
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
140
138
  '触发词:诊断缺口、为什么不能用这个碳源、gapfind。',
141
139
  parameters: {
142
140
  model: { type: 'string', required: true, description: 'SBML 文件绝对路径' },
@@ -193,7 +191,7 @@ export function registerTools(ctx) {
193
191
  '表型回填迭代:对模型跑 G4 表型对照(phenotype_table:substrate<TAB>published 0/1,如 Biolog/文献表),' +
194
192
  '对「应生长但模型不长」的底物逐个 gapfind 分级 → L1/L2 交换/转运规则自动补洞(累积修复)→ L3 内部路径列候选清单 → 重跑 G4 对比匹配率。' +
195
193
  'medium 推荐 {"medium_name": "AB"}。输出 before/after 匹配率 + 修复清单 + L3 待处理项。' +
196
- '生长/通量数值为单点 FBA 值(非硬结论);条件间对比用 gem_fluxscan(区间制)。' +
194
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
197
195
  '触发词:表型回填、提高表型匹配、Biolog 校准、为什么这个底物不长。',
198
196
  parameters: {
199
197
  model: { type: 'string', required: true, description: 'SBML 文件绝对路径(将基于副本修复,原文件不动)' },
@@ -216,7 +214,7 @@ export function registerTools(ctx) {
216
214
  '若提供 gene_table(gem_annotate 返回的 <base>.gene_table.tsv),输出额外含 essential_gene_details(' +
217
215
  '每必需基因带 locus_tag/product 功能注释——坐标型基因 ID 无此表时是不可解读的)。' +
218
216
  '结果可用于模型卡"必需基因"章节(对照文献/实验必需基因集即召回率)。' +
219
- '生长/通量数值为单点 FBA 值(非硬结论);条件间对比用 gem_fluxscan(区间制)。' +
217
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
220
218
  '触发词:必需基因扫描、全量必要基因、essentiality scan、敲除全扫。',
221
219
  parameters: {
222
220
  model: { type: 'string', required: true, description: 'SBML 文件绝对路径' },
@@ -273,7 +271,7 @@ export function registerTools(ctx) {
273
271
  '防过补第五闸门:历史累计新增 ≤ max(5, 5%·总反应),超限返回 confirm_required 需显式 confirm_budget=true。\n' +
274
272
  '补后自动 validate G1-G6 全跑,G6(ATP 泄漏哨兵)非 PASS 自动回滚本批改动。' +
275
273
  '返回每底物 before/after sole 生长 + verdict(fixed/not_fixable + 不可补证据链)。\n' +
276
- '生长/通量数值为单点 FBA 值(非硬结论);条件间对比用 gem_fluxscan(区间制)。' +
274
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
277
275
  '触发词:补内部路径、L3 补洞、白名单补反应、为什么补了交换还是不长。',
278
276
  parameters: {
279
277
  model: { type: 'string', required: true, description: 'SBML 文件绝对路径(原文件不动,修复写到 out)' },
@@ -298,10 +296,10 @@ export function registerTools(ctx) {
298
296
  '(组分个数、原子总量、类别分布:氨基酸/核酸/脂质/辅因子/金属/其他);reference 可选 iML1515/both 对照' +
299
297
  '(含 iNX1344_v4 按代谢物名同义尽力翻译,翻不了明示 unmapped N 个,不强行全翻)。\n' +
300
298
  'action=apply(显式修改):必须给 biomass_profile 覆盖表 [{"met_id","coeff","op":"set|add|remove"}],' +
301
- '基于副本替换 biomass → 强制 G1-G6 重验 + 三联对照(生长率/表型匹配率/必需基因 delta,单位 mmol/gDW/h)' +
299
+ '基于副本替换 biomass → 强制 G1-G6 重验 + 三联对照(生长率/表型匹配率/必需基因 delta;生长率单位 1/h)' +
302
300
  '→ 输出 before/after 对照 + 新模型(原文件不动=天然可回滚)+ 模型卡 lineage 追加(有 card 时)。' +
303
301
  '生长变差 WARN 不阻塞;默认不应用任何 profile。' +
304
- '生长/通量数值为单点 FBA 值(非硬结论);条件间对比用 gem_fluxscan(区间制)。' +
302
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
305
303
  '触发词:biomass 精修、看生物质组成、改 biomass 系数、目标函数调整。',
306
304
  parameters: {
307
305
  action: { type: 'string', enum: ['inspect', 'apply'], required: true, description: 'inspect=只读诊断;apply=显式应用覆盖表' },
@@ -394,7 +392,7 @@ export function registerTools(ctx) {
394
392
  'literature_contradicted/experimentally_verified)/source_refs/comparison_refs(维护 updated_at)。' +
395
393
  '只读/追加/更新,不删行;损坏行跳过并报 corrupt_rows 不阻塞。' +
396
394
  '账本预测默认 status=unverified——实验或文献兑现前不应当作事实引用(基率披露见 gem_report 的 ledger_summary)。' +
397
- '生长/通量数值为单点 FBA 口径(mmol/gDW/h);条件间通量对比用 gem_fluxscan(区间制)。' +
395
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
398
396
  '触发词:预测账本、查询预测、更新预测状态、预测追踪、ledger。',
399
397
  parameters: {
400
398
  action: { type: 'string', enum: ['list', 'query', 'update'], required: true, description: 'list=分页列出;query=条件过滤;update=改状态/来源' },
@@ -424,7 +422,7 @@ export function registerTools(ctx) {
424
422
  '必需性对比(复用 essential_scan;任一侧 wt<=EPS 判退化只报结构信息不做垃圾对比,基因映射尽力而为如实报覆盖率,' +
425
423
  'reference_essential 文献值仅标注不冒充模型输出)/ 表型对比(G4 sole)/ 可复现性评估 / 账本 comparison_refs ' +
426
424
  '回填(update 语义幂等可重入)。export_md 落盘论文级 Markdown。' +
427
- '生长/通量数值为单点 FBA 口径(mmol/gDW/h);条件间通量对比用 gem_fluxscan(区间制)。' +
425
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
428
426
  '触发词:模型对比、两个模型比较、基准、benchmark、跨模型校准。',
429
427
  parameters: {
430
428
  model_a: { type: 'string', required: true, description: '模型 A SBML 绝对路径' },
@@ -453,7 +451,7 @@ export function registerTools(ctx) {
453
451
  '或显式 mode=full(注意可能很大)。export_csv 全量落盘。**边界声明:未考虑毒性/渗透压/调控,纯拓扑/线性规划结果**——可分泌≠实际会分泌。' +
454
452
  '被测模型 wt<=EPS(介质下不生长,如 AB 预设对非根瘤菌模型)→ degenerate=true 不扫描不登记,提示介质适配。' +
455
453
  '每个可分泌代谢物自动登记账本 type=secretion(幂等)。' +
456
- '生长/通量数值为单点 FBA 口径(mmol/gDW/h);条件间通量对比用 gem_fluxscan(区间制)。' +
454
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
457
455
  '触发词:可分泌谱、分泌能力、secretion、能产什么、代谢物分泌。',
458
456
  parameters: {
459
457
  model: { type: 'string', required: true, description: 'SBML 文件绝对路径' },
@@ -478,7 +476,7 @@ export function registerTools(ctx) {
478
476
  '输出 {pair, single_a_growth, single_b_growth, double_growth, rationale(GPR先验/全扫), source}。' +
479
477
  '**假设声明:细菌双敲验证率无大规模实验数据支撑,本结果=假设生成,供实验设计参考非结论**。' +
480
478
  '被测模型 wt<=EPS → degenerate=true 不扫描不登记(提示介质适配)。每对自动登记账本 type=synthetic_lethal(幂等)。' +
481
- '生长/通量数值为单点 FBA 口径(mmol/gDW/h);条件间通量对比用 gem_fluxscan(区间制)。' +
479
+ '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
482
480
  '触发词:双敲、合成致死、double knockout、基因对敲除、互补基因。',
483
481
  parameters: {
484
482
  model: { type: 'string', required: true, description: 'SBML 文件绝对路径' },
@@ -535,11 +533,89 @@ export function registerTools(ctx) {
535
533
  timeoutMs: 120_000,
536
534
  })))
537
535
 
536
+ // gem_precursor_scan:阻塞前体分析(「模型为什么不长」的结构级定位)
537
+ disposers.push(ctx.tools.register(gemTool({
538
+ name: 'gem_precursor_scan',
539
+ description:
540
+ '阻塞前体分析(precursor_scan):定位**哪个 biomass 前体在卡住生长**。' +
541
+ '先测基线 biomass 通量——可生长则直接返回「无阻塞」(不做逐前体测试,天然零假阳性);' +
542
+ '不生长则逐一移除每个前体的需求,看 biomass 是否恢复通量:恢复者即阻塞点。' +
543
+ 'verdict 三态:growable(该条件下可生长)/ blocked(列出阻塞前体)/ ' +
544
+ 'infeasible_or_constrained(逐前体均不恢复 → 病灶在 biomass 方程整体而非单个前体)。' +
545
+ '用在 gem_validate 判定不生长之后、gem_gapfind 之前:先确认阻塞在哪一层,再谈补洞。' +
546
+ '与 gem_gapfind 区分:后者分级报「缺交换 / 缺转运 / 内部路径」,本工具报「卡在哪个前体」;' +
547
+ '模型数据质量存疑时(gem_validate 的 g0 非 PASS)本工具结论更贴近根因。' +
548
+ '触发词:模型为什么不长、哪个前体卡住、阻塞前体、生长为零诊断、precursor。',
549
+ parameters: {
550
+ model: { type: 'string', required: true, description: 'SBML 模型文件绝对路径' },
551
+ medium: {
552
+ type: 'object', additionalProperties: true,
553
+ description: '可选培养基:{"medium_name":"AB"/"M9"}(内置成分)或 {EX_id: lower_bound};缺省用模型自带边界',
554
+ },
555
+ max_precursors: { type: 'number', description: '逐前体测试上限(默认 200)' },
556
+ },
557
+ op: 'precursor_scan',
558
+ timeoutMs: 300_000,
559
+ })))
560
+
561
+ // gem_quality:模型质量报告(gem-qi-v1:MEMOTE 风格分项检查 + 启发式聚合分)
562
+ disposers.push(ctx.tools.register(gemTool({
563
+ name: 'gem_quality',
564
+ description:
565
+ '模型质量报告(quality):一份可分解的质量审计——blocked_reactions(不可通量反应,按当前介质口径)/ ' +
566
+ 'elemental_balance(元素平衡)/ orphan_metabolites(只消耗无生成)/ dead_end_metabolites(只生成无消耗)/ ' +
567
+ 'gpr_coverage(反应级 GPR 覆盖)/ annotation_coverage(注释覆盖)/ ' +
568
+ 'connectivity(代谢物-反应网络连通分量)。输出 quality_index(0-100 启发式聚合,gem-qi-v1)+ ' +
569
+ 'raw_metrics 分项原始值 + failed_checks + not_assessable_checks。' +
570
+ '⚠️ quality_index 仅用于快速横向比较,不得作为单一质量结论引用——分项指标与 failed_checks 才是判断依据。' +
571
+ 'cyclic_reactions(潜在稳态环路)在独立的化学计量+方向锥上以 cobra.fastcc 计算(确定性口径;' +
572
+ '大模型上该检查约数分钟——C58 实测全量约 8 分钟);如需快速子集可只用 checks 指定部分检查。' +
573
+ '与 gem_validate 的分工:validate 是构建流程的六道关卡(G0-G6,含生长/表型真伪验证);' +
574
+ 'quality 是模型质量的横向审计(对标 MEMOTE 检查维度),不改模型、不跑生长。' +
575
+ '触发词:模型质量、质量报告、质量评分、MEMOTE、模型好不好、有多少孤儿代谢物。',
576
+ parameters: {
577
+ model: { type: 'string', required: true, description: 'SBML 文件绝对路径' },
578
+ medium: {
579
+ type: 'object', additionalProperties: true,
580
+ description: '培养基:{"medium_name": "AB"/"M9"}(内置完整成分)或自然名成分字典;缺省=按模型自带 exchange bounds 计算 blocked(口径写入 notes)',
581
+ },
582
+ checks: {
583
+ type: 'array', items: { type: 'string' },
584
+ description: '可选:只跑指定检查子集(blocked_reactions/cyclic_reactions/elemental_balance/orphan_metabolites/dead_end_metabolites/gpr_coverage/annotation_coverage/connectivity)。缺省=全部;大模型上 cyclic_reactions 约数分钟(fastcc 方向锥),如只需快速体检可只传轻量子集',
585
+ },
586
+ export_csv: { type: 'string', description: '完整清单 CSV 落盘路径(ids_sample 只给前 25 条;全量走此参数)' },
587
+ },
588
+ op: 'quality',
589
+ timeoutMs: 900_000,
590
+ })))
591
+
592
+ // gem_sample:通量空间采样(默认 ACHR Windows-safe;growth_floor 受限空间)
593
+ disposers.push(ctx.tools.register(gemTool({
594
+ name: 'gem_sample',
595
+ description:
596
+ '通量空间采样(sample):对模型可行通量空间做统计采样(默认 ACHR,Windows 安全;大样本可 method=optgp)。' +
597
+ '返回 growth 分布(median/mean/min/max/q05-q95)+ 指定反应(缺省=生长反应+变化最大的 top 20)的 ' +
598
+ 'median/q05/q95/sign_probability/near_zero_fraction。' +
599
+ '⚠️ 必读 boundary 字段(边界声明):缺省采样的是全 feasible space——全空间均匀采样 ≠ 生物学上有意义的活跃状态' +
600
+ '(实测 C58 全空间采样生长中位数远低于最大生长,差 2 个数量级);关心近最优生长态请传 growth_floor_fraction(如 0.9)。' +
601
+ '大模型首次调用 ACHR 初始化约 2-3 分钟(属正常等待,勿重复调用);同 seed 结果可复现。' +
602
+ '触发词:通量采样、采样分布、flux sampling、通量的分布范围、这个反应通常有多少通量。',
603
+ parameters: {
604
+ model: { type: 'string', required: true, description: 'SBML 文件绝对路径' },
605
+ medium: { type: 'object', additionalProperties: true, description: '培养基:{"medium_name": "AB"} 或自然名成分字典;缺省用模型自带边界' },
606
+ n: { type: 'number', description: '样本数(默认 1000;范围 10-20000)' },
607
+ method: { type: 'string', enum: ['auto', 'achr', 'optgp'], description: '采样器:auto(默认→ACHR)| achr | optgp(大样本,Windows 下实验性——不可用时会显式拒绝)' },
608
+ thinning: { type: 'number', description: '稀释步长(默认 100;越大越独立、初始化越久)' },
609
+ growth_floor_fraction: { type: 'number', description: '生长下限分数(0-1):给定后仅在「生长 ≥ f×最大生长」的受限空间采样(推荐 0.9);缺省=全空间采样' },
610
+ reactions: { type: 'array', items: { type: 'string' }, description: '要统计的反应 id 列表(缺省=生长反应+变化最大 top 20)' },
611
+ seed: { type: 'number', description: '随机种子(默认 42;同 seed 可复现)' },
612
+ export_csv: { type: 'string', description: '全样本矩阵 CSV 落盘路径' },
613
+ },
614
+ op: 'sample',
615
+ timeoutMs: 900_000,
616
+ })))
617
+
538
618
 
539
619
  return () => disposers.forEach((d) => d())
540
620
  }
541
621
 
542
- export const gemToolNames = ['gem_report', 'gem_validate', 'gem_gapfind', 'gem_gapfill', 'gem_build',
543
- 'gem_gapseq', 'gem_phenotype', 'gem_essentiality', 'gem_annotate', 'gem_media_resolve', 'gem_l3_fix',
544
- 'gem_biomass', 'gem_fluxscan', 'gem_sensitivity', 'gem_ledger', 'gem_benchmark', 'gem_secretion',
545
- 'gem_double_knockout', 'gem_enrichment', 'gem_targets']