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..4281866 100644 --- a/frontend/src/components/UnifiedPromptModal.jsx +++ b/frontend/src/components/UnifiedPromptModal.jsx @@ -1,7 +1,264 @@ -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' +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 +} + +/** 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 ( +
+ {JSON.stringify(val, null, 2)}
+
+
+ {jsonStr}
+
+ {out},
+ `json-fail-${out.length}-${out.slice(0, 80)}`
+ )
+ }
+ }
+ return proseBox(
+ {jsonStr}
+
+
- {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
+ )}
+
+ )}
+
+ {code}
+
+