Files
crm-system/app/models/workflow.py
T
leocrm-bot 14bd4e33fb 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
2026-06-29 02:44:13 +02:00

90 lines
3.8 KiB
Python

"""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)