Improve online mining record conversion

This commit is contained in:
wuyang6
2026-05-14 17:19:23 +08:00
parent e10cb6909b
commit c6b3233769
7 changed files with 651 additions and 63 deletions
@@ -0,0 +1,353 @@
from __future__ import annotations
"""把线上 ELK case 直接转换为 product-data canonical records。
这个脚本承接 online-mining-v2 和 product-data
1. 按 request_id 拉主 NLP 表和前处理表。
2. 用主表 session_id 补齐当前请求前最多 10 轮上下文。
3. 从用户指定 target 或线上 planning/code 结果推断标签。
4. 直接产出 canonical records,并可同步导出流通表格、训练 jsonl、评测 csv。
"""
import json
import re
import sys
from pathlib import Path
from typing import Any
from online_mining_common import (
OnlineMiningError,
compact_text,
emit_error,
emit_success,
extract_case,
fetch_one_by_request_id,
fetch_session_turns,
load_json_payload,
resolve_portable_path,
string_values,
)
PRODUCT_DATA_SCRIPT_DIR = Path(__file__).resolve().parents[1].parent / "product-data" / "scripts"
sys.path.insert(0, str(PRODUCT_DATA_SCRIPT_DIR))
from product_data_portable import ( # noqa: E402
canonical_target,
export_dataset_records,
export_planning_eval_csv,
export_training_jsonl,
record_id,
target_type,
validate_dataset_records,
)
def load_cases(payload: dict[str, Any]) -> list[dict[str, Any]]:
if isinstance(payload.get("cases"), list):
return [item for item in payload["cases"] if isinstance(item, dict)]
cases_path = payload.get("cases_path")
if cases_path:
rows = []
text = resolve_portable_path(str(cases_path)).read_text(encoding="utf-8")
for line in text.splitlines():
if not line.strip():
continue
item = json.loads(line)
if isinstance(item, dict):
rows.append(item)
return rows
return []
def merge_case(request_id: str, main_case: dict[str, Any] | None, pre_case: dict[str, Any] | None) -> dict[str, Any]:
return {
"request_id": request_id,
"query": (pre_case or {}).get("query") or (main_case or {}).get("query"),
"timestamp": (main_case or {}).get("timestamp") or (pre_case or {}).get("timestamp"),
"session_id": (main_case or {}).get("session_id"),
"device_id": (main_case or {}).get("device_id"),
"device": (main_case or {}).get("device"),
"tts": (main_case or {}).get("text"),
"main": main_case,
"pre_processing": pre_case,
}
def fetch_cases_by_request_ids(request_ids: list[str], *, date: str | None) -> list[dict[str, Any]]:
rows = []
for request_id in request_ids:
main_doc = fetch_one_by_request_id("main", request_id, date)
pre_doc = fetch_one_by_request_id("pre_processing", request_id, date)
main_case = extract_case("main", main_doc) if main_doc else None
pre_case = extract_case("pre_processing", pre_doc) if pre_doc else None
rows.append(merge_case(request_id, main_case, pre_case))
return rows
def case_value(case: dict[str, Any], key: str) -> Any:
if key in case:
return case.get(key)
main = case.get("main") if isinstance(case.get("main"), dict) else {}
pre = case.get("pre_processing") if isinstance(case.get("pre_processing"), dict) else {}
return case.get(key) or pre.get(key) or main.get(key)
def enrich_prev_session(case: dict[str, Any], *, date: str | None, limit: int) -> list[dict[str, Any]]:
explicit = case.get("prev_session")
if isinstance(explicit, list):
return normalize_prev_session(explicit)[-limit:]
session_id = str(case_value(case, "session_id") or "").strip()
timestamp = optional_int(case_value(case, "timestamp"))
if not session_id or timestamp is None:
return []
turns = fetch_session_turns(session_id=session_id, before_timestamp=timestamp, date=date, limit=limit)
return [
{
"query": str(turn.get("query") or ""),
"tts": str(turn.get("text") or ""),
"timestamp": optional_int(turn.get("timestamp")) or 0,
}
for turn in turns
if str(turn.get("query") or "").strip()
][-limit:]
def normalize_prev_session(items: list[Any]) -> list[dict[str, Any]]:
rows = []
for item in items:
if not isinstance(item, dict):
continue
query = str(item.get("query") or "").strip()
if not query:
continue
rows.append(
{
"query": query,
"tts": str(item.get("tts") or ""),
"timestamp": optional_int(item.get("timestamp")) or 0,
}
)
rows.sort(key=lambda item: int(item.get("timestamp") or 0))
return rows
def infer_target(case: dict[str, Any], explicit_target: str) -> tuple[str, str]:
if explicit_target.strip():
return canonical_target(explicit_target.strip()), "input.target"
case_target = str(case_value(case, "target") or "").strip()
if case_target:
return canonical_target(case_target), "case.target"
pre = case.get("pre_processing") if isinstance(case.get("pre_processing"), dict) else {}
main = case.get("main") if isinstance(case.get("main"), dict) else {}
for key in ("code", "planning_result"):
value = str(pre.get(key) or "").strip()
if value:
return canonical_target(value), f"pre_processing.{key}"
llm_agent_info = main.get("llm_agent_info")
if isinstance(llm_agent_info, dict):
agent_type = str(llm_agent_info.get("agentType") or "").strip()
if agent_type:
return f'Agent(tag="{agent_type}")', "main.intention.intent_arbitrator_info.llm_agent_info.agentType"
domain = str(main.get("domain") or case_value(case, "domain") or "").strip()
if domain:
if re.fullmatch(r"[A-Z][A-Za-z0-9_]*", domain):
return f"{domain}()", "main.domain"
return f'Agent(tag="{domain}")', "main.domain"
raise OnlineMiningError("target is required because online logs do not contain code/planning_result/domain")
def case_to_record(
case: dict[str, Any],
*,
dataset_label: str,
target: str,
complex_value: bool,
index: int,
date: str | None,
session_limit: int,
) -> tuple[dict[str, Any], dict[str, Any]]:
query = str(case_value(case, "query") or "").strip()
if not query:
raise OnlineMiningError(f"case {index} has no query")
request_id = str(case_value(case, "request_id") or "").strip()
if not request_id:
raise OnlineMiningError(f"case {index} has no request_id")
timestamp = optional_int(case_value(case, "timestamp"))
if timestamp is None:
raise OnlineMiningError(f"case {index} has no timestamp")
final_target, target_source = infer_target(case, target)
prev_session = enrich_prev_session(case, date=date, limit=session_limit)
main = case.get("main") if isinstance(case.get("main"), dict) else {}
pre = case.get("pre_processing") if isinstance(case.get("pre_processing"), dict) else {}
record = {
"record_id": record_id(source_type="online", batch_id="online", request_id=request_id, index=index),
"source": {
"type": "online",
"request_id": request_id,
"timestamp": timestamp,
},
"turn": {
"query": query,
"timestamp": timestamp,
},
"prev_session": prev_session,
"context": {},
"label": {
"dataset_label": dataset_label,
"target": final_target,
"target_type": target_type(final_target),
},
"dimensions": {
"complex": parse_complex(case_value(case, "complex"), default=complex_value),
},
"meta": {
"session_id": str(case_value(case, "session_id") or ""),
"device_id": str(case_value(case, "device_id") or ""),
"device": compact_text(case_value(case, "device"), 500),
"online_tts": str(case_value(case, "tts") or ""),
"main_domain": str(main.get("domain") or ""),
"main_func": str(main.get("func") or ""),
"planning_result": str(pre.get("planning_result") or ""),
"planning_code": str(pre.get("code") or ""),
"hit_rules": pre.get("hit_rules") or main.get("hit_rules") or [],
"candidate_domains": pre.get("candidate_domains") or [],
"target_source": target_source,
},
}
summary = {
"request_id": request_id,
"query": query,
"target": final_target,
"target_source": target_source,
"prev_session_count": len(prev_session),
"main_domain": record["meta"]["main_domain"],
"planning_result": record["meta"]["planning_result"],
}
return record, summary
def optional_int(value: Any) -> int | None:
if value is None or value == "":
return None
try:
return int(value)
except (TypeError, ValueError):
return None
def parse_complex(value: Any, *, default: bool) -> bool:
if value is None or value == "":
return default
if isinstance(value, bool):
return value
text = str(value).strip().lower()
if text in {"true", "1", "yes", "y", "", "复杂", "complex"}:
return True
if text in {"false", "0", "no", "n", "", "不复杂", "简单", "simple"}:
return False
raise OnlineMiningError("complex must be true/false")
def write_records_jsonl(records: list[dict[str, Any]], path: str) -> str:
output = resolve_portable_path(path)
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8") as fh:
for record in records:
fh.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n")
return str(output)
def main() -> int:
try:
payload = load_json_payload()
dataset_label = str(payload.get("dataset_label") or "").strip()
if not dataset_label:
raise OnlineMiningError("dataset_label is required")
target = str(payload.get("target") or "").strip()
complex_value = parse_complex(payload.get("complex"), default=False)
date = str(payload.get("date") or "").strip() or None
session_limit = int(payload.get("session_limit") or 10)
if session_limit < 0 or session_limit > 10:
raise OnlineMiningError("session_limit must be between 0 and 10")
request_ids = string_values(payload.get("request_ids"))
cases = load_cases(payload)
if request_ids:
cases = fetch_cases_by_request_ids(request_ids, date=date)
if not cases:
raise OnlineMiningError("request_ids, cases or cases_path is required")
records = []
summaries = []
skipped = []
for index, case in enumerate(cases, start=1):
try:
record, summary = case_to_record(
case,
dataset_label=dataset_label,
target=target,
complex_value=complex_value,
index=index,
date=date,
session_limit=session_limit,
)
except Exception as exc: # noqa: BLE001
skipped.append({"index": index, "request_id": case.get("request_id"), "error": str(exc)})
continue
records.append(record)
summaries.append(summary)
if not records:
raise OnlineMiningError(f"no records built; skipped={skipped}")
records_path = write_records_jsonl(records, str(payload.get("records_output_path") or "scratchpad/online_records.jsonl"))
validation = validate_dataset_records(records)
output_dir = str(payload.get("output_dir") or "output")
exports: dict[str, Any] = {}
if bool(payload.get("export_records", True)):
exports["records"] = export_dataset_records(
records,
output_path=str(resolve_portable_path(output_dir) / "records.jsonl"),
output_format="jsonl",
require_validation_ok=bool(payload.get("require_validation_ok", True)),
overwrite=bool(payload.get("overwrite", True)),
export_table=bool(payload.get("export_table", True)),
)
if bool(payload.get("export_training", False)):
exports["training"] = export_training_jsonl(
records,
output_path=str(resolve_portable_path(output_dir) / "training.jsonl"),
require_validation_ok=bool(payload.get("require_validation_ok", True)),
overwrite=bool(payload.get("overwrite", True)),
)
if bool(payload.get("export_eval", False)):
exports["eval"] = export_planning_eval_csv(
records,
output_path=str(resolve_portable_path(output_dir) / "eval_planning.csv"),
require_validation_ok=bool(payload.get("require_validation_ok", True)),
overwrite=bool(payload.get("overwrite", True)),
)
emit_success(
{
"date": date or "past-48h",
"record_count": len(records),
"records_path": records_path,
"validation": validation,
"exports": exports,
"summaries": summaries,
"skipped": skipped,
}
)
return 0
except Exception as exc: # noqa: BLE001
emit_error(exc)
return 1
if __name__ == "__main__":
raise SystemExit(main())