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:
leocrm-bot
2026-06-29 02:44:13 +02:00
parent 7a5a48fb4c
commit 14bd4e33fb
31 changed files with 5884 additions and 3 deletions
+112
View File
@@ -0,0 +1,112 @@
"""Onboarding workflow — runs on user creation.
A code-engine workflow that creates a welcome notification and an
approval step for admin confirmation of new users.
"""
from __future__ import annotations
import uuid
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.workflow_service import create_instance
# Onboarding workflow definition (code-engine — hardcoded steps)
ONBOARDING_WORKFLOW_STEPS = [
{
"name": "Send welcome notification",
"type": "notification",
"config": {
"notification_type": "welcome",
"title": "Welcome to LeoCRM!",
"body": "Your account has been created. An admin will approve your access shortly.",
},
"description": "Send a welcome notification to the new user",
},
{
"name": "Admin approval",
"type": "approval",
"config": {
"required_role": "admin",
"description": "Admin must approve the new user account",
},
"description": "Admin reviews and approves the new user",
},
{
"name": "Send confirmation",
"type": "notification",
"config": {
"notification_type": "onboarding_complete",
"title": "Account approved",
"body": "Your account has been approved. You can now use LeoCRM.",
},
"description": "Notify user that their account is approved",
},
]
def get_onboarding_workflow_definition() -> dict[str, Any]:
"""Return the onboarding workflow definition for DB seeding."""
return {
"name": "User Onboarding",
"description": "Automated onboarding workflow triggered on user creation",
"trigger_event": "user.created",
"steps": ONBOARDING_WORKFLOW_STEPS,
"is_active": True,
}
async def ensure_onboarding_workflow_exists(
db: AsyncSession,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
) -> uuid.UUID | None:
"""Ensure the onboarding workflow exists in the DB for this tenant.
If it doesn't exist yet, create it. Returns the workflow ID.
"""
from app.models.workflow import Workflow
from sqlalchemy import select
result = await db.execute(
select(Workflow).where(
Workflow.tenant_id == tenant_id,
Workflow.trigger_event == "user.created",
Workflow.name == "User Onboarding",
)
)
existing = result.scalar_one_or_none()
if existing:
return existing.id
from app.services.workflow_service import create_workflow
wf_dict = await create_workflow(
db, tenant_id, user_id,
get_onboarding_workflow_definition(),
)
return uuid.UUID(wf_dict["id"]) if wf_dict else None
async def trigger_onboarding(
db: AsyncSession,
tenant_id: uuid.UUID,
admin_user_id: uuid.UUID,
new_user_id: uuid.UUID,
) -> dict[str, Any] | None:
"""Trigger the onboarding workflow for a newly created user.
1. Ensure the onboarding workflow exists in DB
2. Create a workflow instance with new_user_id in context
"""
wf_id = await ensure_onboarding_workflow_exists(db, tenant_id, admin_user_id)
if wf_id is None:
return None
instance = await create_instance(
db, tenant_id, admin_user_id,
str(wf_id),
context={"new_user_id": str(new_user_id), "user_id": str(new_user_id)},
)
return instance