@lzhzzzzwill/cofos 1.0.1 → 1.0.4
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/README.md +97 -93
- package/bin/cofos.js +119 -41
- package/package.json +8 -3
- package/requirements.txt +8 -0
- package/runtime/config/config.yaml +313 -0
- package/runtime/config/prompt_templates.yaml +403 -0
- package/runtime/config/qwen3_5_config.md +46 -0
- package/runtime/src/data_processing/__init__.py +31 -0
- package/runtime/src/data_processing/clean_json_records.py +387 -0
- package/runtime/src/data_processing/parse_pdf_text.py +332 -0
- package/runtime/src/data_processing/parse_wos_xlsx.py +255 -0
- package/runtime/src/data_processing/schema_utils.py +56 -0
- package/runtime/src/data_processing/utils.py +74 -0
- package/runtime/src/inference/__init__.py +8 -0
- package/runtime/src/inference/query_router.py +33 -0
- package/runtime/src/inference/run_kg_grounded_inference.py +370 -0
- package/runtime/src/inference/student_adapter_inference.py +520 -0
- package/runtime/src/retrieval/__init__.py +10 -0
- package/runtime/src/retrieval/build_bm25_index.py +382 -0
- package/runtime/src/retrieval/build_faiss_index.py +328 -0
- package/runtime/src/retrieval/retrieve_evidence.py +859 -0
- package/runtime/src/retrieval/text_search.py +60 -0
- package/scripts/chat.py +479 -61
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
COF-ROS-Reasoner: 人工 JSON 清洗脚本
|
|
4
|
+
|
|
5
|
+
目标:
|
|
6
|
+
- 把已有人工 JSON 统一成标准字段
|
|
7
|
+
- 检查 JSON 合法性
|
|
8
|
+
- 统一字段名
|
|
9
|
+
- 标准化 ROS / product 名称
|
|
10
|
+
- 保存坏 JSON 到 error log
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
# 添加 src 路径
|
|
21
|
+
|
|
22
|
+
from data_processing.utils import setup_logging
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JSONCleaner:
|
|
26
|
+
"""清洗和标准化人工标注的 JSON 数据"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: Dict[str, Any]):
|
|
29
|
+
self.config = config
|
|
30
|
+
self.ros_normalization = config.get('data_processing', {}).get('ros_normalization', {})
|
|
31
|
+
self.raw_json_path = Path(config['paths']['raw_json'])
|
|
32
|
+
self.output_path = Path(config['paths']['processed_dir']) / 'json_records_cleaned.jsonl'
|
|
33
|
+
self.error_log_path = Path(config['paths']['outputs_dir']) / 'logs' / 'json_cleaning_errors.log'
|
|
34
|
+
|
|
35
|
+
# 设置日志
|
|
36
|
+
setup_logging(config)
|
|
37
|
+
|
|
38
|
+
# 常用字段映射
|
|
39
|
+
self.field_mappings = {
|
|
40
|
+
'dominant_ros': 'dominant_ros_or_product',
|
|
41
|
+
'secondary_ros': 'secondary_or_intermediate_ros',
|
|
42
|
+
'intermediates': 'mechanism_intermediates',
|
|
43
|
+
'reaction_conditions': 'reaction_environment',
|
|
44
|
+
'material': 'material_context',
|
|
45
|
+
'mechanism_path': 'mechanism',
|
|
46
|
+
'reaction_conditions': 'reaction_environment',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# 验证字段
|
|
50
|
+
self.required_fields = {
|
|
51
|
+
'material_context',
|
|
52
|
+
'reaction_environment',
|
|
53
|
+
'dominant_ros_or_product',
|
|
54
|
+
'secondary_or_intermediate_ros',
|
|
55
|
+
'mechanism_intermediates',
|
|
56
|
+
'mechanism'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
self.material_context_fields = {'material_type', 'active_site', 'structural_feature'}
|
|
60
|
+
self.reaction_environment_fields = {'phase', 'oxidant_source', 'driving_force', 'notes'}
|
|
61
|
+
self.ros_fields = {'species', 'category', 'role', 'basis', 'evidence'}
|
|
62
|
+
self.secondary_ros_fields = {'species', 'role', 'basis', 'evidence'}
|
|
63
|
+
self.intermediate_fields = {'species', 'notation_in_paper', 'role', 'evidence'}
|
|
64
|
+
|
|
65
|
+
def safe_str(self, value: Any) -> str:
|
|
66
|
+
"""Convert arbitrary JSON values to reviewable strings."""
|
|
67
|
+
if value is None:
|
|
68
|
+
return ""
|
|
69
|
+
if isinstance(value, str):
|
|
70
|
+
return value.strip()
|
|
71
|
+
if isinstance(value, (int, float, bool)):
|
|
72
|
+
return str(value)
|
|
73
|
+
return json.dumps(value, ensure_ascii=False)
|
|
74
|
+
|
|
75
|
+
def normalize_ros_name(self, name: str) -> str:
|
|
76
|
+
"""标准化 ROS 名称"""
|
|
77
|
+
if not name:
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
name = name.strip()
|
|
81
|
+
|
|
82
|
+
# 直接匹配
|
|
83
|
+
if name in self.ros_normalization:
|
|
84
|
+
return self.ros_normalization[name]
|
|
85
|
+
|
|
86
|
+
# 忽略大小写匹配
|
|
87
|
+
name_lower = name.lower()
|
|
88
|
+
for key, value in self.ros_normalization.items():
|
|
89
|
+
if key.lower() == name_lower:
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
# 返回原名称(如果无法标准化)
|
|
93
|
+
return name
|
|
94
|
+
|
|
95
|
+
def is_valid_ros_name(self, name: str) -> bool:
|
|
96
|
+
"""检查 ROS 名称是否有效(可以被标准化或本身就是有效名称)"""
|
|
97
|
+
if not name:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
normalized = self.normalize_ros_name(name)
|
|
101
|
+
return normalized != ""
|
|
102
|
+
|
|
103
|
+
def clean_material_context(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
104
|
+
"""清洗 material_context 字段"""
|
|
105
|
+
material = {}
|
|
106
|
+
|
|
107
|
+
# 处理可能的字段名
|
|
108
|
+
raw_material = data.get('material_context', data.get('material', {}))
|
|
109
|
+
|
|
110
|
+
if isinstance(raw_material, dict):
|
|
111
|
+
for field in self.material_context_fields:
|
|
112
|
+
value = raw_material.get(field, '')
|
|
113
|
+
material[field] = self.safe_str(value)
|
|
114
|
+
|
|
115
|
+
return material
|
|
116
|
+
|
|
117
|
+
def clean_reaction_environment(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
118
|
+
"""清洗 reaction_environment 字段"""
|
|
119
|
+
env = {}
|
|
120
|
+
|
|
121
|
+
# 处理可能的字段名
|
|
122
|
+
raw_env = data.get('reaction_environment', data.get('reaction_conditions', data.get('conditions', {})))
|
|
123
|
+
|
|
124
|
+
if isinstance(raw_env, dict):
|
|
125
|
+
for field in self.reaction_environment_fields:
|
|
126
|
+
value = raw_env.get(field, '')
|
|
127
|
+
env[field] = self.safe_str(value)
|
|
128
|
+
|
|
129
|
+
return env
|
|
130
|
+
|
|
131
|
+
def clean_ros_list(self, ros_list: List[Dict[str, Any]], is_dominant: bool = True) -> List[Dict[str, Any]]:
|
|
132
|
+
"""清洗 ROS 列表"""
|
|
133
|
+
if not isinstance(ros_list, list):
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
cleaned = []
|
|
137
|
+
fields = self.ros_fields if is_dominant else self.secondary_ros_fields
|
|
138
|
+
|
|
139
|
+
for item in ros_list:
|
|
140
|
+
if not isinstance(item, dict):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
cleaned_item = {}
|
|
144
|
+
for field in fields:
|
|
145
|
+
value = item.get(field, '')
|
|
146
|
+
cleaned_item[field] = self.safe_str(value)
|
|
147
|
+
|
|
148
|
+
# 标准化 species
|
|
149
|
+
if 'species' in cleaned_item:
|
|
150
|
+
cleaned_item['species'] = self.normalize_ros_name(cleaned_item['species'])
|
|
151
|
+
|
|
152
|
+
# 如果是 dominant ROS,添加 category 字段(如果没有)
|
|
153
|
+
if is_dominant and 'category' not in cleaned_item:
|
|
154
|
+
species = cleaned_item.get('species', '').lower()
|
|
155
|
+
# H2O2 在 2e- ORR 中应为 oxygen-derived target product
|
|
156
|
+
if 'h2o2' in species or 'hydrogen peroxide' in species:
|
|
157
|
+
cleaned_item['category'] = 'oxygen-derived target product'
|
|
158
|
+
else:
|
|
159
|
+
cleaned_item['category'] = 'radical ROS'
|
|
160
|
+
|
|
161
|
+
cleaned.append(cleaned_item)
|
|
162
|
+
|
|
163
|
+
return cleaned
|
|
164
|
+
|
|
165
|
+
def clean_mechanism_intermediates(self, intermediates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
166
|
+
"""清洗 mechanism_intermediates 字段"""
|
|
167
|
+
if not isinstance(intermediates, list):
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
cleaned = []
|
|
171
|
+
for item in intermediates:
|
|
172
|
+
if not isinstance(item, dict):
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
cleaned_item = {}
|
|
176
|
+
for field in self.intermediate_fields:
|
|
177
|
+
value = item.get(field, '')
|
|
178
|
+
cleaned_item[field] = self.safe_str(value)
|
|
179
|
+
|
|
180
|
+
# 标准化 species
|
|
181
|
+
if 'species' in cleaned_item:
|
|
182
|
+
cleaned_item['species'] = self.normalize_ros_name(cleaned_item['species'])
|
|
183
|
+
|
|
184
|
+
cleaned.append(cleaned_item)
|
|
185
|
+
|
|
186
|
+
return cleaned
|
|
187
|
+
|
|
188
|
+
def clean_mechanism(self, mechanism: Any) -> List[str]:
|
|
189
|
+
"""清洗 mechanism 字段"""
|
|
190
|
+
if isinstance(mechanism, list):
|
|
191
|
+
return [self.safe_str(step) for step in mechanism if self.safe_str(step)]
|
|
192
|
+
elif isinstance(mechanism, str):
|
|
193
|
+
if mechanism.strip():
|
|
194
|
+
return [mechanism.strip()]
|
|
195
|
+
elif mechanism:
|
|
196
|
+
return [self.safe_str(mechanism)]
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
def validate_record(self, record: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
|
200
|
+
"""验证清洗后的记录"""
|
|
201
|
+
errors = []
|
|
202
|
+
|
|
203
|
+
# 检查 required fields
|
|
204
|
+
for field in self.required_fields:
|
|
205
|
+
if field not in record:
|
|
206
|
+
errors.append(f"Missing required field: {field}")
|
|
207
|
+
|
|
208
|
+
# 检查 material_context
|
|
209
|
+
material = record.get('material_context', {})
|
|
210
|
+
if not material.get('material_type'):
|
|
211
|
+
errors.append("material_context.material_type is empty")
|
|
212
|
+
|
|
213
|
+
# 检查 reaction_environment
|
|
214
|
+
env = record.get('reaction_environment', {})
|
|
215
|
+
if not env.get('phase'):
|
|
216
|
+
errors.append("reaction_environment.phase is empty")
|
|
217
|
+
|
|
218
|
+
# 检查至少有一个 dominant ROS
|
|
219
|
+
dominant = record.get('dominant_ros_or_product', [])
|
|
220
|
+
if not dominant:
|
|
221
|
+
errors.append("dominant_ros_or_product is empty")
|
|
222
|
+
|
|
223
|
+
# 检查 species 格式
|
|
224
|
+
for item in dominant:
|
|
225
|
+
species = item.get('species', '')
|
|
226
|
+
if species and not self.is_valid_ros_name(species):
|
|
227
|
+
errors.append(f"Invalid ROS species: {species}")
|
|
228
|
+
|
|
229
|
+
return len(errors) == 0, errors
|
|
230
|
+
|
|
231
|
+
def clean_record(self, record: Dict[str, Any]) -> Dict[str, Any]:
|
|
232
|
+
"""清洗单个 JSON 记录"""
|
|
233
|
+
cleaned = {}
|
|
234
|
+
|
|
235
|
+
# 清洗各个字段
|
|
236
|
+
cleaned['material_context'] = self.clean_material_context(record)
|
|
237
|
+
cleaned['reaction_environment'] = self.clean_reaction_environment(record)
|
|
238
|
+
cleaned['dominant_ros_or_product'] = self.clean_ros_list(
|
|
239
|
+
record.get('dominant_ros_or_product', record.get('dominant_ros', [])),
|
|
240
|
+
is_dominant=True
|
|
241
|
+
)
|
|
242
|
+
cleaned['secondary_or_intermediate_ros'] = self.clean_ros_list(
|
|
243
|
+
record.get('secondary_or_intermediate_ros', record.get('secondary_ros', [])),
|
|
244
|
+
is_dominant=False
|
|
245
|
+
)
|
|
246
|
+
cleaned['mechanism_intermediates'] = self.clean_mechanism_intermediates(
|
|
247
|
+
record.get('mechanism_intermediates', [])
|
|
248
|
+
)
|
|
249
|
+
cleaned['mechanism'] = self.clean_mechanism(record.get('mechanism', []))
|
|
250
|
+
|
|
251
|
+
# 添加元数据
|
|
252
|
+
cleaned['_source'] = self.safe_str(record.get('_source', 'unknown'))
|
|
253
|
+
cleaned['_cleaned_at'] = datetime.now().isoformat()
|
|
254
|
+
|
|
255
|
+
return cleaned
|
|
256
|
+
|
|
257
|
+
def process_file(self, file_path: Path) -> Tuple[int, int]:
|
|
258
|
+
"""处理单个 JSON 文件"""
|
|
259
|
+
success_count = 0
|
|
260
|
+
error_count = 0
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
264
|
+
content = f.read()
|
|
265
|
+
|
|
266
|
+
# 尝试解析 JSON
|
|
267
|
+
try:
|
|
268
|
+
data = json.loads(content)
|
|
269
|
+
except json.JSONDecodeError:
|
|
270
|
+
# 可能是 JSONL 格式
|
|
271
|
+
lines = content.strip().split('\n')
|
|
272
|
+
for line in lines:
|
|
273
|
+
if line.strip():
|
|
274
|
+
try:
|
|
275
|
+
data = json.loads(line)
|
|
276
|
+
self.process_single_record(data, file_path)
|
|
277
|
+
success_count += 1
|
|
278
|
+
except json.JSONDecodeError as e:
|
|
279
|
+
self._log_error(file_path, f"Invalid JSON: {e}")
|
|
280
|
+
error_count += 1
|
|
281
|
+
return success_count, error_count
|
|
282
|
+
|
|
283
|
+
# 处理单个记录或记录列表
|
|
284
|
+
if isinstance(data, list):
|
|
285
|
+
for record in data:
|
|
286
|
+
self.process_single_record(record, file_path)
|
|
287
|
+
success_count += 1
|
|
288
|
+
else:
|
|
289
|
+
self.process_single_record(data, file_path)
|
|
290
|
+
success_count += 1
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self._log_error(file_path, str(e))
|
|
294
|
+
error_count += 1
|
|
295
|
+
|
|
296
|
+
return success_count, error_count
|
|
297
|
+
|
|
298
|
+
def process_single_record(self, record: Dict[str, Any], source_file: str = ""):
|
|
299
|
+
"""处理单个记录"""
|
|
300
|
+
try:
|
|
301
|
+
# 添加源文件信息
|
|
302
|
+
if source_file:
|
|
303
|
+
record['_source'] = str(source_file)
|
|
304
|
+
|
|
305
|
+
# 清洗记录
|
|
306
|
+
cleaned = self.clean_record(record)
|
|
307
|
+
|
|
308
|
+
# 验证
|
|
309
|
+
is_valid, errors = self.validate_record(cleaned)
|
|
310
|
+
|
|
311
|
+
if is_valid:
|
|
312
|
+
# 写入输出文件
|
|
313
|
+
with open(self.output_path, 'a', encoding='utf-8') as f:
|
|
314
|
+
f.write(json.dumps(cleaned, ensure_ascii=False) + '\n')
|
|
315
|
+
else:
|
|
316
|
+
# 记录错误
|
|
317
|
+
error_msg = f"Validation errors in {source_file}: {errors}"
|
|
318
|
+
self._log_error(source_file, error_msg)
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
self._log_error(source_file, str(e))
|
|
322
|
+
|
|
323
|
+
def _log_error(self, source: str, message: str):
|
|
324
|
+
"""记录错误到日志文件"""
|
|
325
|
+
error_entry = {
|
|
326
|
+
'timestamp': datetime.now().isoformat(),
|
|
327
|
+
'source': str(source),
|
|
328
|
+
'error': message
|
|
329
|
+
}
|
|
330
|
+
with open(self.error_log_path, 'a', encoding='utf-8') as f:
|
|
331
|
+
f.write(json.dumps(error_entry, ensure_ascii=False) + '\n')
|
|
332
|
+
logging.error(f"Error processing {source}: {message}")
|
|
333
|
+
|
|
334
|
+
def run(self):
|
|
335
|
+
"""运行清洗流程"""
|
|
336
|
+
logging.info(f"Starting JSON cleaning from {self.raw_json_path}")
|
|
337
|
+
logging.info(f"Output will be saved to {self.output_path}")
|
|
338
|
+
|
|
339
|
+
# 清空输出文件
|
|
340
|
+
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
self.error_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
self.output_path.write_text('', encoding='utf-8')
|
|
343
|
+
self.error_log_path.write_text('', encoding='utf-8')
|
|
344
|
+
|
|
345
|
+
total_success = 0
|
|
346
|
+
total_error = 0
|
|
347
|
+
|
|
348
|
+
# 处理所有 JSON 文件
|
|
349
|
+
for file_path in self.raw_json_path.glob('*.json'):
|
|
350
|
+
logging.info(f"Processing {file_path.name}")
|
|
351
|
+
success, error = self.process_file(file_path)
|
|
352
|
+
total_success += success
|
|
353
|
+
total_error += error
|
|
354
|
+
|
|
355
|
+
# 处理 JSONL 文件
|
|
356
|
+
for file_path in self.raw_json_path.glob('*.jsonl'):
|
|
357
|
+
logging.info(f"Processing {file_path.name}")
|
|
358
|
+
success, error = self.process_file(file_path)
|
|
359
|
+
total_success += success
|
|
360
|
+
total_error += error
|
|
361
|
+
|
|
362
|
+
logging.info(f"JSON cleaning completed: {total_success} records cleaned, {total_error} errors")
|
|
363
|
+
logging.info(f"Cleaned records saved to {self.output_path}")
|
|
364
|
+
logging.info(f"Errors logged to {self.error_log_path}")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def main():
|
|
368
|
+
"""主函数"""
|
|
369
|
+
import argparse
|
|
370
|
+
|
|
371
|
+
parser = argparse.ArgumentParser(description='Clean and normalize COF-ROS JSON records')
|
|
372
|
+
parser.add_argument('--config', type=str, default='config/config.yaml',
|
|
373
|
+
help='Path to config file')
|
|
374
|
+
args = parser.parse_args()
|
|
375
|
+
|
|
376
|
+
# 加载配置
|
|
377
|
+
config_path = Path(args.config)
|
|
378
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
379
|
+
config = yaml.safe_load(f)
|
|
380
|
+
|
|
381
|
+
# 运行清洗
|
|
382
|
+
cleaner = JSONCleaner(config)
|
|
383
|
+
cleaner.run()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == '__main__':
|
|
387
|
+
main()
|