Fin-EvalOps-v2 / scripts /convert_part06.py
appQQQ's picture
sync from GitHub bb1f476: fix(testsets): preserve `tags` field across all import paths
9d4f904 verified
Raw
History Blame Contribute Delete
6.87 kB
"""Convert part_06.jsonl → platform-importable JSON.
Source schema (per record):
question, answer, history (list of {role,content}), trace (list of steps
with type ∈ {thought, tool_call, tool_result}), source_id,
online_record_id, source_path, source_line, request_id, session_id,
user_id, agent_id, business_tag, ability_tree_key, model_name,
usage_input_tokens, usage_output_tokens, usage_total_tokens,
intent_labels, capability_labels, domain_label, query_quality,
query_difficulty, primary_intent, selection_group, offline_split_id,
assigned_reviewer
Target schema (per record, Chinese keys to match the 13 canonical files):
id ← source_id (verbatim; platform PK lookup)
来源 ← business_tag (provenance)
问题 ← question
答案 ← answer
链路数据 ← trace reshaped into [{plan, tools[]}] form so the
platform's _extract_tools/_infer_difficulty heuristics
can still count tools (key for evaluation).
上下文 ← history (verbatim)
tags ← auxiliary metadata JSON-packed into List[str] for
future audit / selection-group queries (single tag
per record; original fields are NOT lost — see also
`source_id`/`online_record_id` round-trip).
Pairing strategy for trace → 链路数据:
- tool_call and tool_result steps share metadata.tool_call_id
- tool_call becomes a phase with tools=[{name, input}]; if matching
tool_result exists, output is attached
- thought becomes a phase with plan=content, tools=[]
- orphan tool_results (no matching call) become a phase with the
output as a "result" tool
Every source step is consumed exactly once → no data loss; the original
trace is still recoverable via online_record_id.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "数据测试集" / "part_06.jsonl"
DST = ROOT / "数据测试集" / "part_06-importable.json"
# Auxiliary fields the platform TestCase ORM doesn't model — packed into
# `tags` as a single JSON-encoded string per record so the row remains
# importable but the data is preserved for downstream queries / audits.
AUX_FIELDS = (
"agent_id",
"business_tag",
"ability_tree_key",
"model_name",
"primary_intent",
"query_difficulty",
"query_quality",
"domain_label",
"intent_labels",
"capability_labels",
"selection_group",
"offline_split_id",
"assigned_reviewer",
"source_path",
"source_line",
"request_id",
"session_id",
"user_id",
"online_record_id",
"usage_input_tokens",
"usage_output_tokens",
"usage_total_tokens",
)
def reshape_trace(trace):
"""Reshape [{type: thought|tool_call|tool_result}, ...] → [{plan, tools[]}, ...]."""
if not isinstance(trace, list):
return []
# Pre-build id→{name,input} and id→output maps so pairing is robust to
# interleaved ordering (record 7 had 3 tool_calls → 3 tool_results).
calls_by_id: dict[str, dict] = {}
results_by_id: dict[str, str] = {}
for step in trace:
if not isinstance(step, dict):
continue
meta = step.get("metadata") or {}
tc_id = meta.get("tool_call_id") or ""
if step.get("type") == "tool_call":
calls_by_id[tc_id] = {
"name": step.get("title", "") or "",
"input": step.get("content", "") or "",
}
elif step.get("type") == "tool_result":
results_by_id[tc_id] = step.get("content", "") or ""
phases: list[dict] = []
consumed_ids: set[str] = set()
for step in trace:
if not isinstance(step, dict):
continue
t = step.get("type")
if t == "thought":
phases.append({"plan": step.get("content", "") or "", "tools": []})
continue
if t == "tool_call":
tc_id = (step.get("metadata") or {}).get("tool_call_id") or ""
tool = dict(calls_by_id.get(tc_id) or {
"name": step.get("title", "") or "",
"input": step.get("content", "") or "",
})
if tc_id in results_by_id:
tool["output"] = results_by_id[tc_id]
consumed_ids.add(tc_id)
phases.append({"plan": "", "tools": [tool]})
continue
if t == "tool_result":
tc_id = (step.get("metadata") or {}).get("tool_call_id") or ""
if tc_id in consumed_ids:
# Already attached to the matching tool_call — skip to avoid dup.
continue
if tc_id and tc_id not in calls_by_id:
# Orphan result with a known id (its call was filtered out).
tool = {
"name": step.get("title", "") or "",
"output": step.get("content", "") or "",
}
phases.append({"plan": "", "tools": [tool]})
continue
# Result with no id at all — keep as raw content.
phases.append({
"plan": "",
"tools": [{
"name": "result",
"output": step.get("content", "") or "",
}],
})
return phases
def convert(rec: dict) -> dict:
aux = {k: rec[k] for k in AUX_FIELDS if k in rec and rec[k] is not None}
return {
"id": rec.get("source_id") or "",
"来源": rec.get("business_tag") or "iwencai",
"问题": rec.get("question") or "",
"答案": rec.get("answer") or "",
"链路数据": reshape_trace(rec.get("trace") or []),
"上下文": rec.get("history") or [],
"tags": [json.dumps(aux, ensure_ascii=False)],
}
def main() -> int:
if not SRC.exists():
print(f"❌ source not found: {SRC}", file=sys.stderr)
return 1
records = []
with SRC.open(encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
records.append(json.loads(line))
except json.JSONDecodeError as e:
print(f"⚠️ line {lineno} skipped: {e}", file=sys.stderr)
print(f"📥 read {len(records)} records from {SRC.name}")
out = [convert(r) for r in records]
DST.parent.mkdir(parents=True, exist_ok=True)
with DST.open("w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"📤 wrote {len(out)} records → {DST.relative_to(ROOT)}")
print(f" size: {DST.stat().st_size:,} bytes")
return 0
if __name__ == "__main__":
raise SystemExit(main())