astron-eval 0.0.1

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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/bin/astron-eval.mjs +111 -0
  4. package/package.json +24 -0
  5. package/skills/astron-eval/SKILL.md +60 -0
  6. package/skills/model-evaluation/SKILL.md +180 -0
  7. package/skills/model-evaluation/assets/dimensions//345/206/205/345/256/271/347/233/270/345/205/263/346/200/247/347/273/264/345/272/246.json +20 -0
  8. package/skills/model-evaluation/assets/dimensions//345/206/205/345/256/271/347/262/276/347/241/256/347/273/264/345/272/246.json +19 -0
  9. package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
  10. package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
  11. package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246-/346/227/205/346/270/270/345/207/272/350/241/214.json +20 -0
  12. package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246.json +20 -0
  13. package/skills/model-evaluation/assets/dimensions//345/210/233/346/204/217/346/200/247-/345/220/270/345/274/225/346/200/247/347/273/264/345/272/246.json +21 -0
  14. package/skills/model-evaluation/assets/dimensions//345/210/233/346/226/260/346/200/247/347/273/264/345/272/246.json +20 -0
  15. package/skills/model-evaluation/assets/dimensions//345/256/214/346/225/264/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
  16. package/skills/model-evaluation/assets/dimensions//345/256/214/346/225/264/346/200/247/347/273/264/345/272/246.json +20 -0
  17. package/skills/model-evaluation/assets/dimensions//345/275/242/345/274/217/347/233/270/345/205/263/346/200/247/347/273/264/345/272/246.json +20 -0
  18. package/skills/model-evaluation/assets/dimensions//345/277/240/350/257/232/345/272/246/347/273/264/345/272/246.json +20 -0
  19. package/skills/model-evaluation/assets/dimensions//346/214/207/344/273/244/351/201/265/345/276/252/347/273/264/345/272/246.json +20 -0
  20. package/skills/model-evaluation/assets/dimensions//346/226/207/346/234/254/345/267/256/345/274/202/345/272/246-TER/347/273/264/345/272/246.json +20 -0
  21. package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
  22. package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
  23. package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246-/346/265/201/347/250/213/350/207/252/345/212/250/345/214/226.json +20 -0
  24. package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246.json +21 -0
  25. package/skills/model-evaluation/assets/dimensions//346/240/270/345/277/203/345/205/203/347/264/240/347/273/264/345/272/246.json +20 -0
  26. package/skills/model-evaluation/assets/dimensions//346/240/274/345/274/217/351/201/265/345/276/252/347/273/264/345/272/246.json +19 -0
  27. package/skills/model-evaluation/assets/dimensions//347/211/271/350/211/262/344/272/256/347/202/271/347/273/264/345/272/246.json +20 -0
  28. package/skills/model-evaluation/assets/dimensions//347/224/250/344/276/213/347/272/247/350/257/204/346/265/213/347/273/264/345/272/246/346/250/241/346/235/277.json +25 -0
  29. package/skills/model-evaluation/assets/dimensions//347/233/270/344/274/274/345/272/246-BERTScore/347/273/264/345/272/246.json +20 -0
  30. package/skills/model-evaluation/assets/dimensions//347/233/270/344/274/274/345/272/246-Cosine/347/273/264/345/272/246.json +20 -0
  31. package/skills/model-evaluation/assets/dimensions//347/233/270/344/274/274/345/272/246-ROUGE/347/273/264/345/272/246.json +20 -0
  32. package/skills/model-evaluation/assets/dimensions//347/233/270/345/205/263/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
  33. package/skills/model-evaluation/assets/dimensions//347/233/270/345/205/263/346/200/247/347/273/264/345/272/246.json +21 -0
  34. package/skills/model-evaluation/assets/dimensions//347/262/276/347/241/256/346/200/247-BLUE/347/273/264/345/272/246.json +20 -0
  35. package/skills/model-evaluation/assets/dimensions//347/262/276/347/241/256/346/200/247-COMET/347/273/264/345/272/246.json +20 -0
  36. package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/345/220/210/347/220/206/346/200/247/347/273/264/345/272/246.json +20 -0
  37. package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
  38. package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
  39. package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246-/346/265/201/347/250/213/350/207/252/345/212/250/345/214/226.json +20 -0
  40. package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246.json +21 -0
  41. package/skills/model-evaluation/assets/eval-judge.json +11 -0
  42. package/skills/model-evaluation/assets/experts/business-process-automation.json +71 -0
  43. package/skills/model-evaluation/assets/experts/content-generation.json +75 -0
  44. package/skills/model-evaluation/assets/experts/content-match.json +37 -0
  45. package/skills/model-evaluation/assets/experts/information-analysis.json +87 -0
  46. package/skills/model-evaluation/assets/experts/marketing-digital-human.json +27 -0
  47. package/skills/model-evaluation/assets/experts/personalized-planning.json +87 -0
  48. package/skills/model-evaluation/assets/experts/text-translation.json +103 -0
  49. package/skills/model-evaluation/assets/experts/tourism-travel.json +119 -0
  50. package/skills/model-evaluation/assets/templates/custom-dimension.template.json +30 -0
  51. package/skills/model-evaluation/eval-build.md +281 -0
  52. package/skills/model-evaluation/eval-execute.md +196 -0
  53. package/skills/model-evaluation/eval-init.md +237 -0
  54. package/skills/model-evaluation/processes/dimension-process.md +207 -0
  55. package/skills/model-evaluation/processes/evalset-create-process.md +184 -0
  56. package/skills/model-evaluation/processes/evalset-parse-process.md +171 -0
  57. package/skills/model-evaluation/processes/evalset-supplement-process.md +136 -0
  58. package/skills/model-evaluation/processes/keypoint-process.md +148 -0
  59. package/skills/model-evaluation/processes/python-env-process.md +113 -0
  60. package/skills/model-evaluation/references//344/270/255/351/227/264/344/272/247/347/211/251/350/257/264/346/230/216.md +340 -0
  61. package/skills/model-evaluation/references//345/206/205/347/275/256/346/250/241/346/235/277/350/257/264/346/230/216.md +149 -0
  62. package/skills/model-evaluation/references//350/204/232/346/234/254/345/256/232/344/271/211.md +274 -0
  63. package/skills/model-evaluation/references//350/256/244/350/257/201/346/234/215/345/212/241/346/216/245/345/217/243/350/257/264/346/230/216.md +271 -0
  64. package/skills/model-evaluation/references//350/257/204/346/265/213/346/234/215/345/212/241/346/216/245/345/217/243/350/257/264/346/230/216.md +455 -0
  65. package/skills/model-evaluation/references//350/257/204/346/265/213/347/273/264/345/272/246/350/257/264/346/230/216.md +171 -0
  66. package/skills/model-evaluation/scripts/cfg/eval-auth.cfg +16 -0
  67. package/skills/model-evaluation/scripts/cfg/eval-server.cfg +1 -0
  68. package/skills/model-evaluation/scripts/clients/__init__.py +33 -0
  69. package/skills/model-evaluation/scripts/clients/api_client.py +97 -0
  70. package/skills/model-evaluation/scripts/clients/auth_client.py +96 -0
  71. package/skills/model-evaluation/scripts/clients/http_client.py +199 -0
  72. package/skills/model-evaluation/scripts/clients/oauth_callback.py +397 -0
  73. package/skills/model-evaluation/scripts/clients/token_manager.py +53 -0
  74. package/skills/model-evaluation/scripts/eval_auth.py +588 -0
  75. package/skills/model-evaluation/scripts/eval_dimension.py +240 -0
  76. package/skills/model-evaluation/scripts/eval_set.py +410 -0
  77. package/skills/model-evaluation/scripts/eval_task.py +324 -0
  78. package/skills/model-evaluation/scripts/files/__init__.py +38 -0
  79. package/skills/model-evaluation/scripts/files/file_utils.py +330 -0
  80. package/skills/model-evaluation/scripts/files/streaming.py +245 -0
  81. package/skills/model-evaluation/scripts/utils/__init__.py +128 -0
  82. package/skills/model-evaluation/scripts/utils/constants.py +101 -0
  83. package/skills/model-evaluation/scripts/utils/datetime_utils.py +60 -0
  84. package/skills/model-evaluation/scripts/utils/errors.py +244 -0
  85. package/skills/model-evaluation/scripts/utils/keypoint_prompts.py +73 -0
  86. package/skills/skill-driven-eval/SKILL.md +456 -0
  87. package/skills/skill-driven-eval/agents/grader.md +144 -0
  88. package/skills/skill-driven-eval/eval-viewer/__init__.py +1 -0
  89. package/skills/skill-driven-eval/eval-viewer/generate_report.py +485 -0
  90. package/skills/skill-driven-eval/eval-viewer/viewer.html +767 -0
  91. package/skills/skill-driven-eval/references/schemas.md +282 -0
  92. package/skills/skill-driven-eval/scripts/__init__.py +1 -0
  93. package/skills/skill-driven-eval/scripts/__main__.py +70 -0
  94. package/skills/skill-driven-eval/scripts/aggregate_results.py +681 -0
  95. package/skills/skill-driven-eval/scripts/extract_transcript.py +294 -0
  96. package/skills/skill-driven-eval/scripts/test_aggregate.py +244 -0
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env python3
2
+ """评测任务管理:提交任务、查询状态、轮询结果"""
3
+ import argparse
4
+ import json
5
+ import time
6
+ import requests
7
+ from pathlib import Path
8
+
9
+ from utils import (
10
+ TERMINAL_STATES,
11
+ handle_cli_error,
12
+ )
13
+ from files import (
14
+ load_json,
15
+ save_json,
16
+ load_config_kv,
17
+ )
18
+ from clients import (
19
+ ApiClient,
20
+ TokenManager,
21
+ )
22
+ from eval_dimension import update_config, check_config
23
+
24
+
25
+ # ============================================================================
26
+ # 提交任务
27
+ # ============================================================================
28
+
29
+ def cmd_submit(args):
30
+ """提交评测任务"""
31
+ # 验证文件存在
32
+ for f, desc in [(args.eval_set, "评测集"), (args.eval_dimension, "评测维度"), (args.eval_judge, "评委配置")]:
33
+ if not Path(f).exists():
34
+ raise FileNotFoundError(f"{desc}文件不存在: {f}")
35
+
36
+ # 步骤1:自动填充 judge_id
37
+ update_result = update_config(args.eval_dimension, args.eval_judge, None)
38
+ if not update_result.get("success"):
39
+ raise ValueError(f"填充judge_id失败: {update_result.get('errors')}")
40
+
41
+ # 步骤2:校验维度配置
42
+ check_result = check_config(args.eval_dimension)
43
+ if not check_result.get("success"):
44
+ errors = check_result.get("errors", [])
45
+ raise ValueError(f"维度配置校验失败({len(errors)}个错误): {errors}")
46
+
47
+ # 步骤3:提交任务
48
+ config_result = load_config_kv(args.config)
49
+ if not config_result.get("success"):
50
+ raise ValueError(f"配置文件加载失败: {config_result.get('message')}")
51
+ config = config_result.get("data", {})
52
+
53
+ # 使用 TokenManager 和 ApiClient
54
+ token_manager = TokenManager(args.auth)
55
+ client = ApiClient(token_manager, config.get('base_url', 'http://127.0.0.1:8080'))
56
+
57
+ # 构建请求
58
+ evalset_result = load_json(args.eval_set)
59
+ if not evalset_result.get("success"):
60
+ raise ValueError(f"评测集文件加载失败: {evalset_result.get('message')}")
61
+ evalset_id = evalset_result.get("data", {}).get('dataset')
62
+
63
+ dimensions_result = load_json(args.eval_dimension)
64
+ if not dimensions_result.get("success"):
65
+ raise ValueError(f"维度配置加载失败: {dimensions_result.get('message')}")
66
+ dimensions = dimensions_result.get("data", {})
67
+
68
+ judges_result = load_json(args.eval_judge)
69
+ if not judges_result.get("success"):
70
+ raise ValueError(f"评委配置加载失败: {judges_result.get('message')}")
71
+ judges = judges_result.get("data", {})
72
+
73
+ payload = {
74
+ "apiVersion": "v1",
75
+ "models": [judges] if judges else [],
76
+ "agents": [],
77
+ "spec": {
78
+ "templates": [{
79
+ "name": "模型评测",
80
+ "type": "evaluation",
81
+ "parameters": {"evalset": evalset_id, "eval": dimensions.get("evals")}
82
+ }]
83
+ }
84
+ }
85
+
86
+ task_data = client.post("/open/api/v1/eval/tasks", json=payload)
87
+
88
+ save_json(args.output, {"task_id": task_data.get('id'), "evalset_id": evalset_id})
89
+ return {"task_id": task_data.get('id'), "status": task_data.get('status')}
90
+
91
+
92
+ # ============================================================================
93
+ # 查询状态
94
+ # ============================================================================
95
+
96
+ def check_status(task_id: str, client: ApiClient, output_file: str) -> dict:
97
+ """查询单次任务状态"""
98
+ task_data = client.get(f"/open/api/v1/eval/tasks/{task_id}")
99
+ status = task_data.get('status')
100
+
101
+ result = {"task_id": task_id, "status": status}
102
+
103
+ # 成功时下载报告
104
+ if status == 'Succeeded':
105
+ artifacts = {a['type']: a['url'] for a in task_data.get('artifacts', [])}
106
+ report_url = artifacts.get('report_file')
107
+
108
+ if report_url:
109
+ resp = requests.get(report_url)
110
+ resp.raise_for_status()
111
+ save_json(output_file, resp.json())
112
+
113
+ result["platform_url"] = artifacts.get('platform_page')
114
+ result["report_file"] = output_file if report_url else None
115
+
116
+ return result
117
+
118
+
119
+ def cmd_status(args):
120
+ """查询任务状态"""
121
+ config_result = load_config_kv(args.config)
122
+ if not config_result.get("success"):
123
+ raise ValueError(f"配置文件加载失败: {config_result.get('message')}")
124
+ config = config_result.get("data", {})
125
+
126
+ # 使用 TokenManager 和 ApiClient
127
+ token_manager = TokenManager(args.auth)
128
+ client = ApiClient(token_manager, config.get('base_url', 'http://127.0.0.1:8080'))
129
+
130
+ evaltask_result = load_json(args.evaltask)
131
+ if not evaltask_result.get("success"):
132
+ raise ValueError(f"任务元信息加载失败: {evaltask_result.get('message')}")
133
+ task_id = evaltask_result.get("data", {}).get('task_id')
134
+ if not task_id:
135
+ raise ValueError("评测任务元信息文件中未找到task_id")
136
+
137
+ # 轮询模式
138
+ if args.poll:
139
+ start = time.time()
140
+ while True:
141
+ elapsed = time.time() - start
142
+ if elapsed > args.timeout:
143
+ return {"task_id": task_id, "status": "Timeout", "error": f"轮询超时({args.timeout}秒)"}
144
+
145
+ result_obj = check_status(task_id, client, args.output)
146
+
147
+ if result_obj["status"] in TERMINAL_STATES:
148
+ return result_obj
149
+
150
+ print(json.dumps({"task_id": task_id, "status": result_obj["status"], "elapsed": int(elapsed),
151
+ "message": f"任务执行中,{args.interval}秒后重试..."}, ensure_ascii=False), flush=True)
152
+ time.sleep(args.interval)
153
+
154
+ return check_status(task_id, client, args.output)
155
+
156
+
157
+ # ============================================================================
158
+ # 结果摘要
159
+ # ============================================================================
160
+
161
+ def extract_text_from_content(content: list) -> str:
162
+ """递归提取 content 中的文本"""
163
+ texts = []
164
+ for item in content:
165
+ if item.get('type') == 'paragraph' and item.get('text'):
166
+ texts.append(item['text'])
167
+ elif item.get('type') in ('section',) and item.get('content'):
168
+ texts.extend(extract_text_from_content(item['content']))
169
+ return '\n'.join(texts)
170
+
171
+
172
+ def find_section_by_title(content: list, title: str) -> dict:
173
+ """根据标题查找 section"""
174
+ for item in content:
175
+ if item.get('type') == 'section':
176
+ if item.get('title') == title:
177
+ return item
178
+ if item.get('content'):
179
+ result = find_section_by_title(item['content'], title)
180
+ if result:
181
+ return result
182
+ return None
183
+
184
+
185
+ def find_table_by_title(content: list, title: str) -> list:
186
+ """根据标题查找表格数据"""
187
+ for item in content:
188
+ if item.get('type') == 'table' and title in item.get('title', ''):
189
+ return item.get('dataset', {}).get('source', [])
190
+ if item.get('type') == 'section' and item.get('content'):
191
+ result = find_table_by_title(item['content'], title)
192
+ if result:
193
+ return result
194
+ return []
195
+
196
+
197
+ def cmd_summary(args):
198
+ """生成评测结果摘要"""
199
+ result_file = Path(args.result)
200
+ if not result_file.exists():
201
+ raise FileNotFoundError(f"评测结果文件不存在: {result_file}")
202
+
203
+ load_result = load_json(args.result)
204
+ if not load_result.get("success"):
205
+ raise ValueError(f"评测结果加载失败: {load_result.get('message')}")
206
+ data = load_result.get("data", {})
207
+ output = []
208
+
209
+ # 1. 综合得分 (从顶层 metric.aggregations 中提取)
210
+ aggregations = data.get('metric', {}).get('aggregations', [])
211
+
212
+ if aggregations:
213
+ output.append("## 综合得分")
214
+ output.append("| 模型 | 分类 | 综合得分 |")
215
+ output.append("|------|------|----------|")
216
+
217
+ for agg in aggregations:
218
+ if agg.get('name') == '综合得分':
219
+ for group in agg.get('groups', []):
220
+ model = category = ""
221
+ for g in group.get('group', []):
222
+ if g.get('g') == 'model':
223
+ model = g.get('v', '')
224
+ elif g.get('g') == 'category':
225
+ category = g.get('v', '')
226
+ score = group.get('payload', {}).get('average', 0)
227
+ output.append(f"| {model} | {category} | {score:.2f} |")
228
+ output.append("")
229
+
230
+ # 2. 各维度表现
231
+ summary = data.get('summary', {})
232
+ content = summary.get('content', [])
233
+
234
+ # 查找综合得分表格
235
+ score_table = find_table_by_title(content, '综合得分')
236
+ if score_table and len(score_table) > 1:
237
+ output.append("## 各维度得分")
238
+ headers = score_table[0]
239
+ rows = score_table[1:]
240
+ output.append("| " + " | ".join(headers[:5]) + " |")
241
+ output.append("| " + " | ".join(["---"] * min(5, len(headers))) + " |")
242
+ for row in rows[:10]: # 限制显示前10行
243
+ output.append("| " + " | ".join(str(v) if isinstance(v, (int, float)) else v for v in row[:5]) + " |")
244
+ output.append("")
245
+
246
+ # 查找良好率表格
247
+ good_rate_table = find_table_by_title(content, '良好率')
248
+ if good_rate_table and len(good_rate_table) > 1:
249
+ output.append("## 良好率")
250
+ headers = good_rate_table[0]
251
+ rows = good_rate_table[1:]
252
+ output.append("| " + " | ".join(headers) + " |")
253
+ output.append("| " + " | ".join(["---"] * len(headers)) + " |")
254
+ for row in rows[:5]: # 限制显示前5行
255
+ output.append("| " + " | ".join(f"{v:.1f}" if isinstance(v, float) else str(v) for v in row) + " |")
256
+ output.append("")
257
+
258
+ # 3. 改进建议
259
+ suggestion_section = find_section_by_title(content, '2.3 改进建议')
260
+ if suggestion_section:
261
+ suggestion_text = extract_text_from_content(suggestion_section.get('content', []))
262
+ if suggestion_text:
263
+ output.append("## 改进建议")
264
+ output.append(suggestion_text)
265
+ output.append("")
266
+
267
+ # 4. 在线报告链接
268
+ if args.platform_url:
269
+ output.append("## 在线报告")
270
+ output.append(args.platform_url)
271
+
272
+ return {"summary": "\n".join(output)}
273
+
274
+
275
+ # ============================================================================
276
+ # CLI 入口
277
+ # ============================================================================
278
+
279
+ def main():
280
+ parser = argparse.ArgumentParser(description='评测任务管理')
281
+ subparsers = parser.add_subparsers(dest='command', help='子命令')
282
+
283
+ # submit
284
+ p = subparsers.add_parser('submit', help='提交评测任务')
285
+ p.add_argument('--config', required=True, help='服务配置文件')
286
+ p.add_argument('--auth', required=True, help='鉴权信息文件')
287
+ p.add_argument('--eval_set', required=True, help='评测集标识文件')
288
+ p.add_argument('--eval_dimension', required=True, help='评测维度配置文件')
289
+ p.add_argument('--eval_judge', required=True, help='评委配置文件')
290
+ p.add_argument('--output', required=True, help='评测任务元信息输出文件')
291
+ p.set_defaults(func=cmd_submit)
292
+
293
+ # status
294
+ p = subparsers.add_parser('status', help='查询任务状态')
295
+ p.add_argument('--config', required=True, help='服务配置文件')
296
+ p.add_argument('--auth', required=True, help='鉴权信息文件')
297
+ p.add_argument('--evaltask', required=True, help='评测任务元信息文件')
298
+ p.add_argument('--output', required=True, help='评测报告输出路径')
299
+ p.add_argument('--poll', action='store_true', help='启用自动轮询模式')
300
+ p.add_argument('--interval', type=int, default=30, help='轮询间隔秒数')
301
+ p.add_argument('--timeout', type=int, default=3600, help='轮询超时秒数')
302
+ p.set_defaults(func=cmd_status)
303
+
304
+ # summary
305
+ p = subparsers.add_parser('summary', help='生成评测结果摘要')
306
+ p.add_argument('--result', required=True, help='评测结果文件(evaltask-result.json)')
307
+ p.add_argument('--platform_url', default='', help='在线报告链接(可选)')
308
+ p.set_defaults(func=cmd_summary)
309
+
310
+ args = parser.parse_args()
311
+
312
+ # Python 3.6 兼容:手动检查子命令
313
+ if args.command is None:
314
+ parser.error("请指定子命令: submit, status, summary")
315
+
316
+ try:
317
+ result_obj = args.func(args)
318
+ print(json.dumps(result_obj, ensure_ascii=False))
319
+ except Exception as e:
320
+ handle_cli_error(e)
321
+
322
+
323
+ if __name__ == '__main__':
324
+ main()
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 文件处理模块
4
+
5
+ 包含所有文件操作相关功能:
6
+ - load_json/save_json: JSON 文件读写
7
+ - load_config_yaml/load_config_kv: 配置文件加载
8
+ - load_data: 多格式数据文件加载
9
+ - load_jsonl_stream/load_csv_stream: 流式读取
10
+ - extract_fields/suggest_mapping: 字段映射工具
11
+ """
12
+
13
+ from .file_utils import (
14
+ load_json,
15
+ save_json,
16
+ load_config_yaml,
17
+ load_config_kv,
18
+ load_data,
19
+ extract_fields,
20
+ suggest_mapping,
21
+ )
22
+
23
+ from .streaming import (
24
+ load_jsonl_stream,
25
+ load_csv_stream,
26
+ )
27
+
28
+ __all__ = [
29
+ 'load_json',
30
+ 'save_json',
31
+ 'load_config_yaml',
32
+ 'load_config_kv',
33
+ 'load_data',
34
+ 'extract_fields',
35
+ 'suggest_mapping',
36
+ 'load_jsonl_stream',
37
+ 'load_csv_stream',
38
+ ]
@@ -0,0 +1,330 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 文件工具函数模块
4
+ 统一 JSON/YAML/配置文件的读写操作
5
+ """
6
+ import json
7
+ import csv
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from utils.constants import (
12
+ ERR_FILE_NOT_FOUND,
13
+ ERR_FILE_ENCODING,
14
+ ERR_FILE_PARSE,
15
+ ERR_CONFIG_INVALID,
16
+ REQUIRED_FIELDS,
17
+ OPTIONAL_FIELDS,
18
+ FIELD_PATTERNS,
19
+ CASE_ID_EXACT_MATCH,
20
+ )
21
+ from utils.errors import (
22
+ result,
23
+ FileEncodingError,
24
+ FileParseError,
25
+ ConfigError,
26
+ )
27
+
28
+
29
+ # ============================================================================
30
+ # JSON 文件操作
31
+ # ============================================================================
32
+
33
+ def load_json(path: str, encoding: str = "utf-8") -> Dict[str, Any]:
34
+ """
35
+ 加载 JSON 文件
36
+
37
+ Args:
38
+ path: 文件路径
39
+ encoding: 文件编码(默认 utf-8)
40
+
41
+ Returns:
42
+ 成功: {"success": True, "data": {...}, ...}
43
+ 失败: {"success": False, "code": ..., "message": ...}
44
+
45
+ 注意: D-03 - 文件不存在或无效时返回错误字典,而非 None
46
+ """
47
+ p = Path(path)
48
+
49
+ if not p.exists():
50
+ return result("load", "not_found", f"文件不存在: {path}", code=ERR_FILE_NOT_FOUND)
51
+
52
+ try:
53
+ content = p.read_text(encoding=encoding)
54
+ data = json.loads(content)
55
+ return result("load", "loaded", f"成功加载: {path}", data=data)
56
+ except UnicodeDecodeError:
57
+ return result("load", "encoding_error",
58
+ f"无法使用 {encoding} 编码读取文件: {path}",
59
+ code=ERR_FILE_ENCODING)
60
+ except json.JSONDecodeError as e:
61
+ return result("load", "parse_error",
62
+ f"JSON 解析失败: {path} - {e}",
63
+ code=ERR_FILE_PARSE)
64
+
65
+
66
+ def save_json(path: str, data: Any, encoding: str = "utf-8") -> Dict[str, Any]:
67
+ """
68
+ 保存数据到 JSON 文件
69
+
70
+ Args:
71
+ path: 文件路径
72
+ data: 要保存的数据
73
+ encoding: 文件编码(默认 utf-8)
74
+
75
+ Returns:
76
+ {"success": True, "message": "保存成功", "path": ...}
77
+ """
78
+ p = Path(path)
79
+
80
+ try:
81
+ p.parent.mkdir(parents=True, exist_ok=True)
82
+ content = json.dumps(data, indent=2, ensure_ascii=False)
83
+ p.write_text(content, encoding=encoding)
84
+ return result("save", "saved", f"保存成功: {path}", data={"path": str(p)})
85
+ except Exception as e:
86
+ return result("save", "error", f"保存失败: {e}", success=False)
87
+
88
+
89
+ # ============================================================================
90
+ # 配置文件操作
91
+ # ============================================================================
92
+
93
+ def _parse_simple_yaml(content: str) -> Dict[str, Any]:
94
+ """
95
+ 简单 YAML 解析器(仅支持 key: value 格式)
96
+
97
+ 用于解析项目配置文件,避免依赖 pyyaml 库。
98
+ 支持格式:
99
+ - key: value
100
+ - key: "quoted value"
101
+ - key: 'quoted value'
102
+ - # 注释行
103
+ - 空行
104
+ """
105
+ data = {}
106
+
107
+ for line in content.split('\n'):
108
+ line = line.strip()
109
+
110
+ # 跳过空行和注释
111
+ if not line or line.startswith('#'):
112
+ continue
113
+
114
+ # 解析 key: value
115
+ if ':' in line:
116
+ key, value = line.split(':', 1)
117
+ key = key.strip()
118
+ value = value.strip()
119
+
120
+ # 解析值类型
121
+ if not value:
122
+ data[key] = None
123
+ elif value.startswith('"') and value.endswith('"'):
124
+ data[key] = value[1:-1]
125
+ elif value.startswith("'") and value.endswith("'"):
126
+ data[key] = value[1:-1]
127
+ elif value.lower() == 'true':
128
+ data[key] = True
129
+ elif value.lower() == 'false':
130
+ data[key] = False
131
+ elif value.lower() == 'null':
132
+ data[key] = None
133
+ else:
134
+ # 尝试数字,否则作为字符串
135
+ try:
136
+ if '.' in value:
137
+ data[key] = float(value)
138
+ else:
139
+ data[key] = int(value)
140
+ except ValueError:
141
+ data[key] = value
142
+
143
+ return data
144
+
145
+
146
+ def load_config_yaml(path: str, encoding: str = "utf-8") -> Dict[str, Any]:
147
+ """
148
+ 加载 YAML 配置文件
149
+
150
+ 用于加载 YAML 格式的配置文件(如 eval-auth.cfg 可能是 YAML)。
151
+ 使用内置解析器,无需 pyyaml 依赖。
152
+ """
153
+ p = Path(path)
154
+
155
+ if not p.exists():
156
+ return result("load_config", "not_found", f"配置文件不存在: {path}",
157
+ code=ERR_FILE_NOT_FOUND)
158
+
159
+ try:
160
+ content = p.read_text(encoding=encoding)
161
+ data = _parse_simple_yaml(content)
162
+ if data is None:
163
+ data = {}
164
+ return result("load_config", "loaded", f"配置加载成功: {path}", data=data)
165
+ except Exception as e:
166
+ return result("load_config", "parse_error", f"配置解析失败: {e}",
167
+ code=ERR_CONFIG_INVALID)
168
+ except UnicodeDecodeError:
169
+ return result("load_config", "encoding_error",
170
+ f"无法使用 {encoding} 编码读取文件: {path}",
171
+ code=ERR_FILE_ENCODING)
172
+
173
+
174
+ def load_config_kv(path: str, encoding: str = "utf-8") -> Dict[str, Any]:
175
+ """
176
+ 加载 key:value 格式的配置文件
177
+
178
+ 用于加载服务配置文件(如 eval-server.cfg)。
179
+ 格式为每行一个 key: value 对。
180
+ """
181
+ p = Path(path)
182
+
183
+ if not p.exists():
184
+ return result("load_config", "not_found", f"配置文件不存在: {path}",
185
+ code=ERR_FILE_NOT_FOUND)
186
+
187
+ try:
188
+ config = {}
189
+ for line in p.read_text(encoding=encoding).splitlines():
190
+ if ':' in line:
191
+ key, value = line.split(':', 1)
192
+ config[key.strip()] = value.strip().strip('"')
193
+
194
+ return result("load_config", "loaded", f"配置加载成功: {path}", data=config)
195
+ except UnicodeDecodeError:
196
+ return result("load_config", "encoding_error",
197
+ f"无法使用 {encoding} 编码读取文件: {path}",
198
+ code=ERR_FILE_ENCODING)
199
+
200
+
201
+ # ============================================================================
202
+ # 数据文件操作
203
+ # ============================================================================
204
+
205
+ def load_data(path: str, encoding: str = "utf-8") -> Dict[str, Any]:
206
+ """
207
+ 根据文件类型加载数据
208
+
209
+ 支持: .json, .jsonl, .csv, .xlsx, .xls
210
+
211
+ Returns:
212
+ {"success": True, "data": [...], "format": "jsonl", ...}
213
+ """
214
+ p = Path(path)
215
+
216
+ if not p.exists():
217
+ return result("load_data", "not_found", f"数据文件不存在: {path}",
218
+ code=ERR_FILE_NOT_FOUND)
219
+
220
+ suffix = p.suffix.lower()
221
+
222
+ try:
223
+ if suffix == '.jsonl':
224
+ lines = p.read_text(encoding=encoding).splitlines()
225
+ data = [json.loads(line) for line in lines if line.strip()]
226
+ return result("load_data", "loaded", f"成功加载 {len(data)} 条记录",
227
+ data={"items": data, "format": "jsonl", "total": len(data)})
228
+
229
+ if suffix == '.json':
230
+ content = p.read_text(encoding=encoding)
231
+ data = json.loads(content)
232
+ if not isinstance(data, list):
233
+ data = [data]
234
+ return result("load_data", "loaded", f"成功加载 {len(data)} 条记录",
235
+ data={"items": data, "format": "json", "total": len(data)})
236
+
237
+ if suffix == '.csv':
238
+ import csv
239
+ with open(path, 'r', encoding=encoding) as f:
240
+ reader = csv.DictReader(f)
241
+ data = list(reader)
242
+ return result("load_data", "loaded", f"成功加载 {len(data)} 条记录",
243
+ data={"items": data, "format": "csv", "total": len(data)})
244
+
245
+ if suffix in ('.xlsx', '.xls'):
246
+ try:
247
+ import pandas as pd
248
+ data = pd.read_excel(path).to_dict('records')
249
+ return result("load_data", "loaded", f"成功加载 {len(data)} 条记录",
250
+ data={"items": data, "format": "xlsx", "total": len(data)})
251
+ except ImportError:
252
+ return result("load_data", "error", "处理 Excel 文件需要安装 pandas",
253
+ success=False)
254
+
255
+ return result("load_data", "error", f"不支持的文件格式: {suffix}", success=False)
256
+
257
+ except json.JSONDecodeError as e:
258
+ return result("load_data", "parse_error", f"JSON 解析失败: {e}",
259
+ code=ERR_FILE_PARSE)
260
+ except UnicodeDecodeError:
261
+ return result("load_data", "encoding_error",
262
+ f"无法使用 {encoding} 编码读取文件: {path}",
263
+ code=ERR_FILE_ENCODING)
264
+
265
+
266
+ # ============================================================================
267
+ # 字段映射工具
268
+ # ============================================================================
269
+
270
+ def extract_fields(items: List[dict]) -> Dict[str, Any]:
271
+ """
272
+ 从数据项提取字段信息
273
+ """
274
+ fields = {}
275
+ for item in items[:100]:
276
+ if not isinstance(item, dict):
277
+ continue
278
+ for key, value in item.items():
279
+ if key not in fields:
280
+ fields[key] = {"type": type(value).__name__}
281
+ return fields
282
+
283
+
284
+ def suggest_mapping(fields: Dict) -> Dict[str, Dict]:
285
+ """
286
+ 根据字段名建议映射
287
+
288
+ 返回格式:
289
+ {
290
+ "question": {"source_field": "question", "default": null},
291
+ "answer": {"source_field": "answer", "default": null},
292
+ "model": {"source_field": null, "default": null},
293
+ "case_id": {"source_field": "id", "default": null}
294
+ }
295
+
296
+ 匹配规则:
297
+ 1. 精确匹配优先(字段名完全等于关键词)
298
+ 2. 包含匹配次之(字段名包含关键词,按关键词长度降序优先)
299
+
300
+ 特殊处理:
301
+ - 'id' 字段精确匹配到 case_id(避免 seq_id、user_id 误匹配)
302
+ """
303
+ mapping = {}
304
+ for field_name in fields:
305
+ field_lower = field_name.lower()
306
+
307
+ # 特殊处理:'id' 精确匹配到 case_id
308
+ if field_lower in CASE_ID_EXACT_MATCH and 'case_id' not in mapping:
309
+ mapping['case_id'] = {"source_field": field_name, "default": None}
310
+ continue
311
+
312
+ for target, keywords in FIELD_PATTERNS.items():
313
+ if target in mapping:
314
+ continue
315
+ # 1. 精确匹配
316
+ if field_lower in keywords:
317
+ mapping[target] = {"source_field": field_name, "default": None}
318
+ break
319
+ # 2. 包含匹配,按关键词长度降序优先匹配更精确的关键词
320
+ sorted_keywords = sorted([k for k in keywords if len(k) >= 3], key=len, reverse=True)
321
+ if any(k in field_lower for k in sorted_keywords):
322
+ mapping[target] = {"source_field": field_name, "default": None}
323
+ break
324
+
325
+ # 确保必填字段都有映射条目(即使 source_field 为 null)
326
+ for field in REQUIRED_FIELDS:
327
+ if field not in mapping:
328
+ mapping[field] = {"source_field": None, "default": None}
329
+
330
+ return mapping