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 ( +
+ {JSON.stringify(val, null, 2)}
+
+ )}
+
+ {JSON.stringify(parsed, null, 2)}
+
+ )
+ } catch {
+ return proseBox({out})
+ }
+ }
+ return proseBox(
+ {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 && (
- {JSON.stringify(testResult.debug || testResult, null, 2)}
-
+
+
+ {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
+ )}
+
+ )}
+