@shulex/cxclawctl 0.0.0-stage → 0.1.2

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 (209) hide show
  1. package/README.md +228 -2
  2. package/bin/cxclawctl.mjs +11 -0
  3. package/capabilities.lock.json +898 -0
  4. package/dist/cli.js +363 -0
  5. package/dist/cli.js.map +1 -0
  6. package/package.json +33 -4
  7. package/requirements-cli.txt +2 -0
  8. package/skills/__lib__/__init__.py +0 -0
  9. package/skills/__lib__/skill-runtime/bootstrap.py +134 -0
  10. package/skills/__lib__/solvea-api/__init__.py +0 -0
  11. package/skills/__lib__/solvea-api/client.py +1944 -0
  12. package/skills/__lib__/solvea-api/dashboard.py +100 -0
  13. package/skills/__lib__/solvea-api/solvea_api.py +1341 -0
  14. package/skills/agent-analytics/SKILL.md +74 -0
  15. package/skills/agent-analytics/references/chat-report-fields.md +36 -0
  16. package/skills/agent-analytics/references/report-fields.md +130 -0
  17. package/skills/agent-analytics/references/transcript.md +110 -0
  18. package/skills/agent-builder/SKILL.md +42 -0
  19. package/skills/agent-builder/interfaces/fallback-chatflow.md +47 -0
  20. package/skills/agent-builder/references/chatflow/graph-spec.md +259 -0
  21. package/skills/agent-builder/references/chatflow/node-selection.md +89 -0
  22. package/skills/agent-builder/references/chatflow/nodes/agent.md +350 -0
  23. package/skills/agent-builder/references/chatflow/nodes/business-track.md +43 -0
  24. package/skills/agent-builder/references/chatflow/nodes/code.md +157 -0
  25. package/skills/agent-builder/references/chatflow/nodes/collect-order.md +47 -0
  26. package/skills/agent-builder/references/chatflow/nodes/finish.md +34 -0
  27. package/skills/agent-builder/references/chatflow/nodes/human-review.md +60 -0
  28. package/skills/agent-builder/references/chatflow/nodes/if-else.md +100 -0
  29. package/skills/agent-builder/references/chatflow/nodes/intent.md +79 -0
  30. package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +43 -0
  31. package/skills/agent-builder/references/chatflow/nodes/llm.md +117 -0
  32. package/skills/agent-builder/references/chatflow/nodes/note.md +38 -0
  33. package/skills/agent-builder/references/chatflow/nodes/question.md +97 -0
  34. package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +68 -0
  35. package/skills/agent-builder/references/chatflow/nodes/reply.md +97 -0
  36. package/skills/agent-builder/references/chatflow/nodes/start.md +35 -0
  37. package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +48 -0
  38. package/skills/agent-builder/references/chatflow/nodes/tool.md +138 -0
  39. package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +63 -0
  40. package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +52 -0
  41. package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +50 -0
  42. package/skills/agent-builder/references/chatflow/nodes/variable-process.md +70 -0
  43. package/skills/agent-builder/references/chatflow/overview.md +53 -0
  44. package/skills/agent-builder/references/chatflow/visual-translation.md +64 -0
  45. package/skills/agent-builder/references/chatflow/workflow.md +134 -0
  46. package/skills/agent-builder/references/multi-agent/create-update.md +185 -0
  47. package/skills/agent-builder/references/multi-agent/graph-spec.md +246 -0
  48. package/skills/agent-builder/references/multi-agent/graph-structure.md +12 -0
  49. package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +13 -0
  50. package/skills/agent-builder/references/multi-agent/migration/large.md +554 -0
  51. package/skills/agent-builder/references/multi-agent/migration/principles.md +446 -0
  52. package/skills/agent-builder/references/multi-agent/migration/small.md +225 -0
  53. package/skills/agent-builder/references/multi-agent/orchestration.md +11 -0
  54. package/skills/agent-builder/references/multi-agent/overview.md +48 -0
  55. package/skills/agent-builder/references/multi-agent/update.md +128 -0
  56. package/skills/agent-builder/references/multi-agent/workflow.md +87 -0
  57. package/skills/agent-builder/references/single-agent/overview.md +48 -0
  58. package/skills/agent-builder/references/single-agent/prompt.md +16 -0
  59. package/skills/agent-builder/references/single-agent/tools.md +36 -0
  60. package/skills/agent-builder/references/single-agent/voice.md +46 -0
  61. package/skills/agent-builder/references/tools/http-tool.md +315 -0
  62. package/skills/agent-builder/references/tools/mock-tool.md +279 -0
  63. package/skills/agent-builder/references/tools/overview.md +46 -0
  64. package/skills/agent-builder/references/tools/tool-config.md +43 -0
  65. package/skills/agent-builder/references/tools/x-token.md +34 -0
  66. package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +3458 -0
  67. package/skills/agent-builder/scripts/chatflow/validate_graph.py +1697 -0
  68. package/skills/agent-builder/scripts/copy_marketplace_agent.py +61 -0
  69. package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +5974 -0
  70. package/skills/agent-builder/scripts/multi-agent/replay_compare.py +679 -0
  71. package/skills/agent-builder/scripts/patch_agent_tools.py +135 -0
  72. package/skills/agent-builder/scripts/tools/validate_tool_config.py +384 -0
  73. package/skills/agent-tester/SKILL.md +35 -0
  74. package/skills/agent-tester/batch-test/overview.md +86 -0
  75. package/skills/agent-tester/scripts/batch_test_sheet.py +213 -0
  76. package/skills/agent-tester/single-test/badcase-retest.md +85 -0
  77. package/skills/agent-tester/single-test/direct-test.md +24 -0
  78. package/skills/agent-tester/single-test/overview.md +142 -0
  79. package/skills/delivery/SKILL.md +112 -0
  80. package/skills/delivery/interfaces/classify.md +38 -0
  81. package/skills/delivery/interfaces/create-agents.md +89 -0
  82. package/skills/delivery/interfaces/delivery-test.md +67 -0
  83. package/skills/delivery/references/flow.md +40 -0
  84. package/skills/delivery/references/validation.md +15 -0
  85. package/skills/delivery/scripts/batch_test.py +704 -0
  86. package/skills/delivery/scripts/classify.py +22 -0
  87. package/skills/delivery/scripts/create_agents.py +695 -0
  88. package/skills/delivery/scripts/gen_delivery_report.py +335 -0
  89. package/skills/delivery/scripts/gen_excel.py +234 -0
  90. package/skills/delivery/scripts/gen_report.py +208 -0
  91. package/skills/delivery/scripts/plan.py +163 -0
  92. package/skills/delivery/scripts/render.py +713 -0
  93. package/skills/delivery/scripts/render_summary.py +38 -0
  94. package/skills/delivery/scripts/sample_history_cases.py +109 -0
  95. package/skills/kb-builder/SKILL.md +80 -0
  96. package/skills/kb-builder/interfaces/qa-upload.md +55 -0
  97. package/skills/kb-builder/interfaces/web-kb.md +84 -0
  98. package/skills/kb-builder/references/directory.md +373 -0
  99. package/skills/kb-builder/references/export.md +282 -0
  100. package/skills/kb-builder/references/isolation-keywords.md +113 -0
  101. package/skills/kb-builder/references/knowledge.md +153 -0
  102. package/skills/kb-builder/references/product.md +52 -0
  103. package/skills/kb-builder/references/qa-from-help-center.md +58 -0
  104. package/skills/kb-builder/references/qa-from-pdf.md +94 -0
  105. package/skills/kb-builder/references/qa-from-tickets.md +168 -0
  106. package/skills/kb-builder/references/qa-from-web.md +98 -0
  107. package/skills/kb-builder/references/qa-quality.md +73 -0
  108. package/skills/kb-builder/references/reply-template.md +237 -0
  109. package/skills/kb-builder/references/tag-isolation-migration.md +61 -0
  110. package/skills/kb-builder/references/tags.md +45 -0
  111. package/skills/kb-builder/references/upload.md +130 -0
  112. package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +333 -0
  113. package/skills/kb-builder/scripts/build_directory_plan.py +144 -0
  114. package/skills/kb-builder/scripts/crawl_site.py +433 -0
  115. package/skills/kb-builder/scripts/create_dirs.py +146 -0
  116. package/skills/kb-builder/scripts/export_knowledge.py +454 -0
  117. package/skills/kb-builder/scripts/extract_ticket_qa.py +441 -0
  118. package/skills/kb-builder/scripts/extract_web_qa.py +118 -0
  119. package/skills/kb-builder/scripts/import_qa.py +219 -0
  120. package/skills/kb-builder/scripts/import_source_documents.py +143 -0
  121. package/skills/kb-builder/scripts/import_templates.py +409 -0
  122. package/skills/poc/SKILL.md +125 -0
  123. package/skills/poc/assets/default_manual.css +279 -0
  124. package/skills/poc/assets/notice_escalate.png +0 -0
  125. package/skills/poc/assets/notice_escalate2.png +0 -0
  126. package/skills/poc/assets/notice_new_chat.png +0 -0
  127. package/skills/poc/assets/poc_test_template.html +645 -0
  128. package/skills/poc/assets/poc_test_template.md +104 -0
  129. package/skills/poc/assets/step3_preview.png +0 -0
  130. package/skills/poc/assets/step4_order_note.png +0 -0
  131. package/skills/poc/interfaces/classify.md +38 -0
  132. package/skills/poc/interfaces/create-agents.md +108 -0
  133. package/skills/poc/interfaces/poc-test.md +94 -0
  134. package/skills/poc/references/flow.md +47 -0
  135. package/skills/poc/references/kb-directory.md +22 -0
  136. package/skills/poc/references/validation.md +17 -0
  137. package/skills/poc/scripts/classify.py +22 -0
  138. package/skills/poc/scripts/create_agents.py +818 -0
  139. package/skills/poc/scripts/gen_excel.py +234 -0
  140. package/skills/poc/scripts/gen_poc_manual.py +249 -0
  141. package/skills/poc/scripts/gen_report.py +208 -0
  142. package/skills/poc/scripts/inspect_chatflow.py +209 -0
  143. package/skills/poc/scripts/plan.py +178 -0
  144. package/skills/poc/scripts/poc_test.py +495 -0
  145. package/skills/poc/scripts/render.py +713 -0
  146. package/skills/poc/scripts/render_summary.py +41 -0
  147. package/skills/runtime-inspector/SKILL.md +51 -0
  148. package/skills/runtime-inspector/references/frontend-rendering.md +126 -0
  149. package/skills/runtime-inspector/references/model.md +59 -0
  150. package/skills/runtime-inspector/references/sandbox.md +131 -0
  151. package/skills/runtime-inspector/scripts/model_inspect.py +68 -0
  152. package/skills/runtime-inspector/scripts/prepare.py +98 -0
  153. package/skills/runtime-inspector/scripts/process.py +84 -0
  154. package/skills/runtime-inspector/scripts/read_state.py +32 -0
  155. package/skills/runtime-inspector/scripts/sandbox_inspect.py +463 -0
  156. package/skills/runtime-inspector/scripts/test_claude_proxy.py +49 -0
  157. package/skills/runtime-inspector/scripts/test_openai_proxy.py +41 -0
  158. package/skills/runtime-inspector/scripts/verify_persistence.py +101 -0
  159. package/skills/runtime-inspector/scripts/write_test.py +41 -0
  160. package/skills/setup/SKILL.md +17 -0
  161. package/skills/shulex-support/SKILL.md +39 -0
  162. package/skills/shulex-support/references/badcase-analysis/overview.md +34 -0
  163. package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +105 -0
  164. package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +89 -0
  165. package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +36 -0
  166. package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +53 -0
  167. package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +259 -0
  168. package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +148 -0
  169. package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +57 -0
  170. package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +120 -0
  171. package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +126 -0
  172. package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +121 -0
  173. package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +208 -0
  174. package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +115 -0
  175. package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +72 -0
  176. package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +92 -0
  177. package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +147 -0
  178. package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +283 -0
  179. package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +77 -0
  180. package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +19 -0
  181. package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +23 -0
  182. package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +31 -0
  183. package/skills/shulex-support/references/product-docs.md +70 -0
  184. package/skills/shulex-support/references/tickets.md +184 -0
  185. package/skills/shulex-support/scripts/support.py +427 -0
  186. package/skills/solvea-desk/SKILL.md +39 -0
  187. package/skills/solvea-desk/references/report/fields.md +82 -0
  188. package/skills/solvea-desk/references/report/report-design.md +200 -0
  189. package/skills/solvea-desk/references/report/report-template.html +44 -0
  190. package/skills/solvea-desk/references/report/report.css +517 -0
  191. package/skills/solvea-desk/references/report/workflow.md +464 -0
  192. package/skills/solvea-desk/references/tickets/query.md +103 -0
  193. package/skills/solvea-desk/references/tickets/tags.md +83 -0
  194. package/skills/solvea-desk/scripts/report/fetch_sources.py +580 -0
  195. package/skills/solvea-desk/scripts/report/render_report.py +763 -0
  196. package/skills/solvea-desk/scripts/tickets/desk_api.py +73 -0
  197. package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +403 -0
  198. package/skills/solvea-desk/scripts/tickets/ticket_tags.py +305 -0
  199. package/skills/solvea-kit/SKILL.md +84 -0
  200. package/skills/solvea-kit/references/agent.md +145 -0
  201. package/skills/solvea-kit/references/chat.md +244 -0
  202. package/skills/solvea-kit/references/intent.md +69 -0
  203. package/skills/solvea-kit/references/plugin.md +53 -0
  204. package/skills/solvea-kit/references/quality-inspection.md +75 -0
  205. package/skills/solvea-kit/references/strategy.md +62 -0
  206. package/skills/solvea-kit/references/tagging.md +231 -0
  207. package/skills/solvea-kit/references/variables.md +53 -0
  208. package/skills/solvea-kit/scripts/generate_label_import_file.py +151 -0
  209. package/skills/solvea-kit/scripts/intent_classify.py +843 -0
@@ -0,0 +1,495 @@
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()