@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,333 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Batch wrapper for extract_ticket_qa.py.
4
-
5
- Reads one xlsx, processes it page by page, stores page artifacts in state, then
6
- merges and de-duplicates into a single output JSON. It never uploads knowledge.
7
- """
8
- from __future__ import annotations
9
-
10
- import argparse
11
- import json
12
- import math
13
- import os
14
- import re
15
- import sys
16
- from pathlib import Path
17
-
18
- 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"))
19
- from bootstrap import *
20
-
21
- from openai import OpenAI
22
- import pandas as pd
23
-
24
- from extract_ticket_qa import (
25
- MAX_BATCH_SIZE,
26
- MAX_PAGE_SIZE,
27
- MAX_WORKERS,
28
- build_thread,
29
- chunked,
30
- extract_batch,
31
- resolve_columns,
32
- select_rows,
33
- )
34
-
35
-
36
- STATE_ROOT = STATE_DIR / "ticket_extract"
37
- PAGES_DIR = STATE_ROOT / "pages"
38
- PROGRESS_FILE = STATE_ROOT / "progress.json"
39
-
40
- PRE_FILTER_PATTERNS = [
41
- r"\btracking\b",
42
- r"\bdelivery\b",
43
- r"\bshipping\b",
44
- r"\border status\b",
45
- r"\brefund\b",
46
- r"\breturn\b",
47
- r"\bexchange\b",
48
- r"\bpayment\b",
49
- r"\binvoice\b",
50
- r"\baccount\b",
51
- r"\bplease send (a )?(photo|picture|image|receipt|order number)",
52
- r"\bcontact (customer )?(support|service)\b",
53
- r"\bsubmit (a )?(ticket|case)\b",
54
- r"\bbroken on arrival\b",
55
- r"\bdamaged on arrival\b",
56
- ]
57
-
58
- POST_FILTER_PATTERNS = [
59
- r"contact (customer )?(support|service)",
60
- r"submit (a )?(ticket|case)",
61
- r"send (a )?(photo|picture|image|receipt|order number)",
62
- r"we will (issue|send|process)",
63
- ]
64
-
65
-
66
- def normalize_question(question: str) -> str:
67
- text = question.lower()
68
- text = re.sub(r"[^a-z0-9]+", " ", text)
69
- return re.sub(r"\s+", " ", text).strip()
70
-
71
-
72
- def should_pre_filter(thread: str) -> bool:
73
- lowered = thread.lower()
74
- return any(re.search(pattern, lowered) for pattern in PRE_FILTER_PATTERNS)
75
-
76
-
77
- def should_post_filter(item: dict) -> bool:
78
- answer = str(item.get("answer", "")).lower()
79
- return any(re.search(pattern, answer) for pattern in POST_FILTER_PATTERNS)
80
-
81
-
82
- def merge_and_dedupe(page_files: list[Path]) -> tuple[list[dict], dict]:
83
- merged: list[dict] = []
84
- seen: set[str] = set()
85
- stats = {"input": 0, "post_filtered": 0, "deduped": 0}
86
-
87
- for page_file in page_files:
88
- data = json.loads(page_file.read_text(encoding="utf-8"))
89
- for item in data.get("qa_pairs", []):
90
- stats["input"] += 1
91
- if should_post_filter(item):
92
- stats["post_filtered"] += 1
93
- continue
94
- key = normalize_question(str(item.get("question", "")))
95
- if not key:
96
- continue
97
- if key in seen:
98
- stats["deduped"] += 1
99
- continue
100
- seen.add(key)
101
- merged.append(item)
102
-
103
- merged.sort(key=lambda item: (item.get("source_page") or 0, item.get("source_row") or 0))
104
- stats["output"] = len(merged)
105
- return merged, stats
106
-
107
-
108
- def _page_file(page: int) -> Path:
109
- return PAGES_DIR / f"page_{page:03d}.json"
110
-
111
-
112
- def _write_json(path: Path, data: dict | list) -> None:
113
- path.parent.mkdir(parents=True, exist_ok=True)
114
- path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
115
-
116
-
117
- def process_page(
118
- *,
119
- df: pd.DataFrame,
120
- page: int,
121
- page_size: int,
122
- brand: str,
123
- messages_col: str | None,
124
- title_col: str | None,
125
- reply_col: str | None,
126
- batch_size: int,
127
- workers: int,
128
- client,
129
- ) -> dict:
130
- page_df, window = select_rows(df, page=page, page_size=page_size)
131
- columns = resolve_columns(page_df, messages_col=messages_col, title_col=title_col, reply_col=reply_col)
132
-
133
- items: list[dict] = []
134
- pre_filtered = 0
135
- empty = 0
136
- for source_row, (_, row) in enumerate(page_df.iterrows(), start=window.start_row):
137
- thread = build_thread(row, columns)
138
- if not thread:
139
- empty += 1
140
- continue
141
- if should_pre_filter(thread):
142
- pre_filtered += 1
143
- continue
144
- items.append({"source_row": source_row, "thread": thread})
145
-
146
- qa_pairs: list[dict] = []
147
- skipped = 0
148
- failed = 0
149
- batches = list(chunked(items, batch_size))
150
-
151
- # Keep dependency local so tests can import this module without invoking model code.
152
- from concurrent.futures import ThreadPoolExecutor, as_completed
153
-
154
- with ThreadPoolExecutor(max_workers=workers) as pool:
155
- futures = {pool.submit(extract_batch, client, batch, brand): len(batch) for batch in batches}
156
- for future in as_completed(futures):
157
- results = future.result()
158
- if not results:
159
- failed += futures[future]
160
- continue
161
- for result in results:
162
- if result.get("skip_reason"):
163
- skipped += 1
164
- continue
165
- question = str(result.get("question", "")).strip()
166
- answer = str(result.get("answer", "")).strip()
167
- if not question or not answer or len(answer) <= 20:
168
- failed += 1
169
- continue
170
- qa_pairs.append({
171
- "source_page": page,
172
- "source_row": result.get("source_row"),
173
- "topic": result.get("topic", "troubleshooting"),
174
- "question": question,
175
- "answer": answer,
176
- "dir_path": [],
177
- })
178
-
179
- qa_pairs.sort(key=lambda item: item.get("source_row") or 0)
180
- return {
181
- "page": page,
182
- "rows": {"start": window.start_row, "end": window.end_row, "total": window.total_rows},
183
- "columns": {
184
- "mode": columns.mode,
185
- "messages_col": columns.messages_col,
186
- "title_col": columns.title_col,
187
- "reply_col": columns.reply_col,
188
- "fallback_col": columns.fallback_col,
189
- },
190
- "stats": {
191
- "page_rows": len(page_df),
192
- "valid_threads": len(items),
193
- "pre_filtered": pre_filtered,
194
- "empty": empty,
195
- "model_calls": len(batches),
196
- "extracted": len(qa_pairs),
197
- "skipped": skipped,
198
- "failed": failed,
199
- },
200
- "qa_pairs": qa_pairs,
201
- }
202
-
203
-
204
- def run(args) -> None:
205
- if args.page_size > MAX_PAGE_SIZE:
206
- raise ValueError(f"page-size {args.page_size} exceeds maximum {MAX_PAGE_SIZE}")
207
- if args.batch_size > MAX_BATCH_SIZE:
208
- raise ValueError(f"batch-size {args.batch_size} exceeds maximum {MAX_BATCH_SIZE}")
209
- if args.workers < 1 or args.workers > MAX_WORKERS:
210
- raise ValueError(f"workers must be 1-{MAX_WORKERS}, got {args.workers}")
211
-
212
- print(f"[batch_extract] Reading {args.xlsx}")
213
- df = pd.read_excel(args.xlsx)
214
- total_rows = len(df)
215
- total_pages = math.ceil(total_rows / args.page_size) if total_rows else 0
216
- print(f"[batch_extract] rows={total_rows} pages={total_pages} page-size={args.page_size}")
217
-
218
- STATE_ROOT.mkdir(parents=True, exist_ok=True)
219
- PAGES_DIR.mkdir(parents=True, exist_ok=True)
220
-
221
- # Sandbox injects OPENAI_BASE_URL + dummy OPENAI_API_KEY for the internal proxy.
222
- client = OpenAI()
223
- done_pages: list[int] = []
224
- failed_pages: list[int] = []
225
-
226
- for page in range(1, total_pages + 1):
227
- out_file = _page_file(page)
228
- if args.resume and out_file.exists():
229
- print(f"[batch_extract] page {page}/{total_pages} exists, skip")
230
- done_pages.append(page)
231
- continue
232
- try:
233
- result = process_page(
234
- df=df,
235
- page=page,
236
- page_size=args.page_size,
237
- brand=args.brand,
238
- messages_col=args.messages_col,
239
- title_col=args.title_col,
240
- reply_col=args.reply_col,
241
- batch_size=args.batch_size,
242
- workers=args.workers,
243
- client=client,
244
- )
245
- _write_json(out_file, result)
246
- done_pages.append(page)
247
- print(f"[batch_extract] page {page}/{total_pages} done -> {out_file}")
248
- except Exception as exc:
249
- failed_pages.append(page)
250
- print(f"[batch_extract] page {page}/{total_pages} failed: {exc}", file=sys.stderr)
251
-
252
- _write_json(PROGRESS_FILE, {
253
- "status": "running" if failed_pages else "extracting",
254
- "xlsx": args.xlsx,
255
- "brand": args.brand,
256
- "total_rows": total_rows,
257
- "total_pages": total_pages,
258
- "page_size": args.page_size,
259
- "batch_size": args.batch_size,
260
- "workers": args.workers,
261
- "messages_col": args.messages_col,
262
- "title_col": args.title_col,
263
- "reply_col": args.reply_col,
264
- "done_pages": done_pages,
265
- "failed_pages": failed_pages,
266
- })
267
-
268
- if failed_pages:
269
- _write_json(PROGRESS_FILE, {
270
- "status": "failed",
271
- "xlsx": args.xlsx,
272
- "brand": args.brand,
273
- "total_rows": total_rows,
274
- "total_pages": total_pages,
275
- "done_pages": done_pages,
276
- "failed_pages": failed_pages,
277
- })
278
- raise RuntimeError(f"QA extraction failed pages={failed_pages}; no merged output was generated")
279
-
280
- page_files = [_page_file(page) for page in range(1, total_pages + 1) if _page_file(page).exists()]
281
- merged, merge_stats = merge_and_dedupe(page_files)
282
-
283
- OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
284
- out_path = OUTPUTS_DIR / f"ticket_qa_merged_{timestamp()}.json"
285
- _write_json(out_path, merged)
286
-
287
- final_progress = {
288
- "status": "completed_with_errors" if failed_pages else "completed",
289
- "xlsx": args.xlsx,
290
- "brand": args.brand,
291
- "total_rows": total_rows,
292
- "total_pages": total_pages,
293
- "done_pages": done_pages,
294
- "failed_pages": failed_pages,
295
- "page_files": [str(path) for path in page_files],
296
- "merged_output": str(out_path),
297
- "merge_stats": merge_stats,
298
- }
299
- _write_json(PROGRESS_FILE, final_progress)
300
-
301
- print(f"[batch_extract] merged -> {out_path}")
302
- print(
303
- "[batch_extract] stats "
304
- f"input={merge_stats['input']} post_filtered={merge_stats['post_filtered']} "
305
- f"deduped={merge_stats['deduped']} output={merge_stats['output']}"
306
- )
307
- print("[batch_extract] Not uploaded. Review merged output, then run import_qa.py dry-run after user confirmation.")
308
-
309
-
310
- def main() -> None:
311
- parser = argparse.ArgumentParser()
312
- parser.add_argument("--xlsx", required=True)
313
- parser.add_argument("--brand", required=True)
314
- parser.add_argument("--messages-col", default=None)
315
- parser.add_argument("--title-col", default=None)
316
- parser.add_argument("--reply-col", default=None)
317
- parser.add_argument("--page-size", type=int, default=100, dest="page_size")
318
- parser.add_argument("--batch-size", type=int, default=5, dest="batch_size")
319
- parser.add_argument("--workers", type=int, default=3)
320
- parser.add_argument("--resume", action="store_true")
321
- args = parser.parse_args()
322
- try:
323
- run(args)
324
- except ValueError as exc:
325
- print(f"[error] {exc}", file=sys.stderr)
326
- sys.exit(2)
327
- except RuntimeError as exc:
328
- print(f"[error] {exc}", file=sys.stderr)
329
- sys.exit(1)
330
-
331
-
332
- if __name__ == "__main__":
333
- main()
@@ -1,144 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Build a KB directory plan from crawl_result JSON."""
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import os
8
- import re
9
- import sys
10
- from pathlib import Path
11
- from urllib.parse import urlparse
12
-
13
- 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"))
14
- from bootstrap import * # noqa: F401,F403
15
-
16
-
17
- def resolve_workspace_path(path: str | Path) -> Path:
18
- p = Path(path)
19
- if p.is_absolute() and str(p).startswith("/workspace"):
20
- return WORKSPACE / p.relative_to("/workspace")
21
- return p
22
-
23
-
24
- def virtual_workspace_path(path: Path) -> str:
25
- try:
26
- rel = path.resolve().relative_to(WORKSPACE.resolve())
27
- return str(Path("/workspace") / rel)
28
- except Exception:
29
- return str(path)
30
-
31
-
32
- def clean_name(value: str, fallback: str) -> str:
33
- value = re.sub(r"\s+", " ", (value or "")).strip()
34
- value = re.sub(r"[<>\"{}|\\^`]", "", value).strip()
35
- return value[:80] or fallback
36
-
37
-
38
- def policy_name(page: dict) -> str:
39
- hay = f"{page.get('url', '')} {page.get('title', '')}".lower()
40
- if "warranty" in hay or "guarantee" in hay:
41
- return "Warranty Policy"
42
- if "return" in hay or "refund" in hay:
43
- return "Return Policy"
44
- if "shipping" in hay or "delivery" in hay:
45
- return "Shipping Policy"
46
- if "privacy" in hay:
47
- return "Privacy Policy"
48
- if "terms" in hay:
49
- return "Terms Policy"
50
- return "Brand Policy"
51
-
52
-
53
- def infer_product_group(page: dict) -> str:
54
- path = urlparse(page.get("url", "")).path.strip("/")
55
- parts = [p for p in path.split("/") if p]
56
- for marker in ("collections", "collection", "category", "categories"):
57
- if marker in parts:
58
- idx = parts.index(marker)
59
- if idx + 1 < len(parts):
60
- return clean_name(parts[idx + 1].replace("-", " ").title(), "Products")
61
- return "Products"
62
-
63
-
64
- def build_tree(crawl: dict) -> list[dict]:
65
- pages = crawl.get("pages") or []
66
- products = [p for p in pages if p.get("type") == "product"]
67
- policies = [p for p in pages if p.get("type") == "policy"]
68
- help_pages = [p for p in pages if p.get("type") == "help_center"]
69
-
70
- product_groups: dict[str, list[dict]] = {}
71
- for page in products:
72
- product_groups.setdefault(infer_product_group(page), []).append(page)
73
-
74
- product_children = []
75
- for group, group_pages in sorted(product_groups.items()):
76
- product_children.append({
77
- "name": group,
78
- "children": [
79
- {"name": clean_name(p.get("title") or urlparse(p.get("url", "")).path.rsplit("/", 1)[-1], "Product")}
80
- for p in group_pages[:80]
81
- ],
82
- })
83
-
84
- policy_names = sorted({policy_name(p) for p in policies})
85
- tree = [
86
- {
87
- "name": "Product Introduction",
88
- "children": product_children or [{"name": "Products", "children": []}],
89
- },
90
- {
91
- "name": "Brand Policy",
92
- "children": [{"name": name} for name in (policy_names or ["Brand Policy"])],
93
- },
94
- ]
95
-
96
- if help_pages:
97
- tree.append({
98
- "name": "Help Center",
99
- "children": [{"name": "Help Articles"}],
100
- })
101
-
102
- tree.append({"name": "Pending Assignment", "children": []})
103
- return tree
104
-
105
-
106
- def main() -> None:
107
- parser = argparse.ArgumentParser(description="Build kb-builder directory plan from crawl result")
108
- parser.add_argument("--crawl-result", required=True)
109
- parser.add_argument("--output", default="")
110
- parser.add_argument("--write-confirmed-tree", action="store_true",
111
- help="Also write /workspace/state/kb-builder/confirmed_tree.json after user confirmation")
112
- args = parser.parse_args()
113
-
114
- crawl_path = resolve_workspace_path(args.crawl_result)
115
- crawl = json.loads(crawl_path.read_text(encoding="utf-8"))
116
- tree = build_tree(crawl)
117
- plan = {
118
- "base_url": crawl.get("base_url", ""),
119
- "source_crawl_result": args.crawl_result,
120
- "tree": tree,
121
- "stats": {
122
- "pages": len(crawl.get("pages") or []),
123
- "product": sum(1 for p in crawl.get("pages") or [] if p.get("type") == "product"),
124
- "policy": sum(1 for p in crawl.get("pages") or [] if p.get("type") == "policy"),
125
- "help_center": sum(1 for p in crawl.get("pages") or [] if p.get("type") == "help_center"),
126
- },
127
- }
128
-
129
- OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
130
- out = resolve_workspace_path(args.output) if args.output else OUTPUTS_DIR / f"directory_plan_{timestamp()}.json"
131
- out.parent.mkdir(parents=True, exist_ok=True)
132
- out.write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8")
133
-
134
- if args.write_confirmed_tree:
135
- confirmed = STATE_DIR / "confirmed_tree.json"
136
- confirmed.parent.mkdir(parents=True, exist_ok=True)
137
- confirmed.write_text(json.dumps(tree, ensure_ascii=False, indent=2), encoding="utf-8")
138
- print(f"[build_directory_plan] confirmed_tree={virtual_workspace_path(confirmed)}")
139
-
140
- print(f"[build_directory_plan] output={virtual_workspace_path(out)} root_nodes={len(tree)} pages={plan['stats']['pages']}")
141
-
142
-
143
- if __name__ == "__main__":
144
- main()