@shulex/cxclawctl 0.1.2 → 0.1.4
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.
- package/README.md +25 -22
- package/capabilities.lock.json +535 -131
- package/dist/cli.js +2203 -127
- package/dist/cli.js.map +1 -1
- package/package.json +20 -6
- package/requirements-cli.txt +0 -2
- package/skills/__lib__/__init__.py +0 -0
- package/skills/__lib__/skill-runtime/bootstrap.py +0 -134
- package/skills/__lib__/solvea-api/__init__.py +0 -0
- package/skills/__lib__/solvea-api/client.py +0 -1944
- package/skills/__lib__/solvea-api/dashboard.py +0 -100
- package/skills/__lib__/solvea-api/solvea_api.py +0 -1341
- package/skills/agent-analytics/SKILL.md +0 -74
- package/skills/agent-analytics/references/chat-report-fields.md +0 -36
- package/skills/agent-analytics/references/report-fields.md +0 -130
- package/skills/agent-analytics/references/transcript.md +0 -110
- package/skills/agent-builder/SKILL.md +0 -42
- package/skills/agent-builder/interfaces/fallback-chatflow.md +0 -47
- package/skills/agent-builder/references/chatflow/graph-spec.md +0 -259
- package/skills/agent-builder/references/chatflow/node-selection.md +0 -89
- package/skills/agent-builder/references/chatflow/nodes/agent.md +0 -350
- package/skills/agent-builder/references/chatflow/nodes/business-track.md +0 -43
- package/skills/agent-builder/references/chatflow/nodes/code.md +0 -157
- package/skills/agent-builder/references/chatflow/nodes/collect-order.md +0 -47
- package/skills/agent-builder/references/chatflow/nodes/finish.md +0 -34
- package/skills/agent-builder/references/chatflow/nodes/human-review.md +0 -60
- package/skills/agent-builder/references/chatflow/nodes/if-else.md +0 -100
- package/skills/agent-builder/references/chatflow/nodes/intent.md +0 -79
- package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +0 -43
- package/skills/agent-builder/references/chatflow/nodes/llm.md +0 -117
- package/skills/agent-builder/references/chatflow/nodes/note.md +0 -38
- package/skills/agent-builder/references/chatflow/nodes/question.md +0 -97
- package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +0 -68
- package/skills/agent-builder/references/chatflow/nodes/reply.md +0 -97
- package/skills/agent-builder/references/chatflow/nodes/start.md +0 -35
- package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +0 -48
- package/skills/agent-builder/references/chatflow/nodes/tool.md +0 -138
- package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +0 -63
- package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +0 -52
- package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +0 -50
- package/skills/agent-builder/references/chatflow/nodes/variable-process.md +0 -70
- package/skills/agent-builder/references/chatflow/overview.md +0 -53
- package/skills/agent-builder/references/chatflow/visual-translation.md +0 -64
- package/skills/agent-builder/references/chatflow/workflow.md +0 -134
- package/skills/agent-builder/references/multi-agent/create-update.md +0 -185
- package/skills/agent-builder/references/multi-agent/graph-spec.md +0 -246
- package/skills/agent-builder/references/multi-agent/graph-structure.md +0 -12
- package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +0 -13
- package/skills/agent-builder/references/multi-agent/migration/large.md +0 -554
- package/skills/agent-builder/references/multi-agent/migration/principles.md +0 -446
- package/skills/agent-builder/references/multi-agent/migration/small.md +0 -225
- package/skills/agent-builder/references/multi-agent/orchestration.md +0 -11
- package/skills/agent-builder/references/multi-agent/overview.md +0 -48
- package/skills/agent-builder/references/multi-agent/update.md +0 -128
- package/skills/agent-builder/references/multi-agent/workflow.md +0 -87
- package/skills/agent-builder/references/single-agent/overview.md +0 -48
- package/skills/agent-builder/references/single-agent/prompt.md +0 -16
- package/skills/agent-builder/references/single-agent/tools.md +0 -36
- package/skills/agent-builder/references/single-agent/voice.md +0 -46
- package/skills/agent-builder/references/tools/http-tool.md +0 -315
- package/skills/agent-builder/references/tools/mock-tool.md +0 -279
- package/skills/agent-builder/references/tools/overview.md +0 -46
- package/skills/agent-builder/references/tools/tool-config.md +0 -43
- package/skills/agent-builder/references/tools/x-token.md +0 -34
- package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +0 -3458
- package/skills/agent-builder/scripts/chatflow/validate_graph.py +0 -1697
- package/skills/agent-builder/scripts/copy_marketplace_agent.py +0 -61
- package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +0 -5974
- package/skills/agent-builder/scripts/multi-agent/replay_compare.py +0 -679
- package/skills/agent-builder/scripts/patch_agent_tools.py +0 -135
- package/skills/agent-builder/scripts/tools/validate_tool_config.py +0 -384
- package/skills/agent-tester/SKILL.md +0 -35
- package/skills/agent-tester/batch-test/overview.md +0 -86
- package/skills/agent-tester/scripts/batch_test_sheet.py +0 -213
- package/skills/agent-tester/single-test/badcase-retest.md +0 -85
- package/skills/agent-tester/single-test/direct-test.md +0 -24
- package/skills/agent-tester/single-test/overview.md +0 -142
- package/skills/delivery/SKILL.md +0 -112
- package/skills/delivery/interfaces/classify.md +0 -38
- package/skills/delivery/interfaces/create-agents.md +0 -89
- package/skills/delivery/interfaces/delivery-test.md +0 -67
- package/skills/delivery/references/flow.md +0 -40
- package/skills/delivery/references/validation.md +0 -15
- package/skills/delivery/scripts/batch_test.py +0 -704
- package/skills/delivery/scripts/classify.py +0 -22
- package/skills/delivery/scripts/create_agents.py +0 -695
- package/skills/delivery/scripts/gen_delivery_report.py +0 -335
- package/skills/delivery/scripts/gen_excel.py +0 -234
- package/skills/delivery/scripts/gen_report.py +0 -208
- package/skills/delivery/scripts/plan.py +0 -163
- package/skills/delivery/scripts/render.py +0 -713
- package/skills/delivery/scripts/render_summary.py +0 -38
- package/skills/delivery/scripts/sample_history_cases.py +0 -109
- package/skills/kb-builder/SKILL.md +0 -80
- package/skills/kb-builder/interfaces/qa-upload.md +0 -55
- package/skills/kb-builder/interfaces/web-kb.md +0 -84
- package/skills/kb-builder/references/directory.md +0 -373
- package/skills/kb-builder/references/export.md +0 -282
- package/skills/kb-builder/references/isolation-keywords.md +0 -113
- package/skills/kb-builder/references/knowledge.md +0 -153
- package/skills/kb-builder/references/product.md +0 -52
- package/skills/kb-builder/references/qa-from-help-center.md +0 -58
- package/skills/kb-builder/references/qa-from-pdf.md +0 -94
- package/skills/kb-builder/references/qa-from-tickets.md +0 -168
- package/skills/kb-builder/references/qa-from-web.md +0 -98
- package/skills/kb-builder/references/qa-quality.md +0 -73
- package/skills/kb-builder/references/reply-template.md +0 -237
- package/skills/kb-builder/references/tag-isolation-migration.md +0 -61
- package/skills/kb-builder/references/tags.md +0 -45
- package/skills/kb-builder/references/upload.md +0 -130
- package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +0 -333
- package/skills/kb-builder/scripts/build_directory_plan.py +0 -144
- package/skills/kb-builder/scripts/crawl_site.py +0 -433
- package/skills/kb-builder/scripts/create_dirs.py +0 -146
- package/skills/kb-builder/scripts/export_knowledge.py +0 -454
- package/skills/kb-builder/scripts/extract_ticket_qa.py +0 -441
- package/skills/kb-builder/scripts/extract_web_qa.py +0 -118
- package/skills/kb-builder/scripts/import_qa.py +0 -219
- package/skills/kb-builder/scripts/import_source_documents.py +0 -143
- package/skills/kb-builder/scripts/import_templates.py +0 -409
- package/skills/poc/SKILL.md +0 -125
- package/skills/poc/assets/default_manual.css +0 -279
- package/skills/poc/assets/notice_escalate.png +0 -0
- package/skills/poc/assets/notice_escalate2.png +0 -0
- package/skills/poc/assets/notice_new_chat.png +0 -0
- package/skills/poc/assets/poc_test_template.html +0 -645
- package/skills/poc/assets/poc_test_template.md +0 -104
- package/skills/poc/assets/step3_preview.png +0 -0
- package/skills/poc/assets/step4_order_note.png +0 -0
- package/skills/poc/interfaces/classify.md +0 -38
- package/skills/poc/interfaces/create-agents.md +0 -108
- package/skills/poc/interfaces/poc-test.md +0 -94
- package/skills/poc/references/flow.md +0 -47
- package/skills/poc/references/kb-directory.md +0 -22
- package/skills/poc/references/validation.md +0 -17
- package/skills/poc/scripts/classify.py +0 -22
- package/skills/poc/scripts/create_agents.py +0 -818
- package/skills/poc/scripts/gen_excel.py +0 -234
- package/skills/poc/scripts/gen_poc_manual.py +0 -249
- package/skills/poc/scripts/gen_report.py +0 -208
- package/skills/poc/scripts/inspect_chatflow.py +0 -209
- package/skills/poc/scripts/plan.py +0 -178
- package/skills/poc/scripts/poc_test.py +0 -495
- package/skills/poc/scripts/render.py +0 -713
- package/skills/poc/scripts/render_summary.py +0 -41
- package/skills/runtime-inspector/SKILL.md +0 -51
- package/skills/runtime-inspector/references/frontend-rendering.md +0 -126
- package/skills/runtime-inspector/references/model.md +0 -59
- package/skills/runtime-inspector/references/sandbox.md +0 -131
- package/skills/runtime-inspector/scripts/model_inspect.py +0 -68
- package/skills/runtime-inspector/scripts/prepare.py +0 -98
- package/skills/runtime-inspector/scripts/process.py +0 -84
- package/skills/runtime-inspector/scripts/read_state.py +0 -32
- package/skills/runtime-inspector/scripts/sandbox_inspect.py +0 -463
- package/skills/runtime-inspector/scripts/test_claude_proxy.py +0 -49
- package/skills/runtime-inspector/scripts/test_openai_proxy.py +0 -41
- package/skills/runtime-inspector/scripts/verify_persistence.py +0 -101
- package/skills/runtime-inspector/scripts/write_test.py +0 -41
- package/skills/setup/SKILL.md +0 -17
- package/skills/shulex-support/SKILL.md +0 -39
- package/skills/shulex-support/references/badcase-analysis/overview.md +0 -34
- package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +0 -105
- package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +0 -89
- package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +0 -36
- package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +0 -53
- package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +0 -259
- package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +0 -148
- package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +0 -57
- package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +0 -120
- package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +0 -126
- package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +0 -121
- package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +0 -208
- package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +0 -115
- package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +0 -72
- package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +0 -92
- package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +0 -147
- package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +0 -283
- package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +0 -77
- package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +0 -19
- package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +0 -23
- package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +0 -31
- package/skills/shulex-support/references/product-docs.md +0 -70
- package/skills/shulex-support/references/tickets.md +0 -184
- package/skills/shulex-support/scripts/support.py +0 -427
- package/skills/solvea-desk/SKILL.md +0 -39
- package/skills/solvea-desk/references/report/fields.md +0 -82
- package/skills/solvea-desk/references/report/report-design.md +0 -200
- package/skills/solvea-desk/references/report/report-template.html +0 -44
- package/skills/solvea-desk/references/report/report.css +0 -517
- package/skills/solvea-desk/references/report/workflow.md +0 -464
- package/skills/solvea-desk/references/tickets/query.md +0 -103
- package/skills/solvea-desk/references/tickets/tags.md +0 -83
- package/skills/solvea-desk/scripts/report/fetch_sources.py +0 -580
- package/skills/solvea-desk/scripts/report/render_report.py +0 -763
- package/skills/solvea-desk/scripts/tickets/desk_api.py +0 -73
- package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +0 -403
- package/skills/solvea-desk/scripts/tickets/ticket_tags.py +0 -305
- package/skills/solvea-kit/SKILL.md +0 -84
- package/skills/solvea-kit/references/agent.md +0 -145
- package/skills/solvea-kit/references/chat.md +0 -244
- package/skills/solvea-kit/references/intent.md +0 -69
- package/skills/solvea-kit/references/plugin.md +0 -53
- package/skills/solvea-kit/references/quality-inspection.md +0 -75
- package/skills/solvea-kit/references/strategy.md +0 -62
- package/skills/solvea-kit/references/tagging.md +0 -231
- package/skills/solvea-kit/references/variables.md +0 -53
- package/skills/solvea-kit/scripts/generate_label_import_file.py +0 -151
- 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()
|