From 4b6e1bed11f32e3bef4cefe12f820a29086fb47d Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 12 Apr 2026 11:03:07 +0200 Subject: [PATCH 1/2] feat: Enhance OpenRouter API interaction and error handling - Increased the maximum token limit in the `call_openrouter` function from 1500 to 4096 to allow for more extensive responses. - Implemented robust error handling for API requests, including timeout and request errors, with detailed HTTP exceptions for better debugging. - Improved JSON response handling to ensure valid data is returned, with specific error messages for missing content in the response. - Enhanced the overall reliability of the OpenRouter API integration, providing clearer feedback for users in case of issues. These changes improve the user experience by ensuring more comprehensive responses and clearer error reporting during API interactions. --- backend/prompt_executor.py | 11 +- backend/routers/prompts.py | 61 +++- .../src/components/UnifiedPromptModal.jsx | 342 ++++++++++++++++-- 3 files changed, 377 insertions(+), 37 deletions(-) diff --git a/backend/prompt_executor.py b/backend/prompt_executor.py index 0970892..eddf4c6 100644 --- a/backend/prompt_executor.py +++ b/backend/prompt_executor.py @@ -278,7 +278,11 @@ async def execute_base_prompt( if enable_debug: debug_info['template'] = template - debug_info['final_prompt'] = prompt_text[:500] + ('...' if len(prompt_text) > 500 else '') + # Volltext für Test-UI (Admin); sehr große Prompts nur weich begrenzen + _max = 512 * 1024 + debug_info['final_prompt'] = ( + prompt_text if len(prompt_text) <= _max else prompt_text[:_max] + "\n… [gekürzt, >512KB]" + ) debug_info['available_variables'] = list(variables.keys()) # Call AI @@ -397,7 +401,10 @@ async def execute_pipeline_prompt( if enable_debug: prompt_debug['source'] = 'inline' prompt_debug['template'] = template - prompt_debug['final_prompt'] = prompt_text[:500] + ('...' if len(prompt_text) > 500 else '') + _max = 512 * 1024 + prompt_debug['final_prompt'] = ( + prompt_text if len(prompt_text) <= _max else prompt_text[:_max] + "\n… [gekürzt, >512KB]" + ) prompt_debug.update(placeholder_debug) response = await openrouter_call_func(prompt_text) diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index 5153ce5..1f32011 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -937,27 +937,60 @@ def export_placeholder_catalog_zip( # ── KI-Assisted Prompt Engineering ─────────────────────────────────────────── -async def call_openrouter(prompt: str, max_tokens: int = 1500) -> str: +async def call_openrouter(prompt: str, max_tokens: int = 4096) -> str: """Call OpenRouter API to get AI response.""" if not OPENROUTER_KEY: raise HTTPException(status_code=500, detail="OpenRouter API key not configured") - async with httpx.AsyncClient() as client: - resp = await client.post( - "https://openrouter.ai/api/v1/chat/completions", - headers={"Authorization": f"Bearer {OPENROUTER_KEY}"}, - json={ - "model": OPENROUTER_MODEL, - "messages": [{"role": "user", "content": prompt}], - "max_tokens": max_tokens - }, - timeout=60.0 + try: + async with httpx.AsyncClient() as client: + resp = await client.post( + "https://openrouter.ai/api/v1/chat/completions", + headers={"Authorization": f"Bearer {OPENROUTER_KEY}"}, + json={ + "model": OPENROUTER_MODEL, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": max_tokens + }, + timeout=120.0 + ) + except httpx.TimeoutException as e: + raise HTTPException(status_code=504, detail=f"OpenRouter-Zeitüberschreitung: {e}") from e + except httpx.RequestError as e: + raise HTTPException(status_code=502, detail=f"OpenRouter-Verbindungsfehler: {e}") from e + + if resp.status_code != 200: + raise HTTPException( + status_code=resp.status_code, + detail=f"OpenRouter API error ({resp.status_code}): {resp.text[:2000]}", ) - if resp.status_code != 200: - raise HTTPException(status_code=resp.status_code, detail=f"OpenRouter API error: {resp.text}") + try: + data = resp.json() + except json.JSONDecodeError: + raise HTTPException( + status_code=502, + detail=f"OpenRouter lieferte kein JSON: {resp.text[:800]}", + ) - return resp.json()['choices'][0]['message']['content'].strip() + choices = data.get("choices") or [] + if not choices: + err_snip = json.dumps(data.get("error") or data, ensure_ascii=False)[:1200] + raise HTTPException( + status_code=502, + detail=f"OpenRouter ohne choices in der Antwort: {err_snip}", + ) + + message = choices[0].get("message") or {} + content = message.get("content") + if content is None: + raise HTTPException( + status_code=502, + detail="OpenRouter-Antwort ohne message.content (z. B. Tool-Calls oder abweichendes Schema).", + ) + if not isinstance(content, str): + content = str(content) + return content.strip() def collect_example_data(profile_id: str, data_categories: list[str]) -> dict: diff --git a/frontend/src/components/UnifiedPromptModal.jsx b/frontend/src/components/UnifiedPromptModal.jsx index e99bd99..3052f06 100644 --- a/frontend/src/components/UnifiedPromptModal.jsx +++ b/frontend/src/components/UnifiedPromptModal.jsx @@ -2,6 +2,201 @@ import { useState, useEffect, useRef } from 'react' import { api } from '../utils/api' import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react' import PlaceholderPicker from './PlaceholderPicker' +import Markdown from '../utils/Markdown' + +/** Debug aus erfolgreicher Ausführung oder aus FastAPI-Fehlerdetail */ +function extractExecutionDebug(testResult) { + if (!testResult) return null + const raw = testResult.debug ?? testResult + if (raw && typeof raw === 'object' && raw.detail && raw.detail.debug) { + return { ...raw.detail.debug, ...raw.detail } + } + return raw +} + +/** Vollständiger Prompt-Text für die Anzeige (Basis oder Pipeline-Stufen) */ +function buildPromptTextForDisplay(testResult) { + if (!testResult || testResult.error) return '' + const dbg = extractExecutionDebug(testResult) + const t = testResult.type + if (t === 'base' && dbg?.final_prompt) return dbg.final_prompt + if (t === 'pipeline' && dbg?.stages?.length) { + const parts = [] + for (const st of dbg.stages) { + for (const p of st.prompts || []) { + const label = + p.source === 'reference' + ? `Stage ${st.stage} · Ref: ${p.ref_slug || '?'}` + : `Stage ${st.stage} · Inline` + const text = + p.final_prompt || + (p.ref_debug && typeof p.ref_debug === 'object' ? p.ref_debug.final_prompt : '') || + '' + if (text) parts.push(`── ${label} ──\n${text}`) + } + } + return parts.join('\n\n') + } + return dbg?.final_prompt || '' +} + +function sanitizeDebugForPanel(dbg, resultType) { + if (!dbg) return {} + const o = { ...dbg } + delete o.final_prompt + delete o.template + delete o.ai_response_preview + if (o.stages && resultType === 'pipeline') { + o.stages = o.stages.map((s) => ({ + stage: s.stage, + available_variables: s.available_variables, + output: s.output, + prompts: (s.prompts || []).map((p) => ({ + source: p.source, + ref_slug: p.ref_slug, + output_key: p.output_key, + context_var_key: p.context_var_key, + resolved_placeholders: p.resolved_placeholders || p.ref_debug?.resolved_placeholders, + unresolved_placeholders: p.unresolved_placeholders || p.ref_debug?.unresolved_placeholders, + warnings: p.warnings || p.ref_debug?.warnings, + ai_response_length: p.ai_response_length + })) + })) + } + return o +} + +function renderTestOutput(testResult) { + if (!testResult) return null + if (testResult.error) { + const d = testResult.debug?.detail + const msg = + typeof d === 'string' + ? d + : d?.error || d?.message || testResult.error_message || 'Unbekannter Fehler' + return ( +
+ {typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)} +
+ ) + } + + const out = testResult.output + const fmt = testResult.output_format + const typ = testResult.type + + if (out == null) { + return Keine Ausgabe + } + + const proseBox = (content) => ( +
+ {content} +
+ ) + + if (typ === 'pipeline' && out && typeof out === 'object' && !Array.isArray(out)) { + return ( +
+ {Object.entries(out).map(([key, val]) => ( +
+
+ {key} +
+ {typeof val === 'string' ? ( + proseBox() + ) : ( +
+                {JSON.stringify(val, null, 2)}
+              
+ )} +
+ ))} +
+ ) + } + + if (typeof out === 'string') { + if (fmt === 'json') { + try { + const parsed = JSON.parse(out) + return ( +
+            {JSON.stringify(parsed, null, 2)}
+          
+ ) + } catch { + return proseBox(
{out}
) + } + } + return proseBox() + } + + if (typeof out === 'object') { + return ( +
+        {JSON.stringify(out, null, 2)}
+      
+ ) + } + + return {String(out)} +} /** * Unified Prompt Editor Modal (Issue #28 Phase 3) @@ -47,6 +242,8 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState(null) const [showDebug, setShowDebug] = useState(false) + /** 'response' | 'prompt' | 'debug' */ + const [testResultTab, setTestResultTab] = useState('response') useEffect(() => { loadAvailablePrompts() @@ -280,6 +477,7 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { try { const result = await api.executeUnifiedPrompt(prompt.slug, null, null, true) setTestResult(result) + setTestResultTab('response') setShowDebug(true) } catch (e) { // Show error AND try to extract debug info from error @@ -290,7 +488,12 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { try { const parsed = JSON.parse(errorMsg) if (parsed.detail) { - setError('Test-Fehler: ' + parsed.detail) + const d = parsed.detail + const msg = + typeof d === 'string' + ? d + : d.error || d.message || d.msg || JSON.stringify(d).slice(0, 500) + setError('Test-Fehler: ' + msg) debugData = parsed } else { setError('Test-Fehler: ' + errorMsg) @@ -305,6 +508,7 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { error_message: errorMsg, debug: debugData || { error: errorMsg } }) + setTestResultTab('response') setShowDebug(true) // ALWAYS show debug on test, even on error } finally { setTesting(false) @@ -315,7 +519,12 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { if (!testResult) return // Extract all placeholder data from test result - const debug = testResult.debug || testResult + const raw = testResult.debug || testResult + // API-Fehler: FastAPI liefert { detail: { error, debug: { resolved_placeholders, ... } } } + const debug = + raw && typeof raw === 'object' && raw.detail && raw.detail.debug + ? { ...raw.detail.debug, ...raw.detail } + : raw const exportData = { export_date: new Date().toISOString(), prompt_slug: prompt?.slug || 'unknown', @@ -711,7 +920,7 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { )} - {/* Debug Output */} + {/* Test-Ergebnis: Antwort · Prompt · Debug */} {showDebug && testResult && (

- 🔬 Debug-Info + Test-Ergebnis

-
+
-
-              {JSON.stringify(testResult.debug || testResult, null, 2)}
-            
+ +
+ {[ + { id: 'response', label: 'Antwort' }, + { id: 'prompt', label: 'Prompt (an LLM)' }, + { id: 'debug', label: 'Debug' } + ].map((tab) => ( + + ))} +
+ +
+ {testResultTab === 'response' && ( +
+ {renderTestOutput(testResult)} +
+ )} + + {testResultTab === 'prompt' && ( +
+                  {buildPromptTextForDisplay(testResult) || '(Kein Prompt-Text in der Antwort — evtl. Fehler vor Ausführung.)'}
+                
+ )} + + {testResultTab === 'debug' && ( +
+                  {JSON.stringify(
+                    sanitizeDebugForPanel(
+                      extractExecutionDebug(testResult),
+                      testResult.error ? null : testResult.type
+                    ),
+                    null,
+                    2
+                  )}
+                
+ )} +
)} -- 2.43.0 From 08c9cccdcc0ee7359a38c701e6346c646b12b8e1 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 12 Apr 2026 11:10:39 +0200 Subject: [PATCH 2/2] feat: Add expandable collapsible component for improved content display - Introduced `ExpandableCollapsible` component to manage the visibility of lengthy content, allowing users to toggle between expanded and collapsed views. - Updated `renderTestOutput` to utilize the new component for displaying test results, JSON outputs, and object representations, enhancing user experience by reducing clutter. - Enhanced `Markdown` component to support fenced code blocks, improving the rendering of code snippets with language labels and better styling. These changes improve the readability and organization of content within the application, providing users with a more interactive and manageable interface. --- .../src/components/UnifiedPromptModal.jsx | 164 ++++++++++++------ frontend/src/utils/Markdown.jsx | 53 +++++- 2 files changed, 165 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/UnifiedPromptModal.jsx b/frontend/src/components/UnifiedPromptModal.jsx index 3052f06..4281866 100644 --- a/frontend/src/components/UnifiedPromptModal.jsx +++ b/frontend/src/components/UnifiedPromptModal.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, useLayoutEffect } from 'react' import { api } from '../utils/api' import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react' import PlaceholderPicker from './PlaceholderPicker' @@ -66,6 +66,53 @@ function sanitizeDebugForPanel(dbg, resultType) { return o } +/** Langer Inhalt: zunächst auf maxRem eingeklappt, „Mehr/Weniger anzeigen“ */ +function ExpandableCollapsible({ children, contentKey, maxRem = 28 }) { + const [expanded, setExpanded] = useState(false) + const wrapRef = useRef(null) + const [showToggle, setShowToggle] = useState(false) + + useEffect(() => { + setExpanded(false) + }, [contentKey]) + + useLayoutEffect(() => { + const el = wrapRef.current + if (!el) return + if (expanded) { + setShowToggle(true) + return + } + setShowToggle(el.scrollHeight > el.clientHeight + 2) + }, [contentKey, expanded]) + + return ( +
+
+ {children} +
+ {showToggle && ( +
+ +
+ )} +
+ ) +} + function renderTestOutput(testResult) { if (!testResult) return null if (testResult.error) { @@ -89,7 +136,7 @@ function renderTestOutput(testResult) { return Keine Ausgabe } - const proseBox = (content) => ( + const proseBox = (content, contentKey) => (
- {content} + {contentKey != null ? ( + {content} + ) : ( + content + )}
) @@ -123,23 +174,27 @@ function renderTestOutput(testResult) { {key} {typeof val === 'string' ? ( - proseBox() + proseBox( + , + `pipe-${key}-${val.length}-${val.slice(0, 120)}` + ) ) : ( -
-                {JSON.stringify(val, null, 2)}
-              
+ +
+                  {JSON.stringify(val, null, 2)}
+                
+
)} ))} @@ -151,47 +206,54 @@ function renderTestOutput(testResult) { if (fmt === 'json') { try { const parsed = JSON.parse(out) + const jsonStr = JSON.stringify(parsed, null, 2) return ( -
-            {JSON.stringify(parsed, null, 2)}
-          
+ +
+              {jsonStr}
+            
+
) } catch { - return proseBox(
{out}
) + return proseBox( +
{out}
, + `json-fail-${out.length}-${out.slice(0, 80)}` + ) } } - return proseBox() + return proseBox(, `text-out-${out.length}-${out.slice(0, 120)}`) } if (typeof out === 'object') { + const jsonStr = JSON.stringify(out, null, 2) return ( -
-        {JSON.stringify(out, null, 2)}
-      
+ +
+          {jsonStr}
+        
+
) } diff --git a/frontend/src/utils/Markdown.jsx b/frontend/src/utils/Markdown.jsx index 328fa8b..ece6419 100644 --- a/frontend/src/utils/Markdown.jsx +++ b/frontend/src/utils/Markdown.jsx @@ -1,5 +1,5 @@ // Lightweight Markdown renderer – handles the subset used by the AI: -// ## Headings, **bold**, bullet lists, numbered lists, line breaks +// ## Headings, **bold**, bullet lists, numbered lists, fenced ``` code ```, line breaks export default function Markdown({ text }) { if (!text) return null @@ -7,6 +7,19 @@ export default function Markdown({ text }) { const lines = text.split('\n') const elements = [] let i = 0 + let blockId = 0 + + const codeBlockStyle = { + margin: '10px 0', + padding: 12, + background: 'var(--surface2)', + borderRadius: 8, + border: '1px solid var(--border)', + overflow: 'auto', + fontSize: 12, + lineHeight: 1.5, + fontFamily: 'ui-monospace, Consolas, monospace' + } const parseLine = (line) => { // Parse inline **bold** and *italic* @@ -39,6 +52,44 @@ export default function Markdown({ text }) { while (i < lines.length) { const line = lines[i] + // Fenced code block: ``` or ```lang + const trimmedStart = line.trimStart() + if (trimmedStart.startsWith('```')) { + const lang = trimmedStart.slice(3).trim() || null + i++ + const codeLines = [] + while (i < lines.length) { + if (lines[i].trim().startsWith('```')) { + i++ + break + } + codeLines.push(lines[i]) + i++ + } + const code = codeLines.join('\n') + elements.push( +
+ {lang && ( +
+ {lang} +
+ )} +
+            {code}
+          
+
+ ) + continue + } + // Skip empty lines (add spacing) if (line.trim() === '') { elements.push(
) -- 2.43.0