From d803f39de3664b09e4c104bfdf77d74b5f679234 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 12:04:35 +0200 Subject: [PATCH] feat: Refactor workflow result handling in prompts and analysis components - Introduced a new utility function to streamline the extraction of user-facing content from aggregated workflow results. - Updated backend prompt handling to utilize the new function for improved clarity and maintainability. - Adjusted frontend analysis component to leverage the utility for consistent content display across different workflow result formats. These changes enhance the overall user experience by ensuring more reliable and readable output from workflow executions. --- backend/routers/prompts.py | 48 +++++++++++++++++++++------ frontend/src/pages/Analysis.jsx | 11 ++---- frontend/src/utils/workflowDisplay.js | 37 +++++++++++++++++++++ 3 files changed, 76 insertions(+), 20 deletions(-) create mode 100644 frontend/src/utils/workflowDisplay.js diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index cc90c5c..5e7d76f 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -31,6 +31,42 @@ OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "anthropic/claude-sonnet-4") router = APIRouter(prefix="/api/prompts", tags=["prompts"]) +# Metadaten-Schlüssel in workflow aggregate_results (nicht als „einziger“ Nutzer-Output) +_WORKFLOW_AGG_META_KEYS = frozenset({ + "combined_analysis", + "all_signals", + "total_nodes", + "executed_nodes", + "failed_nodes", + "skipped_nodes", +}) + + +def _workflow_user_facing_content(agg: object) -> str: + """ + Nutzer-sichtbarer Text wie im Admin WorkflowResultViewer („Final Output“): + primär aggregated_result['analysis_core'], nicht das gesamte JSON. + """ + if agg is None: + return "" + if isinstance(agg, str): + return agg + if not isinstance(agg, dict): + return json.dumps(agg, ensure_ascii=False) + core = agg.get("analysis_core") + if isinstance(core, str) and core.strip(): + return core + combined = agg.get("combined_analysis") + if isinstance(combined, str) and combined.strip(): + return combined + non_meta = [k for k in agg.keys() if k not in _WORKFLOW_AGG_META_KEYS] + if len(non_meta) == 1: + v = agg[non_meta[0]] + if isinstance(v, str): + return v + return json.dumps(v, ensure_ascii=False) + return json.dumps(agg, ensure_ascii=False) + @router.get("") def list_prompts(session: dict=Depends(require_auth)): @@ -1254,17 +1290,7 @@ async def execute_unified_prompt( else: content = json.dumps(final_output, ensure_ascii=False) elif result['type'] == 'workflow': - # Graph-Workflows: kein "output", sondern aggregated_result - agg = result.get('aggregated_result') - if isinstance(agg, dict) and len(agg) == 1: - v = list(agg.values())[0] - content = v if isinstance(v, str) else json.dumps(v, ensure_ascii=False) - elif agg is None: - content = '' - elif isinstance(agg, str): - content = agg - else: - content = json.dumps(agg, ensure_ascii=False) + content = _workflow_user_facing_content(result.get('aggregated_result')) else: # For base prompts, use output directly content = result.get('output', '') diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index cea6271..c8f8317 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react' import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { Link } from 'react-router-dom' import { api } from '../utils/api' +import { getWorkflowDisplayContent } from '../utils/workflowDisplay' import { useAuth } from '../context/AuthContext' import Markdown from '../utils/Markdown' import UsageBadge from '../components/UsageBadge' @@ -400,15 +401,7 @@ export default function Analysis() { content = JSON.stringify(finalOutput, null, 2) } } else if (result.type === 'workflow') { - const agg = result.aggregated_result - if (agg != null && typeof agg === 'object' && !Array.isArray(agg) && Object.keys(agg).length === 1) { - const v = Object.values(agg)[0] - content = typeof v === 'string' ? v : JSON.stringify(v, null, 2) - } else if (typeof agg === 'string') { - content = agg - } else { - content = JSON.stringify(agg ?? {}, null, 2) - } + content = getWorkflowDisplayContent(result.aggregated_result) } else { // For base prompts, use output directly content = typeof result.output === 'string' ? result.output : JSON.stringify(result.output, null, 2) diff --git a/frontend/src/utils/workflowDisplay.js b/frontend/src/utils/workflowDisplay.js new file mode 100644 index 0000000..ff971ca --- /dev/null +++ b/frontend/src/utils/workflowDisplay.js @@ -0,0 +1,37 @@ +/** + * Nutzer-sichtbare Textausgabe eines Workflow-Laufs – gleiche Logik wie + * WorkflowResultViewer („Final Output“): primär aggregated_result.analysis_core. + */ +const AGG_META_KEYS = new Set([ + 'combined_analysis', + 'all_signals', + 'total_nodes', + 'executed_nodes', + 'failed_nodes', + 'skipped_nodes', +]) + +export function getWorkflowDisplayContent(aggregatedResult) { + if (aggregatedResult == null) return '' + if (typeof aggregatedResult !== 'object' || Array.isArray(aggregatedResult)) { + return typeof aggregatedResult === 'string' + ? aggregatedResult + : JSON.stringify(aggregatedResult, null, 2) + } + + const core = aggregatedResult.analysis_core + if (typeof core === 'string' && core.trim() !== '') return core + + const combined = aggregatedResult.combined_analysis + if (typeof combined === 'string' && combined.trim() !== '') return combined + + const keys = Object.keys(aggregatedResult).filter((k) => !AGG_META_KEYS.has(k)) + if (keys.length === 1) { + const v = aggregatedResult[keys[0]] + if (typeof v === 'string') return v + if (v != null && typeof v === 'object') return JSON.stringify(v, null, 2) + return String(v) + } + + return JSON.stringify(aggregatedResult, null, 2) +}