638e3f3e1e
Check Cross-Plugin Imports / check (push) Has been cancelled
- F-PERM: app/ai/agent_permissions.py (230 lines) — AgentPermissionContext, resolve_agent_permissions(), filter_visible_agents(), check_agent_execute_permission(), optimistic locking - F-APPR: app/core/approval.py (160 lines) + app/routes/approvals.py (305 lines) + migration 0123 — ApprovalRequest model, CRUD API, approve/reject/expire - F-AIUSE: app/ai/ai_use_case.py (156 lines) — AIUseCaseMetadata Pydantic model, validate_ai_use_case() - F-TRANS: app/ai/transparency.py (60 lines) — mark_as_ai_generated(), is_ai_participant() - F-DATA-POL: app/ai/data_policy.py (210 lines) — enforce_data_policy() with SENSITIVE_FIELDS + provider compliance - F-OVERSIGHT: app/ai/oversight.py (108 lines) — DecisionRecord, create_decision_record() - F-DRY: agent_loop.py updated with dry_run parameter - F-AUDIT: agent_loop.py updated with audit log for tool calls - agent_routes.py: AI use case metadata endpoints added - main.py: approval routes registered - All Python compile checks pass
109 lines
3.6 KiB
Python
109 lines
3.6 KiB
Python
"""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
|