Stabilize product data export flow
This commit is contained in:
@@ -7,9 +7,11 @@ Skill 应该先让模型生成紧凑、便于人工 review 的 dataset draft tex
|
||||
再由这里的逻辑解析并补齐为 canonical records,供后续校验和导出使用。
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -461,6 +463,7 @@ def export_dataset_records(
|
||||
output_format: str = 'jsonl',
|
||||
require_validation_ok: bool = True,
|
||||
overwrite: bool = True,
|
||||
export_table: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""校验并导出 canonical records。
|
||||
|
||||
@@ -480,8 +483,11 @@ def export_dataset_records(
|
||||
)
|
||||
|
||||
path = _resolve_output_path(root, output_path)
|
||||
table_path = path.with_name('records.csv')
|
||||
if path.exists() and not overwrite:
|
||||
raise DataRecordError(f'output_path already exists: {output_path}')
|
||||
if export_table and table_path.exists() and not overwrite:
|
||||
raise DataRecordError('table output_path already exists: records.csv')
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if output_format == 'jsonl':
|
||||
@@ -494,13 +500,83 @@ def export_dataset_records(
|
||||
|
||||
path.write_text(content, encoding='utf-8')
|
||||
rel_path = str(path.relative_to(Path(root).resolve()))
|
||||
return {
|
||||
payload = {
|
||||
'output_path': rel_path,
|
||||
'output_format': output_format,
|
||||
'record_count': len(records),
|
||||
'bytes_written': len(content.encode('utf-8')),
|
||||
'validation': validation,
|
||||
}
|
||||
if export_table:
|
||||
table_content = render_dataset_records_table_csv(records)
|
||||
table_path.write_text(table_content, encoding='utf-8-sig')
|
||||
payload.update(
|
||||
{
|
||||
'table_output_path': str(table_path.relative_to(Path(root).resolve())),
|
||||
'table_output_format': 'csv',
|
||||
'table_bytes_written': len(table_content.encode('utf-8-sig')),
|
||||
}
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def render_dataset_records_table_csv(records: list[dict[str, Any]]) -> str:
|
||||
"""把 canonical records 转成同事流转用的表格 CSV。"""
|
||||
|
||||
output = StringIO()
|
||||
writer = csv.DictWriter(output, fieldnames=_dataset_table_columns(), lineterminator='\n')
|
||||
writer.writeheader()
|
||||
for record in records:
|
||||
writer.writerow(_dataset_record_table_row(record))
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def _dataset_table_columns() -> list[str]:
|
||||
return [
|
||||
'request_id',
|
||||
'timestamp',
|
||||
'query',
|
||||
'prev_session',
|
||||
'context',
|
||||
'label',
|
||||
'是否迁移Function',
|
||||
'function',
|
||||
]
|
||||
|
||||
|
||||
def _dataset_record_table_row(record: dict[str, Any]) -> dict[str, str]:
|
||||
source = record.get('source') if isinstance(record.get('source'), dict) else {}
|
||||
turn = record.get('turn') if isinstance(record.get('turn'), dict) else {}
|
||||
label = record.get('label') if isinstance(record.get('label'), dict) else {}
|
||||
context = record.get('context') if isinstance(record.get('context'), dict) else {}
|
||||
prev_session = record.get('prev_session')
|
||||
if not isinstance(prev_session, list):
|
||||
prev_session = []
|
||||
return {
|
||||
'request_id': str(source.get('request_id') or ''),
|
||||
'timestamp': str(source.get('timestamp') or turn.get('timestamp') or ''),
|
||||
'query': str(turn.get('query') or ''),
|
||||
'prev_session': json.dumps(_table_prev_session(prev_session), ensure_ascii=False, separators=(',', ':')),
|
||||
'context': json.dumps(context, ensure_ascii=False, separators=(',', ':')),
|
||||
'label': str(label.get('dataset_label') or ''),
|
||||
'是否迁移Function': '',
|
||||
'function': str(label.get('target') or ''),
|
||||
}
|
||||
|
||||
|
||||
def _table_prev_session(prev_session: list[Any]) -> list[dict[str, str]]:
|
||||
rows: list[dict[str, str]] = []
|
||||
for item in prev_session[-10:]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
'query': str(item.get('query') or ''),
|
||||
'tts': str(item.get('tts') or ''),
|
||||
'timestamp': str(item.get('timestamp') or ''),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _parse_draft_text(draft_text: str) -> dict[str, Any]:
|
||||
@@ -797,7 +873,7 @@ def _case_to_record(
|
||||
if not current_query:
|
||||
raise DataRecordError(f'case {index} current 用户 line must be non-empty')
|
||||
|
||||
target = str(case.get('target') or '').strip()
|
||||
target = _canonical_target(str(case.get('target') or '').strip())
|
||||
if not target:
|
||||
raise DataRecordError(f'case {index} target is required')
|
||||
|
||||
@@ -866,6 +942,17 @@ def _target_type(target: str) -> str:
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def _canonical_target(target: str) -> str:
|
||||
"""规范常见 Agent 标签写法,允许草稿里用单引号降低 JSON 转义风险。"""
|
||||
|
||||
match = re.fullmatch(r'Agent\s*\(\s*tag\s*=\s*([\'"])(.+?)\1\s*\)', target)
|
||||
if not match:
|
||||
return target
|
||||
tag = match.group(2).strip()
|
||||
escaped = tag.replace('\\', '\\\\').replace('"', '\\"')
|
||||
return f'Agent(tag="{escaped}")'
|
||||
|
||||
|
||||
def _record_id(*, source_type: str, batch_id: str, request_id: str, index: int) -> str:
|
||||
if source_type == 'generated':
|
||||
return f'gen_{_safe_token(batch_id)}_{index:06d}'
|
||||
|
||||
Reference in New Issue
Block a user