@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,73 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Shared authenticated HTTP adapter for Shulex Desk / Tars APIs."""
3
-
4
- import os
5
- import sys
6
- sys.path.insert(0, os.path.join(os.environ["SOLVEA_LIB_DIR"], "skill-runtime"))
7
- from bootstrap import SOLVEA_ENV # noqa: E402
8
-
9
- from dataclasses import dataclass
10
- from typing import Any
11
-
12
- import httpx
13
-
14
-
15
- DEFAULT_APPS_STAGING_URL = "https://apps-staging.voc.ai"
16
- DEFAULT_APPS_PROD_URL = "https://apps.voc.ai"
17
-
18
-
19
- @dataclass(frozen=True)
20
- class DeskApiConfig:
21
- base_url: str
22
- token: str
23
- cookie_name: str
24
-
25
-
26
- def resolve_desk_api_config() -> DeskApiConfig:
27
- """Resolve the authenticated Desk proxy for the current runtime environment."""
28
- env = os.environ.get("SOLVEA_ENV", SOLVEA_ENV) or "prod"
29
- if env == "prod":
30
- return DeskApiConfig(
31
- base_url=os.environ.get("SOLVEA_PROD_URL", DEFAULT_APPS_PROD_URL).rstrip("/"),
32
- token=os.environ.get("SOLVEA_PROD_TOKEN", "") or os.environ.get("SOLVEA_TOKEN", ""),
33
- cookie_name="_token",
34
- )
35
- return DeskApiConfig(
36
- base_url=os.environ.get("SOLVEA_BASE_URL", DEFAULT_APPS_STAGING_URL).rstrip("/"),
37
- token=os.environ.get("SOLVEA_TOKEN", ""),
38
- cookie_name="_token_staging",
39
- )
40
-
41
-
42
- def request_json(
43
- client: httpx.Client,
44
- config: DeskApiConfig,
45
- method: str,
46
- path: str,
47
- *,
48
- params: dict[str, Any] | None = None,
49
- json_body: dict[str, Any] | None = None,
50
- ) -> dict[str, Any]:
51
- """Send one Desk request and validate its transport and business envelope."""
52
- headers = {
53
- "Accept": "application/json, text/plain, */*",
54
- "Origin": config.base_url,
55
- "Referer": f"{config.base_url}/botdesk",
56
- }
57
- if config.token:
58
- headers["Cookie"] = f"{config.cookie_name}={config.token}"
59
-
60
- response = client.request(
61
- method,
62
- f"{config.base_url}{path}",
63
- params=params,
64
- json=json_body,
65
- headers=headers,
66
- )
67
- response.raise_for_status()
68
- payload = response.json()
69
- if not isinstance(payload, dict):
70
- raise ValueError("Desk API must return a JSON object")
71
- if payload.get("code") is not None and str(payload["code"]) != "200":
72
- raise RuntimeError(str(payload.get("message") or "Desk API request failed"))
73
- return payload
@@ -1,403 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Fetch the current Desk ticket list into a local XLSX snapshot."""
3
-
4
- import os
5
- import sys
6
- sys.path.insert(0, os.path.join(os.environ["SOLVEA_LIB_DIR"], "skill-runtime"))
7
- from bootstrap import ( # noqa: E402
8
- OUTPUTS_DIR,
9
- SOLVEA_ENV,
10
- SOLVEA_PROJECT_ID,
11
- SOLVEA_TENANT_ID,
12
- STATE_DIR,
13
- TMP_DIR,
14
- timestamp,
15
- )
16
-
17
- import argparse
18
- import json
19
- from datetime import datetime
20
- from pathlib import Path
21
- from typing import Any
22
- from urllib.parse import urlsplit
23
-
24
- import httpx
25
- import pandas as pd
26
-
27
- from desk_api import DeskApiConfig, request_json, resolve_desk_api_config
28
-
29
-
30
- SOURCE_TYPE = "ticket_list_snapshot"
31
- SOURCE_CONTRACT = "current_user_view_filtered_paginated_api"
32
- TICKET_LIST_PATH = "/api_v2/tars/api/ticket/list"
33
- DEFAULT_PAGE_SIZE = 100
34
- MAX_PAGE_SIZE = 100
35
- DEFAULT_MAX_PAGES = 120
36
- DEFAULT_MAX_ROWS = 200000
37
- BACKEND_FILTER_FIELDS = {
38
- "ticketNumber",
39
- "ticketStatusList",
40
- "channelList",
41
- "platformList",
42
- "channelIdList",
43
- "supportEmailIdList",
44
- "agentIdList",
45
- "tagList",
46
- "replyUserList",
47
- "customerEmail",
48
- "customerName",
49
- "customerEmailFull",
50
- "customerPhone",
51
- "createTimeBegin",
52
- "createTimeEnd",
53
- "keyword",
54
- "marketList",
55
- "sendFailure",
56
- "orderId",
57
- "categoryList",
58
- "brandList",
59
- "humanHandling",
60
- "myTicket",
61
- "snoozed",
62
- "scheduled",
63
- "notSpam",
64
- "notNotification",
65
- "botIdList",
66
- "noTag",
67
- "ticketNumberList",
68
- "supportPhoneList",
69
- "unread",
70
- "personaId",
71
- "campaignName",
72
- "lastVoiceAnswerType",
73
- "lastVoicePhoneType",
74
- }
75
-
76
-
77
- def _now_iso() -> str:
78
- return datetime.now().astimezone().isoformat(timespec="seconds")
79
-
80
-
81
- def resolve_source_config() -> DeskApiConfig:
82
- """Resolve the Desk ticket-list endpoint for the current environment."""
83
- config = resolve_desk_api_config()
84
- explicit_endpoint = os.environ.get("DESK_TICKET_SOURCE_URL", "").strip()
85
- if not explicit_endpoint:
86
- return config
87
- parsed = urlsplit(explicit_endpoint)
88
- if not parsed.scheme or not parsed.netloc:
89
- raise ValueError("DESK_TICKET_SOURCE_URL must be an absolute URL")
90
- return DeskApiConfig(
91
- base_url=f"{parsed.scheme}://{parsed.netloc}",
92
- token=config.token,
93
- cookie_name=config.cookie_name,
94
- )
95
-
96
-
97
- def _ticket_list_path() -> str:
98
- explicit_endpoint = os.environ.get("DESK_TICKET_SOURCE_URL", "").strip()
99
- if not explicit_endpoint:
100
- return TICKET_LIST_PATH
101
- parsed = urlsplit(explicit_endpoint)
102
- return parsed.path or TICKET_LIST_PATH
103
-
104
-
105
- def validate_page_size(page_size: str | int) -> int:
106
- try:
107
- value = int(page_size)
108
- except (TypeError, ValueError) as exc:
109
- raise argparse.ArgumentTypeError("page-size must be an integer") from exc
110
- if value < 1 or value > MAX_PAGE_SIZE:
111
- raise argparse.ArgumentTypeError(f"page-size must be between 1 and {MAX_PAGE_SIZE}")
112
- return value
113
-
114
-
115
- def parse_backend_filters(raw_json: str = "", filters_file: str = "") -> dict[str, Any]:
116
- """Parse the filter-only subset of TicketListPageDTO."""
117
- if raw_json and filters_file:
118
- raise ValueError("use only one of --filters-json and --filters-file")
119
- if filters_file:
120
- raw_json = Path(filters_file).read_text(encoding="utf-8")
121
- if not raw_json:
122
- return {}
123
- payload = json.loads(raw_json)
124
- if not isinstance(payload, dict):
125
- raise ValueError("ticket filters must be a JSON object")
126
- unknown = sorted(set(payload) - BACKEND_FILTER_FIELDS)
127
- if unknown:
128
- raise ValueError(f"unsupported ticket filter fields: {', '.join(unknown)}")
129
- return payload
130
-
131
-
132
- def _write_json(path: Path, payload: dict[str, Any]) -> None:
133
- path.parent.mkdir(parents=True, exist_ok=True)
134
- path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
135
-
136
-
137
- def _normalize_cell(value: Any) -> Any:
138
- if isinstance(value, (dict, list)):
139
- return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
140
- return value
141
-
142
-
143
- def _records(payload: dict[str, Any]) -> list[dict[str, Any]]:
144
- code = payload.get("code")
145
- if code is not None and str(code) != "200":
146
- raise RuntimeError(payload.get("message") or "Desk ticket-list API failed")
147
- body = payload.get("data", payload)
148
- if not isinstance(body, dict):
149
- return []
150
- values = body.get("list") or body.get("records") or body.get("items") or []
151
- return [item for item in values if isinstance(item, dict)] if isinstance(values, list) else []
152
-
153
-
154
- def _page_body(payload: dict[str, Any]) -> dict[str, Any]:
155
- body = payload.get("data", payload)
156
- return body if isinstance(body, dict) else {}
157
-
158
-
159
- def _has_more(payload: dict[str, Any], page: int, page_size: int, row_count: int) -> bool:
160
- body = _page_body(payload)
161
- for key in ("hasMore", "has_more"):
162
- if key in body:
163
- return bool(body[key])
164
- total = body.get("total")
165
- if isinstance(total, int):
166
- return page * page_size < total
167
- return row_count >= page_size
168
-
169
-
170
- def _request_page(
171
- client: httpx.Client,
172
- config: DeskApiConfig,
173
- *,
174
- page: int,
175
- page_size: int,
176
- view_id: str,
177
- sort: str,
178
- order: str,
179
- backend_filters: dict[str, Any] | None = None,
180
- ) -> dict[str, Any]:
181
- request_payload = {
182
- "page": page,
183
- "pageSize": page_size,
184
- "viewId": view_id,
185
- "sort": sort,
186
- "order": order,
187
- }
188
- request_payload.update(backend_filters or {})
189
- return request_json(
190
- client,
191
- config,
192
- "POST",
193
- _ticket_list_path(),
194
- json_body=request_payload,
195
- )
196
-
197
-
198
- def fetch_pages(
199
- config: DeskApiConfig,
200
- *,
201
- page_size: int,
202
- max_pages: int,
203
- max_rows: int,
204
- view_id: str,
205
- sort: str,
206
- order: str,
207
- backend_filters: dict[str, Any] | None = None,
208
- ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], bool, str]:
209
- page_size = validate_page_size(page_size)
210
- pages: list[dict[str, Any]] = []
211
- records: list[dict[str, Any]] = []
212
- truncated = False
213
- completion_reason = "source_exhausted"
214
- with httpx.Client(timeout=60.0) as client:
215
- for page_no in range(1, max_pages + 1):
216
- payload = _request_page(
217
- client,
218
- config,
219
- page=page_no,
220
- page_size=page_size,
221
- view_id=view_id,
222
- sort=sort,
223
- order=order,
224
- backend_filters=backend_filters,
225
- )
226
- page_records = _records(payload)
227
- pages.append(payload)
228
- records.extend(page_records)
229
- if len(records) >= max_rows:
230
- exceeded_limit = len(records) > max_rows
231
- records = records[:max_rows]
232
- has_more = _has_more(payload, page_no, page_size, len(page_records))
233
- truncated = exceeded_limit or has_more
234
- completion_reason = "max_rows" if truncated else "source_exhausted"
235
- break
236
- if not _has_more(payload, page_no, page_size, len(page_records)):
237
- break
238
- else:
239
- truncated = True
240
- completion_reason = "max_pages"
241
- return pages, records, truncated, completion_reason
242
-
243
-
244
- def write_snapshot(
245
- task_id: str,
246
- pages: list[dict[str, Any]],
247
- records: list[dict[str, Any]],
248
- *,
249
- config: DeskApiConfig,
250
- page_size: int,
251
- view_id: str,
252
- sort: str,
253
- order: str,
254
- truncated: bool,
255
- backend_filters: dict[str, Any] | None = None,
256
- completion_reason: str = "source_exhausted",
257
- ) -> dict[str, Any]:
258
- state_task_dir = STATE_DIR / task_id
259
- output_task_dir = OUTPUTS_DIR / task_id
260
- tmp_task_dir = TMP_DIR / task_id
261
- state_task_dir.mkdir(parents=True, exist_ok=True)
262
- tmp_task_dir.mkdir(parents=True, exist_ok=True)
263
-
264
- for index, page in enumerate(pages, start=1):
265
- _write_json(tmp_task_dir / f"raw_page_{index:03d}.json", page)
266
-
267
- rows = [{key: _normalize_cell(value) for key, value in row.items()} for row in records]
268
- dataframe = pd.DataFrame(rows) if rows else pd.DataFrame([{"_empty": ""}])
269
- datasource = state_task_dir / "datasource.xlsx"
270
- dataframe.to_excel(datasource, sheet_name="data", index=False, engine="openpyxl")
271
-
272
- manifest = {
273
- "status": "ready",
274
- "skill": "solvea-desk",
275
- "source_type": SOURCE_TYPE,
276
- "source_contract": SOURCE_CONTRACT,
277
- "task_id": task_id,
278
- "env": SOLVEA_ENV,
279
- "tenant_id": SOLVEA_TENANT_ID,
280
- "project_id": SOLVEA_PROJECT_ID,
281
- "endpoint": f"{config.base_url}{_ticket_list_path()}",
282
- "data_scope": "current_user",
283
- "snapshot_semantics": "current_state",
284
- "view_id": view_id,
285
- "sort": sort,
286
- "order": order,
287
- "backend_filters": backend_filters or {},
288
- "completion_reason": completion_reason,
289
- "page_size": page_size,
290
- "page_count": len(pages),
291
- "row_counts": {"data": len(records)},
292
- "truncated": truncated,
293
- "files": {
294
- "state": {
295
- "datasource": "datasource.xlsx",
296
- "manifest": "manifest.json",
297
- "analysis": "analysis.py",
298
- "report_data": "report_data.json",
299
- "calculation_notes": "calculation_notes.json",
300
- },
301
- "outputs": {
302
- "report_pattern": "<report_slug>.html",
303
- "detail_pattern": "<report_slug>_detail.xlsx",
304
- },
305
- },
306
- "datasource": str(datasource),
307
- "tmp_raw_pages": str(tmp_task_dir),
308
- "generated_at": _now_iso(),
309
- }
310
- _write_json(state_task_dir / "manifest.json", manifest)
311
- _write_json(
312
- STATE_DIR / "latest_task.json",
313
- {
314
- "skill": "solvea-desk",
315
- "status": "ready",
316
- "source_type": SOURCE_TYPE,
317
- "task_id": task_id,
318
- "task_dir": str(state_task_dir),
319
- "state_task_dir": str(state_task_dir),
320
- "output_task_dir": str(output_task_dir),
321
- "tmp_task_dir": str(tmp_task_dir),
322
- "view_id": view_id,
323
- "backend_filters": backend_filters or {},
324
- "updated_at": _now_iso(),
325
- },
326
- )
327
- return {
328
- "ok": True,
329
- "source_type": SOURCE_TYPE,
330
- "task_id": task_id,
331
- "task_dir": str(state_task_dir),
332
- "state_task_dir": str(state_task_dir),
333
- "output_task_dir": str(output_task_dir),
334
- "tmp_task_dir": str(tmp_task_dir),
335
- "datasource": str(datasource),
336
- "manifest": str(state_task_dir / "manifest.json"),
337
- "row_counts": {"data": len(records)},
338
- "truncated": truncated,
339
- }
340
-
341
-
342
- def main() -> int:
343
- parser = argparse.ArgumentParser(description="Fetch current Desk ticket details into datasource.xlsx")
344
- parser.add_argument("--task-id", default="", help="Optional task id; defaults to timestamp")
345
- parser.add_argument("--view-id", default="1", help="Desk ticket view id")
346
- parser.add_argument("--page-size", type=validate_page_size, default=DEFAULT_PAGE_SIZE)
347
- parser.add_argument("--max-pages", type=int, default=DEFAULT_MAX_PAGES)
348
- parser.add_argument("--max-rows", type=int, default=DEFAULT_MAX_ROWS)
349
- parser.add_argument("--sort", default="last_update_time")
350
- parser.add_argument("--order", choices=["ASC", "DESC"], default="DESC")
351
- parser.add_argument(
352
- "--filters-json",
353
- default="",
354
- help="JSON object containing supported TicketListPageDTO filter fields",
355
- )
356
- parser.add_argument("--filters-file", default="", help="Path to a JSON filter object")
357
- args = parser.parse_args()
358
-
359
- try:
360
- backend_filters = parse_backend_filters(args.filters_json, args.filters_file)
361
- except (ValueError, json.JSONDecodeError, OSError) as exc:
362
- parser.error(str(exc))
363
-
364
- config = resolve_source_config()
365
- if not config.token:
366
- print(json.dumps({
367
- "ok": False,
368
- "source_type": SOURCE_TYPE,
369
- "error": "desk_ticket_runtime_not_configured",
370
- "message": "Check SOLVEA_ENV and the corresponding Solvea token environment variable.",
371
- }, ensure_ascii=False, indent=2))
372
- return 2
373
-
374
- task_id = args.task_id.strip() or timestamp()
375
- pages, records, truncated, completion_reason = fetch_pages(
376
- config,
377
- page_size=args.page_size,
378
- max_pages=args.max_pages,
379
- max_rows=args.max_rows,
380
- view_id=args.view_id,
381
- sort=args.sort,
382
- order=args.order,
383
- backend_filters=backend_filters,
384
- )
385
- result = write_snapshot(
386
- task_id,
387
- pages,
388
- records,
389
- config=config,
390
- page_size=args.page_size,
391
- view_id=args.view_id,
392
- sort=args.sort,
393
- order=args.order,
394
- truncated=truncated,
395
- backend_filters=backend_filters,
396
- completion_reason=completion_reason,
397
- )
398
- print(json.dumps(result, ensure_ascii=False, indent=2))
399
- return 3 if truncated else 0
400
-
401
-
402
- if __name__ == "__main__":
403
- raise SystemExit(main())