Files
leocrm/docs/plugin-development-guide.md
T
Agent Zero ec81940178 Phase 0 Complete: Tasks 0.7-0.20
- 0.7: UI-Design-Richtlinien (docs/ui-design-guidelines.md, 535 lines)
- 0.8: Theme-Customization Backend (4 theme fields, migration 0023)
- 0.9: Theme-Customization Frontend (SettingsTheme.tsx, themeStore.ts, live preview)
- 0.10: RBAC-Audit (4 plugins secured, 53 routes with require_permission)
- 0.11: LiteLLM-Cleanup (llm_client.py migrated from httpx to litellm)
- 0.12: KI-Agent-Framework docs (plugin-development-guide.md, agent_capabilities field)
- 0.13: Heartbeat configurable (ProactiveSettings, migration 0024, frontend UI)
- 0.14: Unified Search Field-Level RBAC (resolve_permissions + filter_fields_by_permission)
- 0.15: Undo/History-System (EntityHistory model, service, routes, migration 0025, HistoryViewer)
- 0.16: Storage Backend (LocalStorage + S3Storage, DMS/attachments/mail updated)
- 0.17: Import/Export unified Contact fields (firstname, surname, email_1, phone_1)
- 0.18: .gitignore & Config-Cleanup (webui→frontend, python-jose removed, .env untracked)
- 0.19: Mail-Salt Security-Fix (per-account random salt, migration 0026)
- 0.20: AGPL replaced (PyMuPDF→pypdf, OnlyOffice→Collabora, LICENSE + THIRD_PARTY_LICENSES.md)
2026-07-23 08:42:26 +02:00

10 KiB

LeoCRM Plugin Development Guide

Version: 1.0
Datum: 2026-07-23
Gültig für: Alle Plugin-Entwickler


1. Plugin-Struktur

Jedes Plugin liegt unter app/plugins/builtins/<plugin_name>/:

app/plugins/builtins/my_plugin/
├── __init__.py
├── plugin.py          # Plugin-Klasse mit Manifest
├── routes.py          # API-Routes
├── models.py          # SQLAlchemy-Modelle (optional)
├── schemas.py         # Pydantic-Schemas (optional)
├── services.py        # Business-Logik (optional)
├── migrations/        # SQL-Migrationen
│   └── 0001_initial.sql
└── tests/             # Plugin-Tests (optional)

2. Plugin-Manifest

Das Manifest definiert Metadaten, Abhängigkeiten, Routes, Events und Permissions:

from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest, PluginRouteDef

class MyPlugin(BasePlugin):
    manifest = PluginManifest(
        name="my_plugin",
        version="1.0.0",
        display_name="My Plugin",
        description="Description of what the plugin does.",
        dependencies=["permissions"],  # Other plugins this depends on
        routes=[
            PluginRouteDef(
                path="/api/v1/my-plugin",
                module="app.plugins.builtins.my_plugin.routes",
                router_attr="router",
            ),
        ],
        events=["contact.created", "contact.updated"],
        migrations=["0001_initial.sql"],
        permissions=[
            "my_plugin:read",
            "my_plugin:write",
            "my_plugin:delete",
        ],
        agent_capabilities=[
            "my_plugin:search",
            "my_plugin:analyze",
        ],
    )

Manifest-Felder

Feld Typ Pflicht Beschreibung
name str Ja Eindeutiger Plugin-Name (snake_case)
version str Ja Semantic Version
display_name str Ja Anzeigename
description str Nein Kurzbeschreibung
dependencies list[str] Nein Andere Plugins, die geladen sein müssen
routes list[PluginRouteDef] Nein API-Routen-Definitionen
events list[str] Nein Events, die das Plugin abonniert
migrations list[str] Nein SQL-Migrationsdateien
permissions list[str] Nein RBAC-Permissions, die das Plugin definiert
field_definitions list[FieldDefinition] Nein Feld-Level-Permissions
agent_capabilities list[str] Nein KI-Agent-Fähigkeiten, die das Plugin bietet
is_core bool Nein Core-Plugin (kann nicht deaktiviert werden)

3. RBAC-Permissions

Permissions definieren

Im Manifest werden alle Permissions des Plugins aufgelistet:

permissions=[
    "my_plugin:read",
    "my_plugin:write",
    "my_plugin:delete",
    "my_plugin:admin",
],

Routes absichern

Jede Route muss mit require_permission abgesichert werden:

from app.deps import get_current_user, require_permission
from fastapi import Depends

@router.get("", dependencies=[Depends(require_permission("my_plugin:read"))])
async def list_items(current_user: dict = Depends(get_current_user)):
    ...

@router.post("", status_code=201, dependencies=[Depends(require_permission("my_plugin:write"))])
async def create_item(data: ItemCreate, current_user: dict = Depends(get_current_user)):
    ...

@router.delete("/{item_id}", dependencies=[Depends(require_permission("my_plugin:delete"))])
async def delete_item(item_id: str, current_user: dict = Depends(get_current_user)):
    ...

Permission-Namenskonvention

  • Format: <plugin_name>:<action>
  • Standard-Actions: read, write, delete, share, admin
  • Beispiele: calendar:read, dms:write, tags:delete

4. KI-Agent-Framework

LeoCRM bietet ein integriertes KI-Agent-Framework basierend auf LiteLLM und PydanticAI.

Architektur

Plugin (ai_assistant, ai_proactive, zukünftige)
    ↓
LiteLLM (unified LLM interface — 100+ Provider)
    ↓
Provider (OpenAI, Anthropic, Google, Ollama, ...)
    ↑
Tool Registry (Plugin-Tools für KI-Agenten)

