@shulex/cxclawctl 0.1.2 → 0.1.3

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 (208) hide show
  1. package/README.md +25 -22
  2. package/dist/cli.js +2202 -127
  3. package/dist/cli.js.map +1 -1
  4. package/package.json +19 -6
  5. package/capabilities.lock.json +0 -898
  6. package/requirements-cli.txt +0 -2
  7. package/skills/__lib__/__init__.py +0 -0
  8. package/skills/__lib__/skill-runtime/bootstrap.py +0 -134
  9. package/skills/__lib__/solvea-api/__init__.py +0 -0
  10. package/skills/__lib__/solvea-api/client.py +0 -1944
  11. package/skills/__lib__/solvea-api/dashboard.py +0 -100
  12. package/skills/__lib__/solvea-api/solvea_api.py +0 -1341
  13. package/skills/agent-analytics/SKILL.md +0 -74
  14. package/skills/agent-analytics/references/chat-report-fields.md +0 -36
  15. package/skills/agent-analytics/references/report-fields.md +0 -130
  16. package/skills/agent-analytics/references/transcript.md +0 -110
  17. package/skills/agent-builder/SKILL.md +0 -42
  18. package/skills/agent-builder/interfaces/fallback-chatflow.md +0 -47
  19. package/skills/agent-builder/references/chatflow/graph-spec.md +0 -259
  20. package/skills/agent-builder/references/chatflow/node-selection.md +0 -89
  21. package/skills/agent-builder/references/chatflow/nodes/agent.md +0 -350
  22. package/skills/agent-builder/references/chatflow/nodes/business-track.md +0 -43
  23. package/skills/agent-builder/references/chatflow/nodes/code.md +0 -157
  24. package/skills/agent-builder/references/chatflow/nodes/collect-order.md +0 -47
  25. package/skills/agent-builder/references/chatflow/nodes/finish.md +0 -34
  26. package/skills/agent-builder/references/chatflow/nodes/human-review.md +0 -60
  27. package/skills/agent-builder/references/chatflow/nodes/if-else.md +0 -100
  28. package/skills/agent-builder/references/chatflow/nodes/intent.md +0 -79
  29. package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +0 -43
  30. package/skills/agent-builder/references/chatflow/nodes/llm.md +0 -117
  31. package/skills/agent-builder/references/chatflow/nodes/note.md +0 -38
  32. package/skills/agent-builder/references/chatflow/nodes/question.md +0 -97
  33. package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +0 -68
  34. package/skills/agent-builder/references/chatflow/nodes/reply.md +0 -97
  35. package/skills/agent-builder/references/chatflow/nodes/start.md +0 -35
  36. package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +0 -48
  37. package/skills/agent-builder/references/chatflow/nodes/tool.md +0 -138
  38. package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +0 -63
  39. package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +0 -52
  40. package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +0 -50
  41. package/skills/agent-builder/references/chatflow/nodes/variable-process.md +0 -70
  42. package/skills/agent-builder/references/chatflow/overview.md +0 -53
  43. package/skills/agent-builder/references/chatflow/visual-translation.md +0 -64
  44. package/skills/agent-builder/references/chatflow/workflow.md +0 -134
  45. package/skills/agent-builder/references/multi-agent/create-update.md +0 -185
  46. package/skills/agent-builder/references/multi-agent/graph-spec.md +0 -246
  47. package/skills/agent-builder/references/multi-agent/graph-structure.md +0 -12
  48. package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +0 -13
  49. package/skills/agent-builder/references/multi-agent/migration/large.md +0 -554
  50. package/skills/agent-builder/references/multi-agent/migration/principles.md +0 -446
  51. package/skills/agent-builder/references/multi-agent/migration/small.md +0 -225
  52. package/skills/agent-builder/references/multi-agent/orchestration.md +0 -11
  53. package/skills/agent-builder/references/multi-agent/overview.md +0 -48
  54. package/skills/agent-builder/references/multi-agent/update.md +0 -128
  55. package/skills/agent-builder/references/multi-agent/workflow.md +0 -87
  56. package/skills/agent-builder/references/single-agent/overview.md +0 -48
  57. package/skills/agent-builder/references/single-agent/prompt.md +0 -16
  58. package/skills/agent-builder/references/single-agent/tools.md +0 -36
  59. package/skills/agent-builder/references/single-agent/voice.md +0 -46
  60. package/skills/agent-builder/references/tools/http-tool.md +0 -315
  61. package/skills/agent-builder/references/tools/mock-tool.md +0 -279
  62. package/skills/agent-builder/references/tools/overview.md +0 -46
  63. package/skills/agent-builder/references/tools/tool-config.md +0 -43
  64. package/skills/agent-builder/references/tools/x-token.md +0 -34
  65. package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +0 -3458
  66. package/skills/agent-builder/scripts/chatflow/validate_graph.py +0 -1697
  67. package/skills/agent-builder/scripts/copy_marketplace_agent.py +0 -61
  68. package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +0 -5974
  69. package/skills/agent-builder/scripts/multi-agent/replay_compare.py +0 -679
  70. package/skills/agent-builder/scripts/patch_agent_tools.py +0 -135
  71. package/skills/agent-builder/scripts/tools/validate_tool_config.py +0 -384
  72. package/skills/agent-tester/SKILL.md +0 -35
  73. package/skills/agent-tester/batch-test/overview.md +0 -86
  74. package/skills/agent-tester/scripts/batch_test_sheet.py +0 -213
  75. package/skills/agent-tester/single-test/badcase-retest.md +0 -85
  76. package/skills/agent-tester/single-test/direct-test.md +0 -24
  77. package/skills/agent-tester/single-test/overview.md +0 -142
  78. package/skills/delivery/SKILL.md +0 -112
  79. package/skills/delivery/interfaces/classify.md +0 -38
  80. package/skills/delivery/interfaces/create-agents.md +0 -89
  81. package/skills/delivery/interfaces/delivery-test.md +0 -67
  82. package/skills/delivery/references/flow.md +0 -40
  83. package/skills/delivery/references/validation.md +0 -15
  84. package/skills/delivery/scripts/batch_test.py +0 -704
  85. package/skills/delivery/scripts/classify.py +0 -22
  86. package/skills/delivery/scripts/create_agents.py +0 -695
  87. package/skills/delivery/scripts/gen_delivery_report.py +0 -335
  88. package/skills/delivery/scripts/gen_excel.py +0 -234
  89. package/skills/delivery/scripts/gen_report.py +0 -208
  90. package/skills/delivery/scripts/plan.py +0 -163
  91. package/skills/delivery/scripts/render.py +0 -713
  92. package/skills/delivery/scripts/render_summary.py +0 -38
  93. package/skills/delivery/scripts/sample_history_cases.py +0 -109
  94. package/skills/kb-builder/SKILL.md +0 -80
  95. package/skills/kb-builder/interfaces/qa-upload.md +0 -55
  96. package/skills/kb-builder/interfaces/web-kb.md +0 -84
  97. package/skills/kb-builder/references/directory.md +0 -373
  98. package/skills/kb-builder/references/export.md +0 -282
  99. package/skills/kb-builder/references/isolation-keywords.md +0 -113
  100. package/skills/kb-builder/references/knowledge.md +0 -153
  101. package/skills/kb-builder/references/product.md +0 -52
  102. package/skills/kb-builder/references/qa-from-help-center.md +0 -58
  103. package/skills/kb-builder/references/qa-from-pdf.md +0 -94
  104. package/skills/kb-builder/references/qa-from-tickets.md +0 -168
  105. package/skills/kb-builder/references/qa-from-web.md +0 -98
  106. package/skills/kb-builder/references/qa-quality.md +0 -73
  107. package/skills/kb-builder/references/reply-template.md +0 -237
  108. package/skills/kb-builder/references/tag-isolation-migration.md +0 -61
  109. package/skills/kb-builder/references/tags.md +0 -45
  110. package/skills/kb-builder/references/upload.md +0 -130
  111. package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +0 -333
  112. package/skills/kb-builder/scripts/build_directory_plan.py +0 -144
  113. package/skills/kb-builder/scripts/crawl_site.py +0 -433
  114. package/skills/kb-builder/scripts/create_dirs.py +0 -146
  115. package/skills/kb-builder/scripts/export_knowledge.py +0 -454
  116. package/skills/kb-builder/scripts/extract_ticket_qa.py +0 -441
  117. package/skills/kb-builder/scripts/extract_web_qa.py +0 -118
  118. package/skills/kb-builder/scripts/import_qa.py +0 -219
  119. package/skills/kb-builder/scripts/import_source_documents.py +0 -143
  120. package/skills/kb-builder/scripts/import_templates.py +0 -409
  121. package/skills/poc/SKILL.md +0 -125
  122. package/skills/poc/assets/default_manual.css +0 -279
  123. package/skills/poc/assets/notice_escalate.png +0 -0
  124. package/skills/poc/assets/notice_escalate2.png +0 -0
  125. package/skills/poc/assets/notice_new_chat.png +0 -0
  126. package/skills/poc/assets/poc_test_template.html +0 -645
  127. package/skills/poc/assets/poc_test_template.md +0 -104
  128. package/skills/poc/assets/step3_preview.png +0 -0
  129. package/skills/poc/assets/step4_order_note.png +0 -0
  130. package/skills/poc/interfaces/classify.md +0 -38
  131. package/skills/poc/interfaces/create-agents.md +0 -108
  132. package/skills/poc/interfaces/poc-test.md +0 -94
  133. package/skills/poc/references/flow.md +0 -47
  134. package/skills/poc/references/kb-directory.md +0 -22
  135. package/skills/poc/references/validation.md +0 -17
  136. package/skills/poc/scripts/classify.py +0 -22
  137. package/skills/poc/scripts/create_agents.py +0 -818
  138. package/skills/poc/scripts/gen_excel.py +0 -234
  139. package/skills/poc/scripts/gen_poc_manual.py +0 -249
  140. package/skills/poc/scripts/gen_report.py +0 -208
  141. package/skills/poc/scripts/inspect_chatflow.py +0 -209
  142. package/skills/poc/scripts/plan.py +0 -178
  143. package/skills/poc/scripts/poc_test.py +0 -495
  144. package/skills/poc/scripts/render.py +0 -713
  145. package/skills/poc/scripts/render_summary.py +0 -41
  146. package/skills/runtime-inspector/SKILL.md +0 -51
  147. package/skills/runtime-inspector/references/frontend-rendering.md +0 -126
  148. package/skills/runtime-inspector/references/model.md +0 -59
  149. package/skills/runtime-inspector/references/sandbox.md +0 -131
  150. package/skills/runtime-inspector/scripts/model_inspect.py +0 -68
  151. package/skills/runtime-inspector/scripts/prepare.py +0 -98
  152. package/skills/runtime-inspector/scripts/process.py +0 -84
  153. package/skills/runtime-inspector/scripts/read_state.py +0 -32
  154. package/skills/runtime-inspector/scripts/sandbox_inspect.py +0 -463
  155. package/skills/runtime-inspector/scripts/test_claude_proxy.py +0 -49
  156. package/skills/runtime-inspector/scripts/test_openai_proxy.py +0 -41
  157. package/skills/runtime-inspector/scripts/verify_persistence.py +0 -101
  158. package/skills/runtime-inspector/scripts/write_test.py +0 -41
  159. package/skills/setup/SKILL.md +0 -17
  160. package/skills/shulex-support/SKILL.md +0 -39
  161. package/skills/shulex-support/references/badcase-analysis/overview.md +0 -34
  162. package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +0 -105
  163. package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +0 -89
  164. package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +0 -36
  165. package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +0 -53
  166. package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +0 -259
  167. package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +0 -148
  168. package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +0 -57
  169. package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +0 -120
  170. package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +0 -126
  171. package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +0 -121
  172. package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +0 -208
  173. package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +0 -115
  174. package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +0 -72
  175. package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +0 -92
  176. package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +0 -147
  177. package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +0 -283
  178. package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +0 -77
  179. package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +0 -19
  180. package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +0 -23
  181. package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +0 -31
  182. package/skills/shulex-support/references/product-docs.md +0 -70
  183. package/skills/shulex-support/references/tickets.md +0 -184
  184. package/skills/shulex-support/scripts/support.py +0 -427
  185. package/skills/solvea-desk/SKILL.md +0 -39
  186. package/skills/solvea-desk/references/report/fields.md +0 -82
  187. package/skills/solvea-desk/references/report/report-design.md +0 -200
  188. package/skills/solvea-desk/references/report/report-template.html +0 -44
  189. package/skills/solvea-desk/references/report/report.css +0 -517
  190. package/skills/solvea-desk/references/report/workflow.md +0 -464
  191. package/skills/solvea-desk/references/tickets/query.md +0 -103
  192. package/skills/solvea-desk/references/tickets/tags.md +0 -83
  193. package/skills/solvea-desk/scripts/report/fetch_sources.py +0 -580
  194. package/skills/solvea-desk/scripts/report/render_report.py +0 -763
  195. package/skills/solvea-desk/scripts/tickets/desk_api.py +0 -73
  196. package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +0 -403
  197. package/skills/solvea-desk/scripts/tickets/ticket_tags.py +0 -305
  198. package/skills/solvea-kit/SKILL.md +0 -84
  199. package/skills/solvea-kit/references/agent.md +0 -145
  200. package/skills/solvea-kit/references/chat.md +0 -244
  201. package/skills/solvea-kit/references/intent.md +0 -69
  202. package/skills/solvea-kit/references/plugin.md +0 -53
  203. package/skills/solvea-kit/references/quality-inspection.md +0 -75
  204. package/skills/solvea-kit/references/strategy.md +0 -62
  205. package/skills/solvea-kit/references/tagging.md +0 -231
  206. package/skills/solvea-kit/references/variables.md +0 -53
  207. package/skills/solvea-kit/scripts/generate_label_import_file.py +0 -151
  208. package/skills/solvea-kit/scripts/intent_classify.py +0 -843
