diff --git a/backend/version.py b/backend/version.py index c90e6c0..bb05fce 100644 --- a/backend/version.py +++ b/backend/version.py @@ -7,8 +7,8 @@ Semantic Versioning: MAJOR.MINOR.PATCH - PATCH: Bugfix, kleine Änderung, Refactor """ -APP_VERSION = "0.9p" -BUILD_DATE = "2026-04-09" +APP_VERSION = "0.9q" +BUILD_DATE = "2026-04-11" DB_SCHEMA_VERSION = "20260409c" # 048/049 vitals_baseline.source csv + SAVEPOINT Import MODULE_VERSIONS = { @@ -29,13 +29,26 @@ MODULE_VERSIONS = { "exportdata": "1.1.0", "importdata": "1.0.0", "membership": "2.1.0", - "workflow": "0.6.0", # Phase 4: End Node Template Engine + "workflow": "0.7.0", # Part 3: Inline Prompts (reference + inline mode) "app_dashboard": "1.11.0", # Entitlements: DB-Override widget→features (AND), sonst Katalog "csv_import": "0.3.2", # Import-Fehler: enrich_row_error / freundlichere 500-Hinweise "admin_csv_templates": "0.3.0", # POST /validate + Speichern nur bei valid (422 + warnings in Response) } CHANGELOG = [ + { + "version": "0.9q", + "date": "2026-04-11", + "changes": [ + "Workflow Engine Part 3: Inline Prompts", + "Frontend: Radio Buttons (Reference/Inline), InlineTemplateEditor Component", + "Frontend: Placeholder Picker für Inline-Templates, Cursor-Position Tracking", + "Backend: load_prompt_template() unterstützt inline_template", + "Backend: WorkflowNode.inline_template Feld hinzugefügt", + "Serialization: inline_template speichern/laden in graph_data", + "Validation: Prüft dass entweder prompt_slug ODER inline_template gesetzt", + ], + }, { "version": "0.9p", "date": "2026-04-09", diff --git a/backend/workflow_executor.py b/backend/workflow_executor.py index e238479..4b99bde 100644 --- a/backend/workflow_executor.py +++ b/backend/workflow_executor.py @@ -278,9 +278,10 @@ async def execute_node( # Analysis Nodes if node.type == "analysis": - # 1. Lade Prompt - prompt_template = await load_prompt_template(node.prompt_slug, context) - logger.debug(f"Node {node.id}: Loaded prompt '{node.prompt_slug}'") + # 1. Lade Prompt (Part 3: inline_template support) + prompt_template = await load_prompt_template(node, context) + source_type = "inline" if node.inline_template else "reference" + logger.debug(f"Node {node.id}: Loaded prompt from {source_type}") # 2. Parse question_augmentations questions = [] @@ -812,39 +813,64 @@ def _has_active_incoming_edge(node, graph: WorkflowGraph, context: Dict[str, Any return False -async def load_prompt_template(prompt_slug: str, context: Dict[str, Any]) -> str: +async def load_prompt_template(node: WorkflowNode, context: Dict[str, Any]) -> str: """ - Lädt Prompt-Template aus DB und resolved Platzhalter. + Lädt Prompt-Template aus DB (reference mode) oder direkt vom Node (inline mode). + + Part 3: Inline Prompts - Unterstützt zwei Modi: + - Reference Mode: prompt_slug → Template aus ai_prompts Tabelle + - Inline Mode: inline_template → Template direkt vom Node Args: - prompt_slug: Slug des Prompts (z.B. "pipeline_body") + node: WorkflowNode mit prompt_slug ODER inline_template context: {"variables": {"name": "Lars", ...}, "profile_id": "..."} Returns: Resolved prompt template + Raises: + HTTPException: Wenn weder prompt_slug noch inline_template gesetzt + Beispiel: - >>> template = await load_prompt_template("pipeline_body", {"profile_id": "123"}) + >>> node = WorkflowNode(id="n1", prompt_slug="pipeline_body") + >>> template = await load_prompt_template(node, {"profile_id": "123"}) >>> "{{name}}" not in template True """ from placeholder_resolver import get_placeholder_example_values, get_placeholder_catalog from prompt_executor import resolve_placeholders + from fastapi import HTTPException - with get_db() as conn: - cur = get_cursor(conn) - cur.execute( - "SELECT template FROM ai_prompts WHERE slug = %s AND active = true", - (prompt_slug,) + # Mode 1: Inline Template (NEU) + if node.inline_template: + logger.debug(f"Node {node.id}: Using inline template ({len(node.inline_template)} chars)") + template = node.inline_template + + # Mode 2: Reference (bestehend) + elif node.prompt_slug: + logger.debug(f"Node {node.id}: Loading prompt '{node.prompt_slug}' from DB") + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "SELECT template FROM ai_prompts WHERE slug = %s AND active = true", + (node.prompt_slug,) + ) + row = cur.fetchone() + if not row: + raise HTTPException(status_code=404, detail=f"Prompt not found: {node.prompt_slug}") + template = row['template'] + + # Mode 3: Error - weder inline noch reference + else: + raise HTTPException( + status_code=400, + detail=f"Node {node.id}: Either prompt_slug or inline_template required" ) - row = cur.fetchone() - if not row: - raise ValueError(f"Prompt not found: {prompt_slug}") - - template = row['template'] # Resolve Placeholders using modern prompt_executor method profile_id = context.get("profile_id") + if not profile_id: + raise HTTPException(status_code=400, detail="profile_id required in context") # Build variables dict with ALL registered placeholders variables = {} diff --git a/backend/workflow_models.py b/backend/workflow_models.py index 12134c9..5b92126 100644 --- a/backend/workflow_models.py +++ b/backend/workflow_models.py @@ -190,7 +190,8 @@ class WorkflowNode(BaseModel): position: Optional[Position] = Field(None, description="Position im visuellen Editor") # ANALYSIS-Knoten - prompt_slug: Optional[str] = Field(None, description="Slug des auszuführenden Prompts") + prompt_slug: Optional[str] = Field(None, description="Slug des auszuführenden Prompts (reference mode)") + inline_template: Optional[str] = Field(None, description="Inline-Prompt-Template (inline mode, Part 3)") question_augmentations: Optional[List[QuestionAugmentation]] = Field(None, description="Fragenergänzungen (knotengebunden, überschreiben Prompt-Defaults)") # LOGIC-Knoten diff --git a/frontend/src/components/workflow/panels/InlineTemplateEditor.jsx b/frontend/src/components/workflow/panels/InlineTemplateEditor.jsx new file mode 100644 index 0000000..356a511 --- /dev/null +++ b/frontend/src/components/workflow/panels/InlineTemplateEditor.jsx @@ -0,0 +1,69 @@ +import { useRef } from 'react' + +/** + * InlineTemplateEditor - Template-Editor für Inline-Prompts + * + * Props: + * - value: Template-String + * - onChange: (template) => void + * - onPlaceholderPick: () => void - Öffnet Placeholder Picker + * - textareaRef: Ref für Cursor-Position (von Parent) + */ +export function InlineTemplateEditor({ value, onChange, onPlaceholderPick, textareaRef }) { + return ( +
+ +
+