@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,1944 +0,0 @@
|
|
|
1
|
-
"""SolveaClient — Solvea Bot Backend API client (library only, no CLI).
|
|
2
|
-
|
|
3
|
-
Import example:
|
|
4
|
-
from client import SolveaClient
|
|
5
|
-
c = SolveaClient() # prod (SOLVEA_PROD_TOKEN)
|
|
6
|
-
c = SolveaClient(env="staging")
|
|
7
|
-
|
|
8
|
-
Environment variables:
|
|
9
|
-
SOLVEA_ENV - "prod" (default) or "staging"
|
|
10
|
-
SOLVEA_TOKEN - staging JWT token
|
|
11
|
-
SOLVEA_PROD_TOKEN - production JWT token
|
|
12
|
-
SOLVEA_BASE_URL - staging base URL (default: https://apps-staging.voc.ai)
|
|
13
|
-
SOLVEA_PROD_URL - production base URL (default: https://apps.voc.ai)
|
|
14
|
-
SOLVEA_PROJECT_ID - project ID (auto-detected if omitted)
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import mimetypes
|
|
19
|
-
import os
|
|
20
|
-
import time
|
|
21
|
-
import unicodedata
|
|
22
|
-
from urllib.parse import quote
|
|
23
|
-
|
|
24
|
-
import httpx
|
|
25
|
-
|
|
26
|
-
from dashboard import DashboardMixin
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class SolveaClient(DashboardMixin):
|
|
30
|
-
"""Client for Solvea Bot Backend API.
|
|
31
|
-
|
|
32
|
-
Dual-environment:
|
|
33
|
-
- prod (default): base_url=https://apps.voc.ai, cookie=_token
|
|
34
|
-
- staging: base_url=https://apps-staging.voc.ai, cookie=_token_staging
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
def __init__(self, token=None, base_url=None, project_id=None, env=None):
|
|
38
|
-
if env is None:
|
|
39
|
-
env = os.environ.get("SOLVEA_ENV", "prod")
|
|
40
|
-
self.env = env
|
|
41
|
-
if env == "prod":
|
|
42
|
-
self.token = token or os.environ.get("SOLVEA_PROD_TOKEN", "") or os.environ.get("SOLVEA_TOKEN", "")
|
|
43
|
-
self.base_url = (base_url or os.environ.get("SOLVEA_PROD_URL", "https://apps.voc.ai")).rstrip("/")
|
|
44
|
-
self._cookie_name = "_token"
|
|
45
|
-
self._qam_prefix = "/api_v2/qam"
|
|
46
|
-
else:
|
|
47
|
-
self.token = token or os.environ.get("SOLVEA_TOKEN", "")
|
|
48
|
-
self.base_url = (base_url or os.environ.get("SOLVEA_BASE_URL", "https://apps-staging.voc.ai")).rstrip("/")
|
|
49
|
-
self._cookie_name = "_token_staging"
|
|
50
|
-
self._qam_prefix = "/sns_v1"
|
|
51
|
-
self._project_id = project_id or os.environ.get("SOLVEA_PROJECT_ID", "")
|
|
52
|
-
self._bot_id = None
|
|
53
|
-
|
|
54
|
-
@staticmethod
|
|
55
|
-
def _request_with_retry(method, url, **kwargs):
|
|
56
|
-
"""Bound external calls to connect<=3s/read<=30s and two retries."""
|
|
57
|
-
timeout = httpx.Timeout(30.0, connect=3.0)
|
|
58
|
-
last_error = None
|
|
59
|
-
for attempt in range(3):
|
|
60
|
-
try:
|
|
61
|
-
with httpx.Client(timeout=timeout) as client:
|
|
62
|
-
return client.request(method, url, **kwargs)
|
|
63
|
-
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
|
64
|
-
last_error = exc
|
|
65
|
-
if attempt == 2:
|
|
66
|
-
raise
|
|
67
|
-
time.sleep(0.2 * (2 ** attempt))
|
|
68
|
-
raise last_error
|
|
69
|
-
|
|
70
|
-
@property
|
|
71
|
-
def internal_url(self):
|
|
72
|
-
if self.env == "prod":
|
|
73
|
-
return "https://internal.shulex.com"
|
|
74
|
-
return self.base_url
|
|
75
|
-
|
|
76
|
-
def _curl(self, method, path, data=None, query_params=None, referer_path="/chatbot"):
|
|
77
|
-
url = f"{self.base_url}{path}"
|
|
78
|
-
headers = {
|
|
79
|
-
"Accept": "application/json, text/plain, */*",
|
|
80
|
-
"Origin": self.base_url,
|
|
81
|
-
"Referer": f"{self.base_url}{referer_path}",
|
|
82
|
-
"Cookie": f"{self._cookie_name}={self.token}",
|
|
83
|
-
}
|
|
84
|
-
resp = self._request_with_retry(
|
|
85
|
-
method, url,
|
|
86
|
-
params=query_params,
|
|
87
|
-
headers=headers,
|
|
88
|
-
json=data,
|
|
89
|
-
)
|
|
90
|
-
return self._parse_response(resp, method, path)
|
|
91
|
-
|
|
92
|
-
def _curl_internal(self, method, path, data=None, query_params=None):
|
|
93
|
-
url = f"{self.internal_url}{path}"
|
|
94
|
-
headers = {
|
|
95
|
-
"Accept": "application/json, text/plain, */*",
|
|
96
|
-
"x-language": "cn",
|
|
97
|
-
"x-token-domain": ".shulex.com",
|
|
98
|
-
"Cookie": f"{self._cookie_name}={self.token}",
|
|
99
|
-
"Origin": self.internal_url,
|
|
100
|
-
"Referer": f"{self.internal_url}/chatbot",
|
|
101
|
-
}
|
|
102
|
-
resp = self._request_with_retry(
|
|
103
|
-
method, url,
|
|
104
|
-
params=query_params,
|
|
105
|
-
headers=headers,
|
|
106
|
-
json=data,
|
|
107
|
-
)
|
|
108
|
-
return self._parse_response(resp, method, path)
|
|
109
|
-
|
|
110
|
-
def _download(self, path, target_path, query_params=None, referer_path="/chatbot"):
|
|
111
|
-
"""Download a non-JSON response from the web backend to target_path."""
|
|
112
|
-
url = f"{self.base_url}{path}"
|
|
113
|
-
headers = {
|
|
114
|
-
"Accept": "*/*",
|
|
115
|
-
"Origin": self.base_url,
|
|
116
|
-
"Referer": f"{self.base_url}{referer_path}",
|
|
117
|
-
"Cookie": f"{self._cookie_name}={self.token}",
|
|
118
|
-
}
|
|
119
|
-
resp = self._request_with_retry("GET", url, params=query_params, headers=headers)
|
|
120
|
-
if resp.status_code >= 400:
|
|
121
|
-
raise RuntimeError(f"HTTP {resp.status_code} GET {path}: {resp.text}")
|
|
122
|
-
target_path = os.fspath(target_path)
|
|
123
|
-
os.makedirs(os.path.dirname(os.path.abspath(target_path)) or ".", exist_ok=True)
|
|
124
|
-
with open(target_path, "wb") as f:
|
|
125
|
-
f.write(resp.content)
|
|
126
|
-
return target_path
|
|
127
|
-
|
|
128
|
-
def _parse_response(self, resp, method, path):
|
|
129
|
-
try:
|
|
130
|
-
payload = resp.json()
|
|
131
|
-
except Exception:
|
|
132
|
-
payload = {"raw": resp.text}
|
|
133
|
-
|
|
134
|
-
if resp.status_code >= 400:
|
|
135
|
-
error = json.dumps(payload, ensure_ascii=False)
|
|
136
|
-
raise RuntimeError(f"HTTP {resp.status_code} {method} {path}: {error}")
|
|
137
|
-
|
|
138
|
-
if isinstance(payload, dict):
|
|
139
|
-
for key in ("code", "errno"):
|
|
140
|
-
if key in payload and payload.get(key) not in (None, 0, "0", 200, "200", "OK", "SUCCESS"):
|
|
141
|
-
message = payload.get("msg") or payload.get("message") or payload.get("error") or payload
|
|
142
|
-
raise RuntimeError(f"API error {method} {path}: {message}")
|
|
143
|
-
if payload.get("success") is False:
|
|
144
|
-
message = payload.get("msg") or payload.get("message") or payload.get("error") or payload
|
|
145
|
-
raise RuntimeError(f"API error {method} {path}: {message}")
|
|
146
|
-
|
|
147
|
-
return payload
|
|
148
|
-
|
|
149
|
-
@property
|
|
150
|
-
def project_id(self):
|
|
151
|
-
if not self._project_id:
|
|
152
|
-
info = self.get_project_info()
|
|
153
|
-
if info and len(info) > 0:
|
|
154
|
-
self._project_id = str(info[0].get("projectId", 950))
|
|
155
|
-
self._bot_id = str(info[0].get("id", ""))
|
|
156
|
-
else:
|
|
157
|
-
self._project_id = "950"
|
|
158
|
-
return self._project_id
|
|
159
|
-
|
|
160
|
-
@property
|
|
161
|
-
def bot_id(self):
|
|
162
|
-
if not self._bot_id:
|
|
163
|
-
info = self.get_project_info()
|
|
164
|
-
if info and len(info) > 0:
|
|
165
|
-
self._bot_id = str(info[0].get("id", "723"))
|
|
166
|
-
self._project_id = str(info[0].get("projectId", 950))
|
|
167
|
-
else:
|
|
168
|
-
self._bot_id = "723"
|
|
169
|
-
return self._bot_id
|
|
170
|
-
|
|
171
|
-
# ── Project ──
|
|
172
|
-
|
|
173
|
-
def get_project_info(self):
|
|
174
|
-
"""GET /api_v2/gpt/bots — get projectId and botId."""
|
|
175
|
-
return self._curl("GET", "/api_v2/gpt/bots")
|
|
176
|
-
|
|
177
|
-
# ── Mock data ──
|
|
178
|
-
|
|
179
|
-
@staticmethod
|
|
180
|
-
def _validate_mock_type(mock_type):
|
|
181
|
-
"""Validate a mock type before using it as an API identifier or path segment."""
|
|
182
|
-
if not isinstance(mock_type, str) or not mock_type:
|
|
183
|
-
raise ValueError("mock_type must be a non-empty string")
|
|
184
|
-
if mock_type in (".", ".."):
|
|
185
|
-
raise ValueError("mock_type must be a safe path segment")
|
|
186
|
-
if any(char in "/\\?#%" or unicodedata.category(char) == "Cc" for char in mock_type):
|
|
187
|
-
raise ValueError("mock_type must be a safe path segment")
|
|
188
|
-
return mock_type
|
|
189
|
-
|
|
190
|
-
def replace_mock_data(self, mock_type, records):
|
|
191
|
-
"""Replace all records for one mock data type."""
|
|
192
|
-
mock_type = self._validate_mock_type(mock_type)
|
|
193
|
-
if not isinstance(records, list):
|
|
194
|
-
raise ValueError("records must be a list")
|
|
195
|
-
if len(records) > 10000:
|
|
196
|
-
raise ValueError("records must contain at most 10000 records")
|
|
197
|
-
return self._curl(
|
|
198
|
-
"POST",
|
|
199
|
-
f"/api_v2/gpt/mockdata/reAddMockData/{quote(mock_type, safe='')}",
|
|
200
|
-
data=records,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
def query_mock_data(self, mock_type):
|
|
204
|
-
"""Query project-scoped records for one mock data type."""
|
|
205
|
-
mock_type = self._validate_mock_type(mock_type)
|
|
206
|
-
return self._curl(
|
|
207
|
-
"GET",
|
|
208
|
-
"/api_v2/gpt/mockdata/queryMockData",
|
|
209
|
-
query_params={"type": mock_type, "globalData": "false"},
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
# ── Transcript (单条详情) ──
|
|
213
|
-
|
|
214
|
-
def get_transcript(self, chat_id, is_compare=True):
|
|
215
|
-
"""GET /api_v2/gpt/project/{projectId}/transcript/{chatId}?isCompare=true
|
|
216
|
-
|
|
217
|
-
获取单条会话记录详情;is_compare=true 时同时返回历史上下文与当前资源的比较结果。
|
|
218
|
-
"""
|
|
219
|
-
query = {"isCompare": "true"} if is_compare else None
|
|
220
|
-
return self._curl(
|
|
221
|
-
"GET",
|
|
222
|
-
f"/api_v2/gpt/project/{self.project_id}/transcript/{chat_id}",
|
|
223
|
-
query_params=query,
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
def get_chat_context(self, bot_id, data):
|
|
227
|
-
"""POST /api_v2/gpt/bot/{botId}/chat/context/compare
|
|
228
|
-
|
|
229
|
-
比较已保存的轮次上下文与当前资源状态,不会重新执行该轮对话。
|
|
230
|
-
data 必须是消息中保存的完整 ChatContext(dict)。
|
|
231
|
-
"""
|
|
232
|
-
return self._curl(
|
|
233
|
-
"POST",
|
|
234
|
-
f"/api_v2/gpt/bot/{bot_id}/chat/context/compare",
|
|
235
|
-
data=data,
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
# ── Agents ──
|
|
239
|
-
|
|
240
|
-
def list_agents(self, category="text", no_meta=True):
|
|
241
|
-
"""POST queryAgent — list all agents."""
|
|
242
|
-
return self._curl("POST", "/api_v2/gpt/ap/queryAgent", {
|
|
243
|
-
"projectId": int(self.project_id),
|
|
244
|
-
"needRelation": True,
|
|
245
|
-
"noMeta": no_meta,
|
|
246
|
-
"category": category,
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
def get_agent(self, agent_id):
|
|
250
|
-
"""POST queryAgent with agentId — full agent detail.
|
|
251
|
-
|
|
252
|
-
Note: CHAT_FLOW agents do not return chatFlowGraph here (by design).
|
|
253
|
-
Use get_chatflow_graph(agent_id) to read the graph, update_chatflow() to
|
|
254
|
-
update an existing graph, or deploy_chatflow() to create a new agent.
|
|
255
|
-
"""
|
|
256
|
-
return self._curl("POST", "/api_v2/gpt/ap/queryAgent", {
|
|
257
|
-
"projectId": int(self.project_id),
|
|
258
|
-
"agentId": int(agent_id),
|
|
259
|
-
"needRelation": True,
|
|
260
|
-
"squareAgent": False,
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
def get_chatflow_graph(self, agent_id):
|
|
264
|
-
"""POST chatflow/queryGraph/latest — get the latest chatFlowGraph for a CHAT_FLOW agent.
|
|
265
|
-
|
|
266
|
-
Returns the graph (nodes / edges / viewport) used by the agent at runtime.
|
|
267
|
-
Pair with update_chatflow() for existing-agent edits.
|
|
268
|
-
"""
|
|
269
|
-
return self._curl(
|
|
270
|
-
"POST",
|
|
271
|
-
"/api_v2/gpt/chatflow/queryGraph/latest",
|
|
272
|
-
{"agentId": int(agent_id)},
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
def create_agent(self, data):
|
|
276
|
-
"""POST createAgent — create a new agent. projectId auto-filled if missing."""
|
|
277
|
-
if "projectId" not in data:
|
|
278
|
-
data["projectId"] = int(self.project_id)
|
|
279
|
-
return self._curl("POST", "/api_v2/gpt/ap/createAgent", data)
|
|
280
|
-
|
|
281
|
-
def update_agent(self, data):
|
|
282
|
-
"""POST updateAgent — full object update (agentId required).
|
|
283
|
-
Always query first (get_agent) to get the full object before calling this.
|
|
284
|
-
When changing prompt, also update displayPrompt to keep them in sync.
|
|
285
|
-
"""
|
|
286
|
-
if "agentId" not in data and "id" in data:
|
|
287
|
-
data["agentId"] = data["id"]
|
|
288
|
-
return self._curl("POST", "/api_v2/gpt/ap/updateAgent", data)
|
|
289
|
-
|
|
290
|
-
def update_chatflow(self, agent_id, graph, publish=False):
|
|
291
|
-
"""Update an existing CHAT_FLOW agent graph.
|
|
292
|
-
|
|
293
|
-
Default behavior matches the UI autosave path: save as the current
|
|
294
|
-
user's draft. Pass publish=True only when the user explicitly confirms
|
|
295
|
-
this version should go live.
|
|
296
|
-
"""
|
|
297
|
-
agent_detail = self.get_agent(agent_id)
|
|
298
|
-
agent = agent_detail[0] if isinstance(agent_detail, list) else agent_detail
|
|
299
|
-
if not agent:
|
|
300
|
-
raise ValueError(f"Agent not found: {agent_id}")
|
|
301
|
-
if agent.get("type") != "CHAT_FLOW":
|
|
302
|
-
raise ValueError(f"Agent {agent_id} is not CHAT_FLOW")
|
|
303
|
-
if (
|
|
304
|
-
not isinstance(graph, dict)
|
|
305
|
-
or not isinstance(graph.get("nodes"), list)
|
|
306
|
-
or not isinstance(graph.get("edges"), list)
|
|
307
|
-
):
|
|
308
|
-
raise ValueError("chatFlowGraph must be a dict with nodes and edges")
|
|
309
|
-
|
|
310
|
-
payload = dict(agent)
|
|
311
|
-
payload["agentId"] = payload.get("id") or int(agent_id)
|
|
312
|
-
payload["meta"] = dict(payload.get("meta") or {})
|
|
313
|
-
payload["meta"]["chatFlowGraph"] = graph
|
|
314
|
-
|
|
315
|
-
if publish:
|
|
316
|
-
payload.pop("backupType", None)
|
|
317
|
-
payload["meta"].pop("backupType", None)
|
|
318
|
-
payload["meta"].pop("draft_flow_updatetime", None)
|
|
319
|
-
else:
|
|
320
|
-
payload["backupType"] = "DRAFT"
|
|
321
|
-
payload["meta"]["draft_flow_updatetime"] = int(time.time() * 1000)
|
|
322
|
-
|
|
323
|
-
result = self.update_agent(payload)
|
|
324
|
-
if isinstance(result, dict) and result.get("errno") not in (None, 0, "0"):
|
|
325
|
-
error = json.dumps(result, ensure_ascii=False)
|
|
326
|
-
raise RuntimeError(f"update_chatflow failed: {error}")
|
|
327
|
-
return result
|
|
328
|
-
|
|
329
|
-
# ── Tools / Plugins ──
|
|
330
|
-
|
|
331
|
-
def list_plugins(self):
|
|
332
|
-
"""POST queryPlugin — list available tools."""
|
|
333
|
-
return self._curl("POST", "/api_v2/gpt/ap/queryPlugin", {
|
|
334
|
-
"projectId": int(self.project_id),
|
|
335
|
-
"needRelation": False,
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
def list_chatflow_plugins(self, for_agent_node=True):
|
|
339
|
-
"""List tools using the same catalog and visibility rules as Chatflow PE."""
|
|
340
|
-
return self._curl("POST", "/api_v2/gpt/chatflow/queryPlugin", {
|
|
341
|
-
"projectId": int(self.project_id),
|
|
342
|
-
"forAgentNode": bool(for_agent_node),
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
def get_plugin(self, plugin_id):
|
|
346
|
-
"""POST queryPlugin with pluginId — get full tool detail."""
|
|
347
|
-
return self._curl("POST", "/api_v2/gpt/ap/queryPlugin", {
|
|
348
|
-
"projectId": int(self.project_id),
|
|
349
|
-
"pluginId": int(plugin_id),
|
|
350
|
-
"convertStrategyV5": True,
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
def create_plugin(self, data):
|
|
354
|
-
"""POST createPlugin — create a new tool."""
|
|
355
|
-
data.setdefault("projectId", int(self.project_id))
|
|
356
|
-
return self._curl("POST", "/api_v2/gpt/ap/createPlugin", data)
|
|
357
|
-
|
|
358
|
-
def update_plugin(self, data):
|
|
359
|
-
"""POST updatePlugin — full object update (pluginId required)."""
|
|
360
|
-
data.setdefault("projectId", int(self.project_id))
|
|
361
|
-
if "pluginId" not in data and "id" in data:
|
|
362
|
-
data["pluginId"] = data["id"]
|
|
363
|
-
return self._curl("POST", "/api_v2/gpt/ap/updatePlugin", data)
|
|
364
|
-
|
|
365
|
-
def delete_plugin(self, plugin_id):
|
|
366
|
-
"""POST deletePlugin — delete a plugin by ID."""
|
|
367
|
-
return self._curl("POST", "/api_v2/gpt/ap/deletePlugin",
|
|
368
|
-
{"projectId": int(self.project_id), "pluginId": plugin_id})
|
|
369
|
-
|
|
370
|
-
def run_http_plugin(self, config, request_body=None):
|
|
371
|
-
"""POST runHttpPlugin — test-run an HTTP plugin without saving."""
|
|
372
|
-
if request_body is not None and not isinstance(request_body, dict):
|
|
373
|
-
raise ValueError("request_body must be a JSON object")
|
|
374
|
-
data = {
|
|
375
|
-
"projectId": int(self.project_id),
|
|
376
|
-
"botId": int(self.bot_id),
|
|
377
|
-
"externalHttpCallConfig": config,
|
|
378
|
-
}
|
|
379
|
-
if request_body is not None:
|
|
380
|
-
data["externalHttpCallRequestBody"] = request_body
|
|
381
|
-
return self._curl("POST", "/api_v2/gpt/ap/runHttpPlugin", data)
|
|
382
|
-
|
|
383
|
-
def build_plugin(self, plugin_id, request_body=None):
|
|
384
|
-
"""POST build/plugin — auto-generate response params from a test call."""
|
|
385
|
-
data = {
|
|
386
|
-
"projectId": int(self.project_id),
|
|
387
|
-
"botId": int(self.bot_id),
|
|
388
|
-
"pluginId": plugin_id,
|
|
389
|
-
}
|
|
390
|
-
if request_body:
|
|
391
|
-
data["externalHttpCallRequestBody"] = request_body
|
|
392
|
-
return self._curl("POST", f"/api_v2/gpt/ap/{self.project_id}/build/plugin", data)
|
|
393
|
-
|
|
394
|
-
# ── AI Fields ──
|
|
395
|
-
|
|
396
|
-
def list_ai_fields(self):
|
|
397
|
-
"""List fields exposed by the CHAT_FLOW canvas endpoint."""
|
|
398
|
-
return self._curl("POST", "/api_v2/gpt/chatflow/queryAiFields", {})
|
|
399
|
-
|
|
400
|
-
def list_agent_ai_fields(self):
|
|
401
|
-
"""List fields accepted by the frontend Agent field-range validation."""
|
|
402
|
-
return self._curl(
|
|
403
|
-
"POST",
|
|
404
|
-
"/api_v2/gpt/field/list/range",
|
|
405
|
-
{"fieldRange": ["all", "agent", "agent_preload_visible"]},
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
def create_ai_field(self, data):
|
|
409
|
-
"""POST createAiField — create an AI field."""
|
|
410
|
-
data.setdefault("projectId", int(self.project_id))
|
|
411
|
-
return self._curl("POST", "/api_v2/gpt/chatflow/createAiField", data)
|
|
412
|
-
|
|
413
|
-
def update_ai_field(self, data):
|
|
414
|
-
"""POST updateAiField — full object update (aiFieldId required)."""
|
|
415
|
-
data.setdefault("projectId", int(self.project_id))
|
|
416
|
-
if "aiFieldId" not in data and "id" in data:
|
|
417
|
-
data["aiFieldId"] = data["id"]
|
|
418
|
-
return self._curl("POST", "/api_v2/gpt/chatflow/updateAiField", data)
|
|
419
|
-
|
|
420
|
-
# ── Tars AI Field management ──
|
|
421
|
-
|
|
422
|
-
def get_ai_field_guide_record(self):
|
|
423
|
-
"""Read the tenant's AI Field initialization state from Tars."""
|
|
424
|
-
return self._curl("POST", "/api_v2/tars/api/field/manage/guide/record")
|
|
425
|
-
|
|
426
|
-
def initialize_ai_fields(self):
|
|
427
|
-
"""Initialize Tars system-preinstalled AI Fields for the current tenant."""
|
|
428
|
-
return self._curl("GET", "/api_v2/tars/api/field/manage/pre/install")
|
|
429
|
-
|
|
430
|
-
def list_managed_ai_fields(self, page_index=1, page_size=50, name=""):
|
|
431
|
-
"""Page Tars-managed AI Fields, including custom fields and usage counts."""
|
|
432
|
-
return self._curl("POST", "/api_v2/tars/api/field/manage/page", {
|
|
433
|
-
"pageIndex": int(page_index),
|
|
434
|
-
"pageSize": int(page_size),
|
|
435
|
-
"name": name or "",
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
def get_managed_ai_field(self, field_id):
|
|
439
|
-
"""Read a Tars-managed AI Field definition by ID."""
|
|
440
|
-
return self._curl("GET", f"/api_v2/tars/api/field/manage/detail/{int(field_id)}")
|
|
441
|
-
|
|
442
|
-
def create_managed_ai_field(self, data):
|
|
443
|
-
"""Create a custom AI Field in Tars."""
|
|
444
|
-
return self._curl("POST", "/api_v2/tars/api/field/manage/create", data)
|
|
445
|
-
|
|
446
|
-
def update_managed_ai_field(self, data):
|
|
447
|
-
"""Update a Tars-managed AI Field definition."""
|
|
448
|
-
return self._curl("POST", "/api_v2/tars/api/field/manage/update", data)
|
|
449
|
-
|
|
450
|
-
def delete_managed_ai_field(self, field_id):
|
|
451
|
-
"""Delete an unused custom AI Field from Tars."""
|
|
452
|
-
return self._curl("GET", f"/api_v2/tars/api/field/manage/delete/{int(field_id)}")
|
|
453
|
-
|
|
454
|
-
def toggle_managed_ai_field(self, field_id):
|
|
455
|
-
"""Toggle a Tars-managed AI Field's enabled state."""
|
|
456
|
-
return self._curl("GET", f"/api_v2/tars/api/field/manage/switch/{int(field_id)}")
|
|
457
|
-
|
|
458
|
-
# ── AI quality inspection management ──
|
|
459
|
-
|
|
460
|
-
def get_quality_inspection_status(self):
|
|
461
|
-
"""Read whether AI quality inspection is enabled for the current project."""
|
|
462
|
-
return self._curl("GET", "/api_v2/intelli/inspection/ticket/status")
|
|
463
|
-
|
|
464
|
-
def update_quality_inspection_status(self, enabled):
|
|
465
|
-
"""Enable or disable AI quality inspection for the current project."""
|
|
466
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/open", {
|
|
467
|
-
"open": bool(enabled),
|
|
468
|
-
"projectId": int(self.project_id),
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
def list_quality_inspection_rules(self, begin_timestamp=None, end_timestamp=None):
|
|
472
|
-
"""List quality inspection rules and optional period hit statistics."""
|
|
473
|
-
query_params = {}
|
|
474
|
-
if begin_timestamp is not None:
|
|
475
|
-
query_params["beginTimestamp"] = int(begin_timestamp)
|
|
476
|
-
if end_timestamp is not None:
|
|
477
|
-
query_params["endTimestamp"] = int(end_timestamp)
|
|
478
|
-
return self._curl("GET", "/api_v2/intelli/inspection/ticket/rules",
|
|
479
|
-
query_params=query_params or None)
|
|
480
|
-
|
|
481
|
-
def get_quality_inspection_rule(self, inspection_key):
|
|
482
|
-
"""Read a system quality rule's configuration."""
|
|
483
|
-
return self._curl("GET", "/api_v2/intelli/inspection/ticket/rules/detail",
|
|
484
|
-
query_params={"inspectionKey": inspection_key})
|
|
485
|
-
|
|
486
|
-
def update_quality_inspection_rule(self, data):
|
|
487
|
-
"""Update a system quality rule's scope, action, or enabled state."""
|
|
488
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/rules", data)
|
|
489
|
-
|
|
490
|
-
def create_custom_quality_rule(self, data):
|
|
491
|
-
"""Create a custom quality inspection rule."""
|
|
492
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/customRules/create", data)
|
|
493
|
-
|
|
494
|
-
def get_custom_quality_rule(self, inspection_key):
|
|
495
|
-
"""Read a custom quality inspection rule's full definition."""
|
|
496
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/customRules/detail", {
|
|
497
|
-
"inspectionKey": inspection_key,
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
def update_custom_quality_rule(self, data):
|
|
501
|
-
"""Update a custom quality inspection rule."""
|
|
502
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/customRules/update", data)
|
|
503
|
-
|
|
504
|
-
def delete_custom_quality_rule(self, inspection_key):
|
|
505
|
-
"""Delete a custom quality inspection rule."""
|
|
506
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/customRules/delete", {
|
|
507
|
-
"inspectionKey": inspection_key,
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
def list_quality_alert_rules(self):
|
|
511
|
-
"""List configured quality inspection alert rules."""
|
|
512
|
-
return self._curl("GET", "/api_v2/intelli/inspection/alert/list")
|
|
513
|
-
|
|
514
|
-
def get_quality_alert_rule(self, alert_id):
|
|
515
|
-
"""Read one quality inspection alert rule."""
|
|
516
|
-
return self._curl("GET", "/api_v2/intelli/inspection/alert/detail",
|
|
517
|
-
query_params={"id": int(alert_id)})
|
|
518
|
-
|
|
519
|
-
def create_quality_alert_rule(self, data):
|
|
520
|
-
"""Create a quality inspection alert rule."""
|
|
521
|
-
return self._curl("POST", "/api_v2/intelli/inspection/alert/create", data)
|
|
522
|
-
|
|
523
|
-
def update_quality_alert_rule(self, data):
|
|
524
|
-
"""Update a quality inspection alert rule."""
|
|
525
|
-
return self._curl("POST", "/api_v2/intelli/inspection/alert/update", data)
|
|
526
|
-
|
|
527
|
-
def delete_quality_alert_rule(self, alert_id):
|
|
528
|
-
"""Delete a quality inspection alert rule."""
|
|
529
|
-
return self._curl("GET", "/api_v2/intelli/inspection/alert/delete",
|
|
530
|
-
query_params={"id": int(alert_id)})
|
|
531
|
-
|
|
532
|
-
def list_quality_alert_messages(self, data):
|
|
533
|
-
"""Page quality inspection alert delivery messages."""
|
|
534
|
-
return self._curl("POST", "/api_v2/intelli/inspection/alert/message/list", data)
|
|
535
|
-
|
|
536
|
-
def get_quality_alert_message(self, message_id):
|
|
537
|
-
"""Read one quality inspection alert delivery message."""
|
|
538
|
-
return self._curl("GET", "/api_v2/intelli/inspection/alert/message/detail",
|
|
539
|
-
query_params={"id": int(message_id)})
|
|
540
|
-
|
|
541
|
-
# ── Chat ──
|
|
542
|
-
|
|
543
|
-
def chat(self, message=None, bot_id=None, chat_id=None, touch_point="LIVE_CHAT",
|
|
544
|
-
messages=None, metadata=None, option=None):
|
|
545
|
-
"""POST /api_v2/gpt/bot/{botId}/chat — 同步对话。
|
|
546
|
-
|
|
547
|
-
metadata 可放任意键,常用:
|
|
548
|
-
specified_agent_id int 强制路由到指定 agent,绕策略匹配
|
|
549
|
-
specified_agent_data dict 临时改 agent 配置不入库(调试用,本项目不主动用)
|
|
550
|
-
业务字段 customer_email / order_type / country / ...
|
|
551
|
-
|
|
552
|
-
返回结构:{chatId, content, reason, context.{agent, ext.{chatflow_run_info,...}},
|
|
553
|
-
handoff, strategyName, type, ...}
|
|
554
|
-
"""
|
|
555
|
-
bid = bot_id or self.bot_id
|
|
556
|
-
data = {
|
|
557
|
-
"botId": int(bid),
|
|
558
|
-
"projectId": int(self.project_id),
|
|
559
|
-
"touchPoint": touch_point,
|
|
560
|
-
"option": option or {"debug": True},
|
|
561
|
-
}
|
|
562
|
-
if message is not None:
|
|
563
|
-
data["message"] = message
|
|
564
|
-
if messages is not None:
|
|
565
|
-
data["messages"] = messages
|
|
566
|
-
if chat_id:
|
|
567
|
-
data["chatId"] = chat_id
|
|
568
|
-
if metadata:
|
|
569
|
-
data["metadata"] = metadata
|
|
570
|
-
return self._curl("POST", f"/api_v2/gpt/bot/{bid}/chat", data)
|
|
571
|
-
|
|
572
|
-
def chat_trace(self, message=None, bot_id=None, chat_id=None, touch_point="LIVE_CHAT",
|
|
573
|
-
messages=None, metadata=None, option=None):
|
|
574
|
-
"""Run synchronous chat with debug options and retain timing metadata."""
|
|
575
|
-
merged_option = dict(option or {})
|
|
576
|
-
merged_option["debug"] = True
|
|
577
|
-
merged_option["verbose"] = True
|
|
578
|
-
started_at = time.time()
|
|
579
|
-
started_perf = time.perf_counter()
|
|
580
|
-
action = self.chat(
|
|
581
|
-
message=message,
|
|
582
|
-
bot_id=bot_id,
|
|
583
|
-
chat_id=chat_id,
|
|
584
|
-
touch_point=touch_point,
|
|
585
|
-
messages=messages,
|
|
586
|
-
metadata=metadata,
|
|
587
|
-
option=merged_option,
|
|
588
|
-
)
|
|
589
|
-
if not isinstance(action, dict) or not action:
|
|
590
|
-
raise RuntimeError("synchronous chat returned no AgentAction")
|
|
591
|
-
finished_at = time.time()
|
|
592
|
-
elapsed_ms = round((time.perf_counter() - started_perf) * 1000)
|
|
593
|
-
return {
|
|
594
|
-
"action": action,
|
|
595
|
-
"started_at": started_at,
|
|
596
|
-
"finished_at": finished_at,
|
|
597
|
-
"elapsed_ms": elapsed_ms,
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
def chat_stream_action_trace(self, message=None, bot_id=None, chat_id=None,
|
|
601
|
-
touch_point="LIVE_CHAT", messages=None, metadata=None,
|
|
602
|
-
option=None, on_event=None):
|
|
603
|
-
"""Run stream chat while retaining only the final AgentAction and timing."""
|
|
604
|
-
bid = bot_id or self.bot_id
|
|
605
|
-
merged_option = dict(option or {})
|
|
606
|
-
merged_option["debug"] = True
|
|
607
|
-
merged_option["verbose"] = True
|
|
608
|
-
body = {
|
|
609
|
-
"botId": int(bid),
|
|
610
|
-
"projectId": int(self.project_id),
|
|
611
|
-
"touchPoint": touch_point,
|
|
612
|
-
"option": merged_option,
|
|
613
|
-
}
|
|
614
|
-
if message is not None:
|
|
615
|
-
body["message"] = message
|
|
616
|
-
if messages is not None:
|
|
617
|
-
body["messages"] = messages
|
|
618
|
-
if chat_id:
|
|
619
|
-
body["chatId"] = chat_id
|
|
620
|
-
if metadata:
|
|
621
|
-
body["metadata"] = metadata
|
|
622
|
-
url = f"{self.base_url}/api_v2/gpt/bot/{bid}/chat/stream"
|
|
623
|
-
headers = {
|
|
624
|
-
"Accept": "text/event-stream",
|
|
625
|
-
"Content-Type": "application/json",
|
|
626
|
-
"Origin": self.base_url,
|
|
627
|
-
"Referer": f"{self.base_url}/chatbot",
|
|
628
|
-
"Cookie": f"{self._cookie_name}={self.token}",
|
|
629
|
-
}
|
|
630
|
-
final_action = None
|
|
631
|
-
started_at = time.time()
|
|
632
|
-
started_perf = time.perf_counter()
|
|
633
|
-
with httpx.Client(timeout=httpx.Timeout(30.0, connect=3.0)) as client:
|
|
634
|
-
with client.stream("POST", url, headers=headers, json=body) as response:
|
|
635
|
-
if response.status_code >= 400:
|
|
636
|
-
raise RuntimeError(
|
|
637
|
-
f"HTTP {response.status_code} POST /api_v2/gpt/bot/{bid}/chat/stream: "
|
|
638
|
-
f"{response.text}"
|
|
639
|
-
)
|
|
640
|
-
for line in response.iter_lines():
|
|
641
|
-
if not line or not line.startswith("data:"):
|
|
642
|
-
continue
|
|
643
|
-
payload = line[5:].strip()
|
|
644
|
-
if not payload:
|
|
645
|
-
continue
|
|
646
|
-
try:
|
|
647
|
-
event = json.loads(payload)
|
|
648
|
-
except Exception:
|
|
649
|
-
continue
|
|
650
|
-
if not isinstance(event, dict):
|
|
651
|
-
continue
|
|
652
|
-
if on_event is not None:
|
|
653
|
-
on_event(event)
|
|
654
|
-
if event.get("errno") not in (None, 0, "0"):
|
|
655
|
-
raise RuntimeError(
|
|
656
|
-
f"API error POST /api_v2/gpt/bot/{bid}/chat/stream: "
|
|
657
|
-
f"{event.get('errmsg') or event.get('message') or event.get('errno')}"
|
|
658
|
-
)
|
|
659
|
-
if not event.get("isEnd"):
|
|
660
|
-
continue
|
|
661
|
-
event_data = event.get("data")
|
|
662
|
-
if isinstance(event_data, dict):
|
|
663
|
-
final_action = event_data
|
|
664
|
-
elif any(key in event for key in ("chatId", "type", "handoff")):
|
|
665
|
-
final_action = {
|
|
666
|
-
key: value for key, value in event.items()
|
|
667
|
-
if key not in ("isEnd", "data", "agentExtraResponseParam")
|
|
668
|
-
}
|
|
669
|
-
if not final_action:
|
|
670
|
-
raise RuntimeError("stream chat returned no final AgentAction")
|
|
671
|
-
finished_at = time.time()
|
|
672
|
-
return {
|
|
673
|
-
"action": final_action,
|
|
674
|
-
"started_at": started_at,
|
|
675
|
-
"finished_at": finished_at,
|
|
676
|
-
"elapsed_ms": round((time.perf_counter() - started_perf) * 1000),
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
def chat_stream(self, message=None, bot_id=None, chat_id=None, touch_point="LIVE_CHAT",
|
|
680
|
-
messages=None, metadata=None, option=None):
|
|
681
|
-
"""POST /api_v2/gpt/bot/{botId}/chat/stream — SSE 流式,聚合后返回最终 AgentAction。
|
|
682
|
-
|
|
683
|
-
返回结构与 client.chat 同步对话一致:{chatId, content, reason (noReplyReason),
|
|
684
|
-
context.{agent, ext.{chatflow_run_info, invoke_agent_name}}, handoff, strategyName,
|
|
685
|
-
type, isEnd:True, ...}
|
|
686
|
-
|
|
687
|
-
metadata 同 chat:可放 specified_agent_id / specified_agent_data /
|
|
688
|
-
业务字段(customer_email / order_type / ...)
|
|
689
|
-
"""
|
|
690
|
-
bid = bot_id or self.bot_id
|
|
691
|
-
url = f"{self.base_url}/api_v2/gpt/bot/{bid}/chat/stream"
|
|
692
|
-
body = {
|
|
693
|
-
"botId": int(bid),
|
|
694
|
-
"projectId": int(self.project_id),
|
|
695
|
-
"touchPoint": touch_point,
|
|
696
|
-
"option": option or {"debug": True},
|
|
697
|
-
}
|
|
698
|
-
if message is not None:
|
|
699
|
-
body["message"] = message
|
|
700
|
-
if messages is not None:
|
|
701
|
-
body["messages"] = messages
|
|
702
|
-
if chat_id:
|
|
703
|
-
body["chatId"] = chat_id
|
|
704
|
-
if metadata:
|
|
705
|
-
body["metadata"] = metadata
|
|
706
|
-
headers = {
|
|
707
|
-
"Accept": "text/event-stream",
|
|
708
|
-
"Content-Type": "application/json",
|
|
709
|
-
"Origin": self.base_url,
|
|
710
|
-
"Referer": f"{self.base_url}/chatbot",
|
|
711
|
-
"Cookie": f"{self._cookie_name}={self.token}",
|
|
712
|
-
}
|
|
713
|
-
final_event = None
|
|
714
|
-
parts = []
|
|
715
|
-
with httpx.Client(timeout=httpx.Timeout(30.0, connect=3.0)) as client:
|
|
716
|
-
with client.stream("POST", url, headers=headers, json=body) as resp:
|
|
717
|
-
for line in resp.iter_lines():
|
|
718
|
-
if not line or not line.startswith("data:"):
|
|
719
|
-
continue
|
|
720
|
-
payload = line[5:].strip()
|
|
721
|
-
if not payload:
|
|
722
|
-
continue
|
|
723
|
-
try:
|
|
724
|
-
evt = json.loads(payload)
|
|
725
|
-
except Exception:
|
|
726
|
-
continue
|
|
727
|
-
if isinstance(evt, dict):
|
|
728
|
-
chunk = evt.get("content")
|
|
729
|
-
if chunk:
|
|
730
|
-
parts.append(chunk)
|
|
731
|
-
if evt.get("isEnd"):
|
|
732
|
-
final_event = evt
|
|
733
|
-
if final_event is not None:
|
|
734
|
-
return final_event
|
|
735
|
-
return {"content": "".join(parts), "isEnd": True}
|
|
736
|
-
|
|
737
|
-
def chat_async(self, message=None, bot_id=None, chat_id=None, touch_point="LIVE_CHAT",
|
|
738
|
-
messages=None, metadata=None, option=None,
|
|
739
|
-
callback=None, expire_minutes=None):
|
|
740
|
-
"""POST /api_v2/gpt/bot/{botId}/chat/async — 提交异步对话,返回 AsyncReceipt。
|
|
741
|
-
|
|
742
|
-
AsyncReceipt: {receipt, expireAt, handoff, errorMessage}
|
|
743
|
-
幂等键:chatId + 最后一条 USER message id。
|
|
744
|
-
callback / expire_minutes 透传到 query string。
|
|
745
|
-
"""
|
|
746
|
-
bid = bot_id or self.bot_id
|
|
747
|
-
data = {
|
|
748
|
-
"botId": int(bid),
|
|
749
|
-
"projectId": int(self.project_id),
|
|
750
|
-
"touchPoint": touch_point,
|
|
751
|
-
"option": option or {"debug": True},
|
|
752
|
-
}
|
|
753
|
-
if message is not None:
|
|
754
|
-
data["message"] = message
|
|
755
|
-
if messages is not None:
|
|
756
|
-
data["messages"] = messages
|
|
757
|
-
if chat_id:
|
|
758
|
-
data["chatId"] = chat_id
|
|
759
|
-
if metadata:
|
|
760
|
-
data["metadata"] = metadata
|
|
761
|
-
query = {}
|
|
762
|
-
if callback:
|
|
763
|
-
query["callback"] = callback
|
|
764
|
-
if expire_minutes is not None:
|
|
765
|
-
query["expireMinutes"] = expire_minutes
|
|
766
|
-
return self._curl(
|
|
767
|
-
"POST",
|
|
768
|
-
f"/api_v2/gpt/bot/{bid}/chat/async",
|
|
769
|
-
data=data,
|
|
770
|
-
query_params=query or None,
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
def get_async_result(self, receipt, bot_id=None):
|
|
774
|
-
"""GET /api_v2/gpt/bot/{botId}/chat/async/{receipt} — 轮询异步结果。
|
|
775
|
-
|
|
776
|
-
AsyncResult: {completed, data (AgentAction), error}
|
|
777
|
-
completed=True 时 data 与 chat_stream 返回一致;建议轮询 1-2s,过 expireAt 停。
|
|
778
|
-
"""
|
|
779
|
-
bid = bot_id or self.bot_id
|
|
780
|
-
return self._curl("GET", f"/api_v2/gpt/bot/{bid}/chat/async/{receipt}")
|
|
781
|
-
|
|
782
|
-
# ── Intents ──
|
|
783
|
-
|
|
784
|
-
def list_intents(self, category="E_COMMERCE"):
|
|
785
|
-
"""GET intents — list intents."""
|
|
786
|
-
params = {"category": category} if category else {}
|
|
787
|
-
return self._curl("GET", f"/api_v2/gpt/project/{self.project_id}/intents",
|
|
788
|
-
query_params=params)
|
|
789
|
-
|
|
790
|
-
def create_intent(self, data):
|
|
791
|
-
"""POST intent — create a new intent."""
|
|
792
|
-
return self._curl("POST", f"/api_v2/gpt/project/{self.project_id}/intent", data)
|
|
793
|
-
|
|
794
|
-
def get_intent(self, intent_id):
|
|
795
|
-
"""GET intent detail."""
|
|
796
|
-
return self._curl("GET", f"/api_v2/gpt/project/{self.project_id}/intent/{intent_id}")
|
|
797
|
-
|
|
798
|
-
def update_intent(self, intent_id, data):
|
|
799
|
-
"""PUT intent — update an intent (full object)."""
|
|
800
|
-
return self._curl("PUT", f"/api_v2/gpt/project/{self.project_id}/intent/{intent_id}", data)
|
|
801
|
-
|
|
802
|
-
def toggle_intent(self, intent_id, enabled=True):
|
|
803
|
-
"""PUT intent status — enable or disable."""
|
|
804
|
-
status = "ENABLED" if enabled else "DISABLED"
|
|
805
|
-
return self._curl("PUT",
|
|
806
|
-
f"/api_v2/gpt/project/{self.project_id}/intent/{intent_id}/status",
|
|
807
|
-
{"status": status})
|
|
808
|
-
|
|
809
|
-
def get_conversations_by_intent(self, intent, limit=50):
|
|
810
|
-
"""按意图名拉历史会话,每条带 USER 消息序列。
|
|
811
|
-
|
|
812
|
-
实现:
|
|
813
|
-
1. list_intents(category=None) 拿全量意图,按 name 找 intent_id
|
|
814
|
-
2. POST /api_v2/gpt/project/{projectId}/transcripts/filter
|
|
815
|
-
body: {"pageSize": limit, "pageIndex": 1, "intentId": <int>}
|
|
816
|
-
3. 对每条 chatId 调 GET /api_v2/gpt/project/{projectId}/transcript/{chatId}
|
|
817
|
-
取 role=USER 且 content 非空的消息
|
|
818
|
-
|
|
819
|
-
返回:[{source_chat_id, intent, messages: [...], metadata: {...}}, ...]
|
|
820
|
-
单条 chatId 拉取异常 → 跳过,不阻断整批。
|
|
821
|
-
"""
|
|
822
|
-
intents_resp = self.list_intents(category=None)
|
|
823
|
-
intents = intents_resp if isinstance(intents_resp, list) else \
|
|
824
|
-
intents_resp.get("data", intents_resp.get("list", []))
|
|
825
|
-
intent_id = None
|
|
826
|
-
for it in intents:
|
|
827
|
-
if isinstance(it, dict) and it.get("name") == intent:
|
|
828
|
-
intent_id = it.get("id")
|
|
829
|
-
break
|
|
830
|
-
if intent_id is None:
|
|
831
|
-
return []
|
|
832
|
-
|
|
833
|
-
filter_resp = self._curl(
|
|
834
|
-
"POST",
|
|
835
|
-
f"/api_v2/gpt/project/{self.project_id}/transcripts/filter",
|
|
836
|
-
{"pageSize": limit, "pageIndex": 1, "intentId": int(intent_id)},
|
|
837
|
-
)
|
|
838
|
-
rows = filter_resp.get("list", filter_resp) if isinstance(filter_resp, dict) else filter_resp
|
|
839
|
-
if not isinstance(rows, list):
|
|
840
|
-
return []
|
|
841
|
-
|
|
842
|
-
out = []
|
|
843
|
-
for row in rows:
|
|
844
|
-
chat_id = row.get("chatId") or row.get("id")
|
|
845
|
-
if not chat_id:
|
|
846
|
-
continue
|
|
847
|
-
try:
|
|
848
|
-
detail = self.get_transcript(chat_id)
|
|
849
|
-
except Exception:
|
|
850
|
-
continue
|
|
851
|
-
messages = []
|
|
852
|
-
metadata = {}
|
|
853
|
-
turns = detail.get("turns") if isinstance(detail, dict) else None
|
|
854
|
-
if not turns and isinstance(detail, dict):
|
|
855
|
-
turns = detail.get("messages", [])
|
|
856
|
-
if isinstance(turns, list):
|
|
857
|
-
for turn in turns:
|
|
858
|
-
if not isinstance(turn, dict):
|
|
859
|
-
continue
|
|
860
|
-
role = turn.get("role") or turn.get("type")
|
|
861
|
-
content = turn.get("content") or turn.get("message")
|
|
862
|
-
if role == "USER" and content:
|
|
863
|
-
messages.append(content)
|
|
864
|
-
if not metadata and isinstance(turn.get("metadata"), dict):
|
|
865
|
-
metadata = turn["metadata"]
|
|
866
|
-
if not messages:
|
|
867
|
-
continue
|
|
868
|
-
out.append({
|
|
869
|
-
"source_chat_id": chat_id,
|
|
870
|
-
"intent": intent,
|
|
871
|
-
"messages": messages,
|
|
872
|
-
"metadata": metadata,
|
|
873
|
-
})
|
|
874
|
-
return out
|
|
875
|
-
|
|
876
|
-
# ── Strategy ──
|
|
877
|
-
|
|
878
|
-
def list_entities(self):
|
|
879
|
-
"""GET entities — get strategy condition metadata."""
|
|
880
|
-
return self._curl("GET", f"/api_v2/gpt/project/{self.project_id}/entities")
|
|
881
|
-
|
|
882
|
-
def list_strategies(self, page_size=100):
|
|
883
|
-
"""GET /api_v2/gpt/project/{projectId}/strategies?pageSize={N}&pageNum=1
|
|
884
|
-
|
|
885
|
-
返回 {"list":[{id,name,intent,...}], "total":..., ...}
|
|
886
|
-
sample_history_cases 间接通过策略发现"已配置回答"的意图集合。
|
|
887
|
-
"""
|
|
888
|
-
return self._curl(
|
|
889
|
-
"GET",
|
|
890
|
-
f"/api_v2/gpt/project/{self.project_id}/strategies",
|
|
891
|
-
query_params={"pageSize": page_size, "pageNum": 1},
|
|
892
|
-
)
|
|
893
|
-
|
|
894
|
-
def create_strategy(self, data):
|
|
895
|
-
"""POST strategy — create a strategy."""
|
|
896
|
-
return self._curl("POST", f"/api_v2/gpt/project/{self.project_id}/strategy",
|
|
897
|
-
data, query_params={"v4": "true"})
|
|
898
|
-
|
|
899
|
-
def update_strategy(self, strategy_id, data):
|
|
900
|
-
"""PUT strategy — update a strategy (full object)."""
|
|
901
|
-
return self._curl("PUT",
|
|
902
|
-
f"/api_v2/gpt/project/{self.project_id}/strategy/{strategy_id}",
|
|
903
|
-
data, query_params={"v4": "true"})
|
|
904
|
-
|
|
905
|
-
def toggle_strategy(self, strategy_id, enabled=True):
|
|
906
|
-
"""PUT strategy status — enable or disable."""
|
|
907
|
-
status = "ENABLED" if enabled else "DISABLED"
|
|
908
|
-
return self._curl("PUT",
|
|
909
|
-
f"/api_v2/gpt/project/{self.project_id}/strategy/{strategy_id}",
|
|
910
|
-
{"status": status}, query_params={"v4": "true"})
|
|
911
|
-
|
|
912
|
-
# ── Templates ──
|
|
913
|
-
|
|
914
|
-
def create_template(self, template):
|
|
915
|
-
"""POST template — create one reply template in the template library."""
|
|
916
|
-
data = {
|
|
917
|
-
"name": template.get("name", ""),
|
|
918
|
-
"content": template.get("content", ""),
|
|
919
|
-
"status": template.get("status", "ENABLED"),
|
|
920
|
-
"multiContent": template.get("multiContent") or {"AUTO": template.get("content", "")},
|
|
921
|
-
}
|
|
922
|
-
if template.get("scene"):
|
|
923
|
-
data["scene"] = template.get("scene")
|
|
924
|
-
return self._curl(
|
|
925
|
-
"POST",
|
|
926
|
-
f"/api_v2/gpt/project/{self.project_id}/template",
|
|
927
|
-
data,
|
|
928
|
-
referer_path="/templateBase",
|
|
929
|
-
)
|
|
930
|
-
|
|
931
|
-
def update_template(self, template_id, name=None, content=None, status=None, multi_content=None):
|
|
932
|
-
"""PUT template — partial-update one reply template (only provided fields change).
|
|
933
|
-
|
|
934
|
-
服务端 partial merge:只更新非 null 字段(name / content / status / multiContent);
|
|
935
|
-
传 content 时服务端自动同步 multiContent[AUTO]。改名撞已有模板名会报错。
|
|
936
|
-
见 shulex-gpt TemplateController.updateTemplate。
|
|
937
|
-
"""
|
|
938
|
-
data = {}
|
|
939
|
-
if name is not None:
|
|
940
|
-
data["name"] = name
|
|
941
|
-
if content is not None:
|
|
942
|
-
data["content"] = content
|
|
943
|
-
if status is not None:
|
|
944
|
-
data["status"] = status
|
|
945
|
-
if multi_content is not None:
|
|
946
|
-
data["multiContent"] = multi_content
|
|
947
|
-
if not data:
|
|
948
|
-
raise ValueError(
|
|
949
|
-
"update-template needs at least one field to change: --name / --content / --status / multiContent"
|
|
950
|
-
)
|
|
951
|
-
return self._curl(
|
|
952
|
-
"PUT",
|
|
953
|
-
f"/api_v2/gpt/project/{self.project_id}/template/{template_id}",
|
|
954
|
-
data,
|
|
955
|
-
referer_path="/templateBase",
|
|
956
|
-
)
|
|
957
|
-
|
|
958
|
-
def list_templates(self, page_index=1, page_size=999, keyword="", scenes=None, status=None):
|
|
959
|
-
"""POST templates/filter — list reply templates."""
|
|
960
|
-
data = {
|
|
961
|
-
"pageIndex": page_index,
|
|
962
|
-
"pagesize": page_size,
|
|
963
|
-
"keyword": keyword,
|
|
964
|
-
}
|
|
965
|
-
if scenes:
|
|
966
|
-
data["scenes"] = scenes
|
|
967
|
-
if status:
|
|
968
|
-
data["status"] = status
|
|
969
|
-
return self._curl(
|
|
970
|
-
"POST",
|
|
971
|
-
f"/api_v2/gpt/project/{self.project_id}/templates/filter",
|
|
972
|
-
data,
|
|
973
|
-
referer_path="/templateBase",
|
|
974
|
-
)
|
|
975
|
-
|
|
976
|
-
def delete_templates(self, template_ids):
|
|
977
|
-
"""DELETE templates — batch delete reply templates by IDs."""
|
|
978
|
-
return self._curl(
|
|
979
|
-
"DELETE",
|
|
980
|
-
f"/api_v2/gpt/project/{self.project_id}/templates",
|
|
981
|
-
{"templateIds": [int(template_id) for template_id in template_ids]},
|
|
982
|
-
referer_path="/templateBase",
|
|
983
|
-
)
|
|
984
|
-
|
|
985
|
-
# ── Knowledge ──
|
|
986
|
-
|
|
987
|
-
def list_knowledge(
|
|
988
|
-
self,
|
|
989
|
-
page_index=1,
|
|
990
|
-
page_size=50,
|
|
991
|
-
keyword="",
|
|
992
|
-
knowledge_type="ALL",
|
|
993
|
-
directory_id=None,
|
|
994
|
-
):
|
|
995
|
-
"""POST getKnowledge — list knowledge items."""
|
|
996
|
-
data = {
|
|
997
|
-
"type": knowledge_type,
|
|
998
|
-
"pageIndex": page_index,
|
|
999
|
-
"pageSize": page_size,
|
|
1000
|
-
"keyword": keyword,
|
|
1001
|
-
}
|
|
1002
|
-
if directory_id is not None:
|
|
1003
|
-
data["directoryId"] = int(directory_id)
|
|
1004
|
-
return self._curl("POST", f"{self._qam_prefix}/project/{self.project_id}/getKnowledge", data,
|
|
1005
|
-
referer_path="/knowledge")
|
|
1006
|
-
|
|
1007
|
-
def get_knowledge(self, knowledge_id):
|
|
1008
|
-
"""GET ku/{kuId} — read one QA/self-learning knowledge item."""
|
|
1009
|
-
return self._curl(
|
|
1010
|
-
"GET",
|
|
1011
|
-
f"{self._qam_prefix}/project/{self.project_id}/ku/{int(knowledge_id)}",
|
|
1012
|
-
referer_path="/knowledge",
|
|
1013
|
-
)
|
|
1014
|
-
|
|
1015
|
-
def get_document(self, document_id):
|
|
1016
|
-
"""GET document/{documentId} — read one FILE/WEBSITE document."""
|
|
1017
|
-
return self._curl(
|
|
1018
|
-
"GET",
|
|
1019
|
-
f"{self._qam_prefix}/project/{self.project_id}/document/{int(document_id)}",
|
|
1020
|
-
referer_path="/knowledge",
|
|
1021
|
-
)
|
|
1022
|
-
|
|
1023
|
-
def get_product_knowledge(self, knowledge_id):
|
|
1024
|
-
"""GET ku_product/{id} — read one product knowledge item."""
|
|
1025
|
-
return self._curl(
|
|
1026
|
-
"GET",
|
|
1027
|
-
f"{self._qam_prefix}/project/{self.project_id}/ku_product/{int(knowledge_id)}",
|
|
1028
|
-
referer_path="/knowledge",
|
|
1029
|
-
)
|
|
1030
|
-
|
|
1031
|
-
def get_knowledge_by_type(self, knowledge_id, knowledge_type):
|
|
1032
|
-
"""Read an exact item through the entity endpoint for its UI type."""
|
|
1033
|
-
normalized = str(knowledge_type).upper()
|
|
1034
|
-
if normalized in {"FILE", "WEBSITE"}:
|
|
1035
|
-
return self.get_document(knowledge_id)
|
|
1036
|
-
if normalized == "PRODUCT":
|
|
1037
|
-
return self.get_product_knowledge(knowledge_id)
|
|
1038
|
-
if normalized in {"QA", "LEARN_SELF"}:
|
|
1039
|
-
return self.get_knowledge(knowledge_id)
|
|
1040
|
-
raise ValueError("knowledge_type must be QA, LEARN_SELF, FILE, WEBSITE, or PRODUCT")
|
|
1041
|
-
|
|
1042
|
-
def update_knowledge(self, knowledge_id, data):
|
|
1043
|
-
"""PUT ku — update one knowledge item."""
|
|
1044
|
-
payload = dict(data)
|
|
1045
|
-
payload["kuId"] = int(knowledge_id)
|
|
1046
|
-
return self._curl(
|
|
1047
|
-
"PUT",
|
|
1048
|
-
f"{self._qam_prefix}/project/{self.project_id}/ku",
|
|
1049
|
-
payload,
|
|
1050
|
-
referer_path="/knowledge",
|
|
1051
|
-
)
|
|
1052
|
-
|
|
1053
|
-
def update_kus(self, items):
|
|
1054
|
-
"""PUT kus — batch update knowledge items (content, title, directoryId, etc.)."""
|
|
1055
|
-
if not isinstance(items, list) or not items:
|
|
1056
|
-
raise ValueError("items must be a non-empty array")
|
|
1057
|
-
formatted = []
|
|
1058
|
-
for item in items:
|
|
1059
|
-
row = dict(item)
|
|
1060
|
-
if "id" in row and "kuId" not in row:
|
|
1061
|
-
row["kuId"] = row.pop("id")
|
|
1062
|
-
if "kuId" not in row:
|
|
1063
|
-
raise ValueError("each item in batch update requires kuId or id")
|
|
1064
|
-
row["kuId"] = int(row["kuId"])
|
|
1065
|
-
formatted.append(row)
|
|
1066
|
-
return self._curl(
|
|
1067
|
-
"PUT",
|
|
1068
|
-
f"{self._qam_prefix}/project/{self.project_id}/kus",
|
|
1069
|
-
{"list": formatted},
|
|
1070
|
-
referer_path="/knowledge",
|
|
1071
|
-
)
|
|
1072
|
-
|
|
1073
|
-
def update_document_content(self, document_id, data):
|
|
1074
|
-
"""PUT document content for FILE/WEBSITE knowledge."""
|
|
1075
|
-
payload = dict(data)
|
|
1076
|
-
knowledge_type = str(payload.get("type", "")).upper()
|
|
1077
|
-
if knowledge_type not in {"FILE", "WEBSITE"}:
|
|
1078
|
-
raise ValueError("document type must be FILE or WEBSITE")
|
|
1079
|
-
payload["type"] = knowledge_type
|
|
1080
|
-
payload["knowledgeIds"] = [int(document_id)]
|
|
1081
|
-
# The backend reads metadata even when only the body changes.
|
|
1082
|
-
payload.setdefault("metadata", {})
|
|
1083
|
-
return self._curl(
|
|
1084
|
-
"PUT",
|
|
1085
|
-
f"{self._qam_prefix}/project/{self.project_id}/document/{int(document_id)}/contentV2",
|
|
1086
|
-
payload,
|
|
1087
|
-
referer_path="/knowledge",
|
|
1088
|
-
)
|
|
1089
|
-
|
|
1090
|
-
def create_product_knowledge(self, data):
|
|
1091
|
-
"""POST one PRODUCT knowledge item."""
|
|
1092
|
-
payload = dict(data)
|
|
1093
|
-
payload.setdefault("source", "MANUAL")
|
|
1094
|
-
payload.setdefault("published", True)
|
|
1095
|
-
payload.setdefault("isVisible", True)
|
|
1096
|
-
payload.setdefault("metadata", {"tags": []})
|
|
1097
|
-
return self._curl(
|
|
1098
|
-
"POST",
|
|
1099
|
-
f"{self._qam_prefix}/project/{self.project_id}/ku_product",
|
|
1100
|
-
payload,
|
|
1101
|
-
referer_path="/knowledge",
|
|
1102
|
-
)
|
|
1103
|
-
|
|
1104
|
-
def update_product_knowledge(self, knowledge_id, data):
|
|
1105
|
-
"""PUT one PRODUCT knowledge item using a complete merged payload."""
|
|
1106
|
-
payload = dict(data)
|
|
1107
|
-
payload["kuProductId"] = int(knowledge_id)
|
|
1108
|
-
return self._curl(
|
|
1109
|
-
"PUT",
|
|
1110
|
-
f"{self._qam_prefix}/project/{self.project_id}/ku_product",
|
|
1111
|
-
payload,
|
|
1112
|
-
referer_path="/knowledge",
|
|
1113
|
-
)
|
|
1114
|
-
|
|
1115
|
-
def update_ku_products(self, items):
|
|
1116
|
-
"""PUT ku_products — batch update product status (mainPush, enable/published)."""
|
|
1117
|
-
if not isinstance(items, list) or not items:
|
|
1118
|
-
raise ValueError("items must be a non-empty array")
|
|
1119
|
-
formatted = []
|
|
1120
|
-
for item in items:
|
|
1121
|
-
row = dict(item)
|
|
1122
|
-
if "kuProductId" in row and "id" not in row:
|
|
1123
|
-
row["id"] = row.pop("kuProductId")
|
|
1124
|
-
if "id" not in row:
|
|
1125
|
-
raise ValueError("each item in batch update requires id or kuProductId")
|
|
1126
|
-
entry = {"id": int(row["id"])}
|
|
1127
|
-
if "mainPush" in row:
|
|
1128
|
-
entry["mainPush"] = bool(row["mainPush"])
|
|
1129
|
-
if "enable" in row:
|
|
1130
|
-
entry["enable"] = bool(row["enable"])
|
|
1131
|
-
elif "published" in row:
|
|
1132
|
-
entry["enable"] = bool(row["published"])
|
|
1133
|
-
formatted.append(entry)
|
|
1134
|
-
return self._curl(
|
|
1135
|
-
"PUT",
|
|
1136
|
-
f"{self._qam_prefix}/project/{self.project_id}/ku_products",
|
|
1137
|
-
{"list": formatted},
|
|
1138
|
-
referer_path="/knowledge",
|
|
1139
|
-
)
|
|
1140
|
-
|
|
1141
|
-
def operate_knowledge(self, request_list):
|
|
1142
|
-
"""Delete, move, publish, unpublish, show, or hide mixed knowledge."""
|
|
1143
|
-
if not isinstance(request_list, list) or not request_list:
|
|
1144
|
-
raise ValueError("request_list must be a non-empty array")
|
|
1145
|
-
return self._curl(
|
|
1146
|
-
"POST",
|
|
1147
|
-
f"{self._qam_prefix}/project/{self.project_id}/updateKnowledge",
|
|
1148
|
-
{"requestList": request_list},
|
|
1149
|
-
referer_path="/knowledge",
|
|
1150
|
-
)
|
|
1151
|
-
|
|
1152
|
-
def upload_qa(self, qa_list):
|
|
1153
|
-
"""POST kus — upload QA knowledge. qa_list: [{title, content, ...}]."""
|
|
1154
|
-
items = []
|
|
1155
|
-
for qa in qa_list:
|
|
1156
|
-
items.append({
|
|
1157
|
-
"title": qa.get("title", ""),
|
|
1158
|
-
"content": qa.get("content", ""),
|
|
1159
|
-
"forceIndex": qa.get("forceIndex", True),
|
|
1160
|
-
"type": "QA",
|
|
1161
|
-
"published": qa.get("published", True),
|
|
1162
|
-
"taskType": qa.get("taskType", "MANUAL"),
|
|
1163
|
-
"metadata": qa.get("metadata", {"tags": []}),
|
|
1164
|
-
"directoryId": qa.get("directoryId", ""),
|
|
1165
|
-
"isVisible": qa.get("isVisible", True),
|
|
1166
|
-
})
|
|
1167
|
-
return self._curl("POST", f"{self._qam_prefix}/project/{self.project_id}/kus",
|
|
1168
|
-
items, referer_path="/knowledge")
|
|
1169
|
-
|
|
1170
|
-
def create_documents(self, documents, directory_id="", task_type="FILE",
|
|
1171
|
-
task_url="MANUAL", published=True, is_visible=True):
|
|
1172
|
-
"""Create source-document knowledge from uploaded files or URLs.
|
|
1173
|
-
|
|
1174
|
-
``documents`` contains API-level records with ``filePath``, ``docType``
|
|
1175
|
-
and ``fileType``. Keeping this separate from QA creation is important:
|
|
1176
|
-
FILE/WEBSITE records are DOCUMENT knowledge, not KU/QA entries.
|
|
1177
|
-
"""
|
|
1178
|
-
return self._curl("POST", f"{self._qam_prefix}/project/{self.project_id}/documents", {
|
|
1179
|
-
"documents": documents,
|
|
1180
|
-
"published": published,
|
|
1181
|
-
"taskType": task_type,
|
|
1182
|
-
"taskUrl": task_url,
|
|
1183
|
-
"isFullSiteCrawl": False,
|
|
1184
|
-
"metadata": {"tags": []},
|
|
1185
|
-
"directoryId": directory_id,
|
|
1186
|
-
"isVisible": is_visible,
|
|
1187
|
-
}, referer_path="/knowledge")
|
|
1188
|
-
|
|
1189
|
-
def upload_web(self, urls, directory_id=""):
|
|
1190
|
-
"""Create WEBSITE/DOCUMENT knowledge for URL sources."""
|
|
1191
|
-
docs = [{"filePath": url, "docType": "TC", "fileType": "website"} for url in urls]
|
|
1192
|
-
return self.create_documents(docs, directory_id=directory_id, task_type="WEB")
|
|
1193
|
-
|
|
1194
|
-
# ── Isolation keywords ──
|
|
1195
|
-
def list_isolation_keywords(self, keyword="", page_index=1, page_size=50):
|
|
1196
|
-
"""GET isolation keywords for the current project."""
|
|
1197
|
-
return self._curl(
|
|
1198
|
-
"GET",
|
|
1199
|
-
f"{self._qam_prefix}/project/{self.project_id}/isolation_keywords",
|
|
1200
|
-
query_params={
|
|
1201
|
-
"keyword": keyword,
|
|
1202
|
-
"pageIndex": page_index,
|
|
1203
|
-
"pageSize": page_size,
|
|
1204
|
-
},
|
|
1205
|
-
referer_path="/knowledge",
|
|
1206
|
-
)
|
|
1207
|
-
|
|
1208
|
-
def list_all_isolation_keywords(self, keyword="", page_size=1000):
|
|
1209
|
-
"""Fetch all isolation keywords by following pages until the last page."""
|
|
1210
|
-
if page_size <= 0:
|
|
1211
|
-
raise ValueError("page_size must be positive")
|
|
1212
|
-
all_items = []
|
|
1213
|
-
page_index = 1
|
|
1214
|
-
while True:
|
|
1215
|
-
result = self.list_isolation_keywords(keyword, page_index, page_size)
|
|
1216
|
-
items = result.get("list", result) if isinstance(result, dict) else result
|
|
1217
|
-
if not isinstance(items, list):
|
|
1218
|
-
raise ValueError("isolation keyword list response must be an array")
|
|
1219
|
-
all_items.extend(items)
|
|
1220
|
-
if len(items) < page_size:
|
|
1221
|
-
return all_items
|
|
1222
|
-
page_index += 1
|
|
1223
|
-
|
|
1224
|
-
def create_isolation_keyword(self, keyword):
|
|
1225
|
-
"""POST one or more comma-separated isolation keywords."""
|
|
1226
|
-
return self._curl(
|
|
1227
|
-
"POST",
|
|
1228
|
-
f"{self._qam_prefix}/project/{self.project_id}/isolation_keyword",
|
|
1229
|
-
{"keyword": keyword},
|
|
1230
|
-
referer_path="/knowledge",
|
|
1231
|
-
)
|
|
1232
|
-
|
|
1233
|
-
def update_isolation_keyword(self, keyword_id, keyword):
|
|
1234
|
-
"""POST an updated isolation keyword value."""
|
|
1235
|
-
return self._curl(
|
|
1236
|
-
"POST",
|
|
1237
|
-
f"{self._qam_prefix}/project/{self.project_id}/isolation_keyword/{int(keyword_id)}",
|
|
1238
|
-
{"keyword": keyword},
|
|
1239
|
-
referer_path="/knowledge",
|
|
1240
|
-
)
|
|
1241
|
-
|
|
1242
|
-
def delete_isolation_keyword(self, keyword_id):
|
|
1243
|
-
"""DELETE an isolation keyword by ID."""
|
|
1244
|
-
return self._curl(
|
|
1245
|
-
"DELETE",
|
|
1246
|
-
f"{self._qam_prefix}/project/{self.project_id}/isolation_keyword/{int(keyword_id)}",
|
|
1247
|
-
referer_path="/knowledge",
|
|
1248
|
-
)
|
|
1249
|
-
|
|
1250
|
-
def get_knowledge_isolation_keywords(self, knowledge_id, knowledge_type):
|
|
1251
|
-
"""Read current isolation-keyword bindings for any knowledge UI type."""
|
|
1252
|
-
normalized = str(knowledge_type).upper()
|
|
1253
|
-
if normalized not in {"QA", "LEARN_SELF", "FILE", "WEBSITE", "PRODUCT"}:
|
|
1254
|
-
raise ValueError("knowledge_type must be QA, LEARN_SELF, FILE, WEBSITE, or PRODUCT")
|
|
1255
|
-
isolation_type = {
|
|
1256
|
-
"QA": "QA",
|
|
1257
|
-
"LEARN_SELF": "QA",
|
|
1258
|
-
"FILE": "DOCUMENT",
|
|
1259
|
-
"WEBSITE": "DOCUMENT",
|
|
1260
|
-
"PRODUCT": "PRODUCT",
|
|
1261
|
-
}[normalized]
|
|
1262
|
-
return self._curl(
|
|
1263
|
-
"GET",
|
|
1264
|
-
f"{self._qam_prefix}/project/{self.project_id}/knowledge/isolation_keywords",
|
|
1265
|
-
query_params={"knowledgeType": isolation_type, "knowledgeId": int(knowledge_id)},
|
|
1266
|
-
referer_path="/knowledge",
|
|
1267
|
-
)
|
|
1268
|
-
|
|
1269
|
-
def batch_update_isolation_keyword_bindings(
|
|
1270
|
-
self, items=None, directory_ids=None, keyword_ids=None
|
|
1271
|
-
):
|
|
1272
|
-
"""Replace final isolation-keyword bindings for knowledge and directories.
|
|
1273
|
-
|
|
1274
|
-
``items`` contains knowledge-level bindings, while ``directory_ids``
|
|
1275
|
-
targets directory-level bindings. Callers must compute the complete final
|
|
1276
|
-
keyword set before invoking this raw backend operation.
|
|
1277
|
-
"""
|
|
1278
|
-
items = [] if items is None else items
|
|
1279
|
-
directory_ids = [] if directory_ids is None else directory_ids
|
|
1280
|
-
keyword_ids = [] if keyword_ids is None else keyword_ids
|
|
1281
|
-
if not isinstance(items, list) or not isinstance(directory_ids, list) or not isinstance(keyword_ids, list):
|
|
1282
|
-
raise ValueError("items, directory_ids and keyword_ids must be arrays")
|
|
1283
|
-
if not items and not directory_ids and not keyword_ids:
|
|
1284
|
-
raise ValueError("isolation keyword binding update has no targets or keyword IDs")
|
|
1285
|
-
if items and directory_ids:
|
|
1286
|
-
raise ValueError("submit knowledge items and directory IDs in separate binding updates")
|
|
1287
|
-
|
|
1288
|
-
for value in [*directory_ids, *keyword_ids]:
|
|
1289
|
-
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
1290
|
-
raise ValueError("directory_ids and keyword_ids must contain positive integer IDs")
|
|
1291
|
-
normalized_items = []
|
|
1292
|
-
for item in items:
|
|
1293
|
-
if not isinstance(item, dict):
|
|
1294
|
-
raise ValueError("items must contain objects")
|
|
1295
|
-
knowledge_id = item.get("knowledgeId")
|
|
1296
|
-
item_keyword_ids = item.get("keywordIds")
|
|
1297
|
-
if isinstance(knowledge_id, bool) or not isinstance(knowledge_id, int) or knowledge_id <= 0:
|
|
1298
|
-
raise ValueError("items[].knowledgeId must be a positive integer")
|
|
1299
|
-
if not isinstance(item_keyword_ids, list):
|
|
1300
|
-
raise ValueError("items[].keywordIds must be an array")
|
|
1301
|
-
if any(isinstance(value, bool) or not isinstance(value, int) or value <= 0 for value in item_keyword_ids):
|
|
1302
|
-
raise ValueError("items[].keywordIds must contain positive integer IDs")
|
|
1303
|
-
knowledge_type = str(item.get("knowledgeType", "QA")).upper()
|
|
1304
|
-
if knowledge_type not in {"QA", "LEARN_SELF", "FILE", "WEBSITE", "PRODUCT"}:
|
|
1305
|
-
raise ValueError("items[].knowledgeType must be QA, LEARN_SELF, FILE, WEBSITE, or PRODUCT")
|
|
1306
|
-
isolation_type = {
|
|
1307
|
-
"QA": "QA",
|
|
1308
|
-
"LEARN_SELF": "QA",
|
|
1309
|
-
"FILE": "DOCUMENT",
|
|
1310
|
-
"WEBSITE": "DOCUMENT",
|
|
1311
|
-
"PRODUCT": "PRODUCT",
|
|
1312
|
-
}[knowledge_type]
|
|
1313
|
-
normalized_items.append({
|
|
1314
|
-
"knowledgeType": isolation_type,
|
|
1315
|
-
"knowledgeId": knowledge_id,
|
|
1316
|
-
"keywordIds": list(dict.fromkeys(item_keyword_ids)),
|
|
1317
|
-
})
|
|
1318
|
-
|
|
1319
|
-
return self._curl(
|
|
1320
|
-
"POST",
|
|
1321
|
-
f"{self._qam_prefix}/project/{self.project_id}/knowledge/isolation_keywords/batch",
|
|
1322
|
-
{
|
|
1323
|
-
"items": normalized_items,
|
|
1324
|
-
"directoryIds": list(dict.fromkeys(directory_ids)),
|
|
1325
|
-
"keywordIds": list(dict.fromkeys(keyword_ids)),
|
|
1326
|
-
},
|
|
1327
|
-
referer_path="/knowledge",
|
|
1328
|
-
)
|
|
1329
|
-
|
|
1330
|
-
def list_directories(self):
|
|
1331
|
-
"""GET directories — list the full KB directory tree (raw payload).
|
|
1332
|
-
|
|
1333
|
-
Path prefix differs by env (mirrors front-end formatQamPath):
|
|
1334
|
-
prod → /api_v2/qam/project/{p}/directories
|
|
1335
|
-
staging → /sns_v1/project/{p}/directories
|
|
1336
|
-
"""
|
|
1337
|
-
return self._curl("GET", f"{self._qam_prefix}/project/{self.project_id}/directories",
|
|
1338
|
-
query_params={"keyword": "", "kuPublished": ""},
|
|
1339
|
-
referer_path="/knowledge")
|
|
1340
|
-
|
|
1341
|
-
def edit_knowledge_tags(self, knowledge_ids, tag_list, knowledge_type="QA"):
|
|
1342
|
-
"""Replace tags on the given knowledge items via the KB tag endpoint."""
|
|
1343
|
-
if not isinstance(knowledge_ids, list) or not knowledge_ids:
|
|
1344
|
-
raise ValueError("knowledge_ids must be a non-empty array")
|
|
1345
|
-
if not isinstance(tag_list, list):
|
|
1346
|
-
raise ValueError("tag_list must be an array")
|
|
1347
|
-
if any(isinstance(value, bool) or not isinstance(value, int) or value <= 0 for value in knowledge_ids):
|
|
1348
|
-
raise ValueError("knowledge_ids must contain positive integer IDs")
|
|
1349
|
-
if any(not isinstance(value, str) or not value.strip() for value in tag_list):
|
|
1350
|
-
raise ValueError("tag_list must contain non-empty strings")
|
|
1351
|
-
return self._curl(
|
|
1352
|
-
"POST",
|
|
1353
|
-
f"{self._qam_prefix}/project/{self.project_id}/editTags",
|
|
1354
|
-
{
|
|
1355
|
-
"type": knowledge_type,
|
|
1356
|
-
"knowledgeIds": list(dict.fromkeys(knowledge_ids)),
|
|
1357
|
-
"tagList": list(dict.fromkeys(tag_list)),
|
|
1358
|
-
},
|
|
1359
|
-
referer_path="/knowledge",
|
|
1360
|
-
)
|
|
1361
|
-
|
|
1362
|
-
def list_knowledge_tags(self, keyword=""):
|
|
1363
|
-
"""GET project tag definitions used by knowledge metadata."""
|
|
1364
|
-
return self._curl(
|
|
1365
|
-
"GET",
|
|
1366
|
-
f"{self._qam_prefix}/project/{self.project_id}/searchKeywordAndIntents",
|
|
1367
|
-
query_params={"keyword": keyword},
|
|
1368
|
-
referer_path="/knowledge",
|
|
1369
|
-
)
|
|
1370
|
-
|
|
1371
|
-
def create_folder(self, name, parent_id=0, folder_type="GENERIC"):
|
|
1372
|
-
"""POST directory — create a knowledge folder."""
|
|
1373
|
-
return self._curl("POST", f"{self._qam_prefix}/project/{self.project_id}/directory", {
|
|
1374
|
-
"name": name, "parentId": parent_id, "type": folder_type,
|
|
1375
|
-
}, referer_path="/knowledge")
|
|
1376
|
-
|
|
1377
|
-
def update_directory(self, directory_id, data):
|
|
1378
|
-
"""PUT directory — rename or update one knowledge folder."""
|
|
1379
|
-
return self._curl(
|
|
1380
|
-
"PUT",
|
|
1381
|
-
f"{self._qam_prefix}/project/{self.project_id}/directory/{int(directory_id)}",
|
|
1382
|
-
dict(data),
|
|
1383
|
-
referer_path="/knowledge",
|
|
1384
|
-
)
|
|
1385
|
-
|
|
1386
|
-
def move_directory(self, directory_id, parent_id):
|
|
1387
|
-
"""GET directory/move — move one folder below another folder or root."""
|
|
1388
|
-
return self._curl(
|
|
1389
|
-
"GET",
|
|
1390
|
-
f"{self._qam_prefix}/project/{self.project_id}/directory/move",
|
|
1391
|
-
query_params={"currentId": int(directory_id), "parentId": int(parent_id)},
|
|
1392
|
-
referer_path="/knowledge",
|
|
1393
|
-
)
|
|
1394
|
-
|
|
1395
|
-
def delete_directory(self, directory_id):
|
|
1396
|
-
"""DELETE one directory recursively, including descendants and knowledge."""
|
|
1397
|
-
return self._curl(
|
|
1398
|
-
"DELETE",
|
|
1399
|
-
f"{self._qam_prefix}/project/{self.project_id}/directory/{int(directory_id)}",
|
|
1400
|
-
referer_path="/knowledge",
|
|
1401
|
-
)
|
|
1402
|
-
|
|
1403
|
-
# ── Transcripts ──
|
|
1404
|
-
|
|
1405
|
-
def list_transcripts(self, page_size=20, page_index=1, debug=False,
|
|
1406
|
-
begin_ts=None, end_ts=None, touch_point=None,
|
|
1407
|
-
bot_id=None, agent_id=None, intent_id=None,
|
|
1408
|
-
handoff=None, keyword=None):
|
|
1409
|
-
"""POST transcripts/filter — query inbox conversations."""
|
|
1410
|
-
data = {"pageSize": page_size, "pageIndex": page_index, "debug": debug}
|
|
1411
|
-
optional_fields = {
|
|
1412
|
-
"beginTimestamp": begin_ts,
|
|
1413
|
-
"endTimestamp": end_ts,
|
|
1414
|
-
"touchPoint": touch_point,
|
|
1415
|
-
"botId": int(bot_id) if bot_id is not None else None,
|
|
1416
|
-
"agentId": int(agent_id) if agent_id is not None else None,
|
|
1417
|
-
"intentId": int(intent_id) if intent_id is not None else None,
|
|
1418
|
-
"handoff": handoff,
|
|
1419
|
-
"keyword": keyword,
|
|
1420
|
-
}
|
|
1421
|
-
data.update({
|
|
1422
|
-
key: value
|
|
1423
|
-
for key, value in optional_fields.items()
|
|
1424
|
-
if value is not None
|
|
1425
|
-
})
|
|
1426
|
-
return self._curl("POST",
|
|
1427
|
-
f"/api_v2/gpt/project/{self.project_id}/transcripts/filter", data)
|
|
1428
|
-
|
|
1429
|
-
def resolve_transcript_chat_id(self, external_id, page_size=10, debug=False):
|
|
1430
|
-
"""Resolve an inbox external conversation ID to an internal chatId.
|
|
1431
|
-
|
|
1432
|
-
The Inbox page searches external IDs through transcripts/filter keyword.
|
|
1433
|
-
Reuse the same query here, then prefer an exact externId/externalId match.
|
|
1434
|
-
"""
|
|
1435
|
-
result = self.list_transcripts(
|
|
1436
|
-
page_size=page_size,
|
|
1437
|
-
page_index=1,
|
|
1438
|
-
debug=debug,
|
|
1439
|
-
keyword=external_id,
|
|
1440
|
-
)
|
|
1441
|
-
rows = result.get("list", result) if isinstance(result, dict) else result
|
|
1442
|
-
if not isinstance(rows, list):
|
|
1443
|
-
rows = []
|
|
1444
|
-
|
|
1445
|
-
target = str(external_id)
|
|
1446
|
-
exact = [
|
|
1447
|
-
row for row in rows
|
|
1448
|
-
if isinstance(row, dict)
|
|
1449
|
-
and str(row.get("externId") or row.get("externalId") or row.get("external_id") or "") == target
|
|
1450
|
-
]
|
|
1451
|
-
candidates = exact or [row for row in rows if isinstance(row, dict)]
|
|
1452
|
-
if not candidates:
|
|
1453
|
-
return {"externalId": external_id, "chatId": None, "matches": []}
|
|
1454
|
-
|
|
1455
|
-
def compact(row):
|
|
1456
|
-
return {
|
|
1457
|
-
"chatId": row.get("chatId") or row.get("id"),
|
|
1458
|
-
"externalId": row.get("externId") or row.get("externalId") or row.get("external_id"),
|
|
1459
|
-
"agentName": row.get("agentName", ""),
|
|
1460
|
-
"touchPoint": row.get("touchPoint", row.get("touchpoint", "")),
|
|
1461
|
-
"createdAt": row.get("createdAt", ""),
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
matches = [compact(row) for row in candidates]
|
|
1465
|
-
return {
|
|
1466
|
-
"externalId": external_id,
|
|
1467
|
-
"chatId": matches[0].get("chatId") if exact or len(matches) == 1 else None,
|
|
1468
|
-
"matchedBy": "exactExternalId" if exact else "keyword",
|
|
1469
|
-
"matches": matches,
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
def export_transcripts(self, page_size=20, page_index=1, debug=False,
|
|
1473
|
-
begin_ts=None, end_ts=None, time_offset=8, **filters):
|
|
1474
|
-
"""POST transcripts/export/async — submit async export task."""
|
|
1475
|
-
data = {"pageSize": page_size, "pageIndex": page_index,
|
|
1476
|
-
"debug": debug, "timeOffset": time_offset}
|
|
1477
|
-
if begin_ts:
|
|
1478
|
-
data["beginTimestamp"] = begin_ts
|
|
1479
|
-
if end_ts:
|
|
1480
|
-
data["endTimestamp"] = end_ts
|
|
1481
|
-
for key in ["handoff", "agentId", "strategyId", "intentId", "touchpoint"]:
|
|
1482
|
-
if key in filters and filters[key] is not None:
|
|
1483
|
-
data[key] = filters[key]
|
|
1484
|
-
return self._curl("POST",
|
|
1485
|
-
f"/api_v2/gpt/project/{self.project_id}/transcripts/export/async", data)
|
|
1486
|
-
|
|
1487
|
-
def get_export_tasks(self, page=1, page_size=10):
|
|
1488
|
-
"""POST getLabelTask — poll export task status."""
|
|
1489
|
-
return self.get_label_tasks(page=page, page_size=page_size)
|
|
1490
|
-
|
|
1491
|
-
# ── Batch testing / Download Center ──
|
|
1492
|
-
|
|
1493
|
-
def get_sts_credentials(self):
|
|
1494
|
-
"""Get temporary S3 credentials for GPT-side uploads such as batch tests."""
|
|
1495
|
-
return self._curl("GET", f"/api_v2/gpt/project/{self.project_id}/sts_credentials")
|
|
1496
|
-
|
|
1497
|
-
def get_knowledge_sts_credentials(self):
|
|
1498
|
-
"""Get the same knowledge-upload credentials used by anker-knowledge."""
|
|
1499
|
-
return self._curl("GET", f"{self._qam_prefix}/project/{self.project_id}/sts_credentials")
|
|
1500
|
-
|
|
1501
|
-
def upload_temp_file(self, file_path, *, for_knowledge=False):
|
|
1502
|
-
"""Upload one local file to the temporary S3 location and return its object key."""
|
|
1503
|
-
import boto3
|
|
1504
|
-
|
|
1505
|
-
file_path = os.fspath(file_path)
|
|
1506
|
-
credentials = (self.get_knowledge_sts_credentials()
|
|
1507
|
-
if for_knowledge else self.get_sts_credentials())
|
|
1508
|
-
required = ["accessKeyId", "secretAccessKey", "sessionToken", "region", "bucketName", "path"]
|
|
1509
|
-
missing = [key for key in required if not credentials.get(key)]
|
|
1510
|
-
if missing:
|
|
1511
|
-
raise RuntimeError(f"STS credentials missing fields: {', '.join(missing)}")
|
|
1512
|
-
|
|
1513
|
-
key = f"{credentials['path']}{os.path.basename(file_path)}"
|
|
1514
|
-
session = boto3.session.Session(
|
|
1515
|
-
aws_access_key_id=credentials["accessKeyId"],
|
|
1516
|
-
aws_secret_access_key=credentials["secretAccessKey"],
|
|
1517
|
-
aws_session_token=credentials["sessionToken"],
|
|
1518
|
-
region_name=credentials["region"],
|
|
1519
|
-
)
|
|
1520
|
-
content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
|
1521
|
-
session.client("s3").upload_file(
|
|
1522
|
-
file_path,
|
|
1523
|
-
credentials["bucketName"],
|
|
1524
|
-
key,
|
|
1525
|
-
ExtraArgs={"ContentType": content_type},
|
|
1526
|
-
)
|
|
1527
|
-
return key
|
|
1528
|
-
|
|
1529
|
-
def upload_batch_test_file(self, file_path):
|
|
1530
|
-
"""Upload one generated .xlsx to the temporary S3 location and return its object key."""
|
|
1531
|
-
return self.upload_temp_file(file_path)
|
|
1532
|
-
|
|
1533
|
-
def submit_batch_test(self, file_key, task_type, bot_id=None, metadata=None):
|
|
1534
|
-
"""POST uploadMessage — create a REPLY or MULTI_TURN_REPLY Download Center task."""
|
|
1535
|
-
if task_type not in {"REPLY", "MULTI_TURN_REPLY"}:
|
|
1536
|
-
raise ValueError("task_type must be REPLY or MULTI_TURN_REPLY")
|
|
1537
|
-
payload = {"file": file_key, "type": task_type}
|
|
1538
|
-
if bot_id is not None:
|
|
1539
|
-
payload["botId"] = int(bot_id)
|
|
1540
|
-
if metadata:
|
|
1541
|
-
payload["metadata"] = metadata
|
|
1542
|
-
return self._curl(
|
|
1543
|
-
"POST",
|
|
1544
|
-
f"/api_v2/gpt/label/project/{self.project_id}/uploadMessage",
|
|
1545
|
-
payload,
|
|
1546
|
-
)
|
|
1547
|
-
|
|
1548
|
-
def get_label_tasks(self, page=1, page_size=10, task_type=None):
|
|
1549
|
-
"""POST getLabelTask — query Download Center tasks, optionally by task type."""
|
|
1550
|
-
payload = {"page": page, "pageSize": page_size}
|
|
1551
|
-
if task_type:
|
|
1552
|
-
payload["taskType"] = task_type
|
|
1553
|
-
return self._curl(
|
|
1554
|
-
"POST",
|
|
1555
|
-
f"/api_v2/gpt/label/project/{self.project_id}/getLabelTask",
|
|
1556
|
-
payload,
|
|
1557
|
-
)
|
|
1558
|
-
|
|
1559
|
-
# ── label tree management ──
|
|
1560
|
-
|
|
1561
|
-
def list_label_groups(self, tree_name=None):
|
|
1562
|
-
"""GET getTreeInfo — list label groups."""
|
|
1563
|
-
params = {}
|
|
1564
|
-
if tree_name:
|
|
1565
|
-
params["treeName"] = tree_name
|
|
1566
|
-
return self._curl(
|
|
1567
|
-
"GET",
|
|
1568
|
-
f"/api_v2/gpt/label/project/{self.project_id}/getTreeInfo",
|
|
1569
|
-
query_params=params or None,
|
|
1570
|
-
)
|
|
1571
|
-
|
|
1572
|
-
def create_label_group(self, tree_name, tree_describe="", condition=None, is_multi_tag=None):
|
|
1573
|
-
"""POST updateTreeInfo CREATE — create one label group."""
|
|
1574
|
-
data = {"treeName": tree_name, "treeDescribe": tree_describe, "operateType": "CREATE"}
|
|
1575
|
-
if condition is not None:
|
|
1576
|
-
data["condition"] = condition
|
|
1577
|
-
if is_multi_tag is not None:
|
|
1578
|
-
data["isMultiTag"] = bool(is_multi_tag)
|
|
1579
|
-
return self._curl("POST", f"/api_v2/gpt/label/project/{self.project_id}/updateTreeInfo", data)
|
|
1580
|
-
|
|
1581
|
-
def update_label_group(self, group_id, tree_name=None, tree_describe=None, condition=None, is_multi_tag=None):
|
|
1582
|
-
"""POST updateTreeInfo UPDATE — update one label group."""
|
|
1583
|
-
data = {"id": int(group_id), "operateType": "UPDATE"}
|
|
1584
|
-
if tree_name is not None:
|
|
1585
|
-
data["treeName"] = tree_name
|
|
1586
|
-
if tree_describe is not None:
|
|
1587
|
-
data["treeDescribe"] = tree_describe
|
|
1588
|
-
if condition is not None:
|
|
1589
|
-
data["condition"] = condition
|
|
1590
|
-
if is_multi_tag is not None:
|
|
1591
|
-
data["isMultiTag"] = bool(is_multi_tag)
|
|
1592
|
-
return self._curl("POST", f"/api_v2/gpt/label/project/{self.project_id}/updateTreeInfo", data)
|
|
1593
|
-
|
|
1594
|
-
def delete_label_group(self, group_id):
|
|
1595
|
-
"""POST updateTreeInfo DELETE — delete one label group and its nodes."""
|
|
1596
|
-
return self._curl(
|
|
1597
|
-
"POST",
|
|
1598
|
-
f"/api_v2/gpt/label/project/{self.project_id}/updateTreeInfo",
|
|
1599
|
-
{"id": int(group_id), "operateType": "DELETE"},
|
|
1600
|
-
)
|
|
1601
|
-
|
|
1602
|
-
def get_label_tree(self, tree_id, name=None):
|
|
1603
|
-
"""GET getTagTree — return one label group's tree, optionally filtering by name/description."""
|
|
1604
|
-
params = {"treeId": int(tree_id)}
|
|
1605
|
-
if name:
|
|
1606
|
-
params["name"] = name
|
|
1607
|
-
return self._curl(
|
|
1608
|
-
"GET",
|
|
1609
|
-
f"/api_v2/gpt/label/project/{self.project_id}/getTagTree",
|
|
1610
|
-
query_params=params,
|
|
1611
|
-
)
|
|
1612
|
-
|
|
1613
|
-
def get_label_detail_tree(self, tree_id, node_ids=None, node_names=None, parent_ids=None):
|
|
1614
|
-
"""POST getDetailTree — query label node details."""
|
|
1615
|
-
data = {"treeId": int(tree_id)}
|
|
1616
|
-
if node_ids:
|
|
1617
|
-
data["nodeIds"] = [int(v) for v in node_ids]
|
|
1618
|
-
if node_names:
|
|
1619
|
-
data["nodeNames"] = list(node_names)
|
|
1620
|
-
if parent_ids:
|
|
1621
|
-
data["parentIds"] = [int(v) for v in parent_ids]
|
|
1622
|
-
return self._curl("POST", f"/api_v2/gpt/label/project/{self.project_id}/getDetailTree", data)
|
|
1623
|
-
|
|
1624
|
-
def update_label_nodes(self, tree_id, nodes, operate_type):
|
|
1625
|
-
"""POST updateNode — create/update/delete label nodes."""
|
|
1626
|
-
if operate_type not in {"CREATE", "UPDATE", "DELETE"}:
|
|
1627
|
-
raise ValueError("operate_type must be CREATE, UPDATE or DELETE")
|
|
1628
|
-
if not isinstance(nodes, list) or not nodes:
|
|
1629
|
-
raise ValueError("nodes must be a non-empty list")
|
|
1630
|
-
data = {"treeId": int(tree_id), "operateType": operate_type, "nodes": nodes}
|
|
1631
|
-
return self._curl("POST", f"/api_v2/gpt/label/project/{self.project_id}/updateNode", data)
|
|
1632
|
-
|
|
1633
|
-
def create_label_node(self, tree_id, node_name, describe, parent_id=0, example_list=None, is_tag=True):
|
|
1634
|
-
"""Create one label node."""
|
|
1635
|
-
node = {
|
|
1636
|
-
"nodeName": node_name,
|
|
1637
|
-
"describe": describe,
|
|
1638
|
-
"parentId": int(parent_id or 0),
|
|
1639
|
-
"isTag": bool(is_tag),
|
|
1640
|
-
"exampleList": example_list or [],
|
|
1641
|
-
}
|
|
1642
|
-
return self.update_label_nodes(tree_id, [node], "CREATE")
|
|
1643
|
-
|
|
1644
|
-
def update_label_node(self, tree_id, node_id, node_name=None, describe=None, parent_id=None,
|
|
1645
|
-
example_list=None, is_tag=None):
|
|
1646
|
-
"""Update one label node."""
|
|
1647
|
-
node = {"id": int(node_id)}
|
|
1648
|
-
if node_name is not None:
|
|
1649
|
-
node["nodeName"] = node_name
|
|
1650
|
-
if describe is not None:
|
|
1651
|
-
node["describe"] = describe
|
|
1652
|
-
if parent_id is not None:
|
|
1653
|
-
node["parentId"] = int(parent_id)
|
|
1654
|
-
if example_list is not None:
|
|
1655
|
-
node["exampleList"] = example_list
|
|
1656
|
-
if is_tag is not None:
|
|
1657
|
-
node["isTag"] = bool(is_tag)
|
|
1658
|
-
return self.update_label_nodes(tree_id, [node], "UPDATE")
|
|
1659
|
-
|
|
1660
|
-
def delete_label_node(self, tree_id, node_id):
|
|
1661
|
-
"""Delete one label node and its descendants."""
|
|
1662
|
-
return self.update_label_nodes(tree_id, [{"id": int(node_id)}], "DELETE")
|
|
1663
|
-
|
|
1664
|
-
def download_label_template(self, template_type, target_path):
|
|
1665
|
-
"""Download VOC_LABEL or VOC_LABEL_INIT template xlsx."""
|
|
1666
|
-
if template_type not in {"VOC_LABEL", "VOC_LABEL_INIT"}:
|
|
1667
|
-
raise ValueError("template_type must be VOC_LABEL or VOC_LABEL_INIT")
|
|
1668
|
-
return self._download(
|
|
1669
|
-
f"/api_v2/gpt/label/project/{self.project_id}/downloadLabelTemplate",
|
|
1670
|
-
target_path,
|
|
1671
|
-
query_params={"type": template_type},
|
|
1672
|
-
)
|
|
1673
|
-
|
|
1674
|
-
def download_labels(self, tree_id, target_path):
|
|
1675
|
-
"""Download current labels of one label group as xlsx."""
|
|
1676
|
-
return self._download(
|
|
1677
|
-
f"/api_v2/gpt/label/project/{self.project_id}/downloadLabel",
|
|
1678
|
-
target_path,
|
|
1679
|
-
query_params={"treeId": int(tree_id)},
|
|
1680
|
-
)
|
|
1681
|
-
|
|
1682
|
-
def upload_labels(self, tree_id, file_key, upload_way=None):
|
|
1683
|
-
"""POST uploadLabel — import an already uploaded xlsx S3 key into a label group."""
|
|
1684
|
-
data = {"treeId": int(tree_id), "file": file_key}
|
|
1685
|
-
if upload_way:
|
|
1686
|
-
data["uploadWay"] = upload_way
|
|
1687
|
-
return self._curl("POST", f"/api_v2/gpt/label/project/{self.project_id}/uploadLabel", data)
|
|
1688
|
-
|
|
1689
|
-
def upload_label_file(self, tree_id, file_path, init=False):
|
|
1690
|
-
"""Upload local xlsx to temp S3 and import it through uploadLabel."""
|
|
1691
|
-
file_key = self.upload_temp_file(file_path)
|
|
1692
|
-
return {
|
|
1693
|
-
"fileKey": file_key,
|
|
1694
|
-
"result": self.upload_labels(tree_id, file_key, upload_way="INIT" if init else None),
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
# ── Orders / Tracking ──
|
|
1698
|
-
|
|
1699
|
-
def get_api_key(self):
|
|
1700
|
-
"""GET accounts/apikey — get X-Token for external data APIs."""
|
|
1701
|
-
return self._curl("GET", "/api_v2/gpt/accounts/apikey")
|
|
1702
|
-
|
|
1703
|
-
def query_order(self, order_id, source=None, api_key=None, base="https://desk.shulex.com"):
|
|
1704
|
-
"""GET /standard/order — query order info (items, price, fulfillment, tracking)."""
|
|
1705
|
-
if not api_key:
|
|
1706
|
-
api_key = self.get_api_key()
|
|
1707
|
-
params = {"orderId": order_id}
|
|
1708
|
-
if source:
|
|
1709
|
-
params["source"] = source
|
|
1710
|
-
r = self._request_with_retry(
|
|
1711
|
-
"GET", f"{base}/api_v2/intelli/standard/order",
|
|
1712
|
-
params=params, headers={"X-Token": api_key},
|
|
1713
|
-
)
|
|
1714
|
-
return r.json() if r.text.strip() else {}
|
|
1715
|
-
|
|
1716
|
-
def query_orders(self, order_id=None, email=None, source=None,
|
|
1717
|
-
api_key=None, base="https://desk.shulex.com"):
|
|
1718
|
-
"""GET /standard/orders — query orders by orderId or email."""
|
|
1719
|
-
if not api_key:
|
|
1720
|
-
api_key = self.get_api_key()
|
|
1721
|
-
params = {}
|
|
1722
|
-
if order_id:
|
|
1723
|
-
params["orderId"] = order_id
|
|
1724
|
-
if email:
|
|
1725
|
-
params["email"] = email
|
|
1726
|
-
if source:
|
|
1727
|
-
params["source"] = source
|
|
1728
|
-
r = self._request_with_retry(
|
|
1729
|
-
"GET", f"{base}/api_v2/intelli/standard/orders",
|
|
1730
|
-
params=params, headers={"X-Token": api_key},
|
|
1731
|
-
)
|
|
1732
|
-
return r.json() if r.text.strip() else {}
|
|
1733
|
-
|
|
1734
|
-
def query_tracking(self, tracking_numbers, tracking_number=None, carriers=None,
|
|
1735
|
-
api_key=None, base="https://desk.shulex.com"):
|
|
1736
|
-
"""POST queryTrackingByPollingCarrier — query logistics tracking via 17Track."""
|
|
1737
|
-
if not api_key:
|
|
1738
|
-
api_key = self.get_api_key()
|
|
1739
|
-
if isinstance(tracking_numbers, str):
|
|
1740
|
-
tracking_numbers = [tracking_numbers]
|
|
1741
|
-
if not tracking_number and tracking_numbers:
|
|
1742
|
-
tracking_number = tracking_numbers[0]
|
|
1743
|
-
body = {"trackingNumbers": tracking_numbers, "trackingNumber": tracking_number}
|
|
1744
|
-
if carriers:
|
|
1745
|
-
body["carriers"] = carriers
|
|
1746
|
-
r = self._request_with_retry(
|
|
1747
|
-
"POST", f"{base}/api_v2/intelli/track17api/queryTrackingByPollingCarrier",
|
|
1748
|
-
headers={"X-Token": api_key}, json=body,
|
|
1749
|
-
)
|
|
1750
|
-
return r.json() if r.text.strip() else {}
|
|
1751
|
-
|
|
1752
|
-
# ── Reports ──
|
|
1753
|
-
|
|
1754
|
-
def _report_filter(self, begin_ts, end_ts, time_offset=8, bot_id=None,
|
|
1755
|
-
touch_point=None, date_unit=None, **extra):
|
|
1756
|
-
f = {
|
|
1757
|
-
"beginTimestamp": begin_ts,
|
|
1758
|
-
"endTimestamp": end_ts,
|
|
1759
|
-
"projectId": int(self.project_id),
|
|
1760
|
-
"timeOffset": time_offset,
|
|
1761
|
-
}
|
|
1762
|
-
if bot_id is not None:
|
|
1763
|
-
f["botId"] = int(bot_id)
|
|
1764
|
-
if touch_point:
|
|
1765
|
-
f["touchPoint"] = touch_point
|
|
1766
|
-
if date_unit:
|
|
1767
|
-
f["dateUnit"] = date_unit
|
|
1768
|
-
f.update(extra)
|
|
1769
|
-
return f
|
|
1770
|
-
|
|
1771
|
-
def report_transcript(self, begin_ts, end_ts, time_offset=8, bot_id=None,
|
|
1772
|
-
touch_point=None, date_unit=None, **extra):
|
|
1773
|
-
"""POST statement/transcript — ticket-level stats (resolution/escalation/AI reply rate).
|
|
1774
|
-
Requires CHILD_USER token — ACCOUNT_USER returns total=0 silently.
|
|
1775
|
-
"""
|
|
1776
|
-
return self._curl("POST", "/api_v2/as_bot/statement/transcript",
|
|
1777
|
-
self._report_filter(begin_ts, end_ts, time_offset, bot_id,
|
|
1778
|
-
touch_point, date_unit, **extra),
|
|
1779
|
-
referer_path="/report")
|
|
1780
|
-
|
|
1781
|
-
def report_message(self, begin_ts, end_ts, time_offset=8, bot_id=None,
|
|
1782
|
-
touch_point=None, date_unit=None, **extra):
|
|
1783
|
-
"""POST statement/message — message-level stats and trend."""
|
|
1784
|
-
return self._curl("POST", "/api_v2/as_bot/statement/message",
|
|
1785
|
-
self._report_filter(begin_ts, end_ts, time_offset, bot_id,
|
|
1786
|
-
touch_point, date_unit, **extra),
|
|
1787
|
-
referer_path="/report")
|
|
1788
|
-
|
|
1789
|
-
def report_agent(self, begin_ts, end_ts, time_offset=8, bot_id=None,
|
|
1790
|
-
touch_point=None, date_unit=None, agent_id=None, **extra):
|
|
1791
|
-
"""POST statement/agent — agent-level ticket stats."""
|
|
1792
|
-
f = self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point,
|
|
1793
|
-
date_unit=date_unit, **extra)
|
|
1794
|
-
if agent_id is not None:
|
|
1795
|
-
f["agentId"] = int(agent_id)
|
|
1796
|
-
return self._curl("POST", "/api_v2/as_bot/statement/agent", f, referer_path="/report")
|
|
1797
|
-
|
|
1798
|
-
def report_intent(self, begin_ts, end_ts, time_offset=8, bot_id=None,
|
|
1799
|
-
touch_point=None, intent_id=None, date_unit=None, **extra):
|
|
1800
|
-
"""POST statement/transcript/intent — intent-level ticket stats."""
|
|
1801
|
-
f = self._report_filter(begin_ts, end_ts, time_offset, bot_id,
|
|
1802
|
-
touch_point, date_unit, **extra)
|
|
1803
|
-
if intent_id is not None:
|
|
1804
|
-
f["firstIntentId"] = int(intent_id)
|
|
1805
|
-
return self._curl("POST", "/api_v2/as_bot/statement/transcript/intent", f,
|
|
1806
|
-
referer_path="/report")
|
|
1807
|
-
|
|
1808
|
-
def report_satisfaction_summary(self, begin_ts, end_ts, time_offset=8,
|
|
1809
|
-
bot_id=None, touch_point=None, **extra):
|
|
1810
|
-
"""POST statement/satisfaction/feedback — satisfaction summary (AI vs Human)."""
|
|
1811
|
-
return self._curl("POST", "/api_v2/as_bot/statement/satisfaction/feedback",
|
|
1812
|
-
self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point, **extra),
|
|
1813
|
-
referer_path="/report")
|
|
1814
|
-
|
|
1815
|
-
def report_satisfaction_list(self, begin_ts, end_ts, time_offset=8, page=1,
|
|
1816
|
-
page_size=20, bot_id=None, touch_point=None,
|
|
1817
|
-
assistant_role=None, satisfied=None, **extra):
|
|
1818
|
-
"""POST statement/satisfaction/message — satisfaction detail list (paged).
|
|
1819
|
-
|
|
1820
|
-
assistant_role: 'ASSISTANT' or 'HUMAN'
|
|
1821
|
-
satisfied: True (thumbs up) or False (thumbs down)
|
|
1822
|
-
Response key is 'satisfactions' (not 'records').
|
|
1823
|
-
"""
|
|
1824
|
-
f = self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point,
|
|
1825
|
-
page=page, size=page_size, **extra)
|
|
1826
|
-
if assistant_role:
|
|
1827
|
-
f["assistantRole"] = assistant_role
|
|
1828
|
-
if satisfied is not None:
|
|
1829
|
-
f["satisfied"] = satisfied
|
|
1830
|
-
return self._curl("POST", "/api_v2/as_bot/statement/satisfaction/message", f,
|
|
1831
|
-
referer_path="/report")
|
|
1832
|
-
|
|
1833
|
-
def report_knowledge(self, begin_ts, end_ts, time_offset=8, page=1, page_size=20,
|
|
1834
|
-
sort_by=None, bot_id=None, touch_point=None, source=None, **extra):
|
|
1835
|
-
"""POST statement/knowledge — knowledge usage stats.
|
|
1836
|
-
|
|
1837
|
-
sort_by: dict like {"TOTAL": "DESCEND"} or {"ESCALATION_RATE": "ASCEND"}
|
|
1838
|
-
"""
|
|
1839
|
-
f = self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point,
|
|
1840
|
-
page=page, size=page_size, **extra)
|
|
1841
|
-
if sort_by:
|
|
1842
|
-
f["sortBy"] = sort_by
|
|
1843
|
-
if source:
|
|
1844
|
-
f["source"] = source
|
|
1845
|
-
return self._curl("POST", "/api_v2/as_bot/statement/knowledge", f, referer_path="/report")
|
|
1846
|
-
|
|
1847
|
-
def report_escalation_reasons(self, begin_ts, end_ts, time_offset=8,
|
|
1848
|
-
date_unit="DAY", bot_id=None, touch_point=None, **extra):
|
|
1849
|
-
"""POST statement/transcript/reason2 — escalation reason tree.
|
|
1850
|
-
|
|
1851
|
-
date_unit: 'DAY' (main list) or 'WEEK' (last 4 weeks)
|
|
1852
|
-
"""
|
|
1853
|
-
return self._curl("POST", "/api_v2/as_bot/statement/transcript/reason2",
|
|
1854
|
-
self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point,
|
|
1855
|
-
date_unit=date_unit, **extra),
|
|
1856
|
-
referer_path="/report")
|
|
1857
|
-
|
|
1858
|
-
def report_escalation_drilldown(self, begin_ts, end_ts, reason, time_offset=8,
|
|
1859
|
-
page=1, page_size=10, bot_id=None, touch_point=None, **extra):
|
|
1860
|
-
"""POST statement/transcript/reason/three/level — escalation reason drilldown (paged)."""
|
|
1861
|
-
f = self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point,
|
|
1862
|
-
date_unit="DAY", **extra)
|
|
1863
|
-
f["reason"] = reason
|
|
1864
|
-
f["pageIndex"] = page
|
|
1865
|
-
f["pageSize"] = page_size
|
|
1866
|
-
return self._curl("POST", "/api_v2/as_bot/statement/transcript/reason/three/level", f,
|
|
1867
|
-
referer_path="/report")
|
|
1868
|
-
|
|
1869
|
-
def report_self_learning(self, begin_ts, end_ts, time_offset=8,
|
|
1870
|
-
bot_id=None, touch_point=None, **extra):
|
|
1871
|
-
"""POST statement/self-learning — self-learning topic stats and trend."""
|
|
1872
|
-
return self._curl("POST", "/api_v2/as_bot/statement/self-learning",
|
|
1873
|
-
self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point, **extra),
|
|
1874
|
-
referer_path="/report")
|
|
1875
|
-
|
|
1876
|
-
def report_livechat_summary(self, begin_ts, end_ts, time_offset=8, **extra):
|
|
1877
|
-
"""POST livechat/report/summary — LiveChat pre-sales funnel and intent distribution."""
|
|
1878
|
-
return self._curl("POST", "/api_v2/as_bot/livechat/report/summary",
|
|
1879
|
-
self._report_filter(begin_ts, end_ts, time_offset, **extra),
|
|
1880
|
-
referer_path="/report")
|
|
1881
|
-
|
|
1882
|
-
def report_livechat_table(self, begin_ts, end_ts, time_offset=8,
|
|
1883
|
-
page=1, page_size=10, **extra):
|
|
1884
|
-
"""POST livechat/report/table — LiveChat conversion detail table (paged)."""
|
|
1885
|
-
f = self._report_filter(begin_ts, end_ts, time_offset, **extra)
|
|
1886
|
-
f["page"] = {"page": page, "size": page_size}
|
|
1887
|
-
return self._curl("POST", "/api_v2/as_bot/livechat/report/table", f,
|
|
1888
|
-
referer_path="/report")
|
|
1889
|
-
|
|
1890
|
-
def report_inspection(self, start_date, end_date, dimension="ticket"):
|
|
1891
|
-
"""POST inspection/ticket/count — AI inspection overview (ticket count, pass rate).
|
|
1892
|
-
|
|
1893
|
-
start_date / end_date: 'YYYY-MM-DD' strings
|
|
1894
|
-
"""
|
|
1895
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/count",
|
|
1896
|
-
{"startTime": start_date, "endTime": end_date, "dimension": dimension},
|
|
1897
|
-
referer_path="/report")
|
|
1898
|
-
|
|
1899
|
-
def report_inspection_history(self, start_date, end_date, dimension="ticket"):
|
|
1900
|
-
"""POST inspection/ticket/history — AI inspection issue history trend."""
|
|
1901
|
-
return self._curl("POST", "/api_v2/intelli/inspection/ticket/history",
|
|
1902
|
-
{"startTime": start_date, "endTime": end_date, "dimension": dimension},
|
|
1903
|
-
referer_path="/report")
|
|
1904
|
-
|
|
1905
|
-
def report_feedback(self, begin_ts, end_ts, time_offset=8,
|
|
1906
|
-
bot_id=None, touch_point=None, **extra):
|
|
1907
|
-
"""POST statement/feedback — star rating distribution (1-10) with handoff breakdown."""
|
|
1908
|
-
return self._curl("POST", "/api_v2/as_bot/statement/feedback",
|
|
1909
|
-
self._report_filter(begin_ts, end_ts, time_offset, bot_id, touch_point, **extra),
|
|
1910
|
-
referer_path="/report")
|
|
1911
|
-
|
|
1912
|
-
def report_agent_weekly_graph(self, begin_ts, end_ts, time_offset=8, **extra):
|
|
1913
|
-
"""POST statement/graph/agent — agent weekly chart data."""
|
|
1914
|
-
return self._curl("POST", "/api_v2/as_bot/statement/graph/agent",
|
|
1915
|
-
self._report_filter(begin_ts, end_ts, time_offset, **extra),
|
|
1916
|
-
referer_path="/report")
|
|
1917
|
-
|
|
1918
|
-
# ── Voice ──
|
|
1919
|
-
|
|
1920
|
-
def list_voice_agents(self):
|
|
1921
|
-
"""POST queryAgent with category=voice — list voice agents."""
|
|
1922
|
-
return self._curl("POST", "/api_v2/gpt/ap/queryAgent", {
|
|
1923
|
-
"projectId": int(self.project_id),
|
|
1924
|
-
"needRelation": True,
|
|
1925
|
-
"noMeta": True,
|
|
1926
|
-
"category": "voice",
|
|
1927
|
-
})
|
|
1928
|
-
|
|
1929
|
-
def list_voice_transcripts(self, page_num=1, page_size=20, begin_ts=None, end_ts=None):
|
|
1930
|
-
"""POST voice-report/transcript-page — list voice call transcript pages."""
|
|
1931
|
-
if begin_ts is None:
|
|
1932
|
-
begin_ts = int((time.time() - 30 * 86400) * 1000)
|
|
1933
|
-
if end_ts is None:
|
|
1934
|
-
end_ts = int((time.time() + 2 * 86400) * 1000)
|
|
1935
|
-
return self._curl_internal("POST", "/api_v2/as_bot/voice-report/transcript-page", {
|
|
1936
|
-
"pageNum": page_num, "pageSize": page_size,
|
|
1937
|
-
"beginTimestamp": begin_ts, "endTimestamp": end_ts,
|
|
1938
|
-
})
|
|
1939
|
-
|
|
1940
|
-
def get_voice_transcript(self, history_id):
|
|
1941
|
-
"""GET voice/transcript — get voice call transcript detail."""
|
|
1942
|
-
return self._curl_internal(
|
|
1943
|
-
"GET",
|
|
1944
|
-
f"/api_v2/gpt/voice/transcript/projectId/{self.project_id}/history/{history_id}")
|