T09: KI-Copilot API + Hybrid Workflow Engine + LLM client + event-triggered workflows
- KI-Copilot: NL query → proposed actions, execute with RBAC, history, audit logging - LLM client: mock mode (no API key) + OpenAI-compatible mode (AI_MODEL/AI_API_KEY) - Action mapper: NL intent → API calls (create/update/delete/search company/contact) - Workflow engine: step types (action/approval/notification/condition), JSONB steps - Workflow lifecycle: pending → in_progress → completed/rejected/cancelled - Event-triggered workflows: event bus → auto-start instances - Code-engine workflows: onboarding on user.created event - Approval timeout: auto-reject after configured hours - 5 new tenant-scoped tables with RLS: ai_conversations, ai_messages, workflows, workflow_instances, workflow_step_history - Migration 0004: all tables + RLS policies + tenant_id + indexes - 238 tests pass (30 AC + 105 coverage + 103 existing), 84.12% T09 module coverage - MissingGreenlet fix: safe accessor helpers for async ORM attribute access
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""Workflow, WorkflowInstance, and WorkflowStepHistory models — tenant-scoped with RLS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import String, Text, DateTime, ForeignKey, func, Index, Integer, Boolean
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID, JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
|
||||
|
||||
class Workflow(Base, TenantMixin):
|
||||
"""Workflow definition — a template of steps stored as JSONB, tenant-scoped."""
|
||||
|
||||
__tablename__ = "workflows"
|
||||
__table_args__ = (
|
||||
Index("ix_workflows_tenant_active", "tenant_id", "is_active"),
|
||||
Index("ix_workflows_tenant_trigger", "tenant_id", "trigger_event"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
trigger_event: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
steps: Mapped[list[dict[str, Any]]] = mapped_column(JSONB, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class WorkflowInstance(Base, TenantMixin):
|
||||
"""A running instance of a workflow — tracks current step, status, context."""
|
||||
|
||||
__tablename__ = "workflow_instances"
|
||||
__table_args__ = (
|
||||
Index("ix_wf_instances_tenant_status", "tenant_id", "status"),
|
||||
Index("ix_wf_instances_tenant_workflow", "tenant_id", "workflow_id"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
workflow_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("workflows.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending")
|
||||
# pending → in_progress → completed / rejected / cancelled
|
||||
current_step_index: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
context: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False)
|
||||
initiated_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
timeout_hours: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
timeout_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class WorkflowStepHistory(Base, TenantMixin):
|
||||
"""Immutable record of every step transition in a workflow instance."""
|
||||
|
||||
__tablename__ = "workflow_step_history"
|
||||
__table_args__ = (
|
||||
Index("ix_wf_step_history_tenant_instance", "tenant_id", "instance_id"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
instance_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("workflow_instances.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
step_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
step_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(50), nullable=False) # entered, approved, rejected, skipped, cancelled
|
||||
actor_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
details: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
|
||||
Reference in New Issue
Block a user