feat: Workflow Engine Part 3 - Inline Prompts (v0.9q)
Ermöglicht Analysis Nodes zwischen zwei Prompt-Modi zu wählen: - Reference Mode: Basis-Prompt aus DB referenzieren (bestehend) - Inline Mode: Template direkt im Node editieren (NEU) Frontend: - InlineTemplateEditor Component (~80 Zeilen) - Radio Buttons in WorkflowEditorPage für Mode-Auswahl - Placeholder Picker für beide Modi (End Node + Inline Template) - Cursor-Position Tracking mit textareaRef - Conditional Rendering basierend auf promptSource - Validation: Entweder prompt_slug ODER inline_template Backend: - load_prompt_template() akzeptiert ganzen WorkflowNode (statt nur slug) - Unterstützt inline_template (Mode 1) und prompt_slug (Mode 2) - WorkflowNode.inline_template Feld hinzugefügt - Validation: HTTPException wenn weder slug noch template Serialization: - inline_template in graph_data speichern/laden - Backward-compatible mit bestehenden Workflows Version: 0.9q Module: workflow 0.7.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b453ce63c6
commit
a1723db387
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="inline-template-editor" style={{ marginTop: '12px' }}>
|
||||
<label className="form-label">Template</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Analysiere folgende Daten: Gewicht: {{ weight_current }} Ziel: {{ goal_weight }} Gib eine Empfehlung..."
|
||||
rows={12}
|
||||
style={{
|
||||
width: '100%',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '13px',
|
||||
padding: '12px',
|
||||
paddingRight: '120px', // Platz für Button
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--bg)',
|
||||
color: 'var(--text1)',
|
||||
resize: 'vertical',
|
||||
lineHeight: '1.5'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={onPlaceholderPick}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
right: '8px',
|
||||
fontSize: '11px',
|
||||
padding: '6px 10px',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{'{{ }}'} Platzhalter
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="help-text"
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--text3)',
|
||||
marginTop: '6px'
|
||||
}}
|
||||
>
|
||||
💡 Tipp: Verwende <code style={{
|
||||
background: 'var(--surface2)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace'
|
||||
}}>{'{{ placeholder_name }}'}</code> für dynamische Werte
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import { EndNodeConfig } from '../components/workflow/panels/EndNodeConfig'
|
|||
import { PlaceholderPicker } from '../components/workflow/panels/PlaceholderPicker'
|
||||
import { WorkflowExecutePanel } from '../components/workflow/panels/WorkflowExecutePanel'
|
||||
import { WorkflowResultViewer } from '../components/workflow/panels/WorkflowResultViewer'
|
||||
import { InlineTemplateEditor } from '../components/workflow/panels/InlineTemplateEditor'
|
||||
import '../styles/workflowEditor.css'
|
||||
|
||||
// Node-Type Mapping
|
||||
|
|
@ -50,7 +51,9 @@ export default function WorkflowEditorPage() {
|
|||
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||
const [executionResult, setExecutionResult] = useState(null)
|
||||
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
||||
const [placeholderPickerTarget, setPlaceholderPickerTarget] = useState('end') // 'end' | 'inline'
|
||||
const endNodeTextareaRef = useRef(null)
|
||||
const inlineTemplateTextareaRef = useRef(null)
|
||||
|
||||
// Load available basis prompts for Analysis nodes
|
||||
useEffect(() => {
|
||||
|
|
@ -262,30 +265,60 @@ export default function WorkflowEditorPage() {
|
|||
}
|
||||
|
||||
const handlePlaceholderSelect = (placeholderString) => {
|
||||
if (!selectedNode || selectedNode.type !== 'end') return
|
||||
if (!selectedNode) return
|
||||
|
||||
const textarea = endNodeTextareaRef.current
|
||||
const currentTemplate = selectedNode.data.template || ''
|
||||
// Target bestimmen: End Node oder Inline Template
|
||||
if (placeholderPickerTarget === 'end' && selectedNode.type === 'end') {
|
||||
const textarea = endNodeTextareaRef.current
|
||||
const currentTemplate = selectedNode.data.template || ''
|
||||
|
||||
// Wenn Textarea Ref verfügbar, an Cursor-Position einfügen
|
||||
if (textarea) {
|
||||
const cursorPos = textarea.selectionStart || currentTemplate.length
|
||||
const before = currentTemplate.substring(0, cursorPos)
|
||||
const after = currentTemplate.substring(cursorPos)
|
||||
const newTemplate = before + placeholderString + after
|
||||
// Wenn Textarea Ref verfügbar, an Cursor-Position einfügen
|
||||
if (textarea) {
|
||||
const cursorPos = textarea.selectionStart || currentTemplate.length
|
||||
const before = currentTemplate.substring(0, cursorPos)
|
||||
const after = currentTemplate.substring(cursorPos)
|
||||
const newTemplate = before + placeholderString + after
|
||||
|
||||
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||
|
||||
// Cursor nach eingefügtem Platzhalter positionieren
|
||||
setTimeout(() => {
|
||||
const newPos = cursorPos + placeholderString.length
|
||||
textarea.setSelectionRange(newPos, newPos)
|
||||
textarea.focus()
|
||||
}, 0)
|
||||
} else {
|
||||
// Fallback: Am Ende einfügen
|
||||
const newTemplate = currentTemplate + placeholderString
|
||||
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||
// Cursor nach eingefügtem Platzhalter positionieren
|
||||
setTimeout(() => {
|
||||
const newPos = cursorPos + placeholderString.length
|
||||
textarea.setSelectionRange(newPos, newPos)
|
||||
textarea.focus()
|
||||
}, 0)
|
||||
} else {
|
||||
// Fallback: Am Ende einfügen
|
||||
const newTemplate = currentTemplate + placeholderString
|
||||
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||
}
|
||||
}
|
||||
|
||||
// Inline Template (Analysis Node)
|
||||
else if (placeholderPickerTarget === 'inline' && selectedNode.type === 'analysis') {
|
||||
const textarea = inlineTemplateTextareaRef.current
|
||||
const currentTemplate = selectedNode.data.inline_template || ''
|
||||
|
||||
// Wenn Textarea Ref verfügbar, an Cursor-Position einfügen
|
||||
if (textarea) {
|
||||
const cursorPos = textarea.selectionStart || currentTemplate.length
|
||||
const before = currentTemplate.substring(0, cursorPos)
|
||||
const after = currentTemplate.substring(cursorPos)
|
||||
const newTemplate = before + placeholderString + after
|
||||
|
||||
handleNodeUpdate(selectedNode.id, { inline_template: newTemplate })
|
||||
|
||||
// Cursor nach eingefügtem Platzhalter positionieren
|
||||
setTimeout(() => {
|
||||
const newPos = cursorPos + placeholderString.length
|
||||
textarea.setSelectionRange(newPos, newPos)
|
||||
textarea.focus()
|
||||
}, 0)
|
||||
} else {
|
||||
// Fallback: Am Ende einfügen
|
||||
const newTemplate = currentTemplate + placeholderString
|
||||
handleNodeUpdate(selectedNode.id, { inline_template: newTemplate })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -455,43 +488,100 @@ export default function WorkflowEditorPage() {
|
|||
{/* Type-spezifische Konfiguration */}
|
||||
{selectedNode.type === 'analysis' && (
|
||||
<>
|
||||
{/* Prompt Source Selector */}
|
||||
<div className="config-section">
|
||||
<label>KI-Prompt auswählen</label>
|
||||
<select
|
||||
value={selectedNode.data.prompt_slug ? String(selectedNode.data.prompt_slug) : ''}
|
||||
onChange={(e) => {
|
||||
const promptSlug = e.target.value
|
||||
console.log('🎯 Prompt selected:', promptSlug, 'Type:', typeof promptSlug)
|
||||
const selectedPrompt = availablePrompts.find(p => p.slug === promptSlug)
|
||||
console.log('📋 Selected prompt object:', selectedPrompt)
|
||||
handleNodeUpdate(selectedNode.id, {
|
||||
prompt_slug: promptSlug || null,
|
||||
prompt_name: selectedPrompt?.name || null
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
color: 'var(--text1)'
|
||||
}}
|
||||
>
|
||||
<option value="">-- Basis-Prompt wählen --</option>
|
||||
{availablePrompts.map(prompt => (
|
||||
<option key={prompt.id} value={prompt.slug}>
|
||||
{prompt.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedNode.data.prompt_slug && (
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
|
||||
Prompt: {selectedNode.data.prompt_slug} ({selectedNode.data.prompt_name || 'unbekannt'})
|
||||
</div>
|
||||
)}
|
||||
<label className="form-label">Prompt-Quelle</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name={`promptSource-${selectedNode.id}`}
|
||||
checked={!!selectedNode.data.prompt_slug}
|
||||
onChange={() => {
|
||||
handleNodeUpdate(selectedNode.id, {
|
||||
prompt_slug: '',
|
||||
inline_template: null
|
||||
})
|
||||
}}
|
||||
style={{ marginRight: '8px' }}
|
||||
/>
|
||||
<span style={{ fontSize: '14px' }}>📚 Basis-Prompt referenzieren</span>
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name={`promptSource-${selectedNode.id}`}
|
||||
checked={!!selectedNode.data.inline_template || (!selectedNode.data.prompt_slug && !selectedNode.data.inline_template)}
|
||||
onChange={() => {
|
||||
handleNodeUpdate(selectedNode.id, {
|
||||
prompt_slug: null,
|
||||
inline_template: ''
|
||||
})
|
||||
}}
|
||||
style={{ marginRight: '8px' }}
|
||||
/>
|
||||
<span style={{ fontSize: '14px' }}>✏️ Inline-Template erstellen</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conditional Rendering: Reference oder Inline */}
|
||||
{selectedNode.data.prompt_slug !== null && !selectedNode.data.inline_template && (
|
||||
<div className="config-section">
|
||||
<label>Basis-Prompt auswählen</label>
|
||||
<select
|
||||
value={selectedNode.data.prompt_slug ? String(selectedNode.data.prompt_slug) : ''}
|
||||
onChange={(e) => {
|
||||
const promptSlug = e.target.value
|
||||
console.log('🎯 Prompt selected:', promptSlug, 'Type:', typeof promptSlug)
|
||||
const selectedPrompt = availablePrompts.find(p => p.slug === promptSlug)
|
||||
console.log('📋 Selected prompt object:', selectedPrompt)
|
||||
handleNodeUpdate(selectedNode.id, {
|
||||
prompt_slug: promptSlug || null,
|
||||
prompt_name: selectedPrompt?.name || null,
|
||||
inline_template: null
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
color: 'var(--text1)'
|
||||
}}
|
||||
>
|
||||
<option value="">-- Basis-Prompt wählen --</option>
|
||||
{availablePrompts.map(prompt => (
|
||||
<option key={prompt.id} value={prompt.slug}>
|
||||
{prompt.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedNode.data.prompt_slug && (
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
|
||||
Prompt: {selectedNode.data.prompt_slug} ({selectedNode.data.prompt_name || 'unbekannt'})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inline Template Editor */}
|
||||
{(selectedNode.data.inline_template !== null || !selectedNode.data.prompt_slug) && (
|
||||
<InlineTemplateEditor
|
||||
value={selectedNode.data.inline_template || ''}
|
||||
onChange={(template) => handleNodeUpdate(selectedNode.id, {
|
||||
inline_template: template,
|
||||
prompt_slug: null
|
||||
})}
|
||||
onPlaceholderPick={() => {
|
||||
setPlaceholderPickerTarget('inline')
|
||||
setShowPlaceholderPicker(true)
|
||||
}}
|
||||
textareaRef={inlineTemplateTextareaRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
|
||||
<FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
|
||||
</>
|
||||
|
|
@ -517,7 +607,10 @@ export default function WorkflowEditorPage() {
|
|||
<EndNodeConfig
|
||||
node={selectedNode}
|
||||
onChange={handleNodeUpdate}
|
||||
onOpenPlaceholderPicker={() => setShowPlaceholderPicker(true)}
|
||||
onOpenPlaceholderPicker={() => {
|
||||
setPlaceholderPickerTarget('end')
|
||||
setShowPlaceholderPicker(true)
|
||||
}}
|
||||
textareaRef={endNodeTextareaRef}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export function serializeToWorkflowGraph(nodes, edges, metadata = {}) {
|
|||
// Type-spezifische Felder
|
||||
...(node.type === 'analysis' && {
|
||||
prompt_slug: node.data.prompt_slug || null,
|
||||
inline_template: node.data.inline_template || null, // Part 3: Inline Prompts
|
||||
prompt_name: node.data.prompt_name || null,
|
||||
question_augmentations: node.data.questions || [], // Backend erwartet question_augmentations
|
||||
fallback_strategy: node.data.fallback_strategy || 'conservative_skip'
|
||||
|
|
@ -84,6 +85,7 @@ export function deserializeFromWorkflowGraph(jsonbData) {
|
|||
|
||||
...(node.type === 'analysis' && {
|
||||
prompt_slug: node.prompt_slug || node.prompt_id || null, // Fallback für alte Workflows mit prompt_id
|
||||
inline_template: node.inline_template || null, // Part 3: Inline Prompts
|
||||
prompt_name: node.prompt_name || null, // Falls vom Backend mitgeliefert
|
||||
questions: node.question_augmentations || node.questions || [], // Backend sendet question_augmentations
|
||||
fallback_strategy: node.fallback_strategy || 'conservative_skip'
|
||||
|
|
|
|||
|
|
@ -86,17 +86,29 @@ function validateLogic(nodes, edges, errors, warnings) {
|
|||
// Analysis Nodes
|
||||
if (node.type === 'analysis') {
|
||||
const questions = node.data.questions || []
|
||||
const hasPromptSlug = node.data.prompt_slug != null && node.data.prompt_slug !== ''
|
||||
const hasInlineTemplate = node.data.inline_template != null && node.data.inline_template.trim() !== ''
|
||||
|
||||
// Prompt ausgewählt?
|
||||
if (!node.data.prompt_slug) {
|
||||
// Part 3: Validation - Entweder prompt_slug ODER inline_template
|
||||
if (!hasPromptSlug && !hasInlineTemplate) {
|
||||
errors.push({
|
||||
type: 'config',
|
||||
message: `Analysis-Node "${node.data.label}" hat keinen Prompt`,
|
||||
message: `Analysis-Node "${node.data.label}" benötigt entweder Basis-Prompt oder Inline-Template`,
|
||||
nodeId: node.id,
|
||||
severity: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
// Warning wenn beide gesetzt (sollte nicht passieren, aber zur Sicherheit)
|
||||
if (hasPromptSlug && hasInlineTemplate) {
|
||||
warnings.push({
|
||||
type: 'config',
|
||||
message: `Analysis-Node "${node.data.label}" hat sowohl Basis-Prompt als auch Inline-Template - Inline hat Vorrang`,
|
||||
nodeId: node.id,
|
||||
severity: 'warning'
|
||||
})
|
||||
}
|
||||
|
||||
// Fragen validieren
|
||||
questions.forEach((q, idx) => {
|
||||
if (!q.question?.trim()) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user