Add data agent input and export workflow

This commit is contained in:
武阳
2026-04-30 15:11:37 +08:00
parent e3664f8843
commit 5bdc0ffb01
9 changed files with 1787 additions and 237 deletions
+258
View File
@@ -10,18 +10,109 @@ Skill 应该先让模型生成紧凑、便于人工 review 的 dataset draft tex
import json
import re
import time
from pathlib import Path
from typing import Any
DEFAULT_REQUEST_ID = 'aabbccdd'
DEFAULT_TIMESTAMP_STEP_MS = 60_000
DEFAULT_PLAN_STATE_FILE = '.port_sessions/data_agent_generation_plans.json'
DEFAULT_GOAL_STATE_FILE = '.port_sessions/data_agent_generation_goals.json'
class DataRecordError(ValueError):
"""Raised when dataset draft text or records are structurally invalid."""
def prepare_generation_goal(
*,
root: str,
dataset_label: str,
goal_summary: str,
coverage: str,
exclusions: str,
target: str = '',
target_definitions: list[dict[str, Any]] | None = None,
plan_hint: str = '',
open_questions: list[str] | None = None,
source_refs: list[str] | None = None,
notes: str = '',
) -> dict[str, Any]:
missing = _missing_plan_fields(
{
'dataset_label': dataset_label,
'goal_summary': goal_summary,
'coverage': coverage,
'exclusions': exclusions,
}
)
if missing:
raise DataRecordError('missing required goal fields: ' + ', '.join(missing))
normalized_targets = _normalize_target_definitions(target_definitions)
state = _load_goal_state(root)
sequence = int(state.get('next_sequence') or 1)
goal_id = f'data_goal_{sequence:06d}'
goal = {
'goal_id': goal_id,
'status': 'pending_confirmation',
'dataset_label': dataset_label.strip(),
'goal_summary': goal_summary.strip(),
'target': target.strip(),
'target_definitions': normalized_targets,
'plan_hint': plan_hint.strip(),
'coverage': coverage.strip(),
'exclusions': exclusions.strip(),
'open_questions': _normalize_string_list(open_questions, 'open_questions'),
'source_refs': _normalize_string_list(source_refs, 'source_refs'),
'notes': notes.strip(),
'revision': 1,
'created_at_ms': int(time.time() * 1000),
}
goals = state.setdefault('goals', {})
if not isinstance(goals, dict):
goals = {}
state['goals'] = goals
goals[goal_id] = goal
state['next_sequence'] = sequence + 1
_save_goal_state(root, state)
return {
'goal': goal,
'message': 'generation goal 已创建,状态为 pending_confirmation。请向用户展示 goal,并等待用户确认后再创建 generation plan。',
}
def confirm_generation_goal(
*,
root: str,
goal_id: str,
confirmation: str,
reviewed_revision: int | None = None,
) -> dict[str, Any]:
if not goal_id.strip():
raise DataRecordError('goal_id is required')
if not _is_confirmation_text(confirmation):
raise DataRecordError('confirmation must clearly approve the generation goal')
state = _load_goal_state(root)
goal = _get_goal(state, goal_id)
current_revision = int(goal.get('revision') or 1)
if reviewed_revision is not None and reviewed_revision != current_revision:
raise DataRecordError(
f'reviewed_revision must match current goal revision {current_revision}'
)
if not _allowed_targets_for_goal(goal):
raise DataRecordError('confirmed generation goal must include target or target_definitions')
goal['status'] = 'confirmed'
goal['confirmed_at_ms'] = int(time.time() * 1000)
goal['confirmation'] = confirmation.strip()
goal['confirmed_revision'] = current_revision
_save_goal_state(root, state)
return {
'goal': goal,
'confirmed_goal_id': goal_id,
'message': 'generation goal 已确认,可以把 confirmed_goal_id 传给 data_agent_prepare_generation_plan。',
}
def normalize_dataset_draft(
draft_text: str,
*,
@@ -80,7 +171,11 @@ def prepare_generation_plan(
output_path: str,
target_definitions: list[dict[str, Any]] | None = None,
notes: str = '',
confirmed_goal_id: str | None = None,
) -> dict[str, Any]:
confirmed_goal: dict[str, Any] | None = None
if confirmed_goal_id is not None:
confirmed_goal = _require_confirmed_generation_goal(root, confirmed_goal_id)
missing = _missing_plan_fields(
{
'dataset_label': dataset_label,
@@ -97,6 +192,13 @@ def prepare_generation_plan(
raise DataRecordError('missing required plan fields: ' + ', '.join(missing))
if total_count <= 0:
raise DataRecordError('total_count must be greater than 0')
if confirmed_goal is not None:
_validate_plan_against_goal(
dataset_label=dataset_label,
target=target,
target_definitions=normalized_targets,
goal=confirmed_goal,
)
state = _load_plan_state(root)
sequence = int(state.get('next_sequence') or 1)
@@ -115,6 +217,7 @@ def prepare_generation_plan(
'output_path': resolved_output_path,
'requested_output_path': output_path.strip(),
'notes': notes.strip(),
'confirmed_goal_id': confirmed_goal_id or '',
'revision': 1,
'review_history': [],
'created_at_ms': int(time.time() * 1000),
@@ -342,6 +445,56 @@ def validate_dataset_records(records: list[dict[str, Any]]) -> dict[str, Any]:
}
def export_dataset_records(
records: list[dict[str, Any]],
*,
root: str,
output_path: str,
output_format: str = 'jsonl',
require_validation_ok: bool = True,
overwrite: bool = True,
) -> dict[str, Any]:
"""校验并导出 canonical records。
这个函数用于收口数据智能体的最终落盘动作,避免模型手写 JSON/JSONL。
默认导出紧凑 JSONL:一行一个 canonical record,方便 diff、抽样和增量处理。
"""
if output_format not in {'jsonl', 'json'}:
raise DataRecordError('output_format must be jsonl or json')
if not output_path.strip():
raise DataRecordError('output_path is required')
validation = validate_dataset_records(records)
if require_validation_ok and not validation['ok']:
raise DataRecordError(
f'records failed validation with {validation["error_count"]} errors'
)
path = _resolve_output_path(root, output_path)
if path.exists() and not overwrite:
raise DataRecordError(f'output_path already exists: {output_path}')
path.parent.mkdir(parents=True, exist_ok=True)
if output_format == 'jsonl':
content = ''.join(
json.dumps(record, ensure_ascii=False, separators=(',', ':')) + '\n'
for record in records
)
else:
content = json.dumps(records, ensure_ascii=False, separators=(',', ':')) + '\n'
path.write_text(content, encoding='utf-8')
rel_path = str(path.relative_to(Path(root).resolve()))
return {
'output_path': rel_path,
'output_format': output_format,
'record_count': len(records),
'bytes_written': len(content.encode('utf-8')),
'validation': validation,
}
def _parse_draft_text(draft_text: str) -> dict[str, Any]:
dataset_label = ''
cases: list[dict[str, Any]] = []
@@ -418,6 +571,25 @@ def _normalize_target_definitions(value: list[dict[str, Any]] | None) -> list[di
return normalized
def _normalize_string_list(value: list[str] | None, field_name: str) -> list[str]:
if value is None:
return []
if not isinstance(value, list):
raise DataRecordError(f'{field_name} must be a list')
normalized: list[str] = []
for index, item in enumerate(value):
if not isinstance(item, str):
raise DataRecordError(f'{field_name}[{index}] must be a string')
text = item.strip()
if text:
normalized.append(text)
return normalized
def _allowed_targets_for_goal(goal: dict[str, Any]) -> set[str]:
return _allowed_targets_for_plan(goal)
def _allowed_targets_for_plan(plan: dict[str, Any]) -> set[str]:
target_definitions = plan.get('target_definitions')
allowed: set[str] = set()
@@ -431,6 +603,33 @@ def _allowed_targets_for_plan(plan: dict[str, Any]) -> set[str]:
return allowed
def _validate_plan_against_goal(
*,
dataset_label: str,
target: str,
target_definitions: list[dict[str, str]],
goal: dict[str, Any],
) -> None:
goal_label = str(goal.get('dataset_label') or '').strip()
if goal_label and dataset_label.strip() != goal_label:
raise DataRecordError(
f'plan dataset_label must match confirmed generation goal: {goal_label}'
)
goal_targets = _allowed_targets_for_goal(goal)
plan_targets = set()
if target.strip():
plan_targets.add(target.strip())
for item in target_definitions:
item_target = item.get('target', '').strip()
if item_target:
plan_targets.add(item_target)
unexpected = sorted(plan_targets - goal_targets)
if unexpected:
raise DataRecordError(
'plan targets must match confirmed generation goal: ' + ', '.join(unexpected)
)
def _validate_records_against_plan(records: list[dict[str, Any]], plan: dict[str, Any]) -> None:
allowed_targets = _allowed_targets_for_plan(plan)
if not allowed_targets:
@@ -475,6 +674,12 @@ def _plan_state_path(root: str):
return Path(root).resolve() / DEFAULT_PLAN_STATE_FILE
def _goal_state_path(root: str):
from pathlib import Path
return Path(root).resolve() / DEFAULT_GOAL_STATE_FILE
def _load_plan_state(root: str) -> dict[str, Any]:
path = _plan_state_path(root)
if not path.exists():
@@ -492,12 +697,35 @@ def _load_plan_state(root: str) -> dict[str, Any]:
return payload
def _load_goal_state(root: str) -> dict[str, Any]:
path = _goal_state_path(root)
if not path.exists():
return {'next_sequence': 1, 'goals': {}}
try:
payload = json.loads(path.read_text(encoding='utf-8'))
except json.JSONDecodeError as exc:
raise DataRecordError(f'invalid generation goal state file: {path}') from exc
if not isinstance(payload, dict):
raise DataRecordError(f'invalid generation goal state file: {path}')
if not isinstance(payload.get('goals'), dict):
payload['goals'] = {}
if not isinstance(payload.get('next_sequence'), int):
payload['next_sequence'] = 1
return payload
def _save_plan_state(root: str, state: dict[str, Any]) -> None:
path = _plan_state_path(root)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8')
def _save_goal_state(root: str, state: dict[str, Any]) -> None:
path = _goal_state_path(root)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8')
def _get_plan(state: dict[str, Any], plan_id: str) -> dict[str, Any]:
plans = state.get('plans')
if not isinstance(plans, dict):
@@ -508,6 +736,16 @@ def _get_plan(state: dict[str, Any], plan_id: str) -> dict[str, Any]:
return plan
def _get_goal(state: dict[str, Any], goal_id: str) -> dict[str, Any]:
goals = state.get('goals')
if not isinstance(goals, dict):
raise DataRecordError('generation goal state is invalid')
goal = goals.get(goal_id)
if not isinstance(goal, dict):
raise DataRecordError(f'generation goal not found: {goal_id}')
return goal
def _require_confirmed_generation_plan(root: str, plan_id: str) -> dict[str, Any]:
state = _load_plan_state(root)
plan = _get_plan(state, plan_id)
@@ -516,6 +754,14 @@ def _require_confirmed_generation_plan(root: str, plan_id: str) -> dict[str, Any
return plan
def _require_confirmed_generation_goal(root: str, goal_id: str) -> dict[str, Any]:
state = _load_goal_state(root)
goal = _get_goal(state, goal_id)
if goal.get('status') != 'confirmed':
raise DataRecordError(f'generation goal is not confirmed: {goal_id}')
return goal
def _case_to_record(
case: dict[str, Any],
*,
@@ -622,6 +868,18 @@ def _safe_token(value: str) -> str:
return token.strip('_') or 'unknown'
def _resolve_output_path(root: str, output_path: str) -> Path:
root_path = Path(root).resolve()
candidate = Path(output_path).expanduser()
path = candidate if candidate.is_absolute() else root_path / candidate
resolved = path.resolve(strict=False)
try:
resolved.relative_to(root_path)
except ValueError as exc:
raise DataRecordError(f'output_path escapes workspace root: {output_path}') from exc
return resolved
def _optional_int(value: Any) -> int | None:
if value is None or value == '':
return None