@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,495 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Solvea POC 测试执行器:跑 cases.json → 解析 chatflow_run_info → 输出 results JSON。
3
-
4
- POC 路径:agent 设计完 cases.json → 跑测试 → results 给 gen_poc_manual.py 渲染手册。
5
-
6
- 入参格式(test-file):
7
- [
8
- {
9
- "case_name": "P1_Shipping_Inquiry", // 必填;老字段 "name" 仍兼容兜底
10
- "agent_name": "Return and exchange",
11
- "agent_id": 38050,
12
- "messages": ["I want to return my order", "My order number is 204-..."],
13
- "expected": { "intent": "...", "content_contains": [...], "flow_status": "success", ... }
14
- }
15
- ]
16
-
17
- CHAT_FLOW Agent 默认走策略路由;SINGLE_AGENT 通过 --agent-id 触发
18
- metadata.specified_agent_id 强制路由(服务端 Entities.specifiedAgentId 直接读 ID 查库)。
19
-
20
- 用法:
21
- python3 poc_test.py --test-file cases.json --output results.json --report
22
- python3 poc_test.py --test-file cases.json --agent-id 2274 --report # 强制 SINGLE_AGENT
23
- """
24
-
25
- import argparse
26
- import json
27
- import os
28
- import sys
29
- import time
30
-
31
- _solvea_lib_dir = os.environ.get("SOLVEA_LIB_DIR") or os.path.abspath(
32
- os.path.join(os.path.dirname(__file__), "..", "..", "__lib__")
33
- )
34
- sys.path.insert(0, os.path.join(_solvea_lib_dir, "skill-runtime"))
35
- from bootstrap import * # noqa: F401,F403
36
- from client import SolveaClient
37
-
38
-
39
- # ── Response Parsing ──
40
-
41
- def _extract_intent(resp):
42
- """Extract intent name from response."""
43
- ctx = resp.get("context")
44
- if not ctx or isinstance(ctx, str):
45
- return ""
46
- intent = ctx.get("intent", "")
47
- if isinstance(intent, str):
48
- return intent
49
- if isinstance(intent, dict):
50
- return intent.get("name", "")
51
- return ""
52
-
53
-
54
- def _extract_chatflow_info(resp):
55
- """Extract chatflow execution info from response.
56
-
57
- Pulls chatflow_run_info events out of context.ext, structuring node runs,
58
- final status, errors, and the executed-node sequence.
59
- """
60
- ctx = resp.get("context", {})
61
- if not isinstance(ctx, dict):
62
- return {}
63
- ext = ctx.get("ext", {})
64
- if not isinstance(ext, dict):
65
- return {}
66
-
67
- run_info = ext.get("chatflow_run_info", [])
68
- node_runs = []
69
- errors = []
70
- status = None
71
-
72
- for event in run_info:
73
- ed = event.get("eventData", {})
74
- if event.get("event") == "chat_finished":
75
- status = ed.get("status", "")
76
- if "error" in ed:
77
- errors.append({
78
- "name": ed["error"].get("name", ""),
79
- "detail": ed["error"].get("detail", "")[:200]
80
- })
81
- if event.get("event") == "node_finished":
82
- node_runs.append({
83
- "node_id": ed.get("node_id", ""),
84
- "category": ed.get("category", ""),
85
- "status": ed.get("status", ""),
86
- "title": ed.get("title", ""),
87
- "output": ed.get("output", ""),
88
- })
89
-
90
- return {
91
- "agent_id": ext.get("invoke_agent_id"),
92
- "agent_name": ext.get("invoke_agent_name", ""),
93
- "status": status,
94
- "node_runs": node_runs,
95
- "errors": errors,
96
- "nodes_executed": [nr.get("category", "") for nr in node_runs if nr.get("status") == "success"],
97
- }
98
-
99
-
100
- def _extract_reply_text(resp, flow_info):
101
- """Pick the user-visible reply text.
102
-
103
- Priority:
104
- 1. top-level `content` if it's not the placeholder "No content has been generated"
105
- (this is filled for SINGLE_AGENT and some CHAT_FLOW completions)
106
- 2. last successful REPLY/AGENT/RAG_REPLY/KNOWLEDGE_REPLY node output
107
- """
108
- top = resp.get("content", "")
109
- if top and top != "No content has been generated":
110
- return top
111
-
112
- reply_categories = {"REPLY", "AGENT", "RAG_REPLY", "KNOWLEDGE_REPLY"}
113
- for nr in reversed(flow_info.get("node_runs", [])):
114
- if nr.get("status") == "success" and nr.get("category") in reply_categories:
115
- out = nr.get("output", "")
116
- if out:
117
- return out
118
- return top # fall back to the placeholder so the report still shows something
119
-
120
-
121
- # ── Test Execution ──
122
-
123
- def _is_handoff(resp, flow_info):
124
- """检测是否触发了转人工:top-level handoff=True 或 chatflow 节点出现 TRANSFER_TO_HUMANS。"""
125
- if resp.get("handoff") is True:
126
- return True
127
- for nr in flow_info.get("node_runs", []):
128
- if nr.get("status") == "success" and "TRANSFER_TO_HUMANS" in (nr.get("category", "") or ""):
129
- return True
130
- return False
131
-
132
-
133
- def run_single_test(client, test_case, bot_id=None, force_metadata=None, max_turns=3):
134
- """Run a single test case (may be multi-turn).
135
-
136
- 终止条件(任一满足即停):
137
- - case.messages 已跑完
138
- - 已达到 max_turns(默认 3,POC 模式硬上限)
139
- - 任意一轮触发了转人工(resp.handoff=True 或 TRANSFER_TO_HUMANS 节点 success)
140
-
141
- force_metadata:CLI --agent-id 组装的 {"specified_agent_id": int} 等强制 metadata;
142
- 与 case.metadata 合并,CLI 优先级最高。
143
- """
144
- messages = test_case.get("messages", [])
145
- if not messages and test_case.get("message"):
146
- messages = [test_case["message"]]
147
-
148
- base_metadata = dict(test_case.get("metadata") or {})
149
- if force_metadata:
150
- base_metadata.update(force_metadata)
151
-
152
- expected = test_case.get("expected", {})
153
- results = []
154
- chat_id = None
155
- stopped_reason = "messages_exhausted"
156
-
157
- for i, msg in enumerate(messages):
158
- if i >= max_turns:
159
- stopped_reason = f"max_turns({max_turns})"
160
- break
161
- try:
162
- # 多轮规则:metadata 仅首轮带,后续轮次清空(与老版一致)
163
- metadata = base_metadata if i == 0 else None
164
- resp = client.chat(
165
- message=msg,
166
- bot_id=bot_id,
167
- chat_id=chat_id,
168
- touch_point=test_case.get("touchPoint", "LIVE_CHAT"),
169
- metadata=metadata,
170
- )
171
- chat_id = resp.get("chatId", chat_id)
172
- flow_info = _extract_chatflow_info(resp)
173
- reply_text = _extract_reply_text(resp, flow_info)
174
- handoff = _is_handoff(resp, flow_info)
175
- results.append({
176
- "turn": i + 1,
177
- "input": msg,
178
- "content": reply_text,
179
- "raw_content": resp.get("content", ""),
180
- "handoff": handoff,
181
- "reasonDesc": resp.get("reasonDesc", ""),
182
- "chatId": chat_id,
183
- "type": resp.get("type", ""),
184
- "intent": _extract_intent(resp),
185
- "chatflow": flow_info,
186
- })
187
- if handoff:
188
- stopped_reason = f"handoff(turn {i+1})"
189
- break
190
- if i < len(messages) - 1 and i + 1 < max_turns:
191
- time.sleep(1)
192
- except Exception as e:
193
- results.append({
194
- "turn": i + 1,
195
- "input": msg,
196
- "error": str(e)
197
- })
198
- stopped_reason = f"error(turn {i+1})"
199
- break
200
-
201
- evaluation = evaluate_test(test_case, results, expected)
202
- return {
203
- "name": test_case.get("case_name") or test_case.get("name") or "unnamed",
204
- "agent_name": test_case.get("agent_name", ""),
205
- "agent_id": test_case.get("agent_id"),
206
- "scenario": test_case.get("scenario", ""),
207
- "category": test_case.get("category", ""),
208
- "stopped_reason": stopped_reason,
209
- "results": results,
210
- "expected": expected,
211
- "evaluation": evaluation,
212
- }
213
-
214
-
215
- def evaluate_test(test_case, results, expected):
216
- """Evaluate test results against expected outcomes."""
217
- if not results:
218
- return {"pass": False, "reason": "No results", "checks": []}
219
-
220
- last_result = results[-1]
221
- checks = []
222
- all_pass = True
223
-
224
- # Auto-check: INTERNAL_ERROR is always a failure
225
- for r in results:
226
- if r.get("content") == "INTERNAL_ERROR" or r.get("raw_content") == "INTERNAL_ERROR":
227
- flow_errors = r.get("chatflow", {}).get("errors", [])
228
- error_detail = flow_errors[0]["name"] if flow_errors else "unknown"
229
- checks.append({
230
- "check": f"no_internal_error (turn {r['turn']})",
231
- "pass": False,
232
- "actual": f"INTERNAL_ERROR: {error_detail}"
233
- })
234
- all_pass = False
235
-
236
- # Auto-check: response type STOPPED/CONFUSED is failure
237
- for r in results:
238
- if r.get("type") in ("STOPPED", "CONFUSED"):
239
- checks.append({
240
- "check": f"response_type (turn {r['turn']})",
241
- "pass": False,
242
- "expected": "MESSAGE",
243
- "actual": r.get("type")
244
- })
245
- all_pass = False
246
-
247
- # Intent recognition
248
- if expected.get("intent"):
249
- first_intent = results[0].get("intent", "")
250
- intent_match = expected["intent"].lower() in first_intent.lower() if first_intent else False
251
- checks.append({
252
- "check": "intent_match",
253
- "expected": expected["intent"],
254
- "actual": first_intent,
255
- "pass": intent_match
256
- })
257
- if not intent_match:
258
- all_pass = False
259
-
260
- # Handoff check
261
- if "handoff" in expected:
262
- handoff_match = last_result.get("handoff") == expected["handoff"]
263
- checks.append({
264
- "check": "handoff",
265
- "expected": expected["handoff"],
266
- "actual": last_result.get("handoff"),
267
- "pass": handoff_match
268
- })
269
- if not handoff_match:
270
- all_pass = False
271
-
272
- # Content contains
273
- if expected.get("content_contains"):
274
- content = last_result.get("content", "")
275
- for keyword in expected["content_contains"]:
276
- found = keyword.lower() in content.lower()
277
- checks.append({"check": f"content_contains '{keyword}'", "pass": found})
278
- if not found:
279
- all_pass = False
280
-
281
- # Content NOT contains
282
- if expected.get("content_not_contains"):
283
- content = last_result.get("content", "")
284
- for keyword in expected["content_not_contains"]:
285
- found = keyword.lower() in content.lower()
286
- checks.append({"check": f"content_not_contains '{keyword}'", "pass": not found})
287
- if found:
288
- all_pass = False
289
-
290
- # CHAT_FLOW node execution check
291
- if expected.get("nodes_executed"):
292
- actual_nodes = last_result.get("chatflow", {}).get("nodes_executed", [])
293
- for expected_cat in expected["nodes_executed"]:
294
- found = expected_cat in actual_nodes
295
- checks.append({
296
- "check": f"node_executed '{expected_cat}'",
297
- "expected": expected_cat,
298
- "actual": actual_nodes,
299
- "pass": found
300
- })
301
- if not found:
302
- all_pass = False
303
-
304
- # CHAT_FLOW status check
305
- if expected.get("flow_status"):
306
- actual_status = last_result.get("chatflow", {}).get("status", "")
307
- status_match = actual_status == expected["flow_status"]
308
- checks.append({
309
- "check": "flow_status",
310
- "expected": expected["flow_status"],
311
- "actual": actual_status,
312
- "pass": status_match
313
- })
314
- if not status_match:
315
- all_pass = False
316
-
317
- # Error check
318
- if last_result.get("error"):
319
- checks.append({"check": "no_error", "pass": False, "actual": last_result["error"]})
320
- all_pass = False
321
-
322
- # Fallback: at least got a response
323
- if not expected and not any(c["check"].startswith("no_internal_error") for c in checks):
324
- has_content = bool(last_result.get("content"))
325
- checks.append({"check": "has_response", "pass": has_content})
326
- if not has_content:
327
- all_pass = False
328
-
329
- return {"pass": all_pass, "checks": checks}
330
-
331
-
332
- # ── Batch Runner ──
333
-
334
- def run_batch_test(client, test_cases, bot_id=None, force_metadata=None, delay=2,
335
- max_turns=3):
336
- """Run all test cases and return summary."""
337
- results = []
338
- total = len(test_cases)
339
-
340
- for i, tc in enumerate(test_cases):
341
- case_label = tc.get("case_name") or tc.get("name") or "unnamed"
342
- print(f" [{i+1}/{total}] Testing: {case_label}...", file=sys.stderr)
343
- result = run_single_test(client, tc, bot_id=bot_id,
344
- force_metadata=force_metadata,
345
- max_turns=max_turns)
346
- print(f" → stopped: {result.get('stopped_reason', '?')}", file=sys.stderr)
347
- results.append(result)
348
- if i < total - 1:
349
- time.sleep(delay)
350
-
351
- return _summarize(results)
352
-
353
-
354
- def _summarize(results):
355
- total = len(results)
356
- passed = sum(1 for r in results if r["evaluation"]["pass"])
357
- return {
358
- "total": total,
359
- "passed": passed,
360
- "failed": total - passed,
361
- "pass_rate": f"{(passed / total * 100) if total > 0 else 0:.1f}%",
362
- "results": results,
363
- }
364
-
365
-
366
- # ── Report ──
367
-
368
- def print_report(summary):
369
- """Print a human-readable test report."""
370
- print("\n" + "=" * 70)
371
- print(" SOLVEA BATCH TEST REPORT")
372
- print("=" * 70)
373
- print(f" Total: {summary['total']} | Passed: {summary['passed']} | "
374
- f"Failed: {summary['failed']} | Rate: {summary['pass_rate']}")
375
- print("=" * 70)
376
-
377
- for r in summary["results"]:
378
- status = "PASS" if r["evaluation"]["pass"] else "FAIL"
379
- icon = "✅" if r["evaluation"]["pass"] else "❌"
380
- print(f"\n{icon} [{status}] {r['name']}")
381
- if r.get("agent_name"):
382
- print(f" Agent: {r['agent_name']} (id={r.get('agent_id')})")
383
- if r.get("category"):
384
- print(f" Category: {r['category']}")
385
-
386
- for turn in r["results"]:
387
- print(f" Turn {turn['turn']}: \"{turn['input'][:80]}\"")
388
- if turn.get("error"):
389
- print(f" ⚠️ Error: {turn['error']}")
390
- else:
391
- content_preview = (turn.get("content", ""))[:120]
392
- ellipsis = "..." if len(turn.get("content", "")) > 120 else ""
393
- print(f" → {content_preview}{ellipsis}")
394
- if turn.get("intent"):
395
- print(f" Intent: {turn['intent']}")
396
- if turn.get("handoff"):
397
- print(" Handoff: True")
398
- flow = turn.get("chatflow", {})
399
- if flow.get("nodes_executed"):
400
- print(f" Nodes: {' → '.join(flow['nodes_executed'])}")
401
- if flow.get("errors"):
402
- for fe in flow["errors"]:
403
- print(f' FlowError: {fe["name"]}: {fe["detail"][:100]}')
404
-
405
- if not r["evaluation"]["pass"]:
406
- print(" --- Failed checks ---")
407
- for chk in r["evaluation"].get("checks", []):
408
- if not chk["pass"]:
409
- msg = f" ❌ {chk['check']}"
410
- if "expected" in chk:
411
- msg += f" (expected: {chk['expected']}, actual: {chk.get('actual', 'N/A')})"
412
- elif "actual" in chk:
413
- msg += f" ({chk['actual']})"
414
- print(msg)
415
-
416
- print("\n" + "=" * 70)
417
- print(f" SUMMARY: {summary['pass_rate']} pass rate ({summary['passed']}/{summary['total']})")
418
- print("=" * 70 + "\n")
419
-
420
-
421
- # ── Main ──
422
-
423
- def main():
424
- parser = argparse.ArgumentParser(description="Solvea POC Test Runner")
425
- parser.add_argument("--test-file", required=True,
426
- help="JSON file with test cases (POC 模式:agent 设计的 cases.json)")
427
- parser.add_argument("--max-turns", type=int, default=3,
428
- help="单 case 最大对话轮数(默认 3,POC 硬上限;触发 handoff 也提前终止)")
429
- parser.add_argument("--agent-id", type=int,
430
- help="强制 SINGLE_AGENT 路由(组装 metadata.specified_agent_id;"
431
- "CHAT_FLOW 默认走策略路由,不需要)")
432
- parser.add_argument("--bot-id", help="Bot ID override")
433
- parser.add_argument("--delay", type=float, default=2, help="Delay between cases (seconds)")
434
- parser.add_argument("--report", action="store_true", help="Print human-readable report")
435
- parser.add_argument("--output", help="Save results JSON to this path")
436
- parser.add_argument("--pretty", action="store_true", help="Pretty print compact JSON to stdout")
437
- args = parser.parse_args()
438
-
439
- client = SolveaClient()
440
-
441
- with open(args.test_file, encoding="utf-8") as f:
442
- test_cases = json.load(f)
443
-
444
- force_metadata = None
445
- if args.agent_id:
446
- force_metadata = {"specified_agent_id": int(args.agent_id)}
447
- print(f" Forcing SINGLE_AGENT route via metadata.specified_agent_id={args.agent_id}",
448
- file=sys.stderr)
449
-
450
- print(f" Running {len(test_cases)} test cases (max_turns={args.max_turns})...",
451
- file=sys.stderr)
452
- summary = run_batch_test(
453
- client,
454
- test_cases,
455
- bot_id=args.bot_id,
456
- force_metadata=force_metadata,
457
- delay=args.delay,
458
- max_turns=args.max_turns,
459
- )
460
-
461
- if args.report:
462
- print_report(summary)
463
-
464
- if args.output:
465
- with open(args.output, "w", encoding="utf-8") as f:
466
- json.dump(summary, f, indent=2, ensure_ascii=False)
467
- print(f" Results saved to {args.output}", file=sys.stderr)
468
-
469
- if args.pretty:
470
- compact = {
471
- "total": summary["total"],
472
- "passed": summary["passed"],
473
- "failed": summary["failed"],
474
- "pass_rate": summary["pass_rate"],
475
- "details": [
476
- {
477
- "name": r["name"],
478
- "agent_name": r.get("agent_name", ""),
479
- "pass": r["evaluation"]["pass"],
480
- "last_content": r["results"][-1].get("content", "")[:200] if r["results"] else "",
481
- "intent_detected": r["results"][0].get("intent", "") if r["results"] else "",
482
- "nodes_executed": r["results"][-1].get("chatflow", {}).get("nodes_executed", []) if r["results"] else [],
483
- "failed_checks": [c for c in r["evaluation"].get("checks", []) if not c["pass"]],
484
- }
485
- for r in summary["results"]
486
- ]
487
- }
488
- print(json.dumps(compact, indent=2, ensure_ascii=False))
489
-
490
- if summary["failed"] > 0:
491
- sys.exit(1)
492
-
493
-
494
- if __name__ == "__main__":
495
- main()