-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathukb_handler.py
More file actions
2845 lines (2572 loc) · 140 KB
/
Copy pathukb_handler.py
File metadata and controls
2845 lines (2572 loc) · 140 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
UKBAgentHandler - Self-Evolving MedicalClaw 的核心 Handler
继承 GenericAgentHandler,新增 UKB 诊断专用工具
"""
import os, sys, json, time, re
from datetime import datetime
from agent_loop import BaseHandler, StepOutcome, try_call_generator
from ga import (GenericAgentHandler, smart_format, file_read, file_patch,
log_memory_access, code_run, ask_user, format_error,
driver as tmwd_driver, _ensure_driver_ready as tmwd_ensure,
web_scan as tmwd_web_scan, web_execute_js as tmwd_web_execute_js,
cdp_execute, ensure_cdp_config, is_cdp_available)
# ============ 工具后端导入 ============
from tools.clinical_tools import ClinicalDataLoader
from tools.genomic_tools import GenomicAnalyzer
from tools.proteomic_tools import ProteomicAnalyzer
from tools.imaging_tools import ImagingAnalyzer
from tools.integration_tools import MultiOmicsIntegrator
from tools.interpretation import GeneticInterpreter, ProteomicInterpreter, WearableInterpreter
from evolution.episode_writer import EpisodeWriter
from evolution.memory_policy import (
analyze_retrieval_set,
format_retrieval_context,
infer_modality,
infer_task_family,
normalize_episode,
score_memory_for_query,
select_balanced_memories,
)
from evolution.strategy_distiller import StrategyDistiller
from tools.bioinfo_tools import blast_search, david_enrichment, interpro_scan, run_bioinfo_cli
from tools.fundus_tools import flair_fundus_status, flair_fundus_zero_shot
from tools.gene_reference_tools import (
disease_gene_reference_lookup,
gene_reference_lookup,
geneturing_reference_answer_tool,
variant_reference_lookup,
)
from tools.monai_imaging_tools import monai_bundle_command, monai_bundle_status, run_monai_bundle_command
from tools.protein_reference_tools import esm_local_variant_effect_score, esm_variant_effect_status
from tools.skin_tools import skin_isic_efficientnet_classify, skin_isic_efficientnet_status
from tools.task_tool_router import execute_tool as execute_task_specific_tool
from tools.task_tool_router import route_tools as route_task_specific_tools
from tools.health_data_store import HealthDataStore
from tools.wearable_tools import WearableDataStore
from tools.wearable_importer import WearableDataImporter
_DIAGNOSIS_KEYWORDS = [
'患者', '诊断', '基因', '变异', 'PRS', '蛋白', '影像', '病理', '风险评估',
'UKB', 'eid', 'BRCA', 'APOE', 'load_patient', 'submit_diagnosis',
'癌', '瘤', '综合征', '遗传', '突变', '致病', 'ClinVar',
'病历', '质控', 'ICD', '编码', '编目', '病案首页', '用药审查',
'临床路径', '路径偏差', '医保预审', '合规审查', '审阅材料', '证据分级',
'影像', 'CT', 'MRI', '超声', 'X光', 'PET', '内镜', '结节', '磁共振',
]
_PROFESSIONAL_KEYWORDS = [
'BLAST', 'DAVID', 'InterPro', 'eQTL', '通路', 'pathway', '组学',
'pipeline', 'bioinformatics', '基因组', '转录组', '蛋白质组',
'openclaw', 'CRISPR', '单细胞', 'spatial',
]
def _query_needs_diagnosis(query):
"""检查用户查询是否涉及诊断/医学分析场景"""
q = query.lower()
return any(kw.lower() in q for kw in _DIAGNOSIS_KEYWORDS)
def _query_needs_professional(query):
"""检查用户查询是否涉及专业生信/OpenClaw工具"""
q = query.lower()
return any(kw.lower() in q for kw in _PROFESSIONAL_KEYWORDS)
def _load_l1_sections(path, sections=None):
"""按 section 加载 L1 索引的指定区域,sections=None 时加载全部"""
if not os.path.exists(path):
return ""
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
if sections is None:
return content
result_lines = []
current_section = None
for line in content.split('\n'):
if line.strip().startswith('## ['):
sec_name = line.strip().lstrip('#').strip().strip('[]')
current_section = sec_name
if current_section and any(s.upper() in current_section.upper() for s in sections):
result_lines.append(line)
elif current_section is None:
result_lines.append(line)
return '\n'.join(result_lines)
def get_global_memory(user_query=""):
"""按需加载全局记忆:根据用户查询意图决定注入哪些记忆区域,节省 token。
- 始终注入: 记忆结构说明、L1 的 RULES/HEALTH_MANAGEMENT/USER_PROFILE/INFRA/PHONE CONTROL/PHONE_APP_FLOWS
- 诊断场景追加: L1 疾病映射(HIGH-FREQ/MID-FREQ/LOW-FREQ) + 策略建议
- 专业场景追加: OpenClaw 技能索引
"""
prompt = "\n"
try:
with open('assets/insight_fixed_structure.txt', 'r', encoding='utf-8') as f:
structure = f.read()
prompt += f"\n[Memory]\n"
prompt += f'cwd = {os.path.abspath("./temp")} (用./引用)\n'
prompt += structure + '\n'
except FileNotFoundError:
pass
l1_path = 'memory/L1_disease_insight.txt'
always_sections = ['RULES', 'HEALTH_MANAGEMENT', 'USER_PROFILE', 'USER_TAGS',
'INFRA', 'PHONE CONTROL', 'PHONE_APP_FLOWS']
l1_core = _load_l1_sections(l1_path, always_sections)
if l1_core.strip():
prompt += f"../memory/L1_disease_insight.txt (核心区域):\n{l1_core}\n"
if _query_needs_diagnosis(user_query):
diagnosis_sections = ['HIGH-FREQ MAPPING', 'MID-FREQ MAPPING', 'LOW-FREQ KEYWORDS']
l1_diag = _load_l1_sections(l1_path, diagnosis_sections)
if l1_diag.strip():
prompt += f"\n[诊断模式 — 疾病索引已加载]\n{l1_diag}\n"
strategy_path = 'memory/L4_episodes/disease_strategy.json'
if os.path.exists(strategy_path):
try:
with open(strategy_path, 'r', encoding='utf-8') as f:
strategy = json.load(f)
if strategy:
prompt += "\n[Strategy Hints - 从历史病例蒸馏的工具使用策略]\n"
for disease, s in list(strategy.items())[:10]:
prompt += f" {disease} ({s.get('total_cases',0)}例): "
for tool, u in s.get('tool_utility', {}).items():
prompt += f"{tool}={u.get('recommendation','?')}({u.get('usefulness_rate',0):.0%}) "
prompt += "\n"
except (json.JSONDecodeError, KeyError):
pass
if _query_needs_professional(user_query):
openclaw_index = 'memory/L1_openclaw_skills_index.md'
if os.path.exists(openclaw_index):
try:
with open(openclaw_index, 'r', encoding='utf-8') as f:
oc_content = f.read()
prompt += "\n[OpenClaw Skills Index - 869 个医疗 AI 技能索引,按组检索 L3 SOP]\n"
prompt += oc_content + "\n"
except Exception:
pass
return prompt
MAX_BROWSER_ELEMENTS = 20
_BROWSER_FALLBACK_MSG = (
"篡改猴/TMWebDriver 未连接,browser_* 系列工具不可用。\n"
"替代方案:使用 web_search 获取搜索结果,或 browse_and_learn(url=...) 直接读取页面内容。"
)
def _quick_browser_check():
"""2 秒内快速检查 TMWebDriver 是否可用,避免长时间等待卡死。
返回 (ok: bool, error_msg: str|None)"""
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
result = s.connect_ex(('127.0.0.1', 18766))
s.close()
if result != 0:
return False, _BROWSER_FALLBACK_MSG
except Exception:
return False, _BROWSER_FALLBACK_MSG
if tmwd_driver is None:
return False, _BROWSER_FALLBACK_MSG
try:
sessions = tmwd_driver.get_all_sessions()
if not sessions:
return False, _BROWSER_FALLBACK_MSG + "\n(TMWebDriver 已连接但无活跃浏览器标签页)"
except Exception:
return False, _BROWSER_FALLBACK_MSG
return True, None
def _tmwd_navigate_and_get_elements(url):
"""用 TMWebDriver(篡改猴)跳转并返回 title + 可点击元素列表,与 pc-agent-loop 一致"""
ok, err = tmwd_ensure(timeout=10)
if not ok:
return {"status": "error", "msg": err}
try:
tmwd_driver.jump(url, timeout=10)
time.sleep(1.5)
js = """return JSON.stringify({
title: document.title,
url: location.href,
elements: [].slice.call(document.querySelectorAll('a, button, [onclick]'))
.map(function(e,i){ return {index: i, tag: e.tagName.toLowerCase(),
text: (e.innerText||e.textContent||'').slice(0,80), href: (e.href||'') }; })
});"""
out = tmwd_web_execute_js(js)
if out.get("status") != "success":
return {"status": "error", "msg": out.get("error", "JS 执行失败")}
raw = out.get("js_return") or out.get("data")
data = json.loads(raw) if isinstance(raw, str) else raw
elements = data.get("elements", [])
return {"status": "ok", "title": data.get("title", ""), "url": data.get("url", ""),
"elements": elements, "element_count": len(elements)}
except Exception as e:
return {"status": "error", "msg": str(e)}
class UKBAgentHandler(GenericAgentHandler):
"""UKB 诊断 Agent Handler,继承 GenericAgentHandler 的基础工具,新增医学诊断工具"""
def __init__(self, parent, last_history=None, cwd='./', autonomous=False):
super().__init__(parent, last_history, cwd)
self.autonomous = autonomous
self.current_patient = None
self.current_disease = None
self.evidence_buffer = []
self.tool_call_log = []
self.confidence_before_tools = None
self.clinical_loader = ClinicalDataLoader()
self.genomic_analyzer = GenomicAnalyzer()
self.proteomic_analyzer = ProteomicAnalyzer()
self.imaging_analyzer = ImagingAnalyzer()
self.integrator = MultiOmicsIntegrator()
self.genetic_interp = GeneticInterpreter()
self.proteomic_interp = ProteomicInterpreter()
self.episode_writer = EpisodeWriter()
self.strategy_distiller = StrategyDistiller()
self.wearable_store = WearableDataStore()
self.wearable_importer = WearableDataImporter(self.wearable_store)
self.wearable_interp = WearableInterpreter()
self.health_store = HealthDataStore()
# ================================================================
# 数据加载工具
# ================================================================
def do_load_patient(self, args, response):
"""加载患者基础临床数据,返回自然语言摘要"""
eid = args.get("eid", "")
disease_context = args.get("disease_context", "")
if not eid:
return StepOutcome({"status": "error", "msg": "缺少 eid 参数"},
next_prompt=self._get_anchor_prompt())
self.current_patient = eid
self.current_disease = disease_context
self.evidence_buffer = []
self.tool_call_log = []
yield f"[Action] Loading patient {eid} clinical data...\n"
try:
clinical_data = self.clinical_loader.load(eid)
summary = self.clinical_loader.format_summary(clinical_data)
except Exception as e:
summary = f"[模拟数据] 患者 {eid} 的临床数据加载失败({e}),请检查数据路径配置。"
clinical_data = {"eid": eid, "status": "simulated"}
self.evidence_buffer.append({"source": "clinical", "data": clinical_data})
self.tool_call_log.append(("load_patient", 0.01))
yield f"[Result]\n{summary}\n"
return StepOutcome(
data={"status": "success", "summary": summary},
next_prompt=self._get_anchor_prompt() +
f"\n[患者已加载] 基础临床数据如上。请分析是否需要进一步检索基因或蛋白数据。"
)
# ================================================================
# 基因组查询工具
# ================================================================
def do_query_genetic_risk(self, args, response):
"""查询患者特定基因区域的变异信息或计算PRS"""
if not self.current_patient:
return StepOutcome({"status": "error", "msg": "请先 load_patient"},
next_prompt=self._get_anchor_prompt())
mode = args.get("query_mode", "by_gene")
targets = args.get("targets", [])
yield f"[Action] Querying genetic data: mode={mode}, targets={targets}\n"
try:
if mode == "by_gene":
result = self.genomic_analyzer.query_gene_variants(self.current_patient, targets)
interpreted = self.genetic_interp.interpret_variants(result)
elif mode == "by_pathway":
result = self.genomic_analyzer.query_pathway(self.current_patient, targets)
interpreted = self.genetic_interp.interpret_pathway(result)
elif mode == "by_prs":
disease = targets[0] if targets else self.current_disease
result = self.genomic_analyzer.calculate_prs(self.current_patient, disease)
interpreted = self.genetic_interp.interpret_prs(result)
else:
interpreted = f"未知查询模式: {mode}"
result = {}
except Exception as e:
interpreted = f"[基因组查询异常] {e}"
result = {"status": "error", "msg": str(e)}
self.evidence_buffer.append({"source": "genetic", "mode": mode, "data": result})
self.tool_call_log.append(("query_genetic_risk", 0.2))
yield f"[Result]\n{interpreted}\n"
return StepOutcome(
data={"status": "success", "result": interpreted},
next_prompt=self._get_anchor_prompt()
)
# ================================================================
# 蛋白质组查询工具
# ================================================================
def do_query_proteomics(self, args, response):
"""查询患者特定蛋白质的表达水平"""
if not self.current_patient:
return StepOutcome({"status": "error", "msg": "请先 load_patient"},
next_prompt=self._get_anchor_prompt())
proteins = args.get("proteins", [])
return_format = args.get("return_format", "z_score")
yield f"[Action] Querying proteomic data: {proteins}\n"
try:
result = self.proteomic_analyzer.query(self.current_patient, proteins, return_format)
interpreted = self.proteomic_interp.interpret_panel(result)
except Exception as e:
interpreted = f"[蛋白质组查询异常] {e}"
result = {"status": "error", "msg": str(e)}
self.evidence_buffer.append({"source": "proteomic", "data": result})
self.tool_call_log.append(("query_proteomics", 0.05))
yield f"[Result]\n{interpreted}\n"
return StepOutcome(
data={"status": "success", "result": interpreted},
next_prompt=self._get_anchor_prompt()
)
# ================================================================
# 影像详情查询工具
# ================================================================
def do_query_imaging_detail(self, args, response):
"""查询患者影像数据的详细特征"""
if not self.current_patient:
return StepOutcome({"status": "error", "msg": "请先 load_patient"},
next_prompt=self._get_anchor_prompt())
modality = args.get("modality", "")
yield f"[Action] Querying imaging detail: {modality}\n"
try:
result = self.imaging_analyzer.query(self.current_patient, modality)
interpreted = result.get("summary", str(result))
except Exception as e:
interpreted = f"[影像查询异常] {e}"
result = {"status": "error", "msg": str(e)}
self.evidence_buffer.append({"source": "imaging", "modality": modality, "data": result})
self.tool_call_log.append(("query_imaging_detail", 0.1))
yield f"[Result]\n{interpreted}\n"
return StepOutcome(
data={"status": "success", "result": interpreted},
next_prompt=self._get_anchor_prompt()
)
def do_route_task_specific_tools(self, args, response):
"""按当前任务类型选择专业工具候选,不执行未配置模型。"""
task_key = args.get("task_key", "") or args.get("task_name", "")
case_metadata = args.get("case_metadata", {})
if not isinstance(case_metadata, dict):
case_metadata = {}
max_tool_calls = int(args.get("max_tool_calls", 3) or 3)
yield f"[Action] route_task_specific_tools: task_key={task_key}\n"
try:
result = route_task_specific_tools(
task_key=task_key,
case=case_metadata,
max_tool_calls=max_tool_calls,
)
except Exception as e:
result = {
"status": "error",
"msg": str(e),
"input_visible_only": True,
"gold_label_used": False,
"future_cases_used": False,
}
self.tool_call_log.append(("route_task_specific_tools", 0.01))
selected = result.get("selected_tools") or []
family = result.get("task_family", "unknown")
yield f"[Result]\ntask_family={family}; selected_tools={json.dumps(selected, ensure_ascii=False)}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_execute_task_specific_tool(self, args, response):
"""执行 route_task_specific_tools 返回的已实现专业工具。"""
tool_name = args.get("tool_name", "")
task_key = args.get("task_key", "") or args.get("task_name", "")
case_metadata = args.get("case_metadata", {})
if not isinstance(case_metadata, dict):
case_metadata = {}
yield f"[Action] execute_task_specific_tool: tool={tool_name}, task_key={task_key}\n"
try:
result = execute_task_specific_tool(tool_name=tool_name, case=case_metadata, task_key=task_key)
except Exception as e:
result = {
"status": "error",
"tool_name": tool_name,
"msg": str(e),
"input_visible_only": True,
"gold_label_used": False,
"future_cases_used": False,
}
self.tool_call_log.append((f"execute_task_specific_tool:{tool_name}", 0.05))
top = result.get("top_label_candidate") or {}
yield (
f"[Result]\nstatus={result.get('status')}; "
f"evidence_strength={result.get('evidence_strength')}; "
f"recommended_use={result.get('recommended_use')}; "
f"top_label={json.dumps(top, ensure_ascii=False)}\n"
)
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_flair_fundus_status(self, args, response):
"""检查 FLAIR 眼底 foundation model wrapper 是否可用。"""
source_dir = args.get("source_dir", "")
model_id = args.get("model_id", "jusiro2/FLAIR")
yield "[Action] FLAIR fundus status\n"
try:
result = flair_fundus_status(source_dir=source_dir or None, model_id=model_id)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
self.tool_call_log.append(("flair_fundus_status", 0.01))
yield f"[Result]\nstatus={result.get('status')}; import={result.get('flair_import_available')}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_flair_fundus_zero_shot(self, args, response):
"""使用 FLAIR 对当前可见眼底图像做 zero-shot evidence scoring。"""
image_path = args.get("image_path", "")
label_options = args.get("label_options", [])
source_dir = args.get("source_dir", "")
model_id = args.get("model_id", "jusiro2/FLAIR")
prompt_mode = args.get("prompt_mode", "domain_knowledge")
yield f"[Action] FLAIR fundus zero-shot: image_path={image_path}\n"
try:
result = flair_fundus_zero_shot(
image_path=image_path,
label_options=label_options if isinstance(label_options, list) else [],
source_dir=source_dir or None,
model_id=model_id,
prompt_mode=prompt_mode,
)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
self.tool_call_log.append(("flair_fundus_zero_shot", 0.05))
top = result.get("top_label_candidate") or {}
yield f"[Result]\nstatus={result.get('status')}; top_label={json.dumps(top, ensure_ascii=False)}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_skin_isic_efficientnet_status(self, args, response):
"""检查 ISIC2019 EfficientNet-B1 skin lesion classifier 是否可用。"""
model_dir = args.get("model_dir", "")
yield "[Action] Skin ISIC EfficientNet status\n"
try:
result = skin_isic_efficientnet_status(model_dir=model_dir or None)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
self.tool_call_log.append(("skin_isic_efficientnet_status", 0.01))
yield f"[Result]\nstatus={result.get('status')}; weights={result.get('weights_exist')}; timm={result.get('timm_available')}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_skin_isic_efficientnet_classify(self, args, response):
"""使用真实 ISIC2019 EfficientNet-B1 模型对当前可见皮肤病变图像做分类证据评分。"""
image_path = args.get("image_path", "")
label_options = args.get("label_options", [])
model_dir = args.get("model_dir", "")
yield f"[Action] Skin ISIC EfficientNet classify: image_path={image_path}\n"
try:
result = skin_isic_efficientnet_classify(
image_path=image_path,
label_options=label_options if isinstance(label_options, list) else [],
model_dir=model_dir or None,
)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
self.tool_call_log.append(("skin_isic_efficientnet_classify", 0.05))
top = result.get("top_label_candidate") or {}
yield f"[Result]\nstatus={result.get('status')}; top_label={json.dumps(top, ensure_ascii=False)}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_monai_bundle_status(self, args, response):
"""检查真实 MONAI Model Zoo bundle 是否已安装/下载。"""
bundle_key = args.get("bundle_key", "")
bundle_dir = args.get("bundle_dir", "")
yield f"[Action] MONAI bundle status: {bundle_key}\n"
try:
result = monai_bundle_status(bundle_key=bundle_key, bundle_dir=bundle_dir or None)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
summary = (
f"status={result.get('status')}; monai_installed={result.get('monai_installed')}; "
f"bundle_root={result.get('bundle_root')}; weights={result.get('model_weights_exist')}; "
f"config={result.get('inference_config_exists')}"
)
self.tool_call_log.append(("monai_bundle_status", 0.01))
yield f"[Result]\n{summary}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_esm_variant_effect_status(self, args, response):
"""检查 FAIR ESM variant-effect wrapper 是否可用。"""
source_dir = args.get("source_dir", "")
model_name = args.get("model_name", "esm2_t6_8M_UR50D")
yield "[Action] ESM variant-effect status\n"
try:
result = esm_variant_effect_status(source_dir=source_dir or None, model_name=model_name)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
self.tool_call_log.append(("esm_variant_effect_status", 0.01))
yield f"[Result]\nstatus={result.get('status')}; source_dir={result.get('source_dir')}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_esm_local_variant_effect_score(self, args, response):
"""使用 FAIR ESM 对当前可见蛋白局部窗口做 missense variant effect scoring。"""
mutation = args.get("mutation", "")
local_window = args.get("local_window", "")
visible_input = args.get("visible_input", "")
source_dir = args.get("source_dir", "")
model_name = args.get("model_name", "esm2_t6_8M_UR50D")
yield f"[Action] ESM local variant effect: mutation={mutation or 'auto'}\n"
try:
result = esm_local_variant_effect_score(
mutation=mutation,
local_window=local_window,
visible_input=visible_input,
source_dir=source_dir or None,
model_name=model_name,
)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
self.tool_call_log.append(("esm_local_variant_effect_score", 0.05))
top = result.get("top_label_candidate") or {}
yield f"[Result]\nstatus={result.get('status')}; delta={result.get('delta_log_prob_mt_minus_wt')}; top={json.dumps(top, ensure_ascii=False)}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_monai_bundle_command(self, args, response):
"""生成官方 MONAI bundle inference 命令,不自动执行。"""
bundle_key = args.get("bundle_key", "")
output_dir = args.get("output_dir", "outputs/monai_bundle")
bundle_dir = args.get("bundle_dir", "")
dataset_dir = args.get("dataset_dir", "")
extra_overrides = args.get("extra_overrides", [])
yield f"[Action] Build MONAI bundle command: {bundle_key}\n"
try:
result = monai_bundle_command(
bundle_key=bundle_key,
output_dir=output_dir,
bundle_dir=bundle_dir or None,
dataset_dir=dataset_dir or None,
extra_overrides=extra_overrides if isinstance(extra_overrides, list) else [],
)
except Exception as e:
result = {"status": "error", "msg": str(e), "input_visible_only": True}
self.tool_call_log.append(("monai_bundle_command", 0.01))
yield f"[Result]\nstatus={result.get('status')}; command={result.get('command')}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
# ================================================================
# 多组学整合分析工具
# ================================================================
def do_gene_protein_integration(self, args, response):
"""对已检索的基因和蛋白数据进行整合分析"""
gene_ev = [e for e in self.evidence_buffer if e["source"] == "genetic"]
prot_ev = [e for e in self.evidence_buffer if e["source"] == "proteomic"]
if not gene_ev or not prot_ev:
return StepOutcome(
{"status": "error", "msg": "需要先同时查询基因和蛋白数据才能做整合分析"},
next_prompt=self._get_anchor_prompt()
)
yield f"[Action] Integrating gene-protein evidence...\n"
try:
result = self.integrator.integrate(gene_ev, prot_ev)
interpreted = result.get("summary", str(result))
except Exception as e:
interpreted = f"[整合分析异常] {e}"
result = {"status": "error"}
self.tool_call_log.append(("gene_protein_integration", 0.5))
yield f"[Result]\n{interpreted}\n"
return StepOutcome(
data={"status": "success", "result": interpreted},
next_prompt=self._get_anchor_prompt()
)
# ================================================================
# 情景记忆工具
# ================================================================
def do_import_wearable_data(self, args, response):
eid = args.get("eid") or self.current_patient
source_type = args.get("source_type", "xiaomi_export_dir")
path = args.get("path", "")
if not eid or not path:
return StepOutcome({"status": "error", "msg": "Need eid and path"}, next_prompt=self._get_anchor_prompt())
self.current_patient = eid
yield f"[Action] Importing wearable data: {source_type} from {path}\n"
result = self.wearable_importer.import_source(eid, source_type, path)
interpreted = self.wearable_interp.summarize_import(result)
if result.get("status") == "success":
self.tool_call_log.append(("import_wearable_data", 0.03))
yield f"[Result]\n{interpreted}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_query_wearable_trend(self, args, response):
eid = args.get("eid") or self.current_patient
period = args.get("period", "30d")
if not eid:
return StepOutcome({"status": "error", "msg": "Need eid or load_patient first"}, next_prompt=self._get_anchor_prompt())
result = self.wearable_store.query_trend(eid, period)
if result.get("status") == "success":
self.evidence_buffer = [e for e in self.evidence_buffer if e.get("source") != "wearable_trend"]
self.evidence_buffer.append({
"source": "wearable_trend",
"period": period,
"aggregated": result.get("aggregated", {}),
"baselines": result.get("baselines", {}),
"quality": result.get("quality", {}),
"raw": result,
})
self.tool_call_log.append(("query_wearable_trend", 0.05))
interpreted = self.wearable_interp.summarize_status(result, focus="general")
yield f"[Result]\n{interpreted}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_summarize_wearable_status(self, args, response):
eid = args.get("eid") or self.current_patient
period = args.get("period", "30d")
focus = args.get("focus", "general")
if not eid:
return StepOutcome({"status": "error", "msg": "Need eid or load_patient first"}, next_prompt=self._get_anchor_prompt())
result = self.wearable_store.query_trend(eid, period)
interpreted = self.wearable_interp.summarize_status(result, focus=focus)
if result.get("status") == "success":
self.tool_call_log.append(("summarize_wearable_status", 0.08))
yield f"[Result]\n{interpreted}\n"
return StepOutcome(data={"status": result.get("status"), "summary": interpreted, "trend": result}, next_prompt=self._get_anchor_prompt())
def do_detect_wearable_anomalies(self, args, response):
eid = args.get("eid") or self.current_patient
lookback_days = int(args.get("lookback_days", 30))
sensitivity = args.get("sensitivity", "medium")
if not eid:
return StepOutcome({"status": "error", "msg": "Need eid or load_patient first"}, next_prompt=self._get_anchor_prompt())
result = self.wearable_store.detect_anomalies(eid, lookback_days, sensitivity)
interpreted = self.wearable_interp.summarize_anomalies(result.get("anomalies", []), lookback_days, sensitivity)
if result.get("status") == "success":
self.tool_call_log.append(("detect_wearable_anomalies", 0.1))
self.evidence_buffer = [e for e in self.evidence_buffer if e.get("source") != "wearable_anomalies"]
self.evidence_buffer.append({"source": "wearable_anomalies", "raw": result, "anomalies": result.get("anomalies", [])})
yield f"[Result]\n{interpreted}\n"
return StepOutcome(data=result, next_prompt=self._get_anchor_prompt())
def do_integrate_clinical_wearable(self, args, response):
eid = args.get("eid") or self.current_patient
period = args.get("period", "30d")
if not eid:
return StepOutcome({"status": "error", "msg": "Need eid or load_patient first"}, next_prompt=self._get_anchor_prompt())
clinical = None
wearable = None
for ev in self.evidence_buffer:
if ev.get("source") == "clinical":
clinical = ev.get("data")
if ev.get("source") == "wearable_trend":
wearable = ev.get("raw")
if wearable is None:
wearable = self.wearable_store.query_trend(eid, period)
findings = self._analyze_clinical_wearable_correlation(clinical or {}, wearable)
interpreted = self.wearable_interp.summarize_integration(findings, wearable)
if wearable.get("status") == "success":
self.tool_call_log.append(("integrate_clinical_wearable", 0.3))
yield f"[Result]\n{interpreted}\n"
return StepOutcome(data={"status": "success", "findings": findings, "summary": interpreted}, next_prompt=self._get_anchor_prompt())
def _analyze_clinical_wearable_correlation(self, clinical, wearable):
agg = wearable.get("aggregated", {}) if isinstance(wearable, dict) else {}
findings = []
vitals = clinical.get("vitals", {}) if isinstance(clinical, dict) else {}
demo = clinical.get("demographics", {}) if isinstance(clinical, dict) else {}
labs = clinical.get("labs", {}) if isinstance(clinical, dict) else {}
systolic_bp = self._safe_number(vitals.get("systolic_bp") or vitals.get("sbp") or clinical.get("systolic_bp"))
bmi = self._safe_number(demo.get("bmi") or clinical.get("bmi"))
hba1c = self._safe_number(labs.get("hba1c") or clinical.get("hba1c"))
resting_hr = self._safe_number(agg.get("night_resting_hr"))
steps = self._safe_number(agg.get("avg_steps"))
sleep_hours = self._safe_number(agg.get("avg_sleep_hours"))
spo2 = self._safe_number(agg.get("avg_spo2"))
if resting_hr is not None and systolic_bp is not None and resting_hr >= 80 and systolic_bp >= 140:
findings.append("Elevated resting heart rate together with hypertension suggests higher cardiovascular strain.")
if steps is not None and bmi is not None and steps < 5000 and bmi >= 28:
findings.append("Low daily activity combined with elevated BMI supports lifestyle-driven metabolic risk.")
if sleep_hours is not None and hba1c is not None and sleep_hours < 6 and hba1c >= 6.5:
findings.append("Short sleep duration may be aggravating glycemic control burden.")
if spo2 is not None and spo2 < 92:
findings.append("Average SpO2 is low enough to justify cardiopulmonary or sleep-disordered breathing review.")
if not findings and agg:
findings.append("Wearable data provides baseline lifestyle context but does not yet trigger a strong rule-based interaction.")
return findings
def _safe_number(self, value):
try:
return float(value)
except (TypeError, ValueError):
return None
def do_recall_similar_cases(self, args, response):
"""从情景记忆中检索相似病例的处理经验(规则匹配,不用向量库)"""
disease = args.get("disease", self.current_disease or "")
key_features = args.get("key_features", [])
query_context = {
"disease": disease,
"key_features": key_features,
"dataset": args.get("dataset", ""),
"modality": args.get("modality", ""),
"task_family": args.get("task_family", ""),
}
if not query_context["modality"]:
query_context["modality"] = infer_modality({
"disease": disease,
"tags": key_features,
"task_name": args.get("task_name", ""),
"dataset": query_context["dataset"],
})
if not query_context["task_family"]:
query_context["task_family"] = infer_task_family({
"disease": disease,
"tags": key_features,
"task_name": args.get("task_name", ""),
"dataset": query_context["dataset"],
"modality": query_context["modality"],
}, dataset=query_context["dataset"], modality=query_context["modality"])
yield f"[Action] Recalling similar cases for: {disease}, features={key_features}\n"
episodes_path = os.path.join(os.path.dirname(__file__),
'memory/L4_episodes/case_episodes.jsonl')
cases = self._rule_based_case_retrieval(
episodes_path, disease, key_features, query_context=query_context
)
retrieval_analysis = analyze_retrieval_set(cases)
if cases:
formatted = self._format_case_memories(cases)
yield f"[Found {len(cases)} similar cases]\n"
else:
formatted = "未找到相似病例经验。建议按 L1 索引推荐的标准流程进行。"
return StepOutcome(
data={
"status": "success",
"cases_count": len(cases),
"label_distribution": retrieval_analysis.get("label_distribution", {}),
"class_bias_warning": retrieval_analysis.get("class_bias_warning", ""),
"conflict_warning_count": len(retrieval_analysis.get("conflict_warnings", [])),
},
next_prompt=self._get_anchor_prompt() + f"\n[情景记忆]\n{formatted}"
)
def _rule_based_case_retrieval(self, episodes_path, disease, key_features, max_results=8,
query_context=None):
if not os.path.exists(episodes_path):
return []
scored = []
query_context = dict(query_context or {})
query_context.setdefault("disease", disease)
query_context.setdefault("key_features", key_features)
try:
with open(episodes_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
ep = normalize_episode(json.loads(line))
score, reasons = score_memory_for_query(ep, query_context)
if score > 0:
ep["_retrieval_reasons"] = reasons
scored.append((score, ep))
except Exception:
return []
return select_balanced_memories(scored, max_results=max_results)
def _format_case_memories(self, cases):
return format_retrieval_context(cases)
# ================================================================
# 诊断提交工具
# ================================================================
def do_submit_diagnosis(self, args, response):
"""提交最终诊断结果,记录推理轨迹"""
diagnosis = args.get("diagnosis", "")
confidence = args.get("confidence", 0.5)
evidence_summary = args.get("evidence_summary", "")
tools_contributed = args.get("tools_contributed", [])
yield f"[Action] Submitting diagnosis: {diagnosis} (confidence={confidence})\n"
trajectory = {
"patient": self.current_patient,
"disease": self.current_disease,
"diagnosis": diagnosis,
"confidence": confidence,
"evidence_summary": evidence_summary,
"tools_used": [t for t, _ in self.tool_call_log],
"tools_contributed": tools_contributed,
"total_cost": sum(c for _, c in self.tool_call_log),
"evidence_count": len(self.evidence_buffer),
"timestamp": time.strftime('%Y-%m-%d %H:%M')
}
traj_dir = os.path.join(os.path.dirname(__file__), 'temp', 'trajectories')
os.makedirs(traj_dir, exist_ok=True)
traj_file = os.path.join(traj_dir, f"{self.current_patient}_{int(time.time())}.json")
with open(traj_file, 'w', encoding='utf-8') as f:
json.dump(trajectory, f, ensure_ascii=False, indent=2)
yield f"[Trajectory saved] {traj_file}\n"
return StepOutcome(data=trajectory, next_prompt=None)
# ================================================================
# 反思写入工具
# ================================================================
def do_write_case_reflection(self, args, response):
"""对当前病例进行反思,写入情景记忆(L4)"""
tools_useful = args.get("tools_useful", {})
key_lesson = args.get("key_lesson", "")
confidence_before = args.get("confidence_before_tools", self.confidence_before_tools or 0.5)
confidence_after = args.get("confidence_after_tools", 0.5)
tags = args.get("tags", [])
episode = {
"episode_id": f"EP_{time.strftime('%Y%m%d')}_{int(time.time()) % 10000:04d}",
"disease": self.current_disease or "",
"patient_profile": args.get("patient_profile", ""),
"tools_used": [t for t, _ in self.tool_call_log],
"tools_useful": tools_useful,
"confidence_change": {
"before_tools": confidence_before,
"after_tools": confidence_after
},
"key_lesson": key_lesson,
"outcome_correct": None,
"tags": tags,
"timestamp": time.strftime('%Y-%m-%d %H:%M')
}
yield f"[Action] Writing case reflection to L4...\n"
self.episode_writer.write(episode)
yield f"[Saved] Episode {episode['episode_id']}\n"
new_count = self.episode_writer.count_since_last_distill()
hint = ""
from config import AGENT_CONFIG
threshold = AGENT_CONFIG.get("distill_threshold", 20)
if new_count >= threshold:
hint = f"\n[SYSTEM] 已积累 {new_count} 个新病例经验(阈值={threshold}),建议调用 distill_experience。"
return StepOutcome(
data={"status": "success", "episode_id": episode["episode_id"]},
next_prompt=self._get_anchor_prompt() + hint
)
# ================================================================
# 经验蒸馏工具
# ================================================================
def do_distill_experience(self, args, response):
"""从情景记忆中蒸馏出策略统计,更新 disease_strategy.json"""
yield f"[Action] Distilling experience from case episodes...\n"
try:
result = self.strategy_distiller.distill()
yield f"[Result] Distilled strategy for {len(result)} diseases\n"
for d, s in result.items():
yield f" {d}: {s.get('total_cases',0)} cases, "
for t, u in s.get('tool_utility', {}).items():
yield f"{t}={u.get('recommendation','?')} "
yield "\n"
except Exception as e:
result = {"error": str(e)}
yield f"[Error] Distillation failed: {e}\n"
prompt = (
self._get_anchor_prompt() +
"\n[蒸馏完成] 请检查策略统计结果。如果发现了新的通用规则,"
"请用 file_patch 更新 L1 的 RULES 部分。"
)
return StepOutcome(data=result, next_prompt=prompt)
# ================================================================
# 生信工具 (BLAST / DAVID / InterProScan / CLI)
# ================================================================
def do_gene_reference_lookup(self, args, response):
"""基因 symbol/alias/Ensembl ID 查询,调用 MyGene.info 和 HGNC REST。"""
term = args.get("term", "") or args.get("query", "")
species = args.get("species", "human")
yield f"[Action] gene_reference_lookup: term={term}, species={species}\n"
try:
out = gene_reference_lookup(term=term, species=species)
except Exception as e:
out = {"status": "error", "msg": str(e), "input_visible_only": True}
hints = out.get("answer_hints", {})
yield f"[Result] status={out.get('status')}; answer_hints={json.dumps(hints, ensure_ascii=False)}\n"
return StepOutcome(data=out, next_prompt=self._get_anchor_prompt())
def do_variant_reference_lookup(self, args, response):
"""rsID 查询,调用 Ensembl REST 和 MyVariant.info。"""
rsid = args.get("rsid", "") or args.get("query", "")
species = args.get("species", "human")
yield f"[Action] variant_reference_lookup: rsid={rsid}, species={species}\n"
try:
out = variant_reference_lookup(rsid=rsid, species=species)
except Exception as e:
out = {"status": "error", "msg": str(e), "input_visible_only": True}
hints = out.get("answer_hints", {})
yield f"[Result] status={out.get('status')}; answer_hints={json.dumps(hints, ensure_ascii=False)}\n"
return StepOutcome(data=out, next_prompt=self._get_anchor_prompt())
def do_disease_gene_reference_lookup(self, args, response):
"""疾病-基因关联查询,调用 Open Targets 和 NCBI E-utilities。"""
disease = args.get("disease", "") or args.get("query", "")
size = int(args.get("size", 10) or 10)
yield f"[Action] disease_gene_reference_lookup: disease={disease}\n"
try:
out = disease_gene_reference_lookup(disease=disease, size=size)
except Exception as e:
out = {"status": "error", "msg": str(e), "input_visible_only": True}
hints = out.get("answer_hints", {})
yield f"[Result] status={out.get('status')}; answer_hints={json.dumps(hints, ensure_ascii=False)}\n"
return StepOutcome(data=out, next_prompt=self._get_anchor_prompt())
def do_geneturing_reference_answer_tool(self, args, response):
"""GeneTuring 样式问题路由到真实基因/变异参考数据库。"""
question = args.get("question", "")
task_name = args.get("task_name", "")
species = args.get("species", "human")
run_remote_alignment = bool(args.get("run_remote_alignment", False))
yield f"[Action] geneturing_reference_answer_tool: task={task_name}\n"
try:
out = geneturing_reference_answer_tool(
question=question,
task_name=task_name,
species=species,
run_remote_alignment=run_remote_alignment,
)
except Exception as e:
out = {"status": "error", "msg": str(e), "input_visible_only": True}
hints = out.get("answer_hints", {})
yield f"[Result] status={out.get('status')}; answer_hints={json.dumps(hints, ensure_ascii=False)}\n"
return StepOutcome(data=out, next_prompt=self._get_anchor_prompt())
def do_blast_search(self, args, response):
"""BLAST 序列相似性搜索,调用 NCBI API。详见 L3 bioinformatics_tools_sop。"""
query = args.get("query", "")
program = args.get("program", "blastp")
database = args.get("database", "swissprot")
evalue = float(args.get("evalue", 1e-5))
yield f"[Action] BLAST: program={program}, db={database}\n"
try:
out = blast_search(query=query, program=program, database=database, evalue=evalue)
except Exception as e:
out = {"status": "error", "msg": str(e)}
if out.get("status") == "success":
hits = out.get("hits", [])
summary = f"共 {len(hits)} 个 hit。前几条: " + "; ".join(
f"{h.get('id','')} {h.get('def','')}" for h in hits[:5]
)
else: