@shulex/cxclawctl 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -22
- package/capabilities.lock.json +535 -131
- package/dist/cli.js +2203 -127
- package/dist/cli.js.map +1 -1
- package/package.json +20 -6
- package/requirements-cli.txt +0 -2
- package/skills/__lib__/__init__.py +0 -0
- package/skills/__lib__/skill-runtime/bootstrap.py +0 -134
- package/skills/__lib__/solvea-api/__init__.py +0 -0
- package/skills/__lib__/solvea-api/client.py +0 -1944
- package/skills/__lib__/solvea-api/dashboard.py +0 -100
- package/skills/__lib__/solvea-api/solvea_api.py +0 -1341
- package/skills/agent-analytics/SKILL.md +0 -74
- package/skills/agent-analytics/references/chat-report-fields.md +0 -36
- package/skills/agent-analytics/references/report-fields.md +0 -130
- package/skills/agent-analytics/references/transcript.md +0 -110
- package/skills/agent-builder/SKILL.md +0 -42
- package/skills/agent-builder/interfaces/fallback-chatflow.md +0 -47
- package/skills/agent-builder/references/chatflow/graph-spec.md +0 -259
- package/skills/agent-builder/references/chatflow/node-selection.md +0 -89
- package/skills/agent-builder/references/chatflow/nodes/agent.md +0 -350
- package/skills/agent-builder/references/chatflow/nodes/business-track.md +0 -43
- package/skills/agent-builder/references/chatflow/nodes/code.md +0 -157
- package/skills/agent-builder/references/chatflow/nodes/collect-order.md +0 -47
- package/skills/agent-builder/references/chatflow/nodes/finish.md +0 -34
- package/skills/agent-builder/references/chatflow/nodes/human-review.md +0 -60
- package/skills/agent-builder/references/chatflow/nodes/if-else.md +0 -100
- package/skills/agent-builder/references/chatflow/nodes/intent.md +0 -79
- package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +0 -43
- package/skills/agent-builder/references/chatflow/nodes/llm.md +0 -117
- package/skills/agent-builder/references/chatflow/nodes/note.md +0 -38
- package/skills/agent-builder/references/chatflow/nodes/question.md +0 -97
- package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +0 -68
- package/skills/agent-builder/references/chatflow/nodes/reply.md +0 -97
- package/skills/agent-builder/references/chatflow/nodes/start.md +0 -35
- package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +0 -48
- package/skills/agent-builder/references/chatflow/nodes/tool.md +0 -138
- package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +0 -63
- package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +0 -52
- package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +0 -50
- package/skills/agent-builder/references/chatflow/nodes/variable-process.md +0 -70
- package/skills/agent-builder/references/chatflow/overview.md +0 -53
- package/skills/agent-builder/references/chatflow/visual-translation.md +0 -64
- package/skills/agent-builder/references/chatflow/workflow.md +0 -134
- package/skills/agent-builder/references/multi-agent/create-update.md +0 -185
- package/skills/agent-builder/references/multi-agent/graph-spec.md +0 -246
- package/skills/agent-builder/references/multi-agent/graph-structure.md +0 -12
- package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +0 -13
- package/skills/agent-builder/references/multi-agent/migration/large.md +0 -554
- package/skills/agent-builder/references/multi-agent/migration/principles.md +0 -446
- package/skills/agent-builder/references/multi-agent/migration/small.md +0 -225
- package/skills/agent-builder/references/multi-agent/orchestration.md +0 -11
- package/skills/agent-builder/references/multi-agent/overview.md +0 -48
- package/skills/agent-builder/references/multi-agent/update.md +0 -128
- package/skills/agent-builder/references/multi-agent/workflow.md +0 -87
- package/skills/agent-builder/references/single-agent/overview.md +0 -48
- package/skills/agent-builder/references/single-agent/prompt.md +0 -16
- package/skills/agent-builder/references/single-agent/tools.md +0 -36
- package/skills/agent-builder/references/single-agent/voice.md +0 -46
- package/skills/agent-builder/references/tools/http-tool.md +0 -315
- package/skills/agent-builder/references/tools/mock-tool.md +0 -279
- package/skills/agent-builder/references/tools/overview.md +0 -46
- package/skills/agent-builder/references/tools/tool-config.md +0 -43
- package/skills/agent-builder/references/tools/x-token.md +0 -34
- package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +0 -3458
- package/skills/agent-builder/scripts/chatflow/validate_graph.py +0 -1697
- package/skills/agent-builder/scripts/copy_marketplace_agent.py +0 -61
- package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +0 -5974
- package/skills/agent-builder/scripts/multi-agent/replay_compare.py +0 -679
- package/skills/agent-builder/scripts/patch_agent_tools.py +0 -135
- package/skills/agent-builder/scripts/tools/validate_tool_config.py +0 -384
- package/skills/agent-tester/SKILL.md +0 -35
- package/skills/agent-tester/batch-test/overview.md +0 -86
- package/skills/agent-tester/scripts/batch_test_sheet.py +0 -213
- package/skills/agent-tester/single-test/badcase-retest.md +0 -85
- package/skills/agent-tester/single-test/direct-test.md +0 -24
- package/skills/agent-tester/single-test/overview.md +0 -142
- package/skills/delivery/SKILL.md +0 -112
- package/skills/delivery/interfaces/classify.md +0 -38
- package/skills/delivery/interfaces/create-agents.md +0 -89
- package/skills/delivery/interfaces/delivery-test.md +0 -67
- package/skills/delivery/references/flow.md +0 -40
- package/skills/delivery/references/validation.md +0 -15
- package/skills/delivery/scripts/batch_test.py +0 -704
- package/skills/delivery/scripts/classify.py +0 -22
- package/skills/delivery/scripts/create_agents.py +0 -695
- package/skills/delivery/scripts/gen_delivery_report.py +0 -335
- package/skills/delivery/scripts/gen_excel.py +0 -234
- package/skills/delivery/scripts/gen_report.py +0 -208
- package/skills/delivery/scripts/plan.py +0 -163
- package/skills/delivery/scripts/render.py +0 -713
- package/skills/delivery/scripts/render_summary.py +0 -38
- package/skills/delivery/scripts/sample_history_cases.py +0 -109
- package/skills/kb-builder/SKILL.md +0 -80
- package/skills/kb-builder/interfaces/qa-upload.md +0 -55
- package/skills/kb-builder/interfaces/web-kb.md +0 -84
- package/skills/kb-builder/references/directory.md +0 -373
- package/skills/kb-builder/references/export.md +0 -282
- package/skills/kb-builder/references/isolation-keywords.md +0 -113
- package/skills/kb-builder/references/knowledge.md +0 -153
- package/skills/kb-builder/references/product.md +0 -52
- package/skills/kb-builder/references/qa-from-help-center.md +0 -58
- package/skills/kb-builder/references/qa-from-pdf.md +0 -94
- package/skills/kb-builder/references/qa-from-tickets.md +0 -168
- package/skills/kb-builder/references/qa-from-web.md +0 -98
- package/skills/kb-builder/references/qa-quality.md +0 -73
- package/skills/kb-builder/references/reply-template.md +0 -237
- package/skills/kb-builder/references/tag-isolation-migration.md +0 -61
- package/skills/kb-builder/references/tags.md +0 -45
- package/skills/kb-builder/references/upload.md +0 -130
- package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +0 -333
- package/skills/kb-builder/scripts/build_directory_plan.py +0 -144
- package/skills/kb-builder/scripts/crawl_site.py +0 -433
- package/skills/kb-builder/scripts/create_dirs.py +0 -146
- package/skills/kb-builder/scripts/export_knowledge.py +0 -454
- package/skills/kb-builder/scripts/extract_ticket_qa.py +0 -441
- package/skills/kb-builder/scripts/extract_web_qa.py +0 -118
- package/skills/kb-builder/scripts/import_qa.py +0 -219
- package/skills/kb-builder/scripts/import_source_documents.py +0 -143
- package/skills/kb-builder/scripts/import_templates.py +0 -409
- package/skills/poc/SKILL.md +0 -125
- package/skills/poc/assets/default_manual.css +0 -279
- package/skills/poc/assets/notice_escalate.png +0 -0
- package/skills/poc/assets/notice_escalate2.png +0 -0
- package/skills/poc/assets/notice_new_chat.png +0 -0
- package/skills/poc/assets/poc_test_template.html +0 -645
- package/skills/poc/assets/poc_test_template.md +0 -104
- package/skills/poc/assets/step3_preview.png +0 -0
- package/skills/poc/assets/step4_order_note.png +0 -0
- package/skills/poc/interfaces/classify.md +0 -38
- package/skills/poc/interfaces/create-agents.md +0 -108
- package/skills/poc/interfaces/poc-test.md +0 -94
- package/skills/poc/references/flow.md +0 -47
- package/skills/poc/references/kb-directory.md +0 -22
- package/skills/poc/references/validation.md +0 -17
- package/skills/poc/scripts/classify.py +0 -22
- package/skills/poc/scripts/create_agents.py +0 -818
- package/skills/poc/scripts/gen_excel.py +0 -234
- package/skills/poc/scripts/gen_poc_manual.py +0 -249
- package/skills/poc/scripts/gen_report.py +0 -208
- package/skills/poc/scripts/inspect_chatflow.py +0 -209
- package/skills/poc/scripts/plan.py +0 -178
- package/skills/poc/scripts/poc_test.py +0 -495
- package/skills/poc/scripts/render.py +0 -713
- package/skills/poc/scripts/render_summary.py +0 -41
- package/skills/runtime-inspector/SKILL.md +0 -51
- package/skills/runtime-inspector/references/frontend-rendering.md +0 -126
- package/skills/runtime-inspector/references/model.md +0 -59
- package/skills/runtime-inspector/references/sandbox.md +0 -131
- package/skills/runtime-inspector/scripts/model_inspect.py +0 -68
- package/skills/runtime-inspector/scripts/prepare.py +0 -98
- package/skills/runtime-inspector/scripts/process.py +0 -84
- package/skills/runtime-inspector/scripts/read_state.py +0 -32
- package/skills/runtime-inspector/scripts/sandbox_inspect.py +0 -463
- package/skills/runtime-inspector/scripts/test_claude_proxy.py +0 -49
- package/skills/runtime-inspector/scripts/test_openai_proxy.py +0 -41
- package/skills/runtime-inspector/scripts/verify_persistence.py +0 -101
- package/skills/runtime-inspector/scripts/write_test.py +0 -41
- package/skills/setup/SKILL.md +0 -17
- package/skills/shulex-support/SKILL.md +0 -39
- package/skills/shulex-support/references/badcase-analysis/overview.md +0 -34
- package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +0 -105
- package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +0 -89
- package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +0 -36
- package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +0 -53
- package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +0 -259
- package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +0 -148
- package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +0 -57
- package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +0 -120
- package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +0 -126
- package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +0 -121
- package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +0 -208
- package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +0 -115
- package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +0 -72
- package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +0 -92
- package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +0 -147
- package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +0 -283
- package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +0 -77
- package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +0 -19
- package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +0 -23
- package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +0 -31
- package/skills/shulex-support/references/product-docs.md +0 -70
- package/skills/shulex-support/references/tickets.md +0 -184
- package/skills/shulex-support/scripts/support.py +0 -427
- package/skills/solvea-desk/SKILL.md +0 -39
- package/skills/solvea-desk/references/report/fields.md +0 -82
- package/skills/solvea-desk/references/report/report-design.md +0 -200
- package/skills/solvea-desk/references/report/report-template.html +0 -44
- package/skills/solvea-desk/references/report/report.css +0 -517
- package/skills/solvea-desk/references/report/workflow.md +0 -464
- package/skills/solvea-desk/references/tickets/query.md +0 -103
- package/skills/solvea-desk/references/tickets/tags.md +0 -83
- package/skills/solvea-desk/scripts/report/fetch_sources.py +0 -580
- package/skills/solvea-desk/scripts/report/render_report.py +0 -763
- package/skills/solvea-desk/scripts/tickets/desk_api.py +0 -73
- package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +0 -403
- package/skills/solvea-desk/scripts/tickets/ticket_tags.py +0 -305
- package/skills/solvea-kit/SKILL.md +0 -84
- package/skills/solvea-kit/references/agent.md +0 -145
- package/skills/solvea-kit/references/chat.md +0 -244
- package/skills/solvea-kit/references/intent.md +0 -69
- package/skills/solvea-kit/references/plugin.md +0 -53
- package/skills/solvea-kit/references/quality-inspection.md +0 -75
- package/skills/solvea-kit/references/strategy.md +0 -62
- package/skills/solvea-kit/references/tagging.md +0 -231
- package/skills/solvea-kit/references/variables.md +0 -53
- package/skills/solvea-kit/scripts/generate_label_import_file.py +0 -151
- package/skills/solvea-kit/scripts/intent_classify.py +0 -843
|
@@ -1,679 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Replay one synthetic or historical case against old/new agents and compare behavior.
|
|
3
|
-
|
|
4
|
-
Default mode is dry-run. Pass --execute to send chat-stream requests.
|
|
5
|
-
Use --synthetic for a quick smoke test, or fetch one real transcript by
|
|
6
|
-
--source-chat-id, --source-agent-id, or --intent. --cases-file is a replay
|
|
7
|
-
cache / override. Use --save-cases-file to persist generated/fetched cases.
|
|
8
|
-
|
|
9
|
-
Cases file schema:
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
"cases": [
|
|
13
|
-
{
|
|
14
|
-
"id": "order_not_found",
|
|
15
|
-
"messages": ["Where is order 123?"],
|
|
16
|
-
"metadata": {"customer_email": "x@example.com"},
|
|
17
|
-
"expected": {
|
|
18
|
-
"handoff": true,
|
|
19
|
-
"contains": ["order"],
|
|
20
|
-
"not_contains": ["{{Template_Reply}}"]
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
from __future__ import annotations
|
|
28
|
-
|
|
29
|
-
import argparse
|
|
30
|
-
import difflib
|
|
31
|
-
import html
|
|
32
|
-
import json
|
|
33
|
-
import os
|
|
34
|
-
import sys
|
|
35
|
-
import uuid
|
|
36
|
-
from pathlib import Path
|
|
37
|
-
from typing import Any
|
|
38
|
-
|
|
39
|
-
_solvea_lib_dir = os.environ.get("SOLVEA_LIB_DIR")
|
|
40
|
-
if not _solvea_lib_dir:
|
|
41
|
-
cur = os.path.abspath(os.path.dirname(__file__))
|
|
42
|
-
while True:
|
|
43
|
-
candidate = os.path.join(cur, "__lib__")
|
|
44
|
-
if os.path.isdir(candidate):
|
|
45
|
-
_solvea_lib_dir = candidate
|
|
46
|
-
break
|
|
47
|
-
parent = os.path.dirname(cur)
|
|
48
|
-
if parent == cur:
|
|
49
|
-
raise RuntimeError("Cannot locate skills __lib__")
|
|
50
|
-
cur = parent
|
|
51
|
-
sys.path.insert(0, os.path.join(_solvea_lib_dir, "skill-runtime"))
|
|
52
|
-
from bootstrap import * # noqa: F401,F403
|
|
53
|
-
from client import SolveaClient
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _read_json(path: Path) -> Any:
|
|
57
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _write_json(path: Path, value: Any) -> None:
|
|
61
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
-
path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _norm_text(value: Any) -> str:
|
|
66
|
-
if value is None:
|
|
67
|
-
return ""
|
|
68
|
-
return " ".join(str(value).split())
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _coerce_list_payload(value: Any) -> list[Any]:
|
|
72
|
-
if isinstance(value, list):
|
|
73
|
-
return value
|
|
74
|
-
if not isinstance(value, dict):
|
|
75
|
-
return []
|
|
76
|
-
for key in ("list", "records", "rows", "data", "items"):
|
|
77
|
-
nested = value.get(key)
|
|
78
|
-
if isinstance(nested, list):
|
|
79
|
-
return nested
|
|
80
|
-
if isinstance(nested, dict):
|
|
81
|
-
for nested_key in ("list", "records", "rows", "items"):
|
|
82
|
-
nested_list = nested.get(nested_key)
|
|
83
|
-
if isinstance(nested_list, list):
|
|
84
|
-
return nested_list
|
|
85
|
-
return []
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _safe_metadata(value: Any) -> dict[str, Any]:
|
|
89
|
-
if not isinstance(value, dict):
|
|
90
|
-
return {}
|
|
91
|
-
out: dict[str, Any] = {}
|
|
92
|
-
for key, item in value.items():
|
|
93
|
-
if key in {
|
|
94
|
-
"displayData",
|
|
95
|
-
"displayPrompt",
|
|
96
|
-
"chatflow_run_info",
|
|
97
|
-
"createdAt",
|
|
98
|
-
"updatedAt",
|
|
99
|
-
"createdBy",
|
|
100
|
-
"updatedBy",
|
|
101
|
-
}:
|
|
102
|
-
continue
|
|
103
|
-
if isinstance(item, (str, int, float, bool)) or item is None:
|
|
104
|
-
out[str(key)] = item
|
|
105
|
-
elif isinstance(item, list) and len(item) <= 20 and all(isinstance(v, (str, int, float, bool)) or v is None for v in item):
|
|
106
|
-
out[str(key)] = item
|
|
107
|
-
elif isinstance(item, dict) and len(item) <= 20:
|
|
108
|
-
nested = _safe_metadata(item)
|
|
109
|
-
if nested:
|
|
110
|
-
out[str(key)] = nested
|
|
111
|
-
return out
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _content_from_message(item: dict[str, Any]) -> str:
|
|
115
|
-
for key in ("content", "message", "text", "question", "query"):
|
|
116
|
-
value = item.get(key)
|
|
117
|
-
if isinstance(value, str) and value.strip():
|
|
118
|
-
return value.strip()
|
|
119
|
-
if isinstance(value, dict):
|
|
120
|
-
for nested_key in ("text", "content", "message"):
|
|
121
|
-
nested = value.get(nested_key)
|
|
122
|
-
if isinstance(nested, str) and nested.strip():
|
|
123
|
-
return nested.strip()
|
|
124
|
-
return ""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _is_user_message(item: dict[str, Any]) -> bool:
|
|
128
|
-
role_values = [
|
|
129
|
-
item.get("role"),
|
|
130
|
-
item.get("type"),
|
|
131
|
-
item.get("sender"),
|
|
132
|
-
item.get("senderRole"),
|
|
133
|
-
item.get("messageType"),
|
|
134
|
-
item.get("from"),
|
|
135
|
-
]
|
|
136
|
-
role_text = " ".join(str(value).upper() for value in role_values if value is not None)
|
|
137
|
-
if any(token in role_text for token in ("USER", "CUSTOMER", "VISITOR", "BUYER")):
|
|
138
|
-
return True
|
|
139
|
-
if any(token in role_text for token in ("ASSISTANT", "AGENT", "BOT", "SYSTEM", "AI")):
|
|
140
|
-
return False
|
|
141
|
-
return bool(_content_from_message(item)) and not any(key in item for key in ("answer", "reply", "response"))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _extract_transcript_messages(detail: Any) -> tuple[list[str], dict[str, Any]]:
|
|
145
|
-
if not isinstance(detail, dict):
|
|
146
|
-
return [], {}
|
|
147
|
-
|
|
148
|
-
metadata: dict[str, Any] = {}
|
|
149
|
-
for key in ("metadata", "meta", "variables"):
|
|
150
|
-
metadata = _safe_metadata(detail.get(key))
|
|
151
|
-
if metadata:
|
|
152
|
-
break
|
|
153
|
-
if not metadata and isinstance(detail.get("context"), dict):
|
|
154
|
-
context = detail["context"]
|
|
155
|
-
metadata = _safe_metadata(context.get("metadata") or context.get("meta") or context.get("ext"))
|
|
156
|
-
|
|
157
|
-
candidates: list[Any] = []
|
|
158
|
-
for key in ("turns", "messages", "messageList", "records", "conversation", "history"):
|
|
159
|
-
if isinstance(detail.get(key), list):
|
|
160
|
-
candidates = detail[key]
|
|
161
|
-
break
|
|
162
|
-
if not candidates and isinstance(detail.get("data"), dict):
|
|
163
|
-
data = detail["data"]
|
|
164
|
-
for key in ("turns", "messages", "messageList", "records", "conversation", "history"):
|
|
165
|
-
if isinstance(data.get(key), list):
|
|
166
|
-
candidates = data[key]
|
|
167
|
-
break
|
|
168
|
-
if not metadata:
|
|
169
|
-
metadata = _safe_metadata(data.get("metadata") or data.get("meta"))
|
|
170
|
-
if not candidates:
|
|
171
|
-
candidates = _coerce_list_payload(detail)
|
|
172
|
-
|
|
173
|
-
messages: list[str] = []
|
|
174
|
-
for item in candidates:
|
|
175
|
-
if not isinstance(item, dict):
|
|
176
|
-
continue
|
|
177
|
-
if not metadata:
|
|
178
|
-
metadata = _safe_metadata(item.get("metadata") or item.get("meta"))
|
|
179
|
-
content = _content_from_message(item)
|
|
180
|
-
if content and _is_user_message(item):
|
|
181
|
-
messages.append(content)
|
|
182
|
-
return messages, metadata
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _case_from_transcript(client: SolveaClient, chat_id: str, *, source_type: str, source_label: str | None = None) -> dict[str, Any]:
|
|
186
|
-
detail = client.get_transcript(chat_id)
|
|
187
|
-
messages, metadata = _extract_transcript_messages(detail)
|
|
188
|
-
if not messages:
|
|
189
|
-
raise ValueError(f"no USER messages found in transcript chatId={chat_id}")
|
|
190
|
-
return {
|
|
191
|
-
"id": f"{source_type}_{chat_id}",
|
|
192
|
-
"source": {
|
|
193
|
-
"type": source_type,
|
|
194
|
-
"chatId": chat_id,
|
|
195
|
-
"label": source_label,
|
|
196
|
-
},
|
|
197
|
-
"messages": messages,
|
|
198
|
-
"metadata": metadata,
|
|
199
|
-
"expected": {
|
|
200
|
-
"not_contains": ["{{Template_Reply}}", "<span"],
|
|
201
|
-
},
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def _case_from_synthetic_args(args: argparse.Namespace) -> dict[str, Any]:
|
|
206
|
-
messages = args.synthetic_message or ["Where is my order 123456?"]
|
|
207
|
-
metadata: dict[str, Any] = {}
|
|
208
|
-
if args.synthetic_metadata:
|
|
209
|
-
parsed = json.loads(args.synthetic_metadata)
|
|
210
|
-
if not isinstance(parsed, dict):
|
|
211
|
-
raise ValueError("--synthetic-metadata must be a JSON object")
|
|
212
|
-
metadata = _safe_metadata(parsed)
|
|
213
|
-
return {
|
|
214
|
-
"id": args.synthetic_case_id or "synthetic_order_status_smoke",
|
|
215
|
-
"source": {
|
|
216
|
-
"type": "synthetic",
|
|
217
|
-
"label": "smoke_test",
|
|
218
|
-
},
|
|
219
|
-
"messages": [str(message) for message in messages if str(message).strip()],
|
|
220
|
-
"metadata": metadata,
|
|
221
|
-
"expected": {
|
|
222
|
-
"not_contains": ["{{Template_Reply}}", "<span"],
|
|
223
|
-
},
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _fetch_cases_from_source(client: SolveaClient, args: argparse.Namespace) -> list[dict[str, Any]]:
|
|
228
|
-
source_count = sum(bool(value) for value in (args.synthetic, args.source_chat_id, args.source_agent_id, args.intent))
|
|
229
|
-
if source_count != 1:
|
|
230
|
-
raise ValueError("provide exactly one source: --synthetic, --source-chat-id, --source-agent-id, or --intent")
|
|
231
|
-
|
|
232
|
-
if args.synthetic:
|
|
233
|
-
return [_case_from_synthetic_args(args)]
|
|
234
|
-
|
|
235
|
-
if args.source_chat_id:
|
|
236
|
-
return [_case_from_transcript(client, args.source_chat_id, source_type="chat_id")]
|
|
237
|
-
|
|
238
|
-
if args.intent:
|
|
239
|
-
rows = client.get_conversations_by_intent(args.intent, limit=args.limit)
|
|
240
|
-
cases = []
|
|
241
|
-
for row in rows[: args.limit]:
|
|
242
|
-
if not isinstance(row, dict):
|
|
243
|
-
continue
|
|
244
|
-
chat_id = row.get("source_chat_id") or row.get("chatId") or row.get("id")
|
|
245
|
-
messages = row.get("messages") if isinstance(row.get("messages"), list) else []
|
|
246
|
-
if chat_id and messages:
|
|
247
|
-
cases.append(
|
|
248
|
-
{
|
|
249
|
-
"id": f"intent_{args.intent}_{chat_id}",
|
|
250
|
-
"source": {"type": "intent", "chatId": chat_id, "label": args.intent},
|
|
251
|
-
"messages": [str(message) for message in messages if str(message).strip()],
|
|
252
|
-
"metadata": _safe_metadata(row.get("metadata")),
|
|
253
|
-
"expected": {"not_contains": ["{{Template_Reply}}", "<span"]},
|
|
254
|
-
}
|
|
255
|
-
)
|
|
256
|
-
if not cases:
|
|
257
|
-
raise ValueError(f"no historical conversations found for intent={args.intent}")
|
|
258
|
-
return cases
|
|
259
|
-
|
|
260
|
-
filters: dict[str, Any] = {"agentId": int(args.source_agent_id)}
|
|
261
|
-
if args.source_touch_point:
|
|
262
|
-
filters["touchpoint"] = args.source_touch_point
|
|
263
|
-
rows_resp = client.list_transcripts(
|
|
264
|
-
page_size=args.limit,
|
|
265
|
-
page_index=1,
|
|
266
|
-
debug=True,
|
|
267
|
-
begin_ts=args.begin_ts,
|
|
268
|
-
end_ts=args.end_ts,
|
|
269
|
-
**filters,
|
|
270
|
-
)
|
|
271
|
-
rows = _coerce_list_payload(rows_resp)
|
|
272
|
-
cases = []
|
|
273
|
-
for row in rows[: args.limit]:
|
|
274
|
-
if not isinstance(row, dict):
|
|
275
|
-
continue
|
|
276
|
-
chat_id = row.get("chatId") or row.get("id")
|
|
277
|
-
if chat_id:
|
|
278
|
-
cases.append(_case_from_transcript(client, str(chat_id), source_type="agent_id", source_label=str(args.source_agent_id)))
|
|
279
|
-
if not cases:
|
|
280
|
-
raise ValueError(f"no transcript found for source agentId={args.source_agent_id}")
|
|
281
|
-
return cases
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def _content_similarity(left: str, right: str) -> float:
|
|
285
|
-
left = _norm_text(left)
|
|
286
|
-
right = _norm_text(right)
|
|
287
|
-
if not left and not right:
|
|
288
|
-
return 1.0
|
|
289
|
-
if not left or not right:
|
|
290
|
-
return 0.0
|
|
291
|
-
return round(difflib.SequenceMatcher(None, left, right).ratio(), 4)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def _extract_route(result: dict[str, Any]) -> dict[str, Any]:
|
|
295
|
-
context = result.get("context") if isinstance(result.get("context"), dict) else {}
|
|
296
|
-
ext = context.get("ext") if isinstance(context.get("ext"), dict) else {}
|
|
297
|
-
agent = context.get("agent") if isinstance(context.get("agent"), dict) else {}
|
|
298
|
-
run_info = ext.get("chatflow_run_info")
|
|
299
|
-
final_node = {}
|
|
300
|
-
if isinstance(run_info, list):
|
|
301
|
-
for event in reversed(run_info):
|
|
302
|
-
if not isinstance(event, dict):
|
|
303
|
-
continue
|
|
304
|
-
event_data = event.get("eventData") if isinstance(event.get("eventData"), dict) else {}
|
|
305
|
-
if event.get("event") == "node_finished" or event_data.get("node_id"):
|
|
306
|
-
final_node = event_data
|
|
307
|
-
break
|
|
308
|
-
elif isinstance(run_info, dict):
|
|
309
|
-
final_node = run_info
|
|
310
|
-
return {
|
|
311
|
-
"agentId": agent.get("id") or agent.get("agentId"),
|
|
312
|
-
"agentName": agent.get("name") or ext.get("invoke_agent_name"),
|
|
313
|
-
"strategyName": result.get("strategyName"),
|
|
314
|
-
"nodeId": final_node.get("node_id") or final_node.get("nodeId"),
|
|
315
|
-
"nodeName": final_node.get("title") or final_node.get("node_name") or final_node.get("nodeName"),
|
|
316
|
-
"nodeType": final_node.get("category") or final_node.get("node_type") or final_node.get("nodeType"),
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def _extract_node_runs(result: dict[str, Any]) -> dict[str, Any]:
|
|
321
|
-
context = result.get("context") if isinstance(result.get("context"), dict) else {}
|
|
322
|
-
ext = context.get("ext") if isinstance(context.get("ext"), dict) else {}
|
|
323
|
-
run_info = ext.get("chatflow_run_info", [])
|
|
324
|
-
node_runs = []
|
|
325
|
-
errors = []
|
|
326
|
-
status = None
|
|
327
|
-
if isinstance(run_info, list):
|
|
328
|
-
for event in run_info:
|
|
329
|
-
if not isinstance(event, dict):
|
|
330
|
-
continue
|
|
331
|
-
event_data = event.get("eventData") if isinstance(event.get("eventData"), dict) else {}
|
|
332
|
-
if event.get("event") == "chat_finished":
|
|
333
|
-
status = event_data.get("status", "")
|
|
334
|
-
if isinstance(event_data.get("error"), dict):
|
|
335
|
-
errors.append(
|
|
336
|
-
{
|
|
337
|
-
"nodeId": event_data.get("node_id") or event_data.get("nodeId"),
|
|
338
|
-
"name": event_data["error"].get("name", ""),
|
|
339
|
-
"detail": str(event_data["error"].get("detail", ""))[:300],
|
|
340
|
-
}
|
|
341
|
-
)
|
|
342
|
-
if event.get("event") == "node_finished":
|
|
343
|
-
node_runs.append(
|
|
344
|
-
{
|
|
345
|
-
"nodeId": event_data.get("node_id", ""),
|
|
346
|
-
"nodeName": event_data.get("title", ""),
|
|
347
|
-
"nodeType": event_data.get("category", ""),
|
|
348
|
-
"status": event_data.get("status", ""),
|
|
349
|
-
"outHandle": event_data.get("out_handle") or event_data.get("outHandle"),
|
|
350
|
-
}
|
|
351
|
-
)
|
|
352
|
-
return {
|
|
353
|
-
"status": status,
|
|
354
|
-
"nodeRuns": node_runs,
|
|
355
|
-
"nodePath": [
|
|
356
|
-
{
|
|
357
|
-
"nodeId": item.get("nodeId"),
|
|
358
|
-
"nodeName": item.get("nodeName"),
|
|
359
|
-
"nodeType": item.get("nodeType"),
|
|
360
|
-
"status": item.get("status"),
|
|
361
|
-
"outHandle": item.get("outHandle"),
|
|
362
|
-
}
|
|
363
|
-
for item in node_runs
|
|
364
|
-
],
|
|
365
|
-
"nodePathUnavailable": not bool(node_runs),
|
|
366
|
-
"errors": errors,
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def _summarize_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
371
|
-
execution = _extract_node_runs(result)
|
|
372
|
-
return {
|
|
373
|
-
"chatId": result.get("chatId"),
|
|
374
|
-
"content": result.get("content") or "",
|
|
375
|
-
"reason": result.get("reason") or result.get("noReplyReason"),
|
|
376
|
-
"handoff": result.get("handoff"),
|
|
377
|
-
"type": result.get("type"),
|
|
378
|
-
"isEnd": result.get("isEnd"),
|
|
379
|
-
"route": _extract_route(result),
|
|
380
|
-
"execution": execution,
|
|
381
|
-
"nodePath": execution["nodePath"],
|
|
382
|
-
"nodePathUnavailable": execution["nodePathUnavailable"],
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def _expected_checks(expected: dict[str, Any], new_result: dict[str, Any]) -> list[dict[str, Any]]:
|
|
387
|
-
checks = []
|
|
388
|
-
content = new_result.get("content") or ""
|
|
389
|
-
if "handoff" in expected:
|
|
390
|
-
checks.append(
|
|
391
|
-
{
|
|
392
|
-
"name": "expected_handoff",
|
|
393
|
-
"pass": new_result.get("handoff") == expected["handoff"],
|
|
394
|
-
"expected": expected["handoff"],
|
|
395
|
-
"actual": new_result.get("handoff"),
|
|
396
|
-
}
|
|
397
|
-
)
|
|
398
|
-
for item in expected.get("contains", []) or []:
|
|
399
|
-
checks.append({"name": f"contains:{item}", "pass": str(item) in content})
|
|
400
|
-
for item in expected.get("not_contains", []) or []:
|
|
401
|
-
checks.append({"name": f"not_contains:{item}", "pass": str(item) not in content})
|
|
402
|
-
route = new_result.get("route") or {}
|
|
403
|
-
for key in ("agentName", "nodeName", "nodeType"):
|
|
404
|
-
expected_key = f"route_{key}"
|
|
405
|
-
if expected_key in expected:
|
|
406
|
-
checks.append(
|
|
407
|
-
{
|
|
408
|
-
"name": expected_key,
|
|
409
|
-
"pass": route.get(key) == expected[expected_key],
|
|
410
|
-
"expected": expected[expected_key],
|
|
411
|
-
"actual": route.get(key),
|
|
412
|
-
}
|
|
413
|
-
)
|
|
414
|
-
return checks
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def _side_metadata(base_metadata: dict[str, Any], agent_id: str | None) -> dict[str, Any]:
|
|
418
|
-
metadata = dict(base_metadata or {})
|
|
419
|
-
if agent_id:
|
|
420
|
-
metadata["specified_agent_id"] = int(agent_id)
|
|
421
|
-
return metadata
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
def _run_side(
|
|
425
|
-
client: SolveaClient,
|
|
426
|
-
*,
|
|
427
|
-
bot_id: str | None,
|
|
428
|
-
agent_id: str | None,
|
|
429
|
-
touch_point: str,
|
|
430
|
-
case: dict[str, Any],
|
|
431
|
-
side: str,
|
|
432
|
-
) -> dict[str, Any]:
|
|
433
|
-
chat_id = f"replay-{case.get('id') or uuid.uuid4().hex}-{side}-{uuid.uuid4().hex[:8]}"
|
|
434
|
-
final = {}
|
|
435
|
-
metadata = _side_metadata(case.get("metadata") or {}, agent_id)
|
|
436
|
-
for message in case.get("messages", []) or []:
|
|
437
|
-
final = client.chat_stream(
|
|
438
|
-
message=message,
|
|
439
|
-
bot_id=bot_id,
|
|
440
|
-
chat_id=chat_id,
|
|
441
|
-
touch_point=touch_point,
|
|
442
|
-
metadata=metadata,
|
|
443
|
-
option={"debug": True, "migrationReplay": True},
|
|
444
|
-
)
|
|
445
|
-
return _summarize_result(final if isinstance(final, dict) else {})
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
def _compare_case(old_result: dict[str, Any], new_result: dict[str, Any], expected: dict[str, Any], threshold: float) -> dict[str, Any]:
|
|
449
|
-
similarity = _content_similarity(old_result.get("content", ""), new_result.get("content", ""))
|
|
450
|
-
checks = _expected_checks(expected or {}, new_result)
|
|
451
|
-
route_old = old_result.get("route") or {}
|
|
452
|
-
route_new = new_result.get("route") or {}
|
|
453
|
-
issues = []
|
|
454
|
-
if similarity < threshold:
|
|
455
|
-
issues.append("content_similarity_low")
|
|
456
|
-
if old_result.get("handoff") != new_result.get("handoff"):
|
|
457
|
-
issues.append("handoff_changed")
|
|
458
|
-
if old_result.get("type") != new_result.get("type"):
|
|
459
|
-
issues.append("type_changed")
|
|
460
|
-
if route_old.get("agentName") and route_new.get("agentName") and route_old.get("agentName") != route_new.get("agentName"):
|
|
461
|
-
issues.append("agent_route_changed")
|
|
462
|
-
if any(not check["pass"] for check in checks):
|
|
463
|
-
issues.append("expected_check_failed")
|
|
464
|
-
return {
|
|
465
|
-
"contentSimilarity": similarity,
|
|
466
|
-
"handoffEqual": old_result.get("handoff") == new_result.get("handoff"),
|
|
467
|
-
"typeEqual": old_result.get("type") == new_result.get("type"),
|
|
468
|
-
"routeOld": route_old,
|
|
469
|
-
"routeNew": route_new,
|
|
470
|
-
"expectedChecks": checks,
|
|
471
|
-
"issues": issues,
|
|
472
|
-
"pass": not issues,
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
def _render_html(report: dict[str, Any]) -> str:
|
|
477
|
-
rows = []
|
|
478
|
-
for case in report.get("cases", []):
|
|
479
|
-
status = "PASS" if case.get("compare", {}).get("pass") else "REVIEW"
|
|
480
|
-
issues = ", ".join(case.get("compare", {}).get("issues", []))
|
|
481
|
-
node_path = case.get("new", {}).get("nodePath") or []
|
|
482
|
-
node_path_text = "\n".join(
|
|
483
|
-
f"{idx + 1}. {item.get('nodeType') or ''} {item.get('nodeName') or item.get('nodeId') or ''} [{item.get('status') or ''}]"
|
|
484
|
-
for idx, item in enumerate(node_path)
|
|
485
|
-
)
|
|
486
|
-
if not node_path_text and case.get("new", {}).get("nodePathUnavailable"):
|
|
487
|
-
node_path_text = "node_path_unavailable"
|
|
488
|
-
rows.append(
|
|
489
|
-
"<tr>"
|
|
490
|
-
f"<td>{html.escape(str(case.get('id')))}</td>"
|
|
491
|
-
f"<td>{status}</td>"
|
|
492
|
-
f"<td>{case.get('compare', {}).get('contentSimilarity')}</td>"
|
|
493
|
-
f"<td>{html.escape(issues)}</td>"
|
|
494
|
-
f"<td><pre>{html.escape(node_path_text)}</pre></td>"
|
|
495
|
-
f"<td><pre>{html.escape(_norm_text(case.get('old', {}).get('content')))}</pre></td>"
|
|
496
|
-
f"<td><pre>{html.escape(_norm_text(case.get('new', {}).get('content')))}</pre></td>"
|
|
497
|
-
"</tr>"
|
|
498
|
-
)
|
|
499
|
-
return """<!doctype html>
|
|
500
|
-
<html lang="zh-CN">
|
|
501
|
-
<head>
|
|
502
|
-
<meta charset="utf-8" />
|
|
503
|
-
<title>Multi-agent Replay Compare</title>
|
|
504
|
-
<style>
|
|
505
|
-
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 24px; color: #172033; }}
|
|
506
|
-
table {{ border-collapse: collapse; width: 100%; }}
|
|
507
|
-
th, td {{ border: 1px solid #d0d5dd; padding: 8px; vertical-align: top; }}
|
|
508
|
-
th {{ background: #f2f4f7; }}
|
|
509
|
-
pre {{ white-space: pre-wrap; max-width: 520px; }}
|
|
510
|
-
</style>
|
|
511
|
-
</head>
|
|
512
|
-
<body>
|
|
513
|
-
<h1>Multi-agent Replay Compare</h1>
|
|
514
|
-
<p>Mode: {mode}</p>
|
|
515
|
-
<p>Total: {total}, Pass: {passed}, Review: {review}</p>
|
|
516
|
-
<table>
|
|
517
|
-
<thead><tr><th>Case</th><th>Status</th><th>Similarity</th><th>Issues</th><th>New Node Path</th><th>Old</th><th>New</th></tr></thead>
|
|
518
|
-
<tbody>{rows}</tbody>
|
|
519
|
-
</table>
|
|
520
|
-
</body>
|
|
521
|
-
</html>
|
|
522
|
-
""".format(
|
|
523
|
-
mode=html.escape(report.get("mode", "")),
|
|
524
|
-
total=report.get("summary", {}).get("total", 0),
|
|
525
|
-
passed=report.get("summary", {}).get("pass", 0),
|
|
526
|
-
review=report.get("summary", {}).get("review", 0),
|
|
527
|
-
rows="\n".join(rows),
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
def _select_cases(cases: list[dict[str, Any]], args: argparse.Namespace) -> list[dict[str, Any]]:
|
|
532
|
-
if args.case_id:
|
|
533
|
-
selected = [case for case in cases if str(case.get("id")) == str(args.case_id)]
|
|
534
|
-
if not selected:
|
|
535
|
-
raise ValueError(f"case_id not found: {args.case_id}")
|
|
536
|
-
return selected[:1]
|
|
537
|
-
return cases[: args.limit]
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
541
|
-
source_client = None
|
|
542
|
-
if args.cases_file:
|
|
543
|
-
payload = _read_json(Path(args.cases_file))
|
|
544
|
-
cases = payload.get("cases", payload if isinstance(payload, list) else [])
|
|
545
|
-
else:
|
|
546
|
-
source_client = None if args.synthetic else SolveaClient(env=args.env, project_id=args.project_id)
|
|
547
|
-
cases = _fetch_cases_from_source(source_client, args)
|
|
548
|
-
if not isinstance(cases, list):
|
|
549
|
-
raise ValueError("cases file must be a JSON array or an object with cases[]")
|
|
550
|
-
cases = _select_cases(cases, args)
|
|
551
|
-
if args.save_cases_file:
|
|
552
|
-
_write_json(Path(args.save_cases_file), {"cases": cases})
|
|
553
|
-
|
|
554
|
-
report_cases = []
|
|
555
|
-
client = source_client if args.execute and source_client is not None else SolveaClient(env=args.env, project_id=args.project_id) if args.execute else None
|
|
556
|
-
for case in cases:
|
|
557
|
-
if not isinstance(case, dict):
|
|
558
|
-
continue
|
|
559
|
-
case_id = case.get("id") or f"case_{len(report_cases):03d}"
|
|
560
|
-
if args.execute:
|
|
561
|
-
old_result = _run_side(
|
|
562
|
-
client,
|
|
563
|
-
bot_id=args.old_bot_id,
|
|
564
|
-
agent_id=args.old_agent_id,
|
|
565
|
-
touch_point=args.touch_point,
|
|
566
|
-
case={**case, "id": case_id},
|
|
567
|
-
side="old",
|
|
568
|
-
)
|
|
569
|
-
new_result = _run_side(
|
|
570
|
-
client,
|
|
571
|
-
bot_id=args.new_bot_id or args.old_bot_id,
|
|
572
|
-
agent_id=args.new_agent_id,
|
|
573
|
-
touch_point=args.touch_point,
|
|
574
|
-
case={**case, "id": case_id},
|
|
575
|
-
side="new",
|
|
576
|
-
)
|
|
577
|
-
else:
|
|
578
|
-
old_result = _summarize_result({})
|
|
579
|
-
new_result = _summarize_result({})
|
|
580
|
-
compare = _compare_case(old_result, new_result, case.get("expected") or {}, args.similarity_threshold)
|
|
581
|
-
report_cases.append(
|
|
582
|
-
{
|
|
583
|
-
"id": case_id,
|
|
584
|
-
"source": case.get("source"),
|
|
585
|
-
"messages": case.get("messages", []),
|
|
586
|
-
"metadataKeys": sorted((case.get("metadata") or {}).keys()),
|
|
587
|
-
"old": old_result,
|
|
588
|
-
"new": new_result,
|
|
589
|
-
"compare": compare,
|
|
590
|
-
}
|
|
591
|
-
)
|
|
592
|
-
|
|
593
|
-
passed = sum(1 for case in report_cases if case.get("compare", {}).get("pass"))
|
|
594
|
-
report = {
|
|
595
|
-
"mode": "execute" if args.execute else "dry-run",
|
|
596
|
-
"env": args.env,
|
|
597
|
-
"caseSource": "cases_file" if args.cases_file else "synthetic" if args.synthetic else "transcript_api",
|
|
598
|
-
"savedCasesFile": args.save_cases_file,
|
|
599
|
-
"old": {"botId": args.old_bot_id, "agentId": args.old_agent_id},
|
|
600
|
-
"new": {"botId": args.new_bot_id or args.old_bot_id, "agentId": args.new_agent_id},
|
|
601
|
-
"summary": {"total": len(report_cases), "pass": passed, "review": len(report_cases) - passed},
|
|
602
|
-
"cases": report_cases,
|
|
603
|
-
}
|
|
604
|
-
return report
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
608
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
609
|
-
parser.add_argument("--cases-file", help="Optional replay cache. If omitted, generate/fetch one case from the selected source.")
|
|
610
|
-
parser.add_argument("--save-cases-file", help="Persist generated/fetched cases so the same case can be replayed later.")
|
|
611
|
-
parser.add_argument("--synthetic", action="store_true", help="Generate one synthetic smoke-test case instead of fetching transcript APIs.")
|
|
612
|
-
parser.add_argument("--synthetic-case-id", help="Case id for --synthetic. Default: synthetic_order_status_smoke.")
|
|
613
|
-
parser.add_argument(
|
|
614
|
-
"--synthetic-message",
|
|
615
|
-
action="append",
|
|
616
|
-
help="User message for --synthetic. Can be repeated for multi-turn. Default: order status smoke message.",
|
|
617
|
-
)
|
|
618
|
-
parser.add_argument("--synthetic-metadata", help="JSON object metadata for --synthetic.")
|
|
619
|
-
parser.add_argument("--source-chat-id", help="Fetch exactly this historical chatId and build one replay case.")
|
|
620
|
-
parser.add_argument("--source-agent-id", help="Fetch one historical transcript that matched the old MULTI_AGENT agentId.")
|
|
621
|
-
parser.add_argument("--intent", help="Fetch one historical transcript by intent name.")
|
|
622
|
-
parser.add_argument("--old-bot-id")
|
|
623
|
-
parser.add_argument("--new-bot-id")
|
|
624
|
-
parser.add_argument("--old-agent-id")
|
|
625
|
-
parser.add_argument("--new-agent-id")
|
|
626
|
-
parser.add_argument("--env", choices=["staging", "prod"], default=os.environ.get("SOLVEA_ENV", "staging"))
|
|
627
|
-
parser.add_argument("--project-id")
|
|
628
|
-
parser.add_argument("--touch-point", default="LIVE_CHAT")
|
|
629
|
-
parser.add_argument("--source-touch-point", help="Optional touchpoint filter when using --source-agent-id.")
|
|
630
|
-
parser.add_argument("--begin-ts", type=int, help="Optional begin timestamp in ms when using --source-agent-id.")
|
|
631
|
-
parser.add_argument("--end-ts", type=int, help="Optional end timestamp in ms when using --source-agent-id.")
|
|
632
|
-
parser.add_argument("--similarity-threshold", type=float, default=0.55)
|
|
633
|
-
parser.add_argument("--case-id", help="Run exactly one case by id.")
|
|
634
|
-
parser.add_argument("--limit", type=int, default=1, help="Number of cases to run when --case-id is not set. Default: 1.")
|
|
635
|
-
parser.add_argument("--execute", action="store_true", help="Actually call chat-stream. Default is dry-run.")
|
|
636
|
-
parser.add_argument("--out-json", required=True)
|
|
637
|
-
parser.add_argument("--out-html")
|
|
638
|
-
return parser
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
def main() -> None:
|
|
642
|
-
args = build_parser().parse_args()
|
|
643
|
-
if not args.cases_file and not (args.synthetic or args.source_chat_id or args.source_agent_id or args.intent):
|
|
644
|
-
raise ValueError("provide --cases-file or one source: --synthetic, --source-chat-id, --source-agent-id, or --intent")
|
|
645
|
-
if args.execute and not (args.old_bot_id or args.old_agent_id or args.new_agent_id):
|
|
646
|
-
raise ValueError("--execute requires routing info such as --old-bot-id and/or --old-agent-id/--new-agent-id")
|
|
647
|
-
report = run(args)
|
|
648
|
-
_write_json(Path(args.out_json), report)
|
|
649
|
-
if args.out_html:
|
|
650
|
-
out_html = Path(args.out_html)
|
|
651
|
-
out_html.parent.mkdir(parents=True, exist_ok=True)
|
|
652
|
-
out_html.write_text(_render_html(report), encoding="utf-8")
|
|
653
|
-
first_case = report["cases"][0] if report.get("cases") else {}
|
|
654
|
-
print(
|
|
655
|
-
json.dumps(
|
|
656
|
-
{
|
|
657
|
-
"out_json": args.out_json,
|
|
658
|
-
"out_html": args.out_html,
|
|
659
|
-
"summary": report["summary"],
|
|
660
|
-
"case": first_case.get("id"),
|
|
661
|
-
"case_source": first_case.get("source"),
|
|
662
|
-
"saved_cases_file": report.get("savedCasesFile"),
|
|
663
|
-
"new_final_result": {
|
|
664
|
-
"content": first_case.get("new", {}).get("content"),
|
|
665
|
-
"handoff": first_case.get("new", {}).get("handoff"),
|
|
666
|
-
"type": first_case.get("new", {}).get("type"),
|
|
667
|
-
"route": first_case.get("new", {}).get("route"),
|
|
668
|
-
"nodePath": first_case.get("new", {}).get("nodePath"),
|
|
669
|
-
"nodePathUnavailable": first_case.get("new", {}).get("nodePathUnavailable"),
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
ensure_ascii=False,
|
|
673
|
-
indent=2,
|
|
674
|
-
)
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if __name__ == "__main__":
|
|
679
|
-
main()
|