"""Human oversight and decision records for AI agents. Stores a durable audit trail of AI recommendations and the human decisions made on them. A ``DecisionRecord`` captures the recommendation, the evidence that supported it, and the reviewer's decision (approved / rejected) with an explanation when the decision deviates from the recommendation. Used by: - ``app/ai/agent_loop.py`` — recording recommendations that need review - ``app/plugins/builtins/automation`` — agent run oversight """ from __future__ import annotations import uuid from dataclasses import dataclass, field from datetime import UTC, datetime from typing import Any from sqlalchemy import String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin @dataclass class DecisionRecord: """A recommendation and its human decision, for the audit trail. Attributes: agent_run_id: The agent run this decision belongs to. recommendation: The AI's recommendation text. evidence: Supporting data for the recommendation. reviewer_id: The human reviewer (None while pending). decision: ``approved``, ``rejected``, or ``None`` (pending). decision_timestamp: ISO timestamp of the decision (None while pending). deviation_note: Explanation when the decision differs from the recommendation. """ agent_run_id: uuid.UUID recommendation: str evidence: dict[str, Any] = field(default_factory=dict) reviewer_id: uuid.UUID | None = None decision: str | None = None # "approved", "rejected", None (pending) decision_timestamp: str | None = None deviation_note: str | None = None class DecisionRecordDB(Base, TenantMixin, OwnedMixin): """Persistent storage for AI decision records (audit trail).""" __tablename__ = "ai_decision_records" id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) agent_run_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), nullable=False, index=True ) recommendation: Mapped[str] = mapped_column(Text, nullable=False) evidence: Mapped[dict[str, Any]] = mapped_column( JSONB, nullable=False, default=dict ) reviewer_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), nullable=True ) decision: Mapped[str | None] = mapped_column(String(20), nullable=True) decision_timestamp: Mapped[str | None] = mapped_column( String(40), nullable=True ) deviation_note: Mapped[str | None] = mapped_column(Text, nullable=True) async def create_decision_record( db: AsyncSession, tenant_id: uuid.UUID, record: DecisionRecord, ) -> uuid.UUID: """Store a decision record for the audit trail. Args: db: Async DB session. tenant_id: Tenant ID. record: The decision record to persist. Returns: The UUID of the created record. """ entry = DecisionRecordDB( tenant_id=tenant_id, agent_run_id=record.agent_run_id, recommendation=record.recommendation, evidence=record.evidence or {}, reviewer_id=record.reviewer_id, decision=record.decision, decision_timestamp=record.decision_timestamp or (datetime.now(UTC).isoformat() if record.decision else None), deviation_note=record.deviation_note, owner_id=record.reviewer_id, ) db.add(entry) await db.flush() return entry.id