@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,219 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Import qa_plan into Solvea knowledge base as QA entries.
4
+
5
+ Two input modes:
6
+
7
+ 1. --qa-file <path> already-merged qa_plan_<ts>.json
8
+ 2. --from-dir <dir> merge all *.json files in <dir> (each is a QA list).
9
+ Useful when the agent wrote multiple small batches into
10
+ /workspace/tmp/kb-builder/qa_parts/ to avoid the LLM max_tokens
11
+ truncation that hits any single huge tool_call. The
12
+ merged plan is written to OUTPUTS_DIR/qa_plan_<ts>.json
13
+ for traceability before upload.
14
+
15
+ QA item format (each list entry):
16
+ [
17
+ {
18
+ "dir_path": ["Product Introduction", "Smart Plugs", "Kasa EP25"],
19
+ "question": "Kasa EP25 Smart Plug — What is the Kasa EP25 and what does it do?",
20
+ "answer": "The Kasa EP25 is ...\n\n![Product image](https://cdn.example.com/...)"
21
+ }
22
+ ]
23
+
24
+ Usage:
25
+ python3 import_qa.py --qa-file /workspace/outputs/kb-builder/qa_plan_<ts>.json
26
+ python3 import_qa.py --qa-file /workspace/outputs/kb-builder/qa_plan_<ts>.json --dry-run
27
+ python3 import_qa.py --from-dir /workspace/tmp/kb-builder/qa_parts/
28
+ python3 import_qa.py --from-dir /workspace/tmp/kb-builder/qa_parts/ --dry-run
29
+
30
+ SolveaClient() picks token / project_id / env from environment variables
31
+ (SOLVEA_TOKEN / SOLVEA_PROD_TOKEN / SOLVEA_PROJECT_ID / SOLVEA_ENV).
32
+ """
33
+ from __future__ import annotations
34
+ import argparse, json, os, sys
35
+ 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"))
36
+ from bootstrap import *
37
+ from pathlib import Path
38
+
39
+ def validate_qa_item(item: object, index: int, max_depth: int = 3) -> tuple[list[str], str, str]:
40
+ """Return normalized QA fields or raise a clear schema error."""
41
+ if not isinstance(item, dict):
42
+ raise ValueError(f"QA item #{index} must be an object, got {type(item).__name__}")
43
+
44
+ dir_path = item.get("dir_path")
45
+ question = item.get("question")
46
+ answer = item.get("answer")
47
+
48
+ if not isinstance(dir_path, list) or not dir_path:
49
+ raise ValueError(
50
+ f"QA item #{index} dir_path must be a non-empty list of folder names, "
51
+ f'e.g. ["Product Introduction", "Speakers", "B0117-03018-BK"]; '
52
+ f"got {dir_path!r}"
53
+ )
54
+ if not all(isinstance(name, str) and name.strip() for name in dir_path):
55
+ raise ValueError(f"QA item #{index} dir_path entries must be non-empty strings: {dir_path!r}")
56
+ if len(dir_path) > max_depth:
57
+ raise ValueError(f"QA item #{index} dir_path has {len(dir_path)} levels; maximum is {max_depth}")
58
+ if any("/" in name or "\\" in name for name in dir_path):
59
+ raise ValueError(
60
+ f"QA item #{index} dir_path entries must be folder names, not slash paths: {dir_path!r}"
61
+ )
62
+ if not isinstance(question, str) or not question.strip():
63
+ raise ValueError(f"QA item #{index} question must be a non-empty string")
64
+ if not isinstance(answer, str) or not answer.strip():
65
+ raise ValueError(f"QA item #{index} answer must be a non-empty markdown string")
66
+
67
+ return [name.strip() for name in dir_path], question.strip(), answer.strip()
68
+
69
+
70
+ def validate_plan(plan: object, max_depth: int = 3) -> list[tuple[list[str], str, str]]:
71
+ if not isinstance(plan, list):
72
+ raise ValueError(f"QA plan must be a list, got {type(plan).__name__}")
73
+ return [validate_qa_item(item, i + 1, max_depth=max_depth) for i, item in enumerate(plan)]
74
+
75
+
76
+ def flatten_folders(nodes: list, result: list | None = None) -> list:
77
+ """Recursively flatten a nested folder tree into a flat list."""
78
+ if result is None:
79
+ result = []
80
+ for node in nodes:
81
+ result.append(node)
82
+ children = node.get("children", [])
83
+ if children:
84
+ flatten_folders(children, result)
85
+ return result
86
+
87
+
88
+ def get_or_create_dir(client: SolveaClient, path: list[str], dir_cache: dict) -> str | int:
89
+ """Walk dir_path, creating missing folders. Returns final directory ID."""
90
+ parent_id = 0
91
+ current_path: list[str] = []
92
+ for name in path:
93
+ current_path.append(name)
94
+ key = "/".join(current_path)
95
+ if key in dir_cache:
96
+ parent_id = dir_cache[key]
97
+ continue
98
+ # Search in existing folders (API returns {"list": [nested tree]})
99
+ folders = client.list_directories()
100
+ if isinstance(folders, list):
101
+ all_folders = flatten_folders(folders)
102
+ else:
103
+ raw = folders.get("list") or folders.get("records") or folders.get("data") or []
104
+ all_folders = flatten_folders(raw)
105
+ match = next(
106
+ (f for f in all_folders
107
+ if f.get("name") == name and (f.get("parentId") or 0) == parent_id),
108
+ None,
109
+ )
110
+ if match:
111
+ fid = match["id"]
112
+ else:
113
+ result = client.create_folder(name, parent_id=parent_id)
114
+ fid = result.get("id") if isinstance(result, dict) else None
115
+ if not fid:
116
+ print(f" [warn] Could not create folder '{name}' under parentId={parent_id}: {result}", file=sys.stderr)
117
+ fid = parent_id # fallback: use parent
118
+ dir_cache[key] = fid
119
+ parent_id = fid
120
+ return parent_id
121
+
122
+
123
+ def merge_dir(src_dir: Path) -> tuple[list, Path]:
124
+ """Merge every *.json in src_dir (each must be a QA list) into one list,
125
+ then write the result to OUTPUTS_DIR/qa_plan_{ts}.json for traceability.
126
+
127
+ Files are merged in filename-sorted order so the result is deterministic.
128
+ Files whose top-level JSON isn't a list are skipped with a warning.
129
+ """
130
+ if not src_dir.exists() or not src_dir.is_dir():
131
+ raise FileNotFoundError(f"--from-dir not a directory: {src_dir}")
132
+
133
+ merged: list = []
134
+ parts = sorted(src_dir.glob("*.json"))
135
+ if not parts:
136
+ raise FileNotFoundError(f"No *.json found under {src_dir}")
137
+
138
+ for p in parts:
139
+ try:
140
+ data = json.loads(p.read_text(encoding="utf-8"))
141
+ except Exception as e:
142
+ print(f" [warn] skip {p.name}: {e}", file=sys.stderr)
143
+ continue
144
+ if not isinstance(data, list):
145
+ print(f" [warn] skip {p.name}: top-level is not a list", file=sys.stderr)
146
+ continue
147
+ print(f" [merge] {p.name}: +{len(data)} QA")
148
+ merged.extend(data)
149
+
150
+ OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
151
+ out = OUTPUTS_DIR / f"qa_plan_{timestamp()}.json"
152
+ out.write_text(json.dumps(merged, ensure_ascii=False, indent=2), encoding="utf-8")
153
+ print(f"[merge] {len(merged)} QA total -> {out}")
154
+ return merged, out
155
+
156
+
157
+ def run(qa_file: str | None, from_dir: str | None, dry_run: bool, max_depth: int = 3) -> None:
158
+ if from_dir:
159
+ plan, qa_file_path = merge_dir(Path(from_dir))
160
+ else:
161
+ qa_file_path = Path(qa_file)
162
+ plan = json.loads(qa_file_path.read_text(encoding="utf-8"))
163
+
164
+ normalized_plan = validate_plan(plan, max_depth=max_depth)
165
+
166
+ print(f"[import] {len(normalized_plan)} QA entries to import (dry_run={dry_run}) source={qa_file_path}")
167
+
168
+ dir_cache: dict[str, str | int] = {}
169
+ ok = 0
170
+ fail = 0
171
+ client = None
172
+ if not dry_run:
173
+ from client import SolveaClient
174
+ client = SolveaClient()
175
+
176
+ for i, (dir_path, question, answer) in enumerate(normalized_plan):
177
+ if dry_run:
178
+ print(f" [{i+1}] DRY dir={dir_path}")
179
+ print(f" Q: {question[:80]}")
180
+ print(f" A: {answer[:80]}...")
181
+ continue
182
+
183
+ try:
184
+ assert client is not None
185
+ dir_id = get_or_create_dir(client, dir_path, dir_cache) if dir_path else ""
186
+ client.upload_qa([{
187
+ "title": question,
188
+ "content": answer,
189
+ "directoryId": dir_id,
190
+ }])
191
+ ok += 1
192
+ print(f" [{i+1}/{len(normalized_plan)}] OK {'/'.join(dir_path)} | {question[:60]}")
193
+ except Exception as e:
194
+ fail += 1
195
+ print(f" [{i+1}/{len(normalized_plan)}] FAIL {question[:60]}: {e}")
196
+
197
+ if not dry_run:
198
+ print(f"\n[import] Done: {ok} OK, {fail} failed")
199
+
200
+
201
+ def main():
202
+ ap = argparse.ArgumentParser()
203
+ src = ap.add_mutually_exclusive_group(required=True)
204
+ src.add_argument("--qa-file", dest="qa_file",
205
+ help="Path to a pre-merged qa_plan_<ts>.json")
206
+ src.add_argument("--from-dir", dest="from_dir",
207
+ help="Merge all *.json under this directory (each = QA list); "
208
+ "merged result is written to OUTPUTS_DIR/qa_plan_<ts>.json")
209
+ ap.add_argument("--dry-run", action="store_true", dest="dry_run")
210
+ ap.add_argument("--max-depth", type=int, default=3,
211
+ help="Maximum QA directory depth (default: 3; override only when explicitly requested)")
212
+ args = ap.parse_args()
213
+ if args.max_depth < 1:
214
+ ap.error("--max-depth must be positive")
215
+ run(args.qa_file, args.from_dir, args.dry_run, args.max_depth)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """Import URL/Word/PDF sources as DOCUMENT knowledge.
3
+
4
+ Input is a JSON list of records:
5
+ {"source": "https://...", "dir_path": ["一级", "二级", "三级"]}
6
+ {"source": "/mounted/chat-files/manual.docx", "dir_path": ["..."]}
7
+
8
+ The default is a dry-run. URL duplicates in the input are reported as
9
+ ``skipped_duplicate``; the API response is retained because the backend may
10
+ also deduplicate URLs already present in the knowledge base.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ sys.path.insert(0, os.path.join(os.environ.get("SOLVEA_LIB_DIR", os.path.abspath(
21
+ os.path.join(os.path.dirname(__file__), "..", "..", "__lib__"))), "skill-runtime"))
22
+ from bootstrap import * # noqa: F403
23
+
24
+
25
+ def validate_record(item, index, max_depth=3):
26
+ if not isinstance(item, dict) or not isinstance(item.get("source"), str) or not item["source"].strip():
27
+ raise ValueError(f"source document #{index} needs a non-empty source")
28
+ path = item.get("dir_path", [])
29
+ if not isinstance(path, list) or any(not isinstance(x, str) or not x.strip() for x in path):
30
+ raise ValueError(f"source document #{index} dir_path must be a list of non-empty names")
31
+ if len(path) > max_depth:
32
+ raise ValueError(f"source document #{index} dir_path has {len(path)} levels; maximum is {max_depth}")
33
+ if any("/" in x or "\\" in x for x in path):
34
+ raise ValueError(f"source document #{index} dir_path entries cannot contain slashes")
35
+ source = item["source"].strip()
36
+ is_url = source.lower().startswith(("http://", "https://"))
37
+ if not is_url and not Path(source).is_file():
38
+ raise ValueError(f"source document #{index} file not found: {source}")
39
+ if not is_url and Path(source).suffix.lower() not in {".doc", ".docx", ".pdf"}:
40
+ raise ValueError(f"source document #{index} supports .doc/.docx/.pdf, got {Path(source).suffix}")
41
+ return {"source": source, "dir_path": [x.strip() for x in path], "is_url": is_url}
42
+
43
+
44
+ def validate_plan(data, max_depth=3):
45
+ if not isinstance(data, list):
46
+ raise ValueError("source document plan must be a JSON list")
47
+ normalized = [validate_record(item, i + 1, max_depth) for i, item in enumerate(data)]
48
+ seen = set()
49
+ for item in normalized:
50
+ if item["is_url"]:
51
+ item["status"] = "skipped_duplicate" if item["source"] in seen else "pending"
52
+ seen.add(item["source"])
53
+ else:
54
+ item["status"] = "pending"
55
+ return normalized
56
+
57
+
58
+ def _flatten(nodes, out=None):
59
+ out = [] if out is None else out
60
+ for node in nodes:
61
+ out.append(node)
62
+ _flatten(node.get("children", []), out)
63
+ return out
64
+
65
+
66
+ def resolve_dir(client, path, cache):
67
+ parent = 0
68
+ prefix = []
69
+ for name in path:
70
+ prefix.append(name)
71
+ key = "/".join(prefix)
72
+ if key in cache:
73
+ parent = cache[key]
74
+ continue
75
+ raw = client.list_directories()
76
+ nodes = raw if isinstance(raw, list) else raw.get("list", raw.get("records", raw.get("data", [])))
77
+ match = next((x for x in _flatten(nodes) if x.get("name") == name and (x.get("parentId") or 0) == parent), None)
78
+ if match:
79
+ parent = match["id"]
80
+ else:
81
+ result = client.create_folder(name, parent_id=parent)
82
+ parent = result.get("id") if isinstance(result, dict) else None
83
+ if not parent:
84
+ raise RuntimeError(f"could not create directory {key!r}: {result}")
85
+ cache[key] = parent
86
+ return parent
87
+
88
+
89
+ def run(plan_file, dry_run=True, max_depth=3):
90
+ data = json.loads(Path(plan_file).read_text(encoding="utf-8"))
91
+ plan = validate_plan(data, max_depth)
92
+ print(f"[source-documents] {len(plan)} records (dry_run={dry_run})")
93
+ for item in plan:
94
+ print(f" [{item['status']}] {item['source']} -> {'/'.join(item['dir_path']) or '(root)'}")
95
+ if dry_run:
96
+ return plan
97
+
98
+ from client import SolveaClient
99
+ client = SolveaClient()
100
+ cache = {}
101
+ results = []
102
+ for item in plan:
103
+ if item["status"] == "skipped_duplicate":
104
+ results.append({**item, "result": None})
105
+ continue
106
+ try:
107
+ directory_id = resolve_dir(client, item["dir_path"], cache)
108
+ if item["is_url"]:
109
+ documents = [{"filePath": item["source"], "docType": "TC", "fileType": "website"}]
110
+ response = client.create_documents(documents, directory_id=directory_id, task_type="WEB")
111
+ else:
112
+ key = client.upload_temp_file(item["source"], for_knowledge=True)
113
+ # The file-document endpoint derives the loader from the uploaded
114
+ # object key; unlike WEBSITE, it does not require fileType.
115
+ documents = [{"filePath": key, "docType": "TC"}]
116
+ response = client.create_documents(documents, directory_id=directory_id, task_type="FILE")
117
+ if not response:
118
+ raise RuntimeError("document API returned no created or refreshed record")
119
+ results.append({**item, "status": "submitted", "directory_id": directory_id, "result": response})
120
+ print(f" [submitted] {item['source']}")
121
+ except Exception as exc:
122
+ results.append({**item, "status": "failed", "error": str(exc)})
123
+ print(f" [failed] {item['source']}: {exc}", file=sys.stderr)
124
+ out = OUTPUTS_DIR / f"source_document_import_{timestamp()}.json"
125
+ out.parent.mkdir(parents=True, exist_ok=True)
126
+ out.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
127
+ print(f"[source-documents] result={out}")
128
+ return results
129
+
130
+
131
+ def main():
132
+ ap = argparse.ArgumentParser()
133
+ ap.add_argument("--plan-file", required=True)
134
+ ap.add_argument("--execute", action="store_true", help="Submit documents after explicit user confirmation")
135
+ ap.add_argument("--max-depth", type=int, default=3)
136
+ args = ap.parse_args()
137
+ if args.max_depth < 1:
138
+ ap.error("--max-depth must be positive")
139
+ run(args.plan_file, dry_run=not args.execute, max_depth=args.max_depth)
140
+
141
+
142
+ if __name__ == "__main__":
143
+ main()