@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,333 @@
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()
@@ -0,0 +1,144 @@
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()