@shulex/cxclawctl 0.1.2 → 0.1.3

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