@@ -1,704 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Delivery 批量测试主管道。
3
-
4
- 读 cases.json / .xlsx / .csv(按后缀路由),按 touch-point 走 LIVE_CHAT/EMAIL,
5
- 10 并发 + ≥300 分批,输出 results+summary,可选触发 HTML/MD 报告。
6
-
7
- cases 兼容并集 schema(POC + Delivery + 历史抽样三种来源):
8
- [{name|case_name, messages, agent_name, agent_id, scenario, source_chat_id,
9
- intent, metadata, expected:{...}}, ...]
10
-
11
- stdout 协议(与老 batch_test_job 一致):
12
- =启动: === batch_test_job start: {N} cases, touch_point={TP} ===
13
- 每 10: PROGRESS:{"done":..., "total":..., "currentAgent":"..."}
14
- 完成: JOB_DONE:{"done":..., "total":..., "outputFile":"...", "htmlFile":"...", "mdFile":"..."}
15
- 异常: JOB_ERROR:{"error":"...","traceback":"..."} (退出码 1)
16
-
17
- 用法:
18
- python3 batch_test.py --input cases.json --output results.json --report
19
- python3 batch_test.py --input cases.xlsx --touch-point EMAIL --workers 10
20
- """
21
-
22
- import argparse
23
- import csv
24
- import json
25
- import os
26
- import subprocess
27
- import sys
28
- import threading
29
- import time
30
- import traceback
31
- from collections import defaultdict
32
- from concurrent.futures import ThreadPoolExecutor, as_completed
33
-
34
- sys.path.insert(0, os.path.join(os.environ.get("SOLVEA_LIB_DIR", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "__lib__"))), "skill-runtime"))
35
- from bootstrap import * # noqa: F401,F403
36
- from client import SolveaClient
37
-
38
-
39
- # noReplyReason codes + multi-language "No content" variants.
40
- # If content equals any of these, the reply is treated as a failure regardless of other fields.
41
- SYSTEM_ERROR_PROMPTS = {
42
- # ── Human-readable system messages returned by API ──
43
- "No content has been generated",
44
- # ── Backend noReplyReason enum values ──
45
- "INTERNAL_ERROR",
46
- "STRATEGY_NOT_FOUND",
47
- "LACK_OF_KNOWLEDGE",
48
- "KNOWLEDGE_NOT_RELEVANT",
49
- "INTELLIGENT_CONTROL",
50
- "TRANSFER_TO_AGENT",
51
- "DO_NOTHING",
52
- "STOPPED",
53
- "SOP_STRATEGY_TO_AGENT",
54
- "TOOL_TO_AGENT",
55
- "STRATEGY_TO_AGENT",
56
- "POST_STRATEGY_TO_AGENT",
57
- "INTENT_TO_AGENT",
58
- "MANUAL_TO_AGENT",
59
- "STRATEGY_DO_NOTHING",
60
- "TOOL_DO_NOTHING",
61
- "SYSTEM_ERROR",
62
- "RED_LINE",
63
- "DUPLICATE_REPLY",
64
- "POST_CHECK",
65
- "COLLECT_EXCEED_MAX_TIMES",
66
- "URL_NOT_ACCESSIBLE",
67
- "AGENT_HAS_FINISHED",
68
- "TEXT_CONTENT_EMPTY",
69
- "QUALITY_CHECK",
70
- "HANDOFF_NO_REPLY",
71
- "MAX_TURN_EXCEED",
72
- "MAX_AGENT_INVOKE_COUNT_EXCEED",
73
- "LABEL_ATTACK",
74
- "LABEL_ENTICE",
75
- "LABEL_AMAZON_RED_LINE",
76
- "LABEL_CONTROL",
77
- "ROLE_CONFUSION",
78
- "REQUESTED_PROVIDED_INFO",
79
- "COMPETITIVE_BRAND_REC",
80
- "PROHIBITED_WORD",
81
- "UNCERTAINTY_EXPRESSION",
82
- "WITH_ATTACHMENT",
83
- "ACTIVE_TRANSFER_TO_HUMAN",
84
- "BUY_OTHER_BELITTLE_ONESELF",
85
- "LLM_CONTENT_FILTER",
86
- "LLM_TOKENS_EXCEED",
87
- "TEMPLATE_NOT_FOUND",
88
- "TEMPLATE_NOT_ENABLED",
89
- "SOP_NOT_FOUND",
90
- "SOP_NOT_ENABLED",
91
- "SOP_CALL_LOOP",
92
- "SOP_CALL_DEPTH_EXCEED",
93
- "SOP_EXIT",
94
- "FLOW_NOT_FOUND",
95
- "FLOW_CALL_FAILED",
96
- "TOOL_NOT_WORK",
97
- "TOOL_OCCUR_ERROR",
98
- "TOOL_CALL_LOOP",
99
- "TOOL_CALL_DEPTH_EXCEED",
100
- "TOOL_CALL_TIMES_EXCEED",
101
- "TEMPLATE_ENGINE_ERROR",
102
- "HANDOFF_CANCELLED",
103
- "QUERY_ORDER_FAIL",
104
- "CALL_AGENT_TRANSFER_TO_HUMAN",
105
- "CALL_TOOL_STATUS_TO_HUMAN",
106
- "CALL_MULTI_AGENT_UNKNOWN_ERROR",
107
- "INVOKE_LLM_ERROR",
108
- "REQUEST_TIMEOUT",
109
- "REQUEST_ILLEGAL",
110
- "CALL_TOOL_ERROR",
111
- "AGENT_NOT_FOUND",
112
- "HANDOFF_TOOL_STATUS",
113
- "AI_CONTENT_ILLEGAL",
114
- "REVIEW_MANIPULATION",
115
- "COLLECT_PRIVACY",
116
- "EXTERNAL_LINK",
117
- "REDIRECT_THIRD_PARTY",
118
- "DISPARAGE_COMPETITORS",
119
- "UNRELATED_REPLY",
120
- "REPEATED_EXPRESSION",
121
- "NEGATIVE_ATTITUDE",
122
- "WRONG_LANGUAGE",
123
- "MANUAL_PROCESSING_TRANSFER",
124
- "LACK_OF_KNOWLEDGE_OR_PROCESS",
125
- "CONTACT_TO_CUSTOMER_SERVICE",
126
- "FAKE_INFORMATION",
127
- "COMPLAINT",
128
- "MENTION_PRIVACY",
129
- "LEGAL_ISSUE",
130
- "MENTION_ACCIDENT",
131
- "NEGATIVE_REVIEW_ALERT",
132
- "ATTACK_AI",
133
- "MENTION_THEFT",
134
- "AGENT_TIMEOUT",
135
- "NO_CONTENT_GENERATED",
136
- }
137
-
138
- BATCH_SIZE = 150
139
- BATCH_TRIGGER = 300
140
- WORKERS = 10
141
-
142
- # 用户上传文件的兼容列名 fallback(老版批测的进料习惯)
143
- _FALLBACK_MSG_COLS = ["messages", "message", "content", "问题", "Message"]
144
-
145
-
146
- # ── 入参加载 ────────────────────────────────────────────────────────────────
147
-
148
- def _split_multi_turn(raw):
149
- """老版兜底单列:单格内 \\n 或 | 拆多轮。"""
150
- if raw is None:
151
- return []
152
- s = str(raw)
153
- if not s.strip():
154
- return []
155
- # 优先按换行分;没有换行再按 | 分;都没有则当作单轮
156
- if "\n" in s:
157
- parts = [p.strip() for p in s.split("\n") if p.strip()]
158
- elif "|" in s:
159
- parts = [p.strip() for p in s.split("|") if p.strip()]
160
- else:
161
- parts = [s.strip()]
162
- return parts
163
-
164
-
165
- def _row_to_case(row: dict, idx: int) -> dict | None:
166
- """把 xlsx/csv 的一行(dict)按规则转成标准 case,命中失败返回 None。"""
167
- # Step 1: 新版严格列 message_1 / message_2 / message_3
168
- msgs = []
169
- for n in (1, 2, 3):
170
- v = row.get(f"message_{n}")
171
- if v is not None and str(v).strip():
172
- msgs.append(str(v).strip())
173
-
174
- # Step 2: 老版兜底列 messages / message / content / 问题 / Message
175
- if not msgs:
176
- for col in _FALLBACK_MSG_COLS:
177
- v = row.get(col)
178
- if v is not None and str(v).strip():
179
- msgs = _split_multi_turn(v)
180
- if msgs:
181
- break
182
-
183
- if not msgs:
184
- return None
185
-
186
- expected = {}
187
- if row.get("expected_intent"):
188
- expected["intent"] = str(row["expected_intent"]).strip()
189
- if row.get("expected_handoff") not in (None, ""):
190
- v = row["expected_handoff"]
191
- if isinstance(v, bool):
192
- expected["handoff"] = v
193
- else:
194
- expected["handoff"] = str(v).strip().lower() in ("true", "1", "yes")
195
- if row.get("content_contains"):
196
- expected["content_contains"] = [s.strip() for s in
197
- str(row["content_contains"]).split(",") if s.strip()]
198
- if row.get("content_not_contains"):
199
- expected["content_not_contains"] = [s.strip() for s in
200
- str(row["content_not_contains"]).split(",") if s.strip()]
201
- if row.get("nodes_executed"):
202
- expected["nodes_executed"] = [s.strip() for s in
203
- str(row["nodes_executed"]).split(",") if s.strip()]
204
- if row.get("flow_status"):
205
- expected["flow_status"] = str(row["flow_status"]).strip()
206
-
207
- metadata_raw = row.get("metadata")
208
- metadata = {}
209
- if metadata_raw not in (None, ""):
210
- if isinstance(metadata_raw, dict):
211
- metadata = metadata_raw
212
- else:
213
- try:
214
- metadata = json.loads(str(metadata_raw))
215
- except Exception:
216
- metadata = {}
217
-
218
- case = {
219
- "name": row.get("case_name") or row.get("name") or f"row_{idx}",
220
- "agent_name": row.get("agent_name") or "",
221
- "agent_id": int(row["agent_id"]) if row.get("agent_id") not in (None, "") else None,
222
- "scenario": row.get("scenario") or "",
223
- "source_chat_id": row.get("source_chat_id") or "",
224
- "intent": row.get("intent") or "",
225
- "messages": msgs,
226
- "metadata": metadata,
227
- "expected": expected,
228
- }
229
- return case
230
-
231
-
232
- def _load_cases_from_xlsx(path: str) -> list[dict]:
233
- from openpyxl import load_workbook
234
-
235
- wb = load_workbook(path, data_only=True)
236
- ws = wb.active
237
- rows = list(ws.iter_rows(values_only=True))
238
- if not rows:
239
- return []
240
- headers = [(h or "").strip() if isinstance(h, str) else "" for h in rows[0]]
241
- cases = []
242
- for idx, raw in enumerate(rows[1:], start=2):
243
- if not any(c not in (None, "") for c in raw):
244
- continue
245
- row = {headers[i]: raw[i] if i < len(raw) else None for i in range(len(headers)) if headers[i]}
246
- case = _row_to_case(row, idx)
247
- if case is None:
248
- print(f" WARN: row {idx} 无法解析 messages,已跳过", file=sys.stderr)
249
- continue
250
- cases.append(case)
251
- return cases
252
-
253
-
254
- def _load_cases_from_csv(path: str) -> list[dict]:
255
- cases = []
256
- with open(path, "r", encoding="utf-8-sig", newline="") as f:
257
- reader = csv.DictReader(f)
258
- for idx, row in enumerate(reader, start=2):
259
- if not any((v not in (None, "")) for v in row.values()):
260
- continue
261
- case = _row_to_case(row, idx)
262
- if case is None:
263
- print(f" WARN: row {idx} 无法解析 messages,已跳过", file=sys.stderr)
264
- continue
265
- cases.append(case)
266
- return cases
267
-
268
-
269
- def _load_cases_from_json(path: str) -> list[dict]:
270
- with open(path, "r", encoding="utf-8") as f:
271
- data = json.load(f)
272
- if not isinstance(data, list):
273
- raise ValueError(f"{path} 顶层必须是 JSON list")
274
- out = []
275
- for idx, c in enumerate(data, start=1):
276
- if not isinstance(c, dict):
277
- continue
278
- if not c.get("messages") and c.get("message"):
279
- c["messages"] = [c["message"]]
280
- if not c.get("messages"):
281
- print(f" WARN: case {idx} 缺 messages,已跳过", file=sys.stderr)
282
- continue
283
- c.setdefault("name", f"row_{idx}")
284
- out.append(c)
285
- return out
286
-
287
-
288
- def _detect_and_load(path: str) -> list[dict]:
289
- ext = os.path.splitext(path)[1].lower()
290
- if ext == ".xlsx":
291
- return _load_cases_from_xlsx(path)
292
- if ext == ".csv":
293
- return _load_cases_from_csv(path)
294
- if ext in (".json", ".jsonl"):
295
- return _load_cases_from_json(path)
296
- raise ValueError(f"不支持的文件类型:{ext}(支持 .json/.jsonl/.xlsx/.csv)")
297
-
298
-
299
- # ── 协议输出 ────────────────────────────────────────────────────────────────
300
-
301
- def emit(data: dict):
302
- prefix = "PROGRESS" if data.get("status") != "error" else "JOB_ERROR"
303
- print(f"{prefix}:{json.dumps(data, ensure_ascii=False)}", flush=True)
304
-
305
-
306
- # ── chat 路由 ──────────────────────────────────────────────────────────────
307
-
308
- def _build_metadata(case_metadata, specified_agent_id):
309
- """合并 case.metadata + (--agent-id → specified_agent_id)。"""
310
- md = dict(case_metadata or {})
311
- if specified_agent_id:
312
- md["specified_agent_id"] = int(specified_agent_id)
313
- return md
314
-
315
-
316
- def _call_chat(client, msg, chat_id, touch_point, metadata,
317
- poll_interval=2.0, poll_timeout=300.0):
318
- """LIVE_CHAT → chat_stream;EMAIL → chat_async + 轮询 get_async_result。"""
319
- if touch_point == "LIVE_CHAT":
320
- return client.chat_stream(
321
- message=msg, chat_id=chat_id,
322
- touch_point="LIVE_CHAT", metadata=metadata or None)
323
-
324
- # EMAIL: chat_async → poll
325
- receipt_resp = client.chat_async(
326
- message=msg, chat_id=chat_id,
327
- touch_point="EMAIL", metadata=metadata or None)
328
- receipt = receipt_resp.get("receipt") if isinstance(receipt_resp, dict) else None
329
- if not receipt:
330
- # 客户端层无 receipt → 当作错误抛回
331
- err = receipt_resp.get("errorMessage") if isinstance(receipt_resp, dict) else "no receipt"
332
- raise RuntimeError(f"chat_async no receipt: {err}")
333
-
334
- deadline = time.time() + poll_timeout
335
- while time.time() < deadline:
336
- time.sleep(poll_interval)
337
- result = client.get_async_result(receipt)
338
- if isinstance(result, dict) and result.get("completed"):
339
- data = result.get("data") or {}
340
- if isinstance(data, dict):
341
- return data
342
- return result
343
- return {"_error": "poll timeout", "content": "", "reason": "REQUEST_TIMEOUT"}
344
-
345
-
346
- # ── 单 case / 批量 ─────────────────────────────────────────────────────────
347
-
348
- def run_case(client, case: dict, touch_point: str, specified_agent_id=None,
349
- poll_interval=2.0, poll_timeout=300.0) -> list[dict]:
350
- """跑一个 case(可多轮),返回 detail 行 list。
351
-
352
- 多轮规则(与老版一致):metadata 只在首轮带,后续轮次清空。
353
- """
354
- messages = case.get("messages", [])
355
- case_metadata = _build_metadata(case.get("metadata"), specified_agent_id)
356
- source_chat_id = case.get("source_chat_id", "")
357
- intent = case.get("intent", "")
358
- rows = []
359
- chat_id = None
360
-
361
- for i, msg in enumerate(messages):
362
- msg = msg.get("content", str(msg)) if isinstance(msg, dict) else msg
363
- round_num = i + 1
364
- try:
365
- metadata = case_metadata if i == 0 else {}
366
- resp = _call_chat(
367
- client, msg, chat_id, touch_point, metadata,
368
- poll_interval=poll_interval, poll_timeout=poll_timeout,
369
- )
370
- if chat_id is None:
371
- chat_id = (resp.get("chatId") or resp.get("chat_id")
372
- or (resp.get("data") or {}).get("chatId", ""))
373
- content = resp.get("content") or ""
374
- no_reply_reason = resp.get("reason") or ""
375
- ctx = resp.get("context") or {}
376
- agent = (ctx.get("agent", "")
377
- or resp.get("agentName", "")
378
- or (ctx.get("ext") or {}).get("invoke_agent_name", ""))
379
- strategy = resp.get("strategyName", "") or ""
380
- action = agent or strategy
381
- handoff = bool(resp.get("handoff"))
382
- rows.append({
383
- "source_chat_id": source_chat_id, "new_chat_id": chat_id or "",
384
- "intent": intent, "action": action,
385
- "round": round_num, "question": msg,
386
- "answer": content, "handoff": handoff,
387
- "no_reply_reason": no_reply_reason,
388
- "_replied": no_reply_reason not in SYSTEM_ERROR_PROMPTS, "_error": "",
389
- })
390
- except Exception as e:
391
- rows.append({
392
- "source_chat_id": source_chat_id, "new_chat_id": chat_id or "",
393
- "intent": intent, "action": "",
394
- "round": round_num, "question": msg,
395
- "answer": "", "handoff": False,
396
- "no_reply_reason": "",
397
- "_replied": False, "_error": str(e)[:80],
398
- })
399
- return rows
400
-
401
-
402
- def _dump_partial(path: str, rows: list[dict], done_keys: set[str]):
403
- """把内部 rows + done_keys 落盘到 partial 文件(覆盖式)。"""
404
- try:
405
- os.makedirs(os.path.dirname(os.path.abspath(path)) or ".", exist_ok=True)
406
- with open(path, "w", encoding="utf-8") as f:
407
- json.dump({"rows": rows, "done_keys": sorted(done_keys)}, f,
408
- ensure_ascii=False, indent=2)
409
- except Exception as e:
410
- print(f" WARN: partial save failed: {e}", file=sys.stderr)
411
-
412
-
413
- def run_cases(client, cases, touch_point, specified_agent_id,
414
- workers=WORKERS, batch_size=BATCH_SIZE, batch_trigger=BATCH_TRIGGER,
415
- poll_interval=2.0, poll_timeout=300.0,
416
- partial_path: str | None = None,
417
- prev_rows: list[dict] | None = None,
418
- prev_done_keys: set[str] | None = None):
419
- """≥batch_trigger 切片 → 每片 ThreadPoolExecutor 并发;每 10 case emit PROGRESS。
420
-
421
- 若传入 partial_path,每完成一批后把全量内部 rows+done_keys 落盘,供续跑读取。
422
-
423
- 与老版差异(spec 风险 #2):分批统一应用到 concurrent,老版 _run_cases_concurrent 漏。
424
- """
425
- total = len(cases)
426
- batches = ([cases[i:i + batch_size] for i in range(0, total, batch_size)]
427
- if total >= batch_trigger else [cases])
428
-
429
- new_rows: list[dict] = []
430
- base_rows = list(prev_rows or [])
431
- done_keys: set[str] = set(prev_done_keys or set())
432
- counter = {"done": 0}
433
- lock = threading.Lock()
434
-
435
- def run_one(case):
436
- case_key = _case_key(case)
437
- try:
438
- rows = run_case(client, case, touch_point, specified_agent_id,
439
- poll_interval=poll_interval, poll_timeout=poll_timeout)
440
- except Exception as e:
441
- rows = [{
442
- "source_chat_id": case.get("source_chat_id", ""),
443
- "new_chat_id": "", "intent": case.get("intent", ""),
444
- "action": "", "round": 1,
445
- "question": (case.get("messages") or [""])[0] if case.get("messages") else "",
446
- "answer": "", "handoff": False,
447
- "no_reply_reason": "",
448
- "_replied": False, "_error": str(e)[:80],
449
- }]
450
- with lock:
451
- counter["done"] += 1
452
- done = counter["done"]
453
- current_agent = rows[-1].get("action", "") if rows else ""
454
- if done % 10 == 0 or done == total:
455
- emit({"done": done, "total": total, "currentAgent": current_agent})
456
- return case_key, rows
457
-
458
- for batch in batches:
459
- batch_results: list[tuple[str, list[dict]] | None] = [None] * len(batch)
460
- with ThreadPoolExecutor(max_workers=workers) as executor:
461
- futures = {executor.submit(run_one, c): idx for idx, c in enumerate(batch)}
462
- for f in as_completed(futures):
463
- idx = futures[f]
464
- batch_results[idx] = f.result()
465
- for item in batch_results:
466
- if not item:
467
- continue
468
- case_key, rows = item
469
- if rows:
470
- new_rows.extend(rows)
471
- if case_key:
472
- done_keys.add(case_key)
473
- if partial_path:
474
- _dump_partial(partial_path, base_rows + new_rows, done_keys)
475
-
476
- return new_rows, done_keys
477
-
478
-
479
- # ── summary ────────────────────────────────────────────────────────────────
480
-
481
- def build_summary(all_rows: list) -> dict:
482
- total_messages = len(all_rows)
483
- total_replied = sum(1 for r in all_rows if r["_replied"])
484
- chat_ids = set(r["new_chat_id"] for r in all_rows if r["new_chat_id"])
485
- total_conversations = len(chat_ids)
486
- reply_rate = round(total_replied / total_messages, 4) if total_messages else 0
487
-
488
- agent_rows = defaultdict(list)
489
- for r in all_rows:
490
- agent_rows[r["action"] or "Unknown"].append(r)
491
-
492
- agents = []
493
- for agent_name, rows in agent_rows.items():
494
- by_round = defaultdict(list)
495
- for r in rows:
496
- by_round[r["round"]].append(r)
497
- replied = sum(1 for r in rows if r["_replied"])
498
-
499
- def rr(n, by_round=by_round):
500
- rws = by_round.get(n, [])
501
- return round(sum(1 for r in rws if r["_replied"]) / len(rws), 4) if rws else None
502
-
503
- agents.append({
504
- "name": agent_name,
505
- "total_conversations": len(set(r["new_chat_id"] for r in rows if r["new_chat_id"])),
506
- "total_messages": len(rows),
507
- "replied_messages": replied,
508
- "reply_rate": round(replied / len(rows), 4) if rows else 0,
509
- "r1_reply_rate": rr(1),
510
- "r2_reply_rate": rr(2),
511
- "r3_reply_rate": rr(3),
512
- "rs_reply_rate": round(replied / len(rows), 4) if rows else 0,
513
- })
514
-
515
- handoff_counts = defaultdict(int)
516
- for r in all_rows:
517
- if r["handoff"]:
518
- handoff_counts["直接转人工"] += 1
519
- total_handoff = sum(handoff_counts.values())
520
- handoff = [{"type": t, "count": c,
521
- "rate": round(c / total_handoff, 4) if total_handoff else 0}
522
- for t, c in handoff_counts.items()]
523
-
524
- error_counts = defaultdict(int)
525
- for r in all_rows:
526
- if r["_error"]:
527
- error_counts[r["_error"][:40]] += 1
528
- total_errors = sum(error_counts.values())
529
- errors = [{"type": t, "count": c,
530
- "rate": round(c / total_errors, 4) if total_errors else 0}
531
- for t, c in error_counts.items()]
532
-
533
- no_reply_counts = defaultdict(int)
534
- for r in all_rows:
535
- if r.get("no_reply_reason"):
536
- no_reply_counts[r["no_reply_reason"]] += 1
537
- total_no_reply = sum(no_reply_counts.values())
538
- no_reply_reasons = [{"reason": rsn, "count": c,
539
- "rate": round(c / total_no_reply, 4) if total_no_reply else 0}
540
- for rsn, c in sorted(no_reply_counts.items(), key=lambda x: -x[1])]
541
-
542
- return {
543
- "total_conversations": total_conversations,
544
- "total_messages": total_messages,
545
- "total_replied": total_replied,
546
- "reply_rate": reply_rate,
547
- "agents": agents,
548
- "handoff": handoff,
549
- "errors": errors,
550
- "no_reply_reasons": no_reply_reasons,
551
- }
552
-
553
-
554
- # ── partial 续跑 ───────────────────────────────────────────────────────────
555
-
556
- def _load_partial(path: str) -> tuple[list[dict], set[str]]:
557
- """读 partial JSON,返回 (已完成内部 rows, 已完成的 case 标识集合)。
558
-
559
- partial schema:{"rows": [<内部 row dict 含 _replied/_error/handoff>...],
560
- "done_keys": [<key>...]}。
561
- case 标识用 source_chat_id(历史抽样)或 question 首轮值兜底。
562
- """
563
- if not path or not os.path.exists(path):
564
- return [], set()
565
- try:
566
- with open(path, "r", encoding="utf-8") as f:
567
- data = json.load(f)
568
- except Exception:
569
- return [], set()
570
- if not isinstance(data, dict):
571
- return [], set()
572
- rows = data.get("rows")
573
- if not isinstance(rows, list):
574
- return [], set()
575
- done_keys_raw = data.get("done_keys")
576
- if isinstance(done_keys_raw, list) and done_keys_raw:
577
- done = {str(k) for k in done_keys_raw if k}
578
- else:
579
- # 兜底:从 rows 反推 done_keys
580
- done = set()
581
- for r in rows:
582
- key = r.get("source_chat_id") or r.get("question")
583
- if key:
584
- done.add(str(key))
585
- return rows, done
586
-
587
-
588
- def _case_key(case: dict) -> str:
589
- sid = case.get("source_chat_id") or ""
590
- if sid:
591
- return str(sid)
592
- msgs = case.get("messages") or []
593
- return str(msgs[0]) if msgs else ""
594
-
595
-
596
- # ── main ───────────────────────────────────────────────────────────────────
597
-
598
- def main():
599
- parser = argparse.ArgumentParser(description="Solvea Delivery Batch Tester")
600
- parser.add_argument("--input", required=True,
601
- help="cases.json / .xlsx / .csv(按后缀路由)")
602
- parser.add_argument("--output", required=True, help="results JSON 输出路径")
603
- parser.add_argument("--touch-point", default="LIVE_CHAT",
604
- choices=["LIVE_CHAT", "EMAIL"],
605
- help="LIVE_CHAT→chat_stream;EMAIL→chat_async+轮询")
606
- parser.add_argument("--agent-id", type=int,
607
- help="全局强制路由(脚本组装 metadata.specified_agent_id)")
608
- parser.add_argument("--workers", type=int, default=WORKERS, help="并发 worker 数")
609
- parser.add_argument("--batch-size", type=int, default=BATCH_SIZE, help="单批大小")
610
- parser.add_argument("--batch-trigger", type=int, default=BATCH_TRIGGER,
611
- help="触发分批的最小 case 数")
612
- parser.add_argument("--poll-interval", type=float, default=2.0,
613
- help="EMAIL 轮询间隔(秒)")
614
- parser.add_argument("--poll-timeout", type=float, default=300.0,
615
- help="EMAIL 单 case 最长等待(秒)")
616
- parser.add_argument("--partial", help="断点续传 partial JSON 路径")
617
- parser.add_argument("--report", action="store_true",
618
- help="同步触发 gen_delivery_report.py 渲染 HTML+MD")
619
- args = parser.parse_args()
620
-
621
- cases = _detect_and_load(args.input)
622
- total_cases = len(cases)
623
-
624
- # partial 续跑:跳过已完成的 case
625
- partial_rows, done_keys = _load_partial(args.partial) if args.partial else ([], set())
626
- if done_keys:
627
- cases = [c for c in cases if _case_key(c) not in done_keys]
628
- print(f" partial 续跑:跳过 {total_cases - len(cases)} 条已完成", file=sys.stderr)
629
-
630
- total = len(cases)
631
- print(f"=== batch_test_job start: {total} cases, touch_point={args.touch_point} ===",
632
- flush=True)
633
-
634
- client = SolveaClient()
635
-
636
- new_rows, done_keys = run_cases(
637
- client, cases, args.touch_point, args.agent_id,
638
- workers=args.workers, batch_size=args.batch_size,
639
- batch_trigger=args.batch_trigger,
640
- poll_interval=args.poll_interval, poll_timeout=args.poll_timeout,
641
- partial_path=args.partial,
642
- prev_rows=partial_rows, prev_done_keys=done_keys,
643
- )
644
-
645
- all_rows = list(partial_rows) + new_rows
646
-
647
- summary = build_summary(all_rows)
648
- details = [{
649
- "source_chat_id": r.get("source_chat_id", ""),
650
- "new_chat_id": r.get("new_chat_id", ""),
651
- "intent": r.get("intent", ""),
652
- "action": r.get("action", ""),
653
- "round": r.get("round", ""),
654
- "question": r.get("question", ""),
655
- "answer": r.get("answer", ""),
656
- "no_reply_reason": r.get("no_reply_reason", ""),
657
- "judge": "", "correct": "",
658
- } for r in all_rows]
659
-
660
- base = os.path.splitext(os.path.abspath(args.output))[0]
661
- html_file = base + ".html"
662
- md_file = base + ".md"
663
-
664
- os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True)
665
- with open(args.output, "w", encoding="utf-8") as f:
666
- json.dump({"summary": summary, "details": details}, f,
667
- ensure_ascii=False, indent=2)
668
-
669
- # partial 完成后再补一次最终落盘(保证 done_keys 与 all_rows 一致)
670
- if args.partial:
671
- _dump_partial(args.partial, all_rows, done_keys)
672
-
673
- if args.report:
674
- report_script = os.path.join(os.path.dirname(os.path.abspath(__file__)),
675
- "gen_delivery_report.py")
676
- try:
677
- subprocess.run(
678
- [sys.executable, report_script,
679
- "--results", args.output,
680
- "--html", html_file, "--md", md_file],
681
- check=True,
682
- )
683
- except Exception as e:
684
- print(f" WARN: report render failed: {e}", file=sys.stderr)
685
-
686
- done_rows = len(all_rows)
687
- done_cases = len(done_keys) if done_keys else total_cases
688
- print("JOB_DONE:" + json.dumps({
689
- "done": done_rows, "total": done_rows,
690
- "done_cases": done_cases, "total_cases": total_cases,
691
- "outputFile": args.output,
692
- "htmlFile": html_file if args.report else "",
693
- "mdFile": md_file if args.report else "",
694
- }), flush=True)
695
-
696
-
697
- if __name__ == "__main__":
698
- try:
699
- main()
700
- except Exception as e:
701
- print("JOB_ERROR:" + json.dumps({
702
- "error": str(e), "traceback": traceback.format_exc(),
703
- }), flush=True)
704
- sys.exit(1)