Hermes Agent b9a5810d85 feat(v3): human-in-the-loop — interactive steps, pluggable step_type registry
- New states: waiting (step/task), rejected (step)
- New tables: pipeline_human_tasks, pipeline_step_types
- New module: step_registry.py — pluggable step_type metadata
- New module: human.py — human_complete, approval_approve, approval_reject
- Executor: detects interactive step_types, creates human_tasks, enters waiting
- Reject with rollback: approval_reject(rollback_to=step) resets steps and re-runs
- API: human_task_complete, approval_approve, approval_reject, human_task_list
- API: pipeline_step_types, pipeline_register_step_type, pipeline_unregister_step_type
- Built-in interactive types: human_task, approval_gate
- Updated DDL and appcodes
2026-06-16 11:05:45 +08:00

82 lines
2.6 KiB
Python

"""Step type registry — pluggable step_type metadata.
Each pipeline can define its own step_types. The registry tracks:
- handler function (already in handler.py)
- metadata: display_name, category, is_interactive, form_schema, on_timeout
Built-in interactive types: human_task, approval_gate
"""
import logging
from typing import Dict, Optional
logger = logging.getLogger("pipeline.step_registry")
# step_type -> metadata dict
_REGISTRY: Dict[str, dict] = {}
# Built-in interactive step types
BUILTIN_INTERACTIVE = {
"human_task": {
"display_name": "人工任务",
"category": "interactive",
"is_interactive": True,
"description": "需要人工填写表单或执行操作后继续",
},
"approval_gate": {
"display_name": "审批关卡",
"category": "interactive",
"is_interactive": True,
"description": "需要审批人通过后继续,可驳回",
},
}
def register_step_type(step_type: str, metadata: dict):
"""Register a step type with metadata.
Args:
step_type: unique key matching pipeline_steps.step_type
metadata: dict with keys:
- display_name (str): 显示名称
- category (str): 分类 (media/llm/interactive/devops/...)
- is_interactive (bool): 是否需要人工介入, default False
- description (str): 描述
- form_schema (dict): 人工任务表单JSON Schema (可选)
- on_timeout (str): 超时策略 skip/escalate/fail (可选)
- timeout_hours (int): 超时小时数 (可选)
"""
existing = _REGISTRY.get(step_type, {})
existing.update(metadata)
_REGISTRY[step_type] = existing
logger.info(f"Registered step_type: {step_type} (interactive={metadata.get('is_interactive', False)})")
def get_step_type(step_type: str) -> Optional[dict]:
"""Get step type metadata. Returns None if not registered."""
return _REGISTRY.get(step_type)
def is_interactive(step_type: str) -> bool:
"""Check if a step type requires human interaction."""
meta = _REGISTRY.get(step_type, {})
return meta.get("is_interactive", False)
def list_step_types() -> Dict[str, dict]:
"""List all registered step types with metadata."""
return dict(_REGISTRY)
def unregister_step_type(step_type: str):
"""Remove a step type from registry."""
removed = _REGISTRY.pop(step_type, None)
if removed:
logger.info(f"Unregistered step_type: {step_type}")
def load_builtin_types():
"""Load built-in interactive step types."""
for st, meta in BUILTIN_INTERACTIVE.items():
register_step_type(st, meta)