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
+                  )}
+                
+ )} +
)}