feat(data-process): 支持任务重新生成

This commit is contained in:
caoxiaozhu
2026-07-25 22:40:55 +08:00
parent 64d7414b04
commit 396d3f6f47
5 changed files with 667 additions and 4 deletions

View File

@@ -20,6 +20,28 @@ from app.modules.data_process.algorithms import estimate_token_count, stable_spl
TASK_STATUSES = {"pending", "running", "completed", "failed", "stopped"}
EDITABLE_STATUSES = {"pending", "failed", "stopped", "completed"}
_PREVIEW_CONFIG_ALIASES = {
"preprocess_options": "preprocessOptions",
"chunk_method": "chunkMethod",
"chunk_size": "chunkSize",
"chunk_overlap": "chunkOverlap",
"min_chunk_size": "minChunkSize",
"semantic_breakpoint_percentile": "semanticBreakpointPercentile",
"preserve_tables": "preserveTables",
"preserve_code_blocks": "preserveCodeBlocks",
"preserve_lists": "preserveLists",
}
_UNSTRUCTURED_PREVIEW_DEFAULTS: dict[str, Any] = {
"chunk_method": "layout_hybrid",
"chunk_size": 800,
"chunk_overlap": 100,
"min_chunk_size": 100,
"semantic_breakpoint_percentile": 95,
"preserve_tables": True,
"preserve_code_blocks": True,
"preserve_lists": True,
}
class DataProcessStoreError(RuntimeError):
pass
@@ -64,6 +86,49 @@ def _json_value(value: Any, default: Any) -> Any:
return default
def _preview_config_value(config: dict[str, Any], key: str, default: Any) -> Any:
if key in config:
return config[key]
return config.get(_PREVIEW_CONFIG_ALIASES[key], default)
def _normalized_preprocess_options(config: dict[str, Any]) -> Any:
value = _preview_config_value(config, "preprocess_options", [])
if isinstance(value, (list, tuple, set)):
return tuple(sorted({str(item) for item in value}))
return value
def _preview_config_projection(
process_type: str,
config: dict[str, Any],
) -> dict[str, Any]:
"""只投影会改变预览切片的配置。
生成模型、提示词、温度等参数不影响源文切片,因此不应该
破坏用户已经校对过的预览内容。
"""
projection: dict[str, Any] = {
"preprocess_options": _normalized_preprocess_options(config),
}
if process_type != "unstructured":
return projection
for key, default in _UNSTRUCTURED_PREVIEW_DEFAULTS.items():
projection[key] = _preview_config_value(config, key, default)
return projection
def _preview_config_changed(
process_type: str,
current_config: dict[str, Any],
next_config: dict[str, Any],
) -> bool:
return _preview_config_projection(process_type, current_config) != _preview_config_projection(
process_type, next_config
)
def _serialize_value(value: Any) -> Any:
if isinstance(value, (datetime, date)):
return value.isoformat().replace("+00:00", "Z")
@@ -361,6 +426,102 @@ class DataProcessStore:
raise ConflictError("data process task name already exists") from exc
return _decode_row(row) or {}
def prepare_regeneration(
self,
task_id: str,
payload: dict[str, Any],
) -> dict[str, Any]:
"""在一个事务中将已有任务恢复为可重新生成状态。
已发布数据集是可被其他任务使用的独立产物,此处只解除当前任务的
输出指针,不删除数据集实体。用户完成新一轮生成后,可再次发布
以原子替换三个分割数据集的内容。
"""
try:
with self.connect() as conn:
task = self._task_in_connection(conn, task_id, for_update=True)
if task["status"] == "running":
raise ConflictError("running task cannot be prepared for regeneration")
current_updated_at = _serialize_value(task.get("updated_at"))
if payload["expected_updated_at"] != current_updated_at:
raise ConflictError("data process task was modified by another request")
process_type = str(payload["process_type"])
if process_type != str(task["process_type"]):
raise InvalidStateError("process_type cannot be changed during regeneration")
current_config = dict(task.get("config") or {})
next_config = dict(payload.get("config") or {})
preview_invalidated = _preview_config_changed(
process_type,
current_config,
next_config,
)
published_row = conn.execute(
"""
SELECT EXISTS(
SELECT 1 FROM datasets
WHERE source_task_id=%s AND source='task'
) AS exists
""",
(task_id,),
).fetchone()
published_outputs_preserved = bool(task.get("output_dataset_id")) or bool(
published_row and published_row.get("exists")
)
# 删除顺序保证结果不再指向即将被替换的切片。
conn.execute(
"DELETE FROM data_process_results WHERE task_id=%s",
(task_id,),
)
if preview_invalidated:
conn.execute(
"DELETE FROM data_process_preview_items WHERE task_id=%s",
(task_id,),
)
preview_count = 0
else:
preview_row = conn.execute(
"""
SELECT COUNT(*) AS count FROM data_process_preview_items
WHERE task_id=%s
""",
(task_id,),
).fetchone()
preview_count = int((preview_row or {}).get("count") or 0)
now = utcnow()
row = conn.execute(
"""
UPDATE data_process_tasks
SET name=%s, description=%s, config=%s, status='pending', progress=%s,
output_dataset_id=NULL, output_count=0, filtered_count=0,
duplicate_count=0, error_count=0, failure_reason=NULL,
generation_run_id=NULL, started_at=NULL, completed_at=NULL,
updated_at=%s
WHERE id=%s
RETURNING *
""",
(
payload["name"],
payload.get("description") or "",
json_dumps(next_config),
20 if preview_count else 0,
now,
task_id,
),
).fetchone()
except psycopg.errors.UniqueViolation as exc:
raise ConflictError("data process task name already exists") from exc
return {
"task": _decode_row(row) or {},
"preview_invalidated": preview_invalidated,
"published_outputs_preserved": published_outputs_preserved,
}
def delete_task(self, task_id: str, *, deleted_by: str | None = None) -> None:
with self.connect() as conn:
task = self._task_in_connection(conn, task_id, for_update=True)
@@ -480,12 +641,21 @@ class DataProcessStore:
),
).fetchone()
created.append(_decode_row(row) or {})
# 前端会只对本次新增的源文件构建预览,因此必须保留旧
# 文件的切片。任何源文件增加都会使旧生成结果失效。
conn.execute("DELETE FROM data_process_results WHERE task_id=%s", (task_id,))
conn.execute("DELETE FROM data_process_preview_items WHERE task_id=%s", (task_id,))
preview_row = conn.execute(
"""
SELECT COUNT(*) AS count FROM data_process_preview_items
WHERE task_id=%s
""",
(task_id,),
).fetchone()
preview_count = int((preview_row or {}).get("count") or 0)
conn.execute(
"""
UPDATE data_process_tasks
SET status='pending', progress=0, output_count=0, filtered_count=0,
SET status='pending', progress=%s, output_count=0, filtered_count=0,
duplicate_count=0, error_count=0, failure_reason=NULL,
generation_run_id=NULL, started_at=NULL, completed_at=NULL, input_count=(
SELECT COALESCE(SUM(record_count), 0)
@@ -494,7 +664,7 @@ class DataProcessStore:
), updated_at=%s
WHERE id=%s
""",
(task_id, now, task_id),
(20 if preview_count else 0, task_id, now, task_id),
)
except psycopg.errors.UniqueViolation as exc:
raise ConflictError(