Files
AgentLab/backend/app/db/models_process.py
T

144 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""流程:待办、派给谁、生效记录、通知(应用平台 A4、A5,docs/design/app-platform-and-look.md §2.4、§2.5、§3.7、§3.8)。
一次中断对应一条 Approval(给引擎)和一条 Task(给人)。Task 下挂若干条 TaskAssignment:派给具体的人或组,
按 policy(any / all / quorum)汇总后才恢复运行。核实待办(kind=reconcile)没有 Approval,approval_id 为空。
ActionRecord 是写外部系统的权威记录:先写记录再发送;节点每次执行都先按 action_key 查,已生效的不再发,
结果未知的永不自动重发。
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, TimestampMixin, UTCDateTime, new_id, utcnow
#: 待办的种类。人工节点三种 + 现有的几种中断 + 动作确认、核实、等外部系统
TASK_KINDS = ("approve", "input", "edit", "tool", "code", "dual_check", "action_confirm", "reconcile", "signal")
#: pending 等人;resolving 已凑齐、正在恢复运行(条件 UPDATE 抢到的那个请求负责);resolved / cancelled / expired 终态
TASK_STATUSES = ("pending", "resolving", "resolved", "cancelled", "expired")
#: 动作状态。simulated:探索运行里端点没有 test 环境时的演练,不发送
ACTION_STATES = ("pending", "sent", "confirmed", "failed", "unknown", "simulated")
class Task(Base, TimestampMixin):
__tablename__ = "tasks"
__table_args__ = (Index("ix_tasks_status_due", "status", "due_at"),)
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
approval_id: Mapped[str | None] = mapped_column(String(32), default=None, unique=True)
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id", ondelete="CASCADE"), index=True)
node_id: Mapped[str | None] = mapped_column(String(64), default=None)
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
app_version: Mapped[int | None] = mapped_column(Integer, default=None)
kind: Mapped[str] = mapped_column(String(20), default="approve")
title: Mapped[str] = mapped_column(String(300), default="")
# agentlab.form/1;没有表单的(批准 / 驳回)为空
form: Mapped[dict[str, Any] | None] = mapped_column(default=None)
# {"kind": "any"} | {"kind": "all"} | {"kind": "quorum", "m": 2}
policy: Mapped[dict[str, Any]] = mapped_column(default=lambda: {"kind": "any"})
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
due_at: Mapped[datetime | None] = mapped_column(default=None)
remind_at: Mapped[datetime | None] = mapped_column(default=None)
# {"action": "remind" | "escalate" | "reject", "every"?: ISO 时长, "to_slot"?: 岗位}。没有「自动批准」
on_timeout: Mapped[dict[str, Any] | None] = mapped_column(default=None)
escalation_level: Mapped[int] = mapped_column(Integer, default=0)
# 等外部系统(kind=signal)时,入站 signal 按它找到这条待办
correlation_key: Mapped[str | None] = mapped_column(String(200), default=None, index=True)
# 判定结果:{"decision": ..., "values": ..., "signoffs": [...]}
outcome: Mapped[dict[str, Any] | None] = mapped_column(default=None)
resolved_by: Mapped[str | None] = mapped_column(String(100), default=None)
resolved_at: Mapped[datetime | None] = mapped_column(default=None)
class TaskAssignment(Base):
__tablename__ = "task_assignments"
__table_args__ = (Index("ix_assign_principal", "principal_kind", "principal", "status"),)
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
task_id: Mapped[str] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), index=True)
principal_kind: Mapped[str] = mapped_column(String(20)) # user | group | anyone | hook
principal: Mapped[str] = mapped_column(String(100), default="")
# 从哪来:slot:<岗位> | initiator | expression | escalation | reassign
source: Mapped[str] = mapped_column(String(40), default="")
status: Mapped[str] = mapped_column(String(20), default="pending") # pending | responded | withdrawn
response: Mapped[dict[str, Any] | None] = mapped_column(default=None)
responded_by: Mapped[str | None] = mapped_column(String(100), default=None)
responded_at: Mapped[datetime | None] = mapped_column(default=None)
# 回复人是不是认证过的身份(off 模式下是自报署名,记 False)
verified: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
class ActionRecord(Base, TimestampMixin):
__tablename__ = "action_records"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
# sha256(run_id, node_id, exec);同一次执行第 n 次发送的幂等键是 f"{action_key}:{attempt}"
action_key: Mapped[str] = mapped_column(String(64), index=True)
attempt: Mapped[int] = mapped_column(Integer, default=1)
idem_key: Mapped[str] = mapped_column(String(80), unique=True)
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id", ondelete="CASCADE"), index=True)
node_id: Mapped[str] = mapped_column(String(64))
exec_no: Mapped[int] = mapped_column(Integer, default=1)
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
workflow_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
workflow_version: Mapped[int | None] = mapped_column(Integer, default=None)
graph_hash: Mapped[str | None] = mapped_column(String(64), default=None)
capability: Mapped[str] = mapped_column(String(200))
capability_version: Mapped[int | None] = mapped_column(Integer, default=None)
endpoint_id: Mapped[str | None] = mapped_column(String(32), default=None)
environment: Mapped[str] = mapped_column(String(20), default="prod") # prod | test | simulated
payload_hash: Mapped[str] = mapped_column(String(64))
# 遮罩后的预览(确认卡上给人看的那一份)
preview: Mapped[dict[str, Any] | None] = mapped_column(default=None)
state: Mapped[str] = mapped_column(String(20), default="pending", index=True)
confirm_task_id: Mapped[str | None] = mapped_column(String(32), default=None)
request_at: Mapped[datetime | None] = mapped_column(default=None)
response_status: Mapped[int | None] = mapped_column(Integer, default=None)
# 回执:外部单号等,原文落工件,这里存 {artifact, summary}
receipt: Mapped[dict[str, Any] | None] = mapped_column(default=None)
error: Mapped[str | None] = mapped_column(Text, default=None)
lookup_attempts: Mapped[int] = mapped_column(Integer, default=0)
next_lookup_at: Mapped[datetime | None] = mapped_column(default=None)
# 核实结论:{"by": ..., "how": "lookup" | "manual", "outcome": "confirmed" | "not_applied" | "resend" | "abandon", "external_id"?}
resolution: Mapped[dict[str, Any] | None] = mapped_column(default=None)
class NotifyChannel(Base, TimestampMixin):
"""通知渠道。格式、签名、发送同定时任务的 Webhook(四种:generic / feishu / wecom / dingtalk)。"""
__tablename__ = "notify_channels"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
name: Mapped[str] = mapped_column(String(100))
scope: Mapped[str] = mapped_column(String(20), default="global") # global | app | user
owner_id: Mapped[str | None] = mapped_column(String(32), default=None)
format: Mapped[str] = mapped_column(String(20), default="generic")
url: Mapped[str] = mapped_column(Text) # 密文
secret: Mapped[str | None] = mapped_column(Text, default=None) # 密文
events: Mapped[list[Any]] = mapped_column(default=list)
enabled: Mapped[bool] = mapped_column(default=True)
class NotifyDelivery(Base):
"""通用投递记录。定时任务的投递仍写 webhook_deliveries(schedule_id 非空,SQLite 改不了可空性)。"""
__tablename__ = "notify_deliveries"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
channel_id: Mapped[str] = mapped_column(ForeignKey("notify_channels.id", ondelete="CASCADE"), index=True)
subject_kind: Mapped[str] = mapped_column(String(20)) # task | run | action | channel(渠道的测试消息)
subject_id: Mapped[str] = mapped_column(String(32))
event: Mapped[str] = mapped_column(String(40))
run_id: Mapped[str | None] = mapped_column(String(32), default=None)
task_id: Mapped[str | None] = mapped_column(String(32), default=None)
status: Mapped[str] = mapped_column(String(20))
http_status: Mapped[int | None] = mapped_column(Integer, default=None)
attempts: Mapped[int] = mapped_column(Integer, default=0)
error: Mapped[str | None] = mapped_column(Text, default=None)
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)