""" Pydantic Models for Workflow Engine (Phase 0) Data validation schemas for Workflow-Graph, Knoten, Kanten, Bedingungen. Konzept-Basis: konzept_workflow_engine_konsolidated.md Anforderungsanalyse: anforderungsanalyse_umsetzungsplan.md """ from typing import Optional, List, Dict, Any from pydantic import BaseModel, Field from enum import Enum # ── Enums ───────────────────────────────────────────────────────────────────── class NodeType(str, Enum): """Workflow-Knotentypen""" START = "start" ANALYSIS = "analysis" LOGIC = "logic" JOIN = "join" END = "end" class JoinStrategy(str, Enum): """Join-Strategien für Pfad-Konsolidierung""" WAIT_ALL = "wait_all" # Warte auf alle eingehenden Pfade WAIT_ANY = "wait_any" # Warte auf mindestens einen Pfad BEST_EFFORT = "best_effort" # Verwende verfügbare Pfade class SkipHandling(str, Enum): """Umgang mit übersprungenen Pfaden am Join""" IGNORE_SKIPPED = "ignore_skipped" # Übersprungene Pfade ignorieren USE_PLACEHOLDER = "use_placeholder" # Platzhalter für übersprungene Pfade REQUIRE_MINIMUM = "require_minimum" # Mindestanzahl erforderlich class FallbackStrategy(str, Enum): """Fallback-Strategien bei unklaren/ungültigen Signalen""" CONSERVATIVE_SKIP = "conservative_skip" # Konservativ: Pfad überspringen DEFAULT_PATH = "default_path" # Standard-Pfad ausführen UNCERTAINTY_PATH = "uncertainty_path" # Expliziter Unsicherheits-Pfad DOCUMENT_ONLY = "document_only" # Nur dokumentieren, kein Routing class NodeStatus(str, Enum): """Ausführungsstatus eines Knotens""" PENDING = "pending" EXECUTED = "executed" SKIPPED = "skipped" UNCLEAR = "unclear" FAILED = "failed" class LogicOperator(str, Enum): """Logische Operatoren für Bedingungen""" EQ = "eq" # == NEQ = "neq" # != IN = "in" # in NOT_IN = "not_in" # not in GT = "gt" # > LT = "lt" # < GTE = "gte" # >= LTE = "lte" # <= AND = "and" OR = "or" NOT = "not" # ── Hilfsmodelle ────────────────────────────────────────────────────────────── class Position(BaseModel): """Position eines Knotens im visuellen Editor""" x: float y: float class QuestionAugmentation(BaseModel): """ Fragenergänzung zu einem Analyseprompt. Hybridmodell (Sektion 6 der Anforderungsanalyse): - Primär: Knotengebunden (am Workflow-Knoten definiert) - Sekundär: Prompt-gebundene Standardfragen (optional) - Vorrangregel: Knotenspezifische überschreiben Prompt-Defaults """ id: str = Field(..., description="Eindeutige ID der Frage (für Referenzierung in Logik-Knoten)") type: str = Field(..., description="Fragetyp: relevanz, prioritaet, selektion, ausschluss, eskalation, unsicherheit") question: str = Field(..., description="Fragetext (kann Template-Variablen enthalten)") answer_spectrum: List[str] = Field(..., description="Erlaubte Antworten, z.B. ['ja', 'nein', 'unklar']") # ── Bedingungsmodelle ───────────────────────────────────────────────────────── class LogicOperand(BaseModel): """ Operand einer Logik-Bedingung. Referenziert normalisierte Signale aus vorangegangenen Knoten. Format: "node_id.question_id" (z.B. "node_1.q1") """ ref: str = Field(..., description="Referenz zum Signal: node_id.question_id") operator: LogicOperator = Field(..., description="Vergleichsoperator") value: Any = Field(..., description="Vergleichswert") class LogicExpression(BaseModel): """ Logik-Ausdruck (verschachtelbar). Beispiel: { "operator": "and", "operands": [ {"ref": "node_1.q1", "operator": "eq", "value": "ja"}, {"ref": "node_1.q2", "operator": "in", "value": ["hoch", "mittel"]} ] } oder verschachtelt: { "operator": "or", "operands": [ { "operator": "and", "operands": [...] }, {"ref": "node_2.q1", "operator": "eq", "value": "ja"} ] } """ operator: LogicOperator = Field(..., description="Logischer Operator (and, or, not) oder Vergleichsoperator") operands: Optional[List[Any]] = Field(None, description="Liste von Operanden (LogicOperand oder verschachtelte LogicExpression)") # Bei einfachem Vergleich: ref: Optional[str] = Field(None, description="Signal-Referenz (nur bei Vergleichsoperatoren)") value: Optional[Any] = Field(None, description="Vergleichswert (nur bei Vergleichsoperatoren)") class Condition(BaseModel): """ Bedingung für einen Logik-Knoten. Unterstützt if/else-if/else-Logik. """ type: str = Field(default="if", description="Bedingungstyp: if, else-if, else") expression: Optional[LogicExpression] = Field(None, description="Logischer Ausdruck (null bei 'else')") then_path: Optional[str] = Field(None, description="Edge-ID für 'then'-Pfad") else_path: Optional[str] = Field(None, description="Edge-ID für 'else'-Pfad") class FallbackConfig(BaseModel): """Fallback-Konfiguration für Logik-Knoten""" strategy: FallbackStrategy = Field(..., description="Fallback-Strategie") on_unclear: Optional[str] = Field(None, description="Edge-ID für Unsicherheits-Pfad") on_invalid: Optional[str] = Field(None, description="Edge-ID bei ungültigen Signalen") # ── Workflow-Graph-Modelle ──────────────────────────────────────────────────── class WorkflowNode(BaseModel): """ Workflow-Knoten (Teil des Graph-JSONB). Verschiedene Typen haben unterschiedliche Felder: - START/END: nur id, type, position - ANALYSIS: prompt_slug, question_augmentations - LOGIC: condition, fallback - JOIN: join_strategy, skip_handling """ id: str = Field(..., description="Eindeutige Knoten-ID") type: NodeType = Field(..., description="Knotentyp") position: Optional[Position] = Field(None, description="Position im visuellen Editor") # ANALYSIS-Knoten prompt_slug: Optional[str] = Field(None, description="Slug des auszuführenden Prompts") question_augmentations: Optional[List[QuestionAugmentation]] = Field(None, description="Fragenergänzungen (knotengebunden, überschreiben Prompt-Defaults)") # LOGIC-Knoten condition: Optional[Condition] = Field(None, description="Bedingung für Pfad-Routing") fallback: Optional[FallbackConfig] = Field(None, description="Fallback-Konfiguration") # JOIN-Knoten join_strategy: Optional[JoinStrategy] = Field(None, description="Join-Strategie") skip_handling: Optional[SkipHandling] = Field(None, description="Umgang mit übersprungenen Pfaden") class WorkflowEdge(BaseModel): """ Workflow-Kante (Verbindung zwischen Knoten). """ model_config = {"populate_by_name": True} # Erlaubt sowohl 'from_node' als auch 'from' (Alias) id: str = Field(..., description="Eindeutige Edge-ID") from_node: str = Field(..., alias="from", description="Quell-Knoten-ID") to_node: str = Field(..., alias="to", description="Ziel-Knoten-ID") label: Optional[str] = Field(None, description="Label für visuelle Darstellung (z.B. 'then', 'else')") class WorkflowGraph(BaseModel): """ Workflow-Graph (gespeichert als JSONB in workflow_definitions.graph). Repräsentiert einen DAG (Directed Acyclic Graph). """ nodes: List[WorkflowNode] = Field(..., description="Liste aller Knoten") edges: List[WorkflowEdge] = Field(..., description="Liste aller Kanten") # ── Workflow-Definition (DB-Modell) ─────────────────────────────────────────── class WorkflowDefinitionCreate(BaseModel): """Request-Modell für Workflow-Erstellung""" name: str slug: str description: Optional[str] = None graph: WorkflowGraph class WorkflowDefinitionUpdate(BaseModel): """Request-Modell für Workflow-Update""" name: Optional[str] = None description: Optional[str] = None graph: Optional[WorkflowGraph] = None active: Optional[bool] = None class WorkflowDefinition(BaseModel): """Response-Modell für Workflow-Definition""" id: str name: str slug: str description: Optional[str] = None graph: WorkflowGraph version: int active: bool created_at: str updated_at: str # ── Workflow-Execution (DB-Modell) ──────────────────────────────────────────── class NodeState(BaseModel): """Ausführungsstatus eines Knotens""" status: NodeStatus result: Optional[Dict[str, Any]] = Field(None, description="Ergebnis der Knoten-Ausführung (Prompt-Output, normalisierte Signale, etc.)") timestamp: Optional[str] = Field(None, description="Zeitpunkt der Ausführung") error: Optional[str] = Field(None, description="Fehlermeldung bei Status=failed") class WorkflowExecutionCreate(BaseModel): """Request-Modell für Workflow-Ausführung (Start)""" workflow_id: str # profile_id kommt aus Session class WorkflowExecution(BaseModel): """Response-Modell für Workflow-Execution""" id: str workflow_id: str profile_id: str status: str node_states: Optional[Dict[str, NodeState]] = None execution_log: Optional[List[Dict[str, Any]]] = None started_at: str completed_at: Optional[str] = None # ── Question Catalog (DB-Modell) ────────────────────────────────────────────── class QuestionCatalogEntry(BaseModel): """Eintrag im Fragenkatalog""" id: str question_type: str label: str question_template: str answer_spectrum: List[str] normalization_rules: Optional[Dict[str, Any]] = None active: bool created_at: str