@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,441 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- extract_ticket_qa.py — Extract clean QA knowledge from historical ticket xlsx.
4
-
5
- Supports two xlsx formats:
6
- 1. Solvea inbox export: 'conversations' column with numbered dialog
7
- 2. Custom system export: auto-detects conversation columns by header name/content
8
-
9
- Output: workspace/outputs/kb-builder/ticket_qa_{ts}.json
10
- each item: {topic, question, answer, dir_path:[]}
11
- (agent assigns dir_path based on directory tree + topic)
12
-
13
- Usage:
14
- python3 extract_ticket_qa.py --xlsx mounted/chat-files/user/tickets.xlsx --brand "Anker" --page 1 --page-size 100 --batch-size 5
15
- python3 extract_ticket_qa.py --xlsx mounted/chat-files/user/tickets.xlsx --brand "Anker" --messages-col conversations --title-col subject
16
- python3 extract_ticket_qa.py --xlsx mounted/chat-files/user/tickets.xlsx --brand "Anker" --dry-run
17
- """
18
- from __future__ import annotations
19
- import argparse, json, os, sys, time
20
- from dataclasses import dataclass
21
- from concurrent.futures import ThreadPoolExecutor, as_completed
22
- 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"))
23
- from bootstrap import *
24
-
25
- import pandas as pd
26
- from openai import OpenAI
27
-
28
- MAX_PAGE_SIZE = 100
29
- MAX_BATCH_SIZE = 10
30
- MAX_WORKERS = 4
31
- SERVICE_MANAGED_MODEL = "server-managed"
32
-
33
-
34
- @dataclass(frozen=True)
35
- class PageWindow:
36
- page: int
37
- page_size: int
38
- start_row: int
39
- end_row: int
40
- total_rows: int
41
-
42
-
43
- @dataclass(frozen=True)
44
- class ColumnSelection:
45
- mode: str
46
- messages_col: str | None = None
47
- title_col: str | None = None
48
- reply_col: str | None = None
49
- fallback_col: str | None = None
50
-
51
-
52
- def select_rows(df: pd.DataFrame, page: int, page_size: int) -> tuple[pd.DataFrame, PageWindow]:
53
- if page < 1:
54
- raise ValueError(f"page must be >= 1, got {page}")
55
- if page_size < 1:
56
- raise ValueError(f"page-size must be >= 1, got {page_size}")
57
- if page_size > MAX_PAGE_SIZE:
58
- raise ValueError(f"page-size {page_size} exceeds maximum {MAX_PAGE_SIZE}")
59
-
60
- total_rows = len(df)
61
- start_idx = (page - 1) * page_size
62
- end_idx = min(start_idx + page_size, total_rows)
63
- selected = df.iloc[start_idx:end_idx].copy()
64
- start_row = start_idx + 1 if start_idx < total_rows else total_rows + 1
65
- window = PageWindow(
66
- page=page,
67
- page_size=page_size,
68
- start_row=start_row,
69
- end_row=end_idx,
70
- total_rows=total_rows,
71
- )
72
- return selected, window
73
-
74
- EXTRACT_BATCH_PROMPT = """\
75
- You are a knowledge base engineer extracting reusable product knowledge from a customer support conversation for {brand}.
76
-
77
- CONVERSATIONS:
78
- {items_json}
79
-
80
- For EACH conversation, extract zero or one clean Q&A pair following these strict rules.
81
-
82
- QUESTION rules:
83
- - Rewrite the customer's core issue as a generic, searchable question
84
- - Remove ALL personal info (names, order numbers, addresses, phone numbers, dates)
85
- - Make it product-centric and reusable (e.g. "Anker 737 PowerBank — Why does it stop charging at 80%?")
86
- - Format: "[Brand Product Name] — [question]"
87
-
88
- ANSWER rules:
89
- - Output as **markdown** (Solvea KB renders markdown end-to-end):
90
- * Bullet lists: `- item`
91
- * Ordered steps: `1. ... 2. ... 3. ...` for step-by-step procedures
92
- * Bold key terms: `**term**` for product names, settings, key parameters
93
- * Tables `| col | col |` for spec/comparison data when applicable
94
- * Code/values: `` `value` `` for short code-like tokens (model numbers, paths)
95
- - Extract ONLY reusable product knowledge: steps, specs, solutions, tips
96
- - REMOVE: order-specific details, "contact support" instructions, apologies, case-specific info, customer names
97
- - REMOVE: any instruction that requires agent action (e.g. "we will issue a refund")
98
- - Keep: troubleshooting steps, how-to instructions, compatibility info, product specs
99
-
100
- TOPIC: classify into exactly one of:
101
- product_info | setup_installation | usage_guide | troubleshooting | compatibility | warranty_maintenance
102
-
103
- SKIP_REASON: If this conversation is ENTIRELY about one of these — set skip_reason:
104
- logistics | shipping | delivery | tracking | order_status | order_cancel | order_modify |
105
- return | refund | exchange | account | payment | billing
106
- Otherwise set to null.
107
-
108
- Respond with JSON only, no markdown wrapper. Return an array with one object per input item:
109
- [
110
- {{"source_row": 1, "question": "...", "answer": "...", "topic": "...", "skip_reason": null}},
111
- {{"source_row": 2, "question": "", "answer": "", "topic": "", "skip_reason": "logistics"}}
112
- ]
113
-
114
- If conversation should be skipped:
115
- {{"source_row": <same source_row>, "question": "", "answer": "", "topic": "", "skip_reason": "logistics"}}"""
116
-
117
-
118
- def _require_column(df: pd.DataFrame, col: str | None, arg_name: str) -> str | None:
119
- if not col:
120
- return None
121
- if col not in df.columns:
122
- raise ValueError(f"{arg_name} '{col}' not found. Available columns: {list(df.columns)}")
123
- return col
124
-
125
-
126
- def _score_conversation_column(df: pd.DataFrame, col: str) -> int:
127
- name = col.strip().lower()
128
- score = 0
129
- strong_names = [
130
- "conversations", "conversation", "messages", "comments_json", "comments",
131
- "chat_history", "message_history", "聊天记录", "对话", "对话内容", "工单内容",
132
- ]
133
- weak_names = ["message", "content", "body", "description", "text", "问题", "客户"]
134
- noise_names = ["status", "state", "order", "email", "phone", "tag", "time", "date", "url", "id"]
135
- if any(h in name for h in strong_names):
136
- score += 80
137
- if any(h in name for h in weak_names):
138
- score += 35
139
- if any(h in name for h in noise_names):
140
- score -= 30
141
-
142
- sample = "\n".join(df[col].dropna().astype(str).head(8).tolist())
143
- avg_len = df[col].dropna().astype(str).head(20).str.len().mean()
144
- if avg_len and avg_len > 120:
145
- score += 25
146
- if avg_len and avg_len > 500:
147
- score += 20
148
- markers = ["user:", "customer:", "agent:", "assistant:", "ai_", "(1)", "body", "author", "客服", "客户"]
149
- lowered = sample.lower()
150
- score += sum(8 for marker in markers if marker in lowered)
151
- if sample.strip().startswith("[") and ("body" in lowered or "content" in lowered):
152
- score += 30
153
- return score
154
-
155
-
156
- def detect_message_reply_columns(df: pd.DataFrame) -> tuple[str | None, str | None]:
157
- """For split-column exports: find user-message column and agent-reply column by name hints."""
158
- msg_hints = ["message", "question", "customer", "user", "inquiry", "content", "body",
159
- "问题", "用户", "客户", "来信", "first_message"]
160
- rep_hints = ["reply", "answer", "response", "agent", "support", "resolution",
161
- "客服", "回复", "答案"]
162
- cols = list(df.columns)
163
- cols_lower = [c.lower() for c in cols]
164
- msg_col, rep_col = None, None
165
- for i, cl in enumerate(cols_lower):
166
- if msg_col is None and any(h in cl for h in msg_hints):
167
- msg_col = cols[i]
168
- if rep_col is None and any(h in cl for h in rep_hints):
169
- rep_col = cols[i]
170
- return msg_col, rep_col
171
-
172
-
173
- def detect_best_conversation_column(df: pd.DataFrame) -> str | None:
174
- scored = [(_score_conversation_column(df, col), col) for col in df.columns]
175
- scored.sort(reverse=True)
176
- if scored and scored[0][0] >= 50:
177
- return scored[0][1]
178
- return None
179
-
180
-
181
- def detect_longest_text_column(df: pd.DataFrame) -> str | None:
182
- """Fallback: find the column with the longest average text content."""
183
- best_col, best_avg = None, 0
184
- for col in df.columns:
185
- sample = df[col].dropna().astype(str).head(20)
186
- avg_len = sample.str.len().mean()
187
- if avg_len > best_avg:
188
- best_avg = avg_len
189
- best_col = col
190
- return best_col if best_avg > 80 else None
191
-
192
-
193
- def resolve_columns(
194
- df: pd.DataFrame,
195
- messages_col: str | None,
196
- title_col: str | None,
197
- reply_col: str | None,
198
- ) -> ColumnSelection:
199
- messages_col = _require_column(df, messages_col, "--messages-col")
200
- title_col = _require_column(df, title_col, "--title-col")
201
- reply_col = _require_column(df, reply_col, "--reply-col")
202
-
203
- if messages_col:
204
- return ColumnSelection(mode="split" if reply_col else "single", messages_col=messages_col, title_col=title_col, reply_col=reply_col)
205
-
206
- detected = detect_best_conversation_column(df)
207
- if detected:
208
- return ColumnSelection(mode="single", messages_col=detected, title_col=title_col)
209
-
210
- msg_col, rep_col = detect_message_reply_columns(df)
211
- if msg_col and rep_col:
212
- return ColumnSelection(mode="split", messages_col=msg_col, title_col=title_col, reply_col=rep_col)
213
-
214
- fallback_col = detect_longest_text_column(df)
215
- if fallback_col:
216
- return ColumnSelection(mode="fallback", fallback_col=fallback_col, title_col=title_col)
217
-
218
- raise ValueError("Cannot detect conversation column. Use --messages-col or --messages-col + --reply-col.")
219
-
220
-
221
- def _clean_cell(value) -> str:
222
- text = str(value).strip() if pd.notna(value) else ""
223
- return "" if text.lower() == "nan" else text
224
-
225
-
226
- def build_thread(row: pd.Series, columns: ColumnSelection) -> str | None:
227
- """Convert a dataframe row into a conversation thread string."""
228
- title = _clean_cell(row.get(columns.title_col)) if columns.title_col else ""
229
-
230
- if columns.mode == "single" and columns.messages_col:
231
- text = _clean_cell(row.get(columns.messages_col))
232
- if not text:
233
- return None
234
- return f"Title: {title}\n\n{text}" if title else text
235
-
236
- if columns.mode == "split" and columns.messages_col and columns.reply_col:
237
- q = _clean_cell(row.get(columns.messages_col))
238
- a = _clean_cell(row.get(columns.reply_col))
239
- if q or a:
240
- thread = f"Customer: {q}\n\nAgent: {a}"
241
- return f"Title: {title}\n\n{thread}" if title else thread
242
- return None
243
-
244
- if columns.mode == "fallback" and columns.fallback_col:
245
- text = _clean_cell(row.get(columns.fallback_col))
246
- if not text:
247
- return None
248
- return f"Title: {title}\n\n{text}" if title else text
249
-
250
- return None
251
-
252
-
253
- def _loads_json_response(text: str):
254
- text = text.strip()
255
- if "```" in text:
256
- parts = text.split("```")
257
- text = parts[1] if len(parts) > 1 else text
258
- if text.startswith("json"):
259
- text = text[4:]
260
- return json.loads(text.strip())
261
-
262
-
263
- def extract_batch(client: OpenAI, items: list[dict], brand: str) -> list[dict]:
264
- payload = [
265
- {"source_row": item["source_row"], "conversation": item["thread"][:2500]}
266
- for item in items
267
- ]
268
- prompt = EXTRACT_BATCH_PROMPT.format(
269
- brand=brand,
270
- items_json=json.dumps(payload, ensure_ascii=False, indent=2),
271
- )
272
- try:
273
- resp = client.chat.completions.create(
274
- # /internal/openai/v1 overwrites this sentinel with the service-managed model.
275
- model=SERVICE_MANAGED_MODEL,
276
- max_tokens=max(800, 500 * len(items)),
277
- messages=[{"role": "user", "content": prompt}],
278
- temperature=0,
279
- )
280
- text = (resp.choices[0].message.content or "").strip()
281
- result = _loads_json_response(text)
282
- if isinstance(result, dict):
283
- result = [result]
284
- return result if isinstance(result, list) else []
285
- except Exception as e:
286
- rows = [item["source_row"] for item in items]
287
- raise RuntimeError(f"batch extraction failed rows={rows}: {e}") from e
288
-
289
-
290
- def chunked(items: list, size: int):
291
- if size < 1:
292
- raise ValueError(f"batch-size must be >= 1, got {size}")
293
- for i in range(0, len(items), size):
294
- yield items[i:i + size]
295
-
296
-
297
- def _normalize_result(result: dict) -> dict | None:
298
- if result.get("skip_reason"):
299
- return {"skip_reason": result.get("skip_reason"), "source_row": result.get("source_row")}
300
- q = str(result.get("question", "")).strip()
301
- a = str(result.get("answer", "")).strip()
302
- topic = result.get("topic", "troubleshooting")
303
- if q and a and len(a) > 20:
304
- return {
305
- "source_row": result.get("source_row"),
306
- "topic": topic,
307
- "question": q,
308
- "answer": a,
309
- "dir_path": [],
310
- }
311
- return None
312
-
313
-
314
- def run(
315
- xlsx_path: str,
316
- brand: str,
317
- page: int,
318
- page_size: int,
319
- dry_run: bool,
320
- messages_col: str | None,
321
- title_col: str | None,
322
- reply_col: str | None,
323
- batch_size: int,
324
- workers: int,
325
- ) -> None:
326
- if batch_size > MAX_BATCH_SIZE:
327
- raise ValueError(f"batch-size {batch_size} exceeds maximum {MAX_BATCH_SIZE}")
328
- if workers < 1 or workers > MAX_WORKERS:
329
- raise ValueError(f"workers must be 1-{MAX_WORKERS}, got {workers}")
330
-
331
- print(f"[extract] Reading {xlsx_path}")
332
- df = pd.read_excel(xlsx_path)
333
- df, window = select_rows(df, page=page, page_size=page_size)
334
-
335
- print(
336
- f"[extract] Processing page {window.page} pageSize {window.page_size} "
337
- f"| rows {window.start_row}-{window.end_row} of {window.total_rows}"
338
- )
339
- print(f"[extract] {len(df)} rows in this page | Columns: {list(df.columns)}")
340
-
341
- columns = resolve_columns(df, messages_col=messages_col, title_col=title_col, reply_col=reply_col)
342
- print(
343
- "[detect] "
344
- f"mode={columns.mode} messages_col={columns.messages_col} "
345
- f"reply_col={columns.reply_col} title_col={columns.title_col} fallback_col={columns.fallback_col}"
346
- )
347
-
348
- items: list[dict] = []
349
- for offset, (_, row) in enumerate(df.iterrows(), start=window.start_row):
350
- thread = build_thread(row, columns)
351
- if thread:
352
- items.append({"source_row": offset, "thread": thread})
353
-
354
- if dry_run:
355
- sample = None
356
- if items:
357
- sample = items[0]["thread"]
358
- print(f"\n[dry-run] Sample thread preview:\n{'-'*60}")
359
- print((sample or "(no content found)")[:600])
360
- print(f"{'-'*60}")
361
- calls = (len(items) + batch_size - 1) // batch_size if items else 0
362
- print(f"\n[dry-run] {len(items)}/{len(df)} rows have conversation content")
363
- print(f"[dry-run] Would call the service-managed model {calls} times with batch-size={batch_size}, workers={workers}")
364
- print("[dry-run] Output -> workspace/outputs/kb-builder/ticket_qa_<ts>.json")
365
- return
366
-
367
- # Sandbox injects OPENAI_BASE_URL + dummy OPENAI_API_KEY for the internal proxy.
368
- client = OpenAI()
369
-
370
- qa_pairs: list[dict] = []
371
- skipped = 0
372
- failed = 0
373
-
374
- batches = list(chunked(items, batch_size))
375
- print(f"[extract] {len(items)} valid rows -> {len(batches)} model calls | batch-size={batch_size} workers={workers}")
376
-
377
- with ThreadPoolExecutor(max_workers=workers) as pool:
378
- futures = {pool.submit(extract_batch, client, batch, brand): len(batch) for batch in batches}
379
- for done, future in enumerate(as_completed(futures), start=1):
380
- results = future.result()
381
- if not results:
382
- failed += futures[future]
383
- for result in results:
384
- normalized = _normalize_result(result)
385
- if not normalized:
386
- failed += 1
387
- elif normalized.get("skip_reason"):
388
- skipped += 1
389
- else:
390
- qa_pairs.append(normalized)
391
- print(f" [batch {done}/{len(batches)}] extracted={len(qa_pairs)} skipped={skipped} failed={failed}")
392
-
393
- qa_pairs.sort(key=lambda item: item.get("source_row") or 0)
394
-
395
- ts = timestamp()
396
- OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
397
- out_path = OUTPUTS_DIR / f"ticket_qa_{ts}.json"
398
- out_path.write_text(json.dumps(qa_pairs, ensure_ascii=False, indent=2), encoding="utf-8")
399
- print(f"[extract_ticket_qa] Output -> {out_path}")
400
-
401
- print(f"\n[extract] Done: {len(qa_pairs)} QA extracted | {skipped} skipped (non-product) | {failed} failed/empty")
402
- print(f"[extract] Output: {out_path}")
403
- print(f"\nNext step: assign dir_path for each item, merge into qa_plan, then run import_qa.py")
404
-
405
-
406
- def main():
407
- ap = argparse.ArgumentParser()
408
- ap.add_argument("--xlsx", required=True, help="Path to ticket xlsx (e.g. mounted/chat-files/user/tickets.xlsx)")
409
- ap.add_argument("--brand", required=True, help="Brand name prefix for questions")
410
- ap.add_argument("--page", type=int, default=1, help="1-based page number to process")
411
- ap.add_argument("--page-size", type=int, default=100, dest="page_size", help=f"Rows per page, max {MAX_PAGE_SIZE}")
412
- ap.add_argument("--messages-col", default=None, help="Column containing full conversation or customer message")
413
- ap.add_argument("--title-col", default=None, help="Optional ticket title/subject column")
414
- ap.add_argument("--reply-col", default=None, help="Optional agent reply column when customer/reply are split")
415
- ap.add_argument("--batch-size", type=int, default=5, dest="batch_size", help=f"Conversations per model call, max {MAX_BATCH_SIZE}")
416
- ap.add_argument("--workers", type=int, default=3, help=f"Parallel model calls, max {MAX_WORKERS}")
417
- ap.add_argument("--dry-run", action="store_true", dest="dry_run")
418
- args = ap.parse_args()
419
- try:
420
- run(
421
- args.xlsx,
422
- args.brand,
423
- args.page,
424
- args.page_size,
425
- args.dry_run,
426
- args.messages_col,
427
- args.title_col,
428
- args.reply_col,
429
- args.batch_size,
430
- args.workers,
431
- )
432
- except ValueError as exc:
433
- print(f"[error] {exc}", file=sys.stderr)
434
- sys.exit(2)
435
- except RuntimeError as exc:
436
- print(f"[error] {exc}", file=sys.stderr)
437
- sys.exit(1)
438
-
439
-
440
- if __name__ == "__main__":
441
- main()
@@ -1,118 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Extract basic web QA parts from crawl_result JSON.
3
-
4
- Phase 4 provides deterministic starter QA so the upload pipeline can be tested.
5
- Future phases may replace the extraction internals with Crawl4AI fit_markdown and LLM
6
- summarization while keeping this CLI / output contract stable.
7
- """
8
- from __future__ import annotations
9
-
10
- import argparse
11
- import json
12
- import os
13
- import re
14
- import sys
15
- from pathlib import Path
16
-
17
- 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"))
18
- from bootstrap import * # noqa: F401,F403
19
-
20
-
21
- def resolve_workspace_path(path: str | Path) -> Path:
22
- p = Path(path)
23
- if p.is_absolute() and str(p).startswith("/workspace"):
24
- return WORKSPACE / p.relative_to("/workspace")
25
- return p
26
-
27
-
28
- def slugify(value: str, fallback: str) -> str:
29
- text = re.sub(r"[^a-zA-Z0-9._-]+", "-", value.strip().lower()).strip("-")
30
- return text[:80] or fallback
31
-
32
-
33
- def compact_markdown(text: str, limit: int) -> str:
34
- lines = []
35
- for line in text.splitlines():
36
- s = line.strip()
37
- if not s or s.startswith("URL:"):
38
- continue
39
- if len(s) < 3:
40
- continue
41
- lines.append(s)
42
- if sum(len(x) for x in lines) >= limit:
43
- break
44
- body = "\n".join(lines).strip()
45
- return body[:limit].strip()
46
-
47
-
48
- def dir_path_for(page: dict) -> list[str]:
49
- title = page.get("title") or "Knowledge"
50
- if page.get("type") == "policy":
51
- lower = f"{page.get('url', '')} {title}".lower()
52
- if "warranty" in lower or "guarantee" in lower:
53
- return ["Brand Policy", "Warranty Policy"]
54
- if "return" in lower or "refund" in lower:
55
- return ["Brand Policy", "Return Policy"]
56
- if "shipping" in lower or "delivery" in lower:
57
- return ["Brand Policy", "Shipping Policy"]
58
- if "privacy" in lower:
59
- return ["Brand Policy", "Privacy Policy"]
60
- return ["Brand Policy", "Brand Policy"]
61
- if page.get("type") == "help_center":
62
- return ["Help Center", "Help Articles"]
63
- if page.get("type") == "product":
64
- return ["Product Introduction", "Products", title[:80]]
65
- return ["Pending Assignment"]
66
-
67
-
68
- def question_for(page: dict) -> str:
69
- title = page.get("title") or "this page"
70
- if page.get("type") == "policy":
71
- return f"{title} — What should customers know?"
72
- if page.get("type") == "help_center":
73
- return f"{title} — How can customers use this help article?"
74
- if page.get("type") == "product":
75
- return f"{title} — What is the product overview?"
76
- return f"{title} — What information is available?"
77
-
78
-
79
- def main() -> None:
80
- parser = argparse.ArgumentParser(description="Extract web QA parts from crawl_result JSON")
81
- parser.add_argument("--crawl-result", required=True)
82
- parser.add_argument("--output-dir", default="/workspace/tmp/kb-builder/qa_parts")
83
- parser.add_argument("--max-pages", type=int, default=80)
84
- parser.add_argument("--answer-limit", type=int, default=3500)
85
- args = parser.parse_args()
86
-
87
- crawl_path = resolve_workspace_path(args.crawl_result)
88
- crawl = json.loads(crawl_path.read_text(encoding="utf-8"))
89
- out_dir = resolve_workspace_path(args.output_dir)
90
- out_dir.mkdir(parents=True, exist_ok=True)
91
-
92
- written = 0
93
- for idx, page in enumerate((crawl.get("pages") or [])[: max(1, args.max_pages)], 1):
94
- md_path_raw = page.get("fit_markdown_path") or page.get("markdown_path")
95
- if not md_path_raw:
96
- continue
97
- md_path = resolve_workspace_path(md_path_raw)
98
- if not md_path.exists():
99
- continue
100
- answer = compact_markdown(md_path.read_text(encoding="utf-8"), max(500, args.answer_limit))
101
- if not answer:
102
- continue
103
- item = {
104
- "dir_path": dir_path_for(page),
105
- "question": question_for(page),
106
- "answer": answer,
107
- "source_url": page.get("url", ""),
108
- }
109
- slug = slugify(page.get("title") or page.get("url") or f"page-{idx}", f"page-{idx:03d}")
110
- part = out_dir / f"qa_web_{idx:03d}_{slug}.json"
111
- part.write_text(json.dumps([item], ensure_ascii=False, indent=2), encoding="utf-8")
112
- written += 1
113
-
114
- print(f"[extract_web_qa] output_dir={args.output_dir} parts={written}")
115
-
116
-
117
- if __name__ == "__main__":
118
- main()