Fix product data eval export format

This commit is contained in:
wuyang6
2026-05-11 15:27:11 +08:00
parent be731caed7
commit f22de8a40b
15 changed files with 414 additions and 71 deletions
+131 -19
View File
@@ -53,6 +53,7 @@ def prepare_generation_goal(
if missing:
raise DataRecordError('missing required goal fields: ' + ', '.join(missing))
normalized_targets = _normalize_target_definitions(target_definitions)
target = _normalize_target_expression(target)
if not target.strip() and not normalized_targets:
raise DataRecordError('generation goal must include target or target_definitions')
state = _load_goal_state(root)
@@ -63,7 +64,7 @@ def prepare_generation_goal(
'status': 'pending_confirmation',
'dataset_label': dataset_label.strip(),
'goal_summary': goal_summary.strip(),
'target': target.strip(),
'target': target,
'target_definitions': normalized_targets,
'plan_hint': plan_hint.strip(),
'coverage': coverage.strip(),
@@ -191,13 +192,14 @@ def prepare_generation_plan(
'output_path': output_path,
}
)
target = _normalize_target_expression(target)
normalized_targets = _normalize_target_definitions(target_definitions)
if confirmed_goal is not None and not target.strip() and not normalized_targets:
normalized_targets = _normalize_target_definitions(
confirmed_goal.get('target_definitions') if isinstance(confirmed_goal, dict) else None
)
if not normalized_targets:
target = str(confirmed_goal.get('target') or '').strip()
target = _normalize_target_expression(str(confirmed_goal.get('target') or ''))
if not target.strip() and not normalized_targets:
missing.append('target')
if missing:
@@ -220,7 +222,7 @@ def prepare_generation_plan(
'plan_id': plan_id,
'status': 'pending_confirmation',
'dataset_label': dataset_label.strip(),
'target': target.strip(),
'target': target,
'target_definitions': normalized_targets,
'total_count': total_count,
'turn_mix': turn_mix.strip(),
@@ -448,6 +450,14 @@ def validate_dataset_records(records: list[dict[str, Any]]) -> dict[str, Any]:
if label.get('target_type') not in {'agent', 'function', 'unknown'}:
errors.append(_issue(f'{prefix}.label.target_type', 'label.target_type is invalid'))
dimensions = record.get('dimensions')
if not isinstance(dimensions, dict):
warnings.append(_issue(f'{prefix}.dimensions', 'dimensions.complex is missing; false will be used as fallback'))
elif 'complex' not in dimensions:
warnings.append(_issue(f'{prefix}.dimensions.complex', 'complex is missing; false will be used as fallback'))
elif not isinstance(dimensions.get('complex'), bool):
errors.append(_issue(f'{prefix}.dimensions.complex', 'complex must be a boolean'))
return {
'ok': not errors,
'error_count': len(errors),
@@ -579,6 +589,7 @@ def export_planning_eval_csv(
session_num: int = 5,
session_time_minutes: int = 5,
context_fields: list[str] | None = None,
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
complex_default: bool = False,
require_validation_ok: bool = True,
overwrite: bool = True,
@@ -604,6 +615,7 @@ def export_planning_eval_csv(
session_num=session_num,
session_time_minutes=session_time_minutes,
context_fields=fields,
system_prompt=system_prompt,
complex_default=complex_default,
)
path.write_text(content, encoding='utf-8-sig')
@@ -633,6 +645,7 @@ def render_planning_eval_csv(
session_num: int = 5,
session_time_minutes: int = 5,
context_fields: list[str] | None = None,
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
complex_default: bool = False,
) -> str:
"""把 canonical records 转成含 newPrompt 的评测 CSV。"""
@@ -648,21 +661,21 @@ def render_planning_eval_csv(
for record in records:
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 {}
target = str(label.get('target') or '')
target = _record_target(record)
writer.writerow(
{
'request_id': str(source.get('request_id') or ''),
'newPrompt': build_training_instruction(
'newPrompt': build_planning_prompt(
record,
session_num=session_num,
session_time_minutes=session_time_minutes,
context_fields=fields,
system_prompt=system_prompt,
),
'query': str(turn.get('query') or ''),
'类别真实标签': _category_label_from_target(target),
'code标签': target,
'complex': 'true' if complex_default else 'false',
'complex': _eval_complex_literal(_record_complex(record, default=complex_default)),
}
)
return output.getvalue()
@@ -709,6 +722,29 @@ def build_training_instruction(
return instruction
def build_planning_prompt(
record: dict[str, Any],
*,
session_num: int = 5,
session_time_minutes: int = 5,
context_fields: list[str] | None = None,
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
) -> str:
"""把训练 instruction 包成评测侧使用的 chat template。"""
instruction = build_training_instruction(
record,
session_num=session_num,
session_time_minutes=session_time_minutes,
context_fields=context_fields,
)
return (
f'<|im_start|>system\n{system_prompt}<|im_end|>\n'
f'<|im_start|>user\n{instruction}<|im_end|>\n'
'<|im_start|>assistant\n'
)
def _training_jsonl_line(
record: dict[str, Any],
*,
@@ -717,7 +753,6 @@ def _training_jsonl_line(
context_fields: list[str],
system_prompt: str,
) -> str:
label = record.get('label') if isinstance(record.get('label'), dict) else {}
payload = {
'system': system_prompt,
'instruction': build_training_instruction(
@@ -726,7 +761,7 @@ def _training_jsonl_line(
session_time_minutes=session_time_minutes,
context_fields=context_fields,
),
'output': str(label.get('target') or ''),
'output': _combined_function_label(record),
}
return json.dumps(payload, ensure_ascii=False, separators=(',', ':'))
@@ -797,6 +832,7 @@ def _optional_int_for_export(value: Any) -> int | None:
def _category_label_from_target(target: str) -> str:
target = _strip_complex_prefix(target)
agent_match = re.match(r'''^Agent\s*\(\s*tag\s*=\s*["']([^"']+)["']\s*\)$''', target.strip())
if agent_match:
return agent_match.group(1)
@@ -835,7 +871,7 @@ def _dataset_record_table_row(record: dict[str, Any]) -> dict[str, str]:
'context': json.dumps(context, ensure_ascii=False, separators=(',', ':')),
'label': str(label.get('dataset_label') or ''),
'是否迁移Function': '',
'function': str(label.get('target') or ''),
'function': _combined_function_label(record),
}
@@ -875,7 +911,7 @@ def _parse_draft_text(draft_text: str) -> dict[str, Any]:
if current is None:
continue
field_match = re.match(r'^(用户|小爱|target|notes|dataset_label|request_id|timestamp)\s*[:]\s*(.*)$', line, re.IGNORECASE)
field_match = re.match(r'^(用户|小爱|target|complex|notes|dataset_label|request_id|timestamp)\s*[:]\s*(.*)$', line, re.IGNORECASE)
if not field_match:
continue
key = field_match.group(1).lower()
@@ -925,7 +961,7 @@ def _normalize_target_definitions(value: list[dict[str, Any]] | None) -> list[di
if not isinstance(item, dict):
raise DataRecordError(f'target_definitions[{index}] must be an object')
name = str(item.get('name') or '').strip()
target = str(item.get('target') or '').strip()
target = _normalize_target_expression(str(item.get('target') or ''))
rule = str(item.get('rule') or '').strip()
if not target:
raise DataRecordError(f'target_definitions[{index}].target is required')
@@ -958,10 +994,10 @@ def _allowed_targets_for_plan(plan: dict[str, Any]) -> set[str]:
if isinstance(target_definitions, list):
for item in target_definitions:
if isinstance(item, dict) and isinstance(item.get('target'), str) and item['target'].strip():
allowed.add(item['target'].strip())
allowed.add(_normalize_target_expression(item['target']))
target = plan.get('target')
if isinstance(target, str) and target.strip():
allowed.add(target.strip())
allowed.add(_normalize_target_expression(target))
return allowed
@@ -980,7 +1016,7 @@ def _validate_plan_against_goal(
goal_targets = _allowed_targets_for_goal(goal)
plan_targets = set()
if target.strip():
plan_targets.add(target.strip())
plan_targets.add(_normalize_target_expression(target))
for item in target_definitions:
item_target = item.get('target', '').strip()
if item_target:
@@ -998,9 +1034,8 @@ def _validate_records_against_plan(records: list[dict[str, Any]], plan: dict[str
return
unexpected: list[str] = []
for index, record in enumerate(records):
label = record.get('label')
target = label.get('target') if isinstance(label, dict) else None
if isinstance(target, str) and target.strip() in allowed_targets:
target = _record_target(record)
if target and target in allowed_targets:
continue
unexpected.append(f'records[{index}].label.target={target!r}')
if unexpected:
@@ -1148,9 +1183,14 @@ def _case_to_record(
if not current_query:
raise DataRecordError(f'case {index} current 用户 line must be non-empty')
target = _canonical_target(str(case.get('target') or '').strip())
raw_target, prefixed_complex = _split_complex_prefixed_target(str(case.get('target') or '').strip())
target = _canonical_target(raw_target)
if not target:
raise DataRecordError(f'case {index} target is required')
complex_value = _parse_complex(
case.get('complex'),
default=False if prefixed_complex is None else prefixed_complex,
)
prev_session = _build_prev_session(turns[:final_user_index], case_index=index)
if len(prev_session) > 10:
@@ -1183,6 +1223,9 @@ def _case_to_record(
'target': target,
'target_type': _target_type(target),
},
'dimensions': {
'complex': complex_value,
},
'meta': {
'case_name': str(case.get('case_name') or '').strip(),
'notes': str(case.get('notes') or '').strip(),
@@ -1209,7 +1252,75 @@ def _build_prev_session(turns: list[dict[str, Any]], *, case_index: int) -> list
return prev_session
def _normalize_target_expression(target: str) -> str:
"""把可能带 complex 前缀的标签表达式收敛为纯 target。"""
return _canonical_target(_strip_complex_prefix(target))
def _strip_complex_prefix(target: str) -> str:
stripped_target, _complex_value = _split_complex_prefixed_target(target)
return stripped_target
def _split_complex_prefixed_target(target: str) -> tuple[str, bool | None]:
lines = target.strip().splitlines()
if not lines:
return '', None
first_line = lines[0].strip()
match = re.fullmatch(r'complex\s*=\s*(.+)', first_line, flags=re.IGNORECASE)
if not match:
return target.strip(), None
complex_value = _parse_complex(match.group(1), default=False)
return '\n'.join(lines[1:]).strip(), complex_value
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 DataRecordError('complex must be a boolean value such as true/false')
def _complex_literal(value: bool) -> str:
return 'true' if value else 'false'
def _eval_complex_literal(value: bool) -> str:
return 'TRUE' if value else 'FALSE'
def _record_target(record: dict[str, Any]) -> str:
label = record.get('label') if isinstance(record.get('label'), dict) else {}
return _normalize_target_expression(str(label.get('target') or ''))
def _record_complex(record: dict[str, Any], *, default: bool = False) -> bool:
dimensions = record.get('dimensions')
if isinstance(dimensions, dict) and 'complex' in dimensions:
return _parse_complex(dimensions.get('complex'), default=default)
label = record.get('label') if isinstance(record.get('label'), dict) else {}
if 'complex' in label:
return _parse_complex(label.get('complex'), default=default)
_target, prefixed_complex = _split_complex_prefixed_target(str(label.get('target') or ''))
if prefixed_complex is not None:
return prefixed_complex
return default
def _combined_function_label(record: dict[str, Any], *, default_complex: bool = False) -> str:
target = _record_target(record)
return f'complex={_complex_literal(_record_complex(record, default=default_complex))}\n{target}'
def _target_type(target: str) -> str:
target = _strip_complex_prefix(target)
if re.match(r'^Agent\s*\(\s*tag\s*=', target):
return 'agent'
if target:
@@ -1220,6 +1331,7 @@ def _target_type(target: str) -> str:
def _canonical_target(target: str) -> str:
"""规范常见 Agent 标签写法,允许草稿里用单引号降低 JSON 转义风险。"""
target = _strip_complex_prefix(target)
match = re.fullmatch(r'Agent\s*\(\s*tag\s*=\s*([\'"])(.+?)\1\s*\)', target)
if not match:
return target