LiteLLM — Unified LLM Interface

LiteLLM bietet eine einheitliche API für über 100 LLM-Provider. Alle KI-Funktionen in LeoCRM nutzen litellm.acompletion():

import litellm

response = await litellm.acompletion(
    model="openai/gpt-4o",           # oder anthropic/claude-3-sonnet, ollama/llama3
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_query},
    ],
    temperature=0.3,
    max_tokens=1000,
    api_key=os.environ.get("AI_API_KEY"),
    api_base=os.environ.get("AI_API_BASE"),  # optional für self-hosted
)
content = response.choices[0].message.content

Konfiguration

Env-Var Beschreibung Standard
AI_MODEL Modell-Name (z.B. gpt-4o, claude-3-sonnet, llama3)
AI_API_KEY API-Key für den Provider
AI_API_BASE Custom API-Base-URL (optional) Provider-Standard
AI_PROVIDER Provider-Präfix (openai, anthropic, google, ollama) openai

Wenn AI_MODEL und AI_API_KEY nicht gesetzt sind, läuft der LLM-Client im Mock-Modus (keyword-basierte Action-Mapping für Tests).

Tool Registry — KI-Tools registrieren

Plugins können Tools registrieren, die KI-Agenten während Chat-Sessions aufrufen können. Jedes Tool deklariert Name, Beschreibung, JSON-Schema für Parameter und einen async Handler.

from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry

registry = get_tool_registry()

registry.register(
    name="search_contacts",
    description="Search contacts by name, email, or phone number",
    parameters={
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "Search query"},
            "limit": {"type": "integer", "description": "Max results", "default": 10},
        },
        "required": ["query"],
    },
    handler=my_search_handler,
    plugin_name="my_plugin",
    required_permission="contacts:read",
    category="search",
)

Tool Handler

Der Handler ist eine async Funktion, die Argumente und Kontext empfängt:

async def my_search_handler(arguments: dict, context: dict) -> str:
    query = arguments.get("query", "")
    limit = arguments.get("limit", 10)
    # ... perform search ...
    return json.dumps({"results": results})

Tools bei Plugin-Deaktivierung abmelden

def on_deactivate(self):
    registry = get_tool_registry()
    registry.unregister_plugin("my_plugin")

Agent Capabilities im Manifest

Das agent_capabilities Feld im Manifest deklariert, welche KI-Fähigkeiten ein Plugin bietet:

agent_capabilities=[
    "contact_search",       # Kontakt-Suche
    "email_draft",          # E-Mail-Entwürfe generieren
    "calendar_scheduling", # Terminvorschläge
],

Diese Informationen werden vom AI Assistant verwendet, um Nutzern zu zeigen, welche KI-Funktionen verfügbar sind.

5. Events

Plugins können Events abonnieren und auslösen:

# Im Manifest:
events=["contact.created", "contact.updated", "contact.deleted"]

# Event-Handler im Plugin:
async def on_contact_created(self, event_data: dict):
    # Reagiere auf neues Kontakt-Event
    pass

Events werden vom Event-Publisher im Contact-Service ausgelöst:

from app.core.events import publish_event
await publish_event(db, "contact.created", {"contact_id": str(contact.id)})

6. Datenbank-Migrationen

SQL-Migrationen liegen unter migrations/ im Plugin-Verzeichnis:

-- migrations/0001_initial.sql
CREATE TABLE my_plugin_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    name VARCHAR(200) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

Im Manifest referenzieren:

migrations=["0001_initial.sql"]

7. UI-Integration

Siehe docs/ui-design-guidelines.md für Frontend-Konventionen.

  • Plugin-Seiten verwenden das 3-Spalten-Explorer-Layout
  • PluginToolbar für Aktionen
  • Plugin-Settings als eigene Settings-Sub-Seite
  • i18n-Keys mit Plugin-Präfix

8. Testing

Tests liegen unter tests/ im Plugin-Verzeichnis oder im zentralen tests/ Ordner:

# tests/test_my_plugin.py
import pytest
from httpx import AsyncClient

@pytest.mark.asyncio
async def test_list_items_requires_permission(client: AsyncClient, auth_headers):
    response = await client.get("/api/v1/my-plugin", headers=auth_headers)
    assert response.status_code == 200

@pytest.mark.asyncio
async def test_list_items_without_permission_returns_403(client: AsyncClient, no_perm_headers):
    response = await client.get("/api/v1/my-plugin", headers=no_perm_headers)
    assert response.status_code == 403

9. Plugin-Beispiel

Minimal-Beispiel für ein neues Plugin:

# app/plugins/builtins/my_plugin/plugin.py
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest, PluginRouteDef

class MyPlugin(BasePlugin):
    manifest = PluginManifest(
        name="my_plugin",
        version="1.0.0",
        display_name="My Plugin",
        description="A minimal example plugin.",
        dependencies=[],
        routes=[
            PluginRouteDef(
                path="/api/v1/my-plugin",
                module="app.plugins.builtins.my_plugin.routes",
                router_attr="router",
            ),
        ],
        events=[],
        migrations=[],
        permissions=["my_plugin:read", "my_plugin:write"],
        agent_capabilities=[],
    )
# app/plugins/builtins/my_plugin/routes.py
from fastapi import APIRouter, Depends
from app.deps import get_current_user, require_permission

router = APIRouter()

@router.get("", dependencies=[Depends(require_permission("my_plugin:read"))])
async def list_items(current_user: dict = Depends(get_current_user)):
    return {"items": []}

Dieses Dokument ist verbindlich für alle Plugin-Entwicklung an LeoCRM.