@shulex/cxclawctl 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +228 -2
  2. package/bin/cxclawctl.mjs +11 -0
  3. package/capabilities.lock.json +898 -0
  4. package/dist/cli.js +363 -0
  5. package/dist/cli.js.map +1 -0
  6. package/package.json +33 -4
  7. package/requirements-cli.txt +2 -0
  8. package/skills/__lib__/__init__.py +0 -0
  9. package/skills/__lib__/skill-runtime/bootstrap.py +134 -0
  10. package/skills/__lib__/solvea-api/__init__.py +0 -0
  11. package/skills/__lib__/solvea-api/client.py +1944 -0
  12. package/skills/__lib__/solvea-api/dashboard.py +100 -0
  13. package/skills/__lib__/solvea-api/solvea_api.py +1341 -0
  14. package/skills/agent-analytics/SKILL.md +74 -0
  15. package/skills/agent-analytics/references/chat-report-fields.md +36 -0
  16. package/skills/agent-analytics/references/report-fields.md +130 -0
  17. package/skills/agent-analytics/references/transcript.md +110 -0
  18. package/skills/agent-builder/SKILL.md +42 -0
  19. package/skills/agent-builder/interfaces/fallback-chatflow.md +47 -0
  20. package/skills/agent-builder/references/chatflow/graph-spec.md +259 -0
  21. package/skills/agent-builder/references/chatflow/node-selection.md +89 -0
  22. package/skills/agent-builder/references/chatflow/nodes/agent.md +350 -0
  23. package/skills/agent-builder/references/chatflow/nodes/business-track.md +43 -0
  24. package/skills/agent-builder/references/chatflow/nodes/code.md +157 -0
  25. package/skills/agent-builder/references/chatflow/nodes/collect-order.md +47 -0
  26. package/skills/agent-builder/references/chatflow/nodes/finish.md +34 -0
  27. package/skills/agent-builder/references/chatflow/nodes/human-review.md +60 -0
  28. package/skills/agent-builder/references/chatflow/nodes/if-else.md +100 -0
  29. package/skills/agent-builder/references/chatflow/nodes/intent.md +79 -0
  30. package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +43 -0
  31. package/skills/agent-builder/references/chatflow/nodes/llm.md +117 -0
  32. package/skills/agent-builder/references/chatflow/nodes/note.md +38 -0
  33. package/skills/agent-builder/references/chatflow/nodes/question.md +97 -0
  34. package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +68 -0
  35. package/skills/agent-builder/references/chatflow/nodes/reply.md +97 -0
  36. package/skills/agent-builder/references/chatflow/nodes/start.md +35 -0
  37. package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +48 -0
  38. package/skills/agent-builder/references/chatflow/nodes/tool.md +138 -0
  39. package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +63 -0
  40. package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +52 -0
  41. package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +50 -0
  42. package/skills/agent-builder/references/chatflow/nodes/variable-process.md +70 -0
  43. package/skills/agent-builder/references/chatflow/overview.md +53 -0
  44. package/skills/agent-builder/references/chatflow/visual-translation.md +64 -0
  45. package/skills/agent-builder/references/chatflow/workflow.md +134 -0
  46. package/skills/agent-builder/references/multi-agent/create-update.md +185 -0
  47. package/skills/agent-builder/references/multi-agent/graph-spec.md +246 -0
  48. package/skills/agent-builder/references/multi-agent/graph-structure.md +12 -0
  49. package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +13 -0
  50. package/skills/agent-builder/references/multi-agent/migration/large.md +554 -0
  51. package/skills/agent-builder/references/multi-agent/migration/principles.md +446 -0
  52. package/skills/agent-builder/references/multi-agent/migration/small.md +225 -0
  53. package/skills/agent-builder/references/multi-agent/orchestration.md +11 -0
  54. package/skills/agent-builder/references/multi-agent/overview.md +48 -0
  55. package/skills/agent-builder/references/multi-agent/update.md +128 -0
  56. package/skills/agent-builder/references/multi-agent/workflow.md +87 -0
  57. package/skills/agent-builder/references/single-agent/overview.md +48 -0
  58. package/skills/agent-builder/references/single-agent/prompt.md +16 -0
  59. package/skills/agent-builder/references/single-agent/tools.md +36 -0
  60. package/skills/agent-builder/references/single-agent/voice.md +46 -0
  61. package/skills/agent-builder/references/tools/http-tool.md +315 -0
  62. package/skills/agent-builder/references/tools/mock-tool.md +279 -0
  63. package/skills/agent-builder/references/tools/overview.md +46 -0
  64. package/skills/agent-builder/references/tools/tool-config.md +43 -0
  65. package/skills/agent-builder/references/tools/x-token.md +34 -0
  66. package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +3458 -0
  67. package/skills/agent-builder/scripts/chatflow/validate_graph.py +1697 -0
  68. package/skills/agent-builder/scripts/copy_marketplace_agent.py +61 -0
  69. package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +5974 -0
  70. package/skills/agent-builder/scripts/multi-agent/replay_compare.py +679 -0
  71. package/skills/agent-builder/scripts/patch_agent_tools.py +135 -0
  72. package/skills/agent-builder/scripts/tools/validate_tool_config.py +384 -0
  73. package/skills/agent-tester/SKILL.md +35 -0
  74. package/skills/agent-tester/batch-test/overview.md +86 -0
  75. package/skills/agent-tester/scripts/batch_test_sheet.py +213 -0
  76. package/skills/agent-tester/single-test/badcase-retest.md +85 -0
  77. package/skills/agent-tester/single-test/direct-test.md +24 -0
  78. package/skills/agent-tester/single-test/overview.md +142 -0
  79. package/skills/delivery/SKILL.md +112 -0
  80. package/skills/delivery/interfaces/classify.md +38 -0
  81. package/skills/delivery/interfaces/create-agents.md +89 -0
  82. package/skills/delivery/interfaces/delivery-test.md +67 -0
  83. package/skills/delivery/references/flow.md +40 -0
  84. package/skills/delivery/references/validation.md +15 -0
  85. package/skills/delivery/scripts/batch_test.py +704 -0
  86. package/skills/delivery/scripts/classify.py +22 -0
  87. package/skills/delivery/scripts/create_agents.py +695 -0
  88. package/skills/delivery/scripts/gen_delivery_report.py +335 -0
  89. package/skills/delivery/scripts/gen_excel.py +234 -0
  90. package/skills/delivery/scripts/gen_report.py +208 -0
  91. package/skills/delivery/scripts/plan.py +163 -0
  92. package/skills/delivery/scripts/render.py +713 -0
  93. package/skills/delivery/scripts/render_summary.py +38 -0
  94. package/skills/delivery/scripts/sample_history_cases.py +109 -0
  95. package/skills/kb-builder/SKILL.md +80 -0
  96. package/skills/kb-builder/interfaces/qa-upload.md +55 -0
  97. package/skills/kb-builder/interfaces/web-kb.md +84 -0
  98. package/skills/kb-builder/references/directory.md +373 -0
  99. package/skills/kb-builder/references/export.md +282 -0
  100. package/skills/kb-builder/references/isolation-keywords.md +113 -0
  101. package/skills/kb-builder/references/knowledge.md +153 -0
  102. package/skills/kb-builder/references/product.md +52 -0
  103. package/skills/kb-builder/references/qa-from-help-center.md +58 -0
  104. package/skills/kb-builder/references/qa-from-pdf.md +94 -0
  105. package/skills/kb-builder/references/qa-from-tickets.md +168 -0
  106. package/skills/kb-builder/references/qa-from-web.md +98 -0
  107. package/skills/kb-builder/references/qa-quality.md +73 -0
  108. package/skills/kb-builder/references/reply-template.md +237 -0
  109. package/skills/kb-builder/references/tag-isolation-migration.md +61 -0
  110. package/skills/kb-builder/references/tags.md +45 -0
  111. package/skills/kb-builder/references/upload.md +130 -0
  112. package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +333 -0
  113. package/skills/kb-builder/scripts/build_directory_plan.py +144 -0
  114. package/skills/kb-builder/scripts/crawl_site.py +433 -0
  115. package/skills/kb-builder/scripts/create_dirs.py +146 -0
  116. package/skills/kb-builder/scripts/export_knowledge.py +454 -0
  117. package/skills/kb-builder/scripts/extract_ticket_qa.py +441 -0
  118. package/skills/kb-builder/scripts/extract_web_qa.py +118 -0
  119. package/skills/kb-builder/scripts/import_qa.py +219 -0
  120. package/skills/kb-builder/scripts/import_source_documents.py +143 -0
  121. package/skills/kb-builder/scripts/import_templates.py +409 -0
  122. package/skills/poc/SKILL.md +125 -0
  123. package/skills/poc/assets/default_manual.css +279 -0
  124. package/skills/poc/assets/notice_escalate.png +0 -0
  125. package/skills/poc/assets/notice_escalate2.png +0 -0
  126. package/skills/poc/assets/notice_new_chat.png +0 -0
  127. package/skills/poc/assets/poc_test_template.html +645 -0
  128. package/skills/poc/assets/poc_test_template.md +104 -0
  129. package/skills/poc/assets/step3_preview.png +0 -0
  130. package/skills/poc/assets/step4_order_note.png +0 -0
  131. package/skills/poc/interfaces/classify.md +38 -0
  132. package/skills/poc/interfaces/create-agents.md +108 -0
  133. package/skills/poc/interfaces/poc-test.md +94 -0
  134. package/skills/poc/references/flow.md +47 -0
  135. package/skills/poc/references/kb-directory.md +22 -0
  136. package/skills/poc/references/validation.md +17 -0
  137. package/skills/poc/scripts/classify.py +22 -0
  138. package/skills/poc/scripts/create_agents.py +818 -0
  139. package/skills/poc/scripts/gen_excel.py +234 -0
  140. package/skills/poc/scripts/gen_poc_manual.py +249 -0
  141. package/skills/poc/scripts/gen_report.py +208 -0
  142. package/skills/poc/scripts/inspect_chatflow.py +209 -0
  143. package/skills/poc/scripts/plan.py +178 -0
  144. package/skills/poc/scripts/poc_test.py +495 -0
  145. package/skills/poc/scripts/render.py +713 -0
  146. package/skills/poc/scripts/render_summary.py +41 -0
  147. package/skills/runtime-inspector/SKILL.md +51 -0
  148. package/skills/runtime-inspector/references/frontend-rendering.md +126 -0
  149. package/skills/runtime-inspector/references/model.md +59 -0
  150. package/skills/runtime-inspector/references/sandbox.md +131 -0
  151. package/skills/runtime-inspector/scripts/model_inspect.py +68 -0
  152. package/skills/runtime-inspector/scripts/prepare.py +98 -0
  153. package/skills/runtime-inspector/scripts/process.py +84 -0
  154. package/skills/runtime-inspector/scripts/read_state.py +32 -0
  155. package/skills/runtime-inspector/scripts/sandbox_inspect.py +463 -0
  156. package/skills/runtime-inspector/scripts/test_claude_proxy.py +49 -0
  157. package/skills/runtime-inspector/scripts/test_openai_proxy.py +41 -0
  158. package/skills/runtime-inspector/scripts/verify_persistence.py +101 -0
  159. package/skills/runtime-inspector/scripts/write_test.py +41 -0
  160. package/skills/setup/SKILL.md +17 -0
  161. package/skills/shulex-support/SKILL.md +39 -0
  162. package/skills/shulex-support/references/badcase-analysis/overview.md +34 -0
  163. package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +105 -0
  164. package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +89 -0
  165. package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +36 -0
  166. package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +53 -0
  167. package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +259 -0
  168. package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +148 -0
  169. package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +57 -0
  170. package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +120 -0
  171. package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +126 -0
  172. package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +121 -0
  173. package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +208 -0
  174. package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +115 -0
  175. package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +72 -0
  176. package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +92 -0
  177. package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +147 -0
  178. package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +283 -0
  179. package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +77 -0
  180. package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +19 -0
  181. package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +23 -0
  182. package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +31 -0
  183. package/skills/shulex-support/references/product-docs.md +70 -0
  184. package/skills/shulex-support/references/tickets.md +184 -0
  185. package/skills/shulex-support/scripts/support.py +427 -0
  186. package/skills/solvea-desk/SKILL.md +39 -0
  187. package/skills/solvea-desk/references/report/fields.md +82 -0
  188. package/skills/solvea-desk/references/report/report-design.md +200 -0
  189. package/skills/solvea-desk/references/report/report-template.html +44 -0
  190. package/skills/solvea-desk/references/report/report.css +517 -0
  191. package/skills/solvea-desk/references/report/workflow.md +464 -0
  192. package/skills/solvea-desk/references/tickets/query.md +103 -0
  193. package/skills/solvea-desk/references/tickets/tags.md +83 -0
  194. package/skills/solvea-desk/scripts/report/fetch_sources.py +580 -0
  195. package/skills/solvea-desk/scripts/report/render_report.py +763 -0
  196. package/skills/solvea-desk/scripts/tickets/desk_api.py +73 -0
  197. package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +403 -0
  198. package/skills/solvea-desk/scripts/tickets/ticket_tags.py +305 -0
  199. package/skills/solvea-kit/SKILL.md +84 -0
  200. package/skills/solvea-kit/references/agent.md +145 -0
  201. package/skills/solvea-kit/references/chat.md +244 -0
  202. package/skills/solvea-kit/references/intent.md +69 -0
  203. package/skills/solvea-kit/references/plugin.md +53 -0
  204. package/skills/solvea-kit/references/quality-inspection.md +75 -0
  205. package/skills/solvea-kit/references/strategy.md +62 -0
  206. package/skills/solvea-kit/references/tagging.md +231 -0
  207. package/skills/solvea-kit/references/variables.md +53 -0
  208. package/skills/solvea-kit/scripts/generate_label_import_file.py +151 -0
  209. package/skills/solvea-kit/scripts/intent_classify.py +843 -0
@@ -0,0 +1,695 @@
1
+ """
2
+ Delivery intent / agent / strategy creation pipeline.
3
+
4
+ Creates all standard delivery intents, copies fixed marketplace agents by
5
+ squareAgent_id when available, binds strategy routing, and patches X-Token in
6
+ order tools by default. Same-name intents are reused. Missing preset agents are
7
+ recorded as recoverable gaps instead of crashing the run.
8
+
9
+ Override the X-Token behaviour explicitly via --update-x-token / --no-update-x-token.
10
+
11
+ Usage:
12
+ # delivery: create all 7 intents + update X-Token
13
+ python3 scripts/create_agents.py
14
+
15
+ # disable SYSTEM intents after a creation run
16
+ python3 scripts/create_agents.py --disable-system
17
+
18
+ # dry-run preview (no API calls beyond marketplace listing in non-disable mode)
19
+ python3 scripts/create_agents.py --dry-run
20
+ """
21
+
22
+ import os, sys
23
+ sys.path.insert(0, os.path.join(os.environ.get("SOLVEA_LIB_DIR", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "__lib__"))), "skill-runtime"))
24
+ from bootstrap import *
25
+
26
+ import json, argparse
27
+ from pathlib import Path
28
+
29
+ from client import SolveaClient
30
+
31
+
32
+ def _read_state() -> dict:
33
+ _sf = STATE_DIR / "progress.json"
34
+ if _sf.exists():
35
+ try:
36
+ return json.loads(_sf.read_text(encoding="utf-8"))
37
+ except Exception:
38
+ pass
39
+ return {}
40
+
41
+
42
+ def _write_state(data: dict) -> None:
43
+ _sf = STATE_DIR / "progress.json"
44
+ _sf.parent.mkdir(parents=True, exist_ok=True)
45
+ _sf.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
46
+
47
+
48
+ # ── Fixed mapping table ────────────────────────────────────────────────────────
49
+
50
+ # Delivery 固定绑定:每个意图 → 一个广场 agent(按 squareAgent_id 直接克隆,
51
+ # 不再走"查广场列表 + 按名字匹配"链路,避免名字漂移。
52
+ # 不在此 map 中的意图(数据中新发现的 L1)→ 跳过 agent / 策略,标 SKIPPED。
53
+ # 固定 squareAgent_id 是首选来源,不是硬阻塞;复制失败时记录可恢复缺口。
54
+ DELIVERY_AGENT_BINDINGS = {
55
+ "Pre-Sales Inquiry": 37625,
56
+ "Invoices": 37627,
57
+ "Order Issues": 37631,
58
+ "Logistics Issues": 37633,
59
+ "Package issues": 37623,
60
+ "Product issues": 38114,
61
+ "Return and exchange": 37626,
62
+ }
63
+
64
+ # ── Data loading ───────────────────────────────────────────────────────────────
65
+
66
+ def load_results(state: dict) -> dict:
67
+ """Load classification results pointed to by state.results_path.
68
+
69
+ eager 模型下 workspace/outputs/ 跨轮路径稳定,state 里存的绝对路径下轮直接可用。
70
+ """
71
+ path = state.get("results_path")
72
+ if not path:
73
+ raise FileNotFoundError("state.results_path missing. Run classify.py first.")
74
+ if not os.path.exists(path):
75
+ raise FileNotFoundError(f"Results file missing: {path}")
76
+ with open(path) as f:
77
+ return json.load(f)
78
+
79
+
80
+ # ── Feature pre-flight checks ─────────────────────────────────────────────────
81
+
82
+ def check_and_enable_intent_feature(solvea: SolveaClient) -> None:
83
+ """创建意图前确保意图功能已开启(configurationWizard.intent=true)。
84
+
85
+ 查询:GET /api_v2/gpt/project/{projectId}/config → configurationWizard.intent
86
+ 开启:PUT /api_v2/gpt/project/{projectId} → body: {configurationWizard: {intent: true}}
87
+ """
88
+ resp = solvea._curl("GET", f"/api_v2/gpt/project/{solvea.project_id}/config")
89
+ payload = resp.get("data", resp) or {}
90
+ wizard = payload.get("configurationWizard") or {}
91
+ if wizard.get("intent"):
92
+ return
93
+ solvea._curl("PUT", f"/api_v2/gpt/project/{solvea.project_id}", {
94
+ "configurationWizard": {"intent": True}
95
+ })
96
+ print("[NOTICE] 该租户意图功能(Intent)原本未开启,已自动启用")
97
+
98
+
99
+ def check_and_enable_variable_feature(solvea: SolveaClient) -> None:
100
+ """复制 agent 前确保变量功能已初始化(isInitAiField=true)。
101
+
102
+ 查询:POST /api_v2/tars/api/field/manage/guide/record → isInitAiField
103
+ 开启:GET /api_v2/tars/api/field/manage/pre/install
104
+ """
105
+ resp = solvea._curl("POST", "/api_v2/tars/api/field/manage/guide/record")
106
+ payload = resp.get("data", resp) or {}
107
+ if payload.get("isInitAiField"):
108
+ return
109
+ solvea._curl("GET", "/api_v2/tars/api/field/manage/pre/install")
110
+ print("[NOTICE] 该租户变量功能(AI Field)原本未开启,已自动初始化预装字段")
111
+
112
+
113
+ # ── Marketplace agent lookup ───────────────────────────────────────────────────
114
+
115
+ def copy_marketplace_agent_by_id(solvea: SolveaClient, square_agent_id: int) -> str:
116
+ """直接按广场 agent ID 克隆到当前租户,返回新 agent_id。
117
+
118
+ Delivery 按固定 squareAgent_id 复制广场 agent,取代"查广场→按名字匹配"。
119
+ 链路,避免广场 agent 名字漂移。
120
+ """
121
+ result = solvea._curl("POST", "/api_v2/gpt/ap/copyAgent", {
122
+ "projectId": int(solvea.project_id),
123
+ "agentId": int(square_agent_id),
124
+ })
125
+ new_id = str(
126
+ result.get("id") or
127
+ result.get("agentId") or
128
+ result.get("data", {}).get("id", "")
129
+ )
130
+ if not new_id:
131
+ raise RuntimeError(f"copyAgent(square_agent_id={square_agent_id}) returned no id: {result}")
132
+ print(f"[copy_agent] square_id={square_agent_id} → new id={new_id}")
133
+ return new_id
134
+
135
+
136
+ # ── L1 intent creation ─────────────────────────────────────────────────────────
137
+
138
+ def _intent_items(result) -> list:
139
+ if isinstance(result, list):
140
+ return result
141
+ if isinstance(result, dict):
142
+ for key in ("data", "list", "records"):
143
+ if isinstance(result.get(key), list):
144
+ return result[key]
145
+ return []
146
+
147
+
148
+ def _find_existing_intent_by_name(solvea: SolveaClient, name: str) -> dict | None:
149
+ result = solvea.list_intents()
150
+ for item in _intent_items(result):
151
+ if isinstance(item, dict) and item.get("name") == name:
152
+ intent_id = item.get("id") or item.get("intentId")
153
+ if intent_id:
154
+ return item
155
+ return None
156
+
157
+
158
+ def create_l1_intent(
159
+ solvea: SolveaClient,
160
+ intent: dict,
161
+ dry_run: bool = False,
162
+ existing_by_name: dict | None = None,
163
+ ) -> dict | None:
164
+ name = intent["name"]
165
+ definition = intent.get("definition") or f"Customer messages related to {name}."
166
+ data = {
167
+ "name": name,
168
+ "description": definition,
169
+ "example": definition,
170
+ "status": "ENABLED",
171
+ }
172
+ if dry_run:
173
+ print(f"[dry-run] create_intent: {name}")
174
+ return None
175
+
176
+ existing = (existing_by_name or {}).get(name)
177
+ if existing:
178
+ intent_id = existing.get("id") or existing.get("intentId")
179
+ print(f"[create_intent] existing same-name intent found, reusing id={intent_id} name={name}")
180
+ return {"id": intent_id, **existing}
181
+
182
+ print(f"[create_intent] {name} ...")
183
+ try:
184
+ result = solvea.create_intent(data)
185
+ except RuntimeError as exc:
186
+ if "already exists" not in str(exc):
187
+ raise
188
+ existing = _find_existing_intent_by_name(solvea, name)
189
+ if not existing:
190
+ raise
191
+ intent_id = existing.get("id") or existing.get("intentId")
192
+ print(f"[create_intent] already exists, reusing id={intent_id} name={name}")
193
+ return {"id": intent_id, **existing}
194
+ intent_id = result.get("id") or result.get("intentId")
195
+ if not intent_id:
196
+ if "already exists" in str(result.get("errmsg", "")):
197
+ existing = _find_existing_intent_by_name(solvea, name)
198
+ if existing:
199
+ intent_id = existing.get("id") or existing.get("intentId")
200
+ print(f"[create_intent] already exists, reusing id={intent_id} name={name}")
201
+ result["id"] = intent_id
202
+ if not intent_id:
203
+ raise RuntimeError(f"create_intent returned no id for '{name}': {result}")
204
+ else:
205
+ print(f"[create_intent] OK → id={intent_id}")
206
+ return result
207
+
208
+
209
+ # ── Strategy creation ──────────────────────────────────────────────────────────
210
+
211
+ def create_strategy_for_intent(
212
+ solvea: SolveaClient,
213
+ intent_id: str,
214
+ agent_id: str,
215
+ intent_name: str,
216
+ dry_run: bool = False,
217
+ ) -> dict | None:
218
+ if dry_run:
219
+ print(f"[dry-run] create_strategy: intent={intent_id} → agent={agent_id}")
220
+ return None
221
+ data = {
222
+ "name": f"Route_{intent_name}",
223
+ "status": "ENABLED",
224
+ "priority": 0,
225
+ "trigger": "MESSAGE",
226
+ "condition": {
227
+ "and": [{
228
+ "rules": [{
229
+ "apiName": "__intent_ids",
230
+ "apiType": "TEXT",
231
+ "relation": "IN",
232
+ "type": "GENERIC",
233
+ "values": [int(intent_id)],
234
+ }]
235
+ }]
236
+ },
237
+ "action": {
238
+ "type": "AGENT",
239
+ "agentId": int(agent_id),
240
+ },
241
+ }
242
+ print(f"[create_strategy] Route_{intent_name} ...")
243
+ result = solvea.create_strategy(data)
244
+ strategy_id = result.get("id") or result.get("strategyId")
245
+ if not strategy_id:
246
+ raise RuntimeError(
247
+ f"create_strategy returned no id for '{intent_name}' "
248
+ f"(intent={intent_id}, agent={agent_id}): {result}"
249
+ )
250
+ print(f"[create_strategy] OK → id={strategy_id}")
251
+ return result
252
+
253
+
254
+ def create_strategy_for_intents(
255
+ solvea: SolveaClient,
256
+ intent_ids: list,
257
+ agent_id: str,
258
+ name: str,
259
+ priority: int = 0,
260
+ dry_run: bool = False,
261
+ ) -> dict | None:
262
+ """N 个意图 → 1 个 agent 的策略(rules.values 多值)。"""
263
+ if not intent_ids:
264
+ raise ValueError(f"create_strategy_for_intents '{name}': intent_ids is empty")
265
+ if dry_run:
266
+ print(f"[dry-run] create_strategy: intents={intent_ids} → agent={agent_id} (priority={priority})")
267
+ return None
268
+ data = {
269
+ "name": f"Route_{name}",
270
+ "status": "ENABLED",
271
+ "priority": priority,
272
+ "trigger": "MESSAGE",
273
+ "condition": {
274
+ "and": [{
275
+ "rules": [{
276
+ "apiName": "__intent_ids",
277
+ "apiType": "TEXT",
278
+ "relation": "IN",
279
+ "type": "GENERIC",
280
+ "values": [int(i) for i in intent_ids],
281
+ }]
282
+ }]
283
+ },
284
+ "action": {
285
+ "type": "AGENT",
286
+ "agentId": int(agent_id),
287
+ },
288
+ }
289
+ print(f"[create_strategy] Route_{name} (intents={intent_ids}, priority={priority}) ...")
290
+ result = solvea.create_strategy(data)
291
+ strategy_id = result.get("id") or result.get("strategyId")
292
+ if not strategy_id:
293
+ raise RuntimeError(
294
+ f"create_strategy returned no id for '{name}' "
295
+ f"(intents={intent_ids}, agent={agent_id}): {result}"
296
+ )
297
+ print(f"[create_strategy] OK → id={strategy_id}")
298
+ return result
299
+
300
+
301
+ # ── Update order tool X-Token ────────────────────────────────────────────────
302
+
303
+ def update_order_tool_x_token(solvea: SolveaClient, creation_log: list, dry_run: bool = False) -> None:
304
+ """After all agent copies, patch X-Token header in any order information tools found."""
305
+ TARGET_TOOLS = {
306
+ "new_Order_information_product",
307
+ "new_Order_information_FBA",
308
+ "new_Order_information_ship",
309
+ }
310
+ copied_agent_ids = [
311
+ item["agent_id"] for item in creation_log
312
+ if not item.get("skipped") and item.get("agent_id")
313
+ ]
314
+ if not copied_agent_ids:
315
+ return
316
+
317
+ raw = solvea.get_api_key()
318
+ if isinstance(raw, dict):
319
+ x_token = raw.get("apiKey") or raw.get("key") or raw.get("value") or raw.get("data") or str(raw)
320
+ else:
321
+ x_token = str(raw)
322
+
323
+ updated = 0
324
+ for agent_id in copied_agent_ids:
325
+ tools_resp = solvea._curl("GET", f"/api_v2/gpt/ap/agent/{agent_id}/tools", {})
326
+ for tool in (tools_resp if isinstance(tools_resp, list) else []):
327
+ if tool.get("name") not in TARGET_TOOLS:
328
+ continue
329
+ tool_id = tool["id"]
330
+ if dry_run:
331
+ print(f"[dry-run] would update X-Token for tool '{tool['name']}' (id={tool_id})")
332
+ continue
333
+ config = tool.get("config") or {}
334
+ headers = config.get("headers") or {}
335
+ headers["X-Token"] = x_token
336
+ config["headers"] = headers
337
+ solvea._curl("PUT", f"/api_v2/gpt/ap/tools/{tool_id}", {"config": config})
338
+ print(f"[x_token] updated '{tool['name']}' (tool_id={tool_id})")
339
+ updated += 1
340
+
341
+ if updated:
342
+ print(f"[x_token] {updated} tool(s) X-Token updated.")
343
+ elif not dry_run:
344
+ print("[x_token] No order information tools found — skipped.")
345
+
346
+
347
+ # ── Disable SYSTEM intents ────────────────────────────────────────────────────
348
+
349
+ def disable_system_intents(solvea: SolveaClient, keep_names: set, dry_run: bool = False) -> int:
350
+ """Disable SYSTEM-source intents whose name is not in keep_names."""
351
+ result = solvea.list_intents()
352
+ intents = result if isinstance(result, list) else result.get("data", result.get("list", []))
353
+ to_disable = [
354
+ i for i in intents
355
+ if i.get("source") == "SYSTEM"
356
+ and i.get("name") not in keep_names
357
+ and i.get("status") != "DISABLED"
358
+ ]
359
+ print(f"[disable_system] {len(to_disable)} SYSTEM intents to disable "
360
+ f"(keeping {len(keep_names)} by name match)")
361
+ for i in to_disable:
362
+ if dry_run:
363
+ print(f"[dry-run] disable intent: {i['name']} id={i['id']}")
364
+ else:
365
+ solvea.toggle_intent(i["id"], enabled=False)
366
+ print(f"[disable_system] disabled: {i['name']} id={i['id']}")
367
+ return len(to_disable)
368
+
369
+
370
+ def _load_agent_map(path: str) -> dict:
371
+ if not path or not os.path.exists(path):
372
+ raise FileNotFoundError(f"custom agent map not found: {path}")
373
+ with open(path, encoding="utf-8") as f:
374
+ data = json.load(f)
375
+ if isinstance(data, dict) and isinstance(data.get("intents"), dict):
376
+ data = data["intents"]
377
+ if not isinstance(data, dict):
378
+ raise ValueError("custom agent map must be a JSON object or {'intents': {...}}")
379
+ return {str(k): str(v) for k, v in data.items() if v not in (None, "")}
380
+
381
+
382
+ def _run_delivery_custom_agent_link(
383
+ solvea: SolveaClient,
384
+ log_path: str,
385
+ map_path: str,
386
+ dry_run: bool = False,
387
+ ) -> None:
388
+ """把用户确认的新建/替代 Agent 按 Delivery 意图写入策略。"""
389
+ if not log_path or not os.path.exists(log_path):
390
+ raise FileNotFoundError(
391
+ f"created_log not found (state.created_log_path={log_path}). "
392
+ "Run delivery creation first."
393
+ )
394
+ with open(log_path, encoding="utf-8") as f:
395
+ log_data = json.load(f)
396
+ if log_data.get("mode") != "delivery":
397
+ raise RuntimeError(f"created_log mode is {log_data.get('mode')!r}, expected 'delivery'.")
398
+
399
+ agent_map = _load_agent_map(map_path)
400
+ linked = []
401
+ for item in log_data.get("intents", []):
402
+ intent_name = item.get("intent_name")
403
+ if not intent_name or intent_name not in agent_map:
404
+ continue
405
+ intent_id = item.get("intent_id")
406
+ agent_id = agent_map[intent_name]
407
+ if not intent_id:
408
+ print(f"[delivery.custom] WARNING intent '{intent_name}' has no intent_id; skipped")
409
+ continue
410
+ print(f"[delivery.custom] Binding {intent_name}: intent={intent_id} → agent {agent_id}")
411
+ strategy_result = create_strategy_for_intent(
412
+ solvea,
413
+ str(intent_id),
414
+ str(agent_id),
415
+ intent_name,
416
+ dry_run=dry_run,
417
+ )
418
+ strategy_id = str(strategy_result.get("id")) if strategy_result else f"dry_strategy_{intent_name}"
419
+ item["agent_id"] = str(agent_id)
420
+ item["strategy_id"] = strategy_id
421
+ item["skipped"] = False
422
+ item["skip_reason"] = ""
423
+ item["recoverable"] = False
424
+ item["custom_agent"] = True
425
+ linked.append({"intent_name": intent_name, "agent_id": str(agent_id), "intent_id": intent_id, "strategy_id": strategy_id})
426
+
427
+ if not linked:
428
+ raise ValueError("custom agent map did not match any created Delivery intent name")
429
+
430
+ log_data["custom_agent_strategies"] = [*(log_data.get("custom_agent_strategies") or []), *linked]
431
+ log_data["next_action"] = "Continue knowledge-base setup and delivery regression test."
432
+ with open(log_path, "w", encoding="utf-8") as f:
433
+ json.dump(log_data, f, ensure_ascii=False, indent=2)
434
+
435
+ state = _read_state()
436
+ state.update({
437
+ "status": "created",
438
+ "mode": "delivery",
439
+ "created_log_path": log_path,
440
+ "updated_at": timestamp(),
441
+ })
442
+ _write_state(state)
443
+ print(f"\n[delivery.custom] Done. Linked {len(linked)} intent(s); log updated → {log_path}")
444
+
445
+
446
+ # ── Main ───────────────────────────────────────────────────────────────────────
447
+
448
+ def main():
449
+ parser = argparse.ArgumentParser(
450
+ description="Create intents + copy marketplace agents + strategy routing"
451
+ )
452
+ parser.add_argument("--mode", choices=["delivery"], default="delivery",
453
+ help="Delivery only: create all intents + update X-Token.")
454
+ parser.add_argument("--select", default=None,
455
+ help="DEPRECATED — ignored in all modes (all intents are now created).")
456
+ parser.add_argument("--dry-run", action="store_true")
457
+ parser.add_argument("--disable-system", action="store_true",
458
+ help="Disable active SYSTEM intents not matching created intent names")
459
+ parser.add_argument("--custom-agent-map", default=None,
460
+ help="Delivery recovery stage: JSON map of intent name to custom/existing agent id, "
461
+ "for example {'Pre-Sales Inquiry': 2642, 'Product issues': 2643}.")
462
+ x_token_grp = parser.add_mutually_exclusive_group()
463
+ x_token_grp.add_argument("--update-x-token", dest="update_x_token", action="store_true",
464
+ help="Force X-Token patch after copy (default for --mode delivery)")
465
+ x_token_grp.add_argument("--no-update-x-token", dest="update_x_token", action="store_false",
466
+ help="Skip X-Token patch")
467
+ parser.set_defaults(update_x_token=None)
468
+ args = parser.parse_args()
469
+
470
+ # ── Early args sanity ──────────────────────────────────────────────────────
471
+ if args.select:
472
+ print(f"[main] WARNING: --select is deprecated and ignored (mode={args.mode}). "
473
+ "All intents will be created.")
474
+
475
+ # Resolve update_x_token default by mode when caller did not set it explicitly
476
+ if args.update_x_token is None:
477
+ update_x_token = (args.mode == "delivery")
478
+ else:
479
+ update_x_token = args.update_x_token
480
+
481
+ state = _read_state()
482
+ if not state:
483
+ raise FileNotFoundError(
484
+ "No state found. Run classify.py first (state should contain run_id and results_path)."
485
+ )
486
+
487
+ run_id = state.get("run_id")
488
+ if not run_id:
489
+ raise ValueError("state.run_id missing. Re-run classify.py.")
490
+
491
+ # ── Disable-only mode ──────────────────────────────────────────────────────
492
+ if args.disable_system:
493
+ log_path = state.get("created_log_path")
494
+ if not log_path or not os.path.exists(log_path):
495
+ raise FileNotFoundError(
496
+ f"No creation log found (state.created_log_path={log_path}). "
497
+ "Run without --disable-system first."
498
+ )
499
+ with open(log_path) as f:
500
+ existing_log = json.load(f)
501
+ our_names = {item["intent_name"] for item in existing_log.get("intents", [])}
502
+ solvea = SolveaClient()
503
+ print(f"[main] Disable-only mode. project_id={solvea.project_id}")
504
+ disable_system_intents(solvea, keep_names=our_names, dry_run=args.dry_run)
505
+ return
506
+
507
+ if args.custom_agent_map:
508
+ solvea = SolveaClient()
509
+ if not args.dry_run:
510
+ print(f"[main] project_id={solvea.project_id}")
511
+ _run_delivery_custom_agent_link(
512
+ solvea,
513
+ log_path=state.get("created_log_path"),
514
+ map_path=args.custom_agent_map,
515
+ dry_run=args.dry_run,
516
+ )
517
+ return
518
+
519
+ results = load_results(state)
520
+ all_intents = results.get("intents", []) + results.get("new_intents", [])
521
+ print(f"[main] {len(all_intents)} intents loaded, run_id={run_id} (mode={args.mode})")
522
+
523
+ if not all_intents:
524
+ print("[main] No intents found. Run classify.py first.")
525
+ return
526
+
527
+ solvea = SolveaClient()
528
+ if not args.dry_run:
529
+ print(f"[main] project_id={solvea.project_id}")
530
+
531
+ # ── 补全 Delivery 固定 7 个意图 ─────────────────────────────────────────────
532
+ # 客户数据里如果没有某个固定意图的样本,classify.py 不会产生它;
533
+ # Delivery 要建全部 7 个标准意图,这里强制补入缺失项。
534
+ found_names = {i["name"] for i in all_intents}
535
+ for fixed_name in DELIVERY_AGENT_BINDINGS:
536
+ if fixed_name not in found_names:
537
+ print(f"[main] '{fixed_name}' missing from classification results → added from fixed taxonomy")
538
+ all_intents.append({"name": fixed_name, "definition": ""})
539
+
540
+ # ── Feature pre-flight ─────────────────────────────────────────────────────
541
+ if not args.dry_run:
542
+ check_and_enable_intent_feature(solvea) # 建意图前
543
+ check_and_enable_variable_feature(solvea) # copyAgent 前
544
+
545
+ # ── Delivery ───────────────────────────────────────────────────────────────
546
+ # 走固定 squareAgent_id 直接 copyAgent,不查广场 / 不按名字匹配。
547
+ # 不在 DELIVERY_AGENT_BINDINGS 中的意图(数据中新发现的 L1)→ 跳过 agent/策略。
548
+
549
+ creation_log = []
550
+ preset_agent_errors = []
551
+ existing_by_name = {}
552
+ if not args.dry_run:
553
+ existing_by_name = {
554
+ item.get("name"): item
555
+ for item in _intent_items(solvea.list_intents())
556
+ if isinstance(item, dict) and item.get("name") and (item.get("id") or item.get("intentId"))
557
+ }
558
+
559
+ for intent in all_intents:
560
+ name = intent["name"]
561
+ print(f"\n{'='*60}")
562
+ print(f"[main] Processing: {name}")
563
+
564
+ # 1. Create L1 intent
565
+ intent_result = create_l1_intent(
566
+ solvea,
567
+ intent,
568
+ dry_run=args.dry_run,
569
+ existing_by_name=existing_by_name,
570
+ )
571
+ intent_id = str(intent_result.get("id")) if intent_result else f"dry_{name}"
572
+
573
+ # 2. Resolve square_agent_id from fixed binding
574
+ square_id = DELIVERY_AGENT_BINDINGS.get(name)
575
+ if not square_id:
576
+ print(f"[main] No mapping for '{name}' in DELIVERY_AGENT_BINDINGS — skipping")
577
+ creation_log.append({
578
+ "intent_name": name, "intent_id": intent_id,
579
+ "agent_name": None, "agent_id": None, "strategy_id": None,
580
+ "skipped": True, "skip_reason": "no mapping in DELIVERY_AGENT_BINDINGS",
581
+ })
582
+ continue
583
+
584
+ # 3. Copy by squareAgent_id
585
+ agent_name = f"{name}【Delivery Version】"
586
+ if args.dry_run:
587
+ print(f"[dry-run] copy square_agent_id={square_id} ('{agent_name}')")
588
+ agent_id = f"dry_agent_{name}"
589
+ else:
590
+ try:
591
+ agent_id = copy_marketplace_agent_by_id(solvea, square_id)
592
+ except Exception as exc:
593
+ preset_agent_errors.append({
594
+ "intent_name": name,
595
+ "intent_id": intent_id,
596
+ "square_agent_id": square_id,
597
+ "agent_name": agent_name,
598
+ "error": str(exc),
599
+ })
600
+ print(
601
+ f"[main] PRESET_AGENT_MISSING intent={name} "
602
+ f"square_agent_id={square_id}: {exc}"
603
+ )
604
+ creation_log.append({
605
+ "intent_name": name, "intent_id": intent_id,
606
+ "agent_name": agent_name, "agent_id": None, "strategy_id": None,
607
+ "skipped": True, "skip_reason": "preset agent unavailable",
608
+ "recoverable": True,
609
+ "square_agent_id": square_id,
610
+ })
611
+ continue
612
+
613
+ # 4. Create strategy routing
614
+ strategy_result = create_strategy_for_intent(
615
+ solvea, intent_id, agent_id, name, dry_run=args.dry_run
616
+ )
617
+ strategy_id = str(strategy_result.get("id")) if strategy_result else f"dry_strategy_{name}"
618
+
619
+ creation_log.append({
620
+ "intent_name": name, "intent_id": intent_id,
621
+ "agent_name": agent_name, "agent_id": agent_id,
622
+ "strategy_id": strategy_id, "skipped": False,
623
+ })
624
+
625
+ # 4. Patch X-Token in order information tools (delivery default; togglable)
626
+ if update_x_token:
627
+ update_order_tool_x_token(solvea, creation_log, dry_run=args.dry_run)
628
+ else:
629
+ print("[x_token] skipped (--no-update-x-token)")
630
+
631
+ OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
632
+ log_path = str(OUTPUTS_DIR / f"created_{timestamp()}.json")
633
+ with open(log_path, "w", encoding="utf-8") as f:
634
+ json.dump({
635
+ "intents": creation_log,
636
+ "mode": args.mode,
637
+ "run_id": run_id,
638
+ "preset_agent_errors": preset_agent_errors,
639
+ "next_action": (
640
+ "For skipped preset-agent intents, ask the user whether to build new "
641
+ "CHAT_FLOW agents from scratch or use substitute existing agent ids."
642
+ if preset_agent_errors else ""
643
+ ),
644
+ }, f, ensure_ascii=False, indent=2)
645
+
646
+ status = "preset_agent_missing" if preset_agent_errors else "created"
647
+ state = _read_state()
648
+ state.update({
649
+ "status": status,
650
+ "mode": args.mode,
651
+ "created_log_path": log_path,
652
+ "preset_agent_errors": preset_agent_errors,
653
+ "updated_at": timestamp(),
654
+ })
655
+ _write_state(state)
656
+
657
+ # Report SYSTEM intents that could be disabled (user confirms separately)
658
+ if not args.dry_run:
659
+ our_names = {item["intent_name"] for item in creation_log}
660
+ result2 = solvea.list_intents()
661
+ intents2 = result2 if isinstance(result2, list) else result2.get("data", result2.get("list", []))
662
+ pending_disable = [
663
+ i for i in intents2
664
+ if i.get("source") == "SYSTEM"
665
+ and i.get("name") not in our_names
666
+ and i.get("status") != "DISABLED"
667
+ ]
668
+ if pending_disable:
669
+ print(f"\n[system_intents] {len(pending_disable)} SYSTEM intents can be disabled:")
670
+ for i in pending_disable:
671
+ print(f" id={i['id']} name={i['name']}")
672
+ print("[system_intents] Run with --disable-system to disable them.")
673
+ else:
674
+ print("\n[system_intents] No active SYSTEM intents to disable.")
675
+
676
+ created = [x for x in creation_log if not x.get("skipped")]
677
+ skipped = [x for x in creation_log if x.get("skipped")]
678
+ print(f"\n[main] Done. {len(created)} created, {len(skipped)} skipped.")
679
+ print(f"[main] Log → {log_path}")
680
+ if preset_agent_errors:
681
+ print("[main] Recoverable preset-agent gap detected.")
682
+ print("[main] Ask the user to choose one option for each skipped standard intent:")
683
+ print(" 1. Build a new CHAT_FLOW agent from scratch.")
684
+ print(" 2. Use an existing substitute agent id.")
685
+ print(f"\n[main] Summary:")
686
+ for item in creation_log:
687
+ if item.get("skipped"):
688
+ print(f" {item['intent_name']}: SKIPPED ({item.get('skip_reason', '')})")
689
+ else:
690
+ print(f" {item['intent_name']}: intent={item['intent_id']} "
691
+ f"agent={item['agent_id']} strategy={item['strategy_id']}")
692
+
693
+
694
+ if __name__ == "__main__":
695
+ main()