{
"tool_response": {
"filePath": "\/opt\/scripts\/pipeline\/generate.py",
"oldString": "def run_critic(content, critic_id, model=\"anthropic\"):\n \"\"\"\n Run a single critic on content.\n\n Returns:\n dict with feedback and rating\n \"\"\"\n db.connect()\n\n try:\n critic = get_critic(critic_id)\n if not critic:\n return {\"error\": f\"Critic {critic_id} not found\"}\n\n fokus = json.loads(critic[\"fokus\"]) if isinstance(critic[\"fokus\"], str) else critic[\"fokus\"]\n\n prompt = f\"\"\"Du bist ein kritischer Lektor mit dem Fokus auf: {\", \".join(fokus)}\n\nAnalysiere den folgenden Text und gib strukturiertes Feedback:\n\n## Text:\n{content}\n\n## Deine Aufgabe:\n1. Prüfe den Text auf die Aspekte: {\", \".join(fokus)}\n2. Identifiziere konkrete Verbesserungspunkte\n3. Bewerte die Qualität (1-10)\n\nAntworte im JSON-Format:\n{{\n \"rating\": 8,\n \"passed\": true,\n \"issues\": [\"Issue 1\", \"Issue 2\"],\n \"suggestions\": [\"Suggestion 1\"],\n \"summary\": \"Kurze Zusammenfassung\"\n}}\"\"\"",
"newString": "def get_prompt(name):\n \"\"\"Load prompt from database by name.\"\"\"\n cursor = db.execute(\n \"\"\"SELECT content FROM prompts WHERE name = %s AND is_active = 1 ORDER BY version DESC LIMIT 1\"\"\",\n (name,),\n )\n result = cursor.fetchone()\n cursor.close()\n return result[\"content\"] if result else None\n\n\ndef run_critic(content, critic_id, model=\"anthropic\"):\n \"\"\"\n Run a single critic on content.\n\n Returns:\n dict with feedback and rating\n \"\"\"\n db.connect()\n\n try:\n critic = get_critic(critic_id)\n if not critic:\n return {\"error\": f\"Critic {critic_id} not found\"}\n\n fokus = json.loads(critic[\"fokus\"]) if isinstance(critic[\"fokus\"], str) else critic[\"fokus\"]\n fokus_str = \", \".join(fokus)\n\n # Load prompt from database (via critic.prompt_id or fallback to generic)\n prompt_template = critic.get(\"prompt_content\")\n if not prompt_template:\n prompt_template = get_prompt(\"critic-generic\")\n if not prompt_template:\n # Ultimate fallback - should never happen if DB is properly set up\n prompt_template = \"\"\"Du bist ein kritischer Lektor mit dem Fokus auf: {fokus}\n\nAnalysiere den folgenden Text und gib strukturiertes Feedback:\n\n## Text:\n{content}\n\n## Deine Aufgabe:\n1. Prüfe den Text auf die Aspekte: {fokus}\n2. Identifiziere konkrete Verbesserungspunkte\n3. Bewerte die Qualität (1-10)\n\nAntworte im JSON-Format:\n{{\n \"rating\": 8,\n \"passed\": true,\n \"issues\": [\"Issue 1\", \"Issue 2\"],\n \"suggestions\": [\"Suggestion 1\"],\n \"summary\": \"Kurze Zusammenfassung\"\n}}\"\"\"\n\n # Format prompt with variables\n prompt = prompt_template.format(fokus=fokus_str, content=content)",
"originalFile": "#!\/usr\/bin\/env python3\n\"\"\"\nContent Generation for KI-System\nGenerates content using RAG context, author profiles, and contracts.\n\"\"\"\n\nimport json\nimport sys\n\nsys.path.insert(0, \"\/opt\/scripts\/pipeline\")\n\nfrom config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL, OLLAMA_CHAT_MODEL, OLLAMA_HOST # noqa: I001, E402\nfrom db import db # noqa: E402\nfrom embed import search_similar # noqa: E402\n\n\ndef _repair_json(json_str):\n \"\"\"\n Attempt to repair common JSON issues from LLM output.\n\n Fixes:\n - Unescaped quotes in strings\n - Missing commas between array elements\n - Trailing commas\n - Control characters in strings\n \"\"\"\n import re\n\n # Remove control characters except newlines and tabs\n json_str = re.sub(r'[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]', '', json_str)\n\n # Fix common issue: missing comma before closing bracket in arrays\n json_str = re.sub(r'\"\\s*\\n\\s*]', '\"\\n]', json_str)\n\n # Fix trailing commas before closing brackets\/braces\n json_str = re.sub(r',\\s*}', '}', json_str)\n json_str = re.sub(r',\\s*]', ']', json_str)\n\n # Fix missing commas between array elements (string followed by string)\n json_str = re.sub(r'\"\\s*\\n\\s*\"', '\",\\n\"', json_str)\n\n # Fix unescaped quotes within strings (heuristic: quotes not at boundaries)\n # This is tricky, so we do a simple fix for common patterns\n lines = json_str.split('\\n')\n fixed_lines = []\n for line in lines:\n # Count quotes - if odd number and line has content, try to fix\n quote_count = line.count('\"') - line.count('\\\\\"')\n if quote_count % 2 != 0 and ':' in line:\n # Try to escape internal quotes (very basic heuristic)\n parts = line.split(':', 1)\n if len(parts) == 2:\n key_part = parts[0]\n value_part = parts[1]\n # If value has odd quotes, try to balance\n if value_part.count('\"') % 2 != 0:\n # Add escaped quote or remove problematic char\n value_part = value_part.rstrip().rstrip(',')\n if not value_part.endswith('\"'):\n value_part += '\"'\n line = key_part + ':' + value_part\n fixed_lines.append(line)\n\n return '\\n'.join(fixed_lines)\n\n\ndef get_rag_context(briefing, collection=\"documents\", limit=5):\n \"\"\"\n Get relevant context from Qdrant based on briefing.\n Returns list of chunks with content and metadata.\n \"\"\"\n results = search_similar(briefing, collection=collection, limit=limit)\n\n context_items = []\n for result in results:\n context_items.append(\n {\n \"content\": result[\"payload\"].get(\"content\", \"\"),\n \"source\": result[\"payload\"].get(\"document_title\", \"Unknown\"),\n \"score\": round(result[\"score\"], 4),\n }\n )\n\n return context_items\n\n\ndef get_config_item(item_id, item_type):\n \"\"\"Load configuration item from content_config table.\"\"\"\n if not item_id:\n return None\n\n cursor = db.execute(\n \"SELECT name, content FROM content_config WHERE id = %s AND type = %s AND status = 'active'\",\n (item_id, item_type),\n )\n result = cursor.fetchone()\n cursor.close()\n\n if result:\n config = json.loads(result[\"content\"]) if isinstance(result[\"content\"], str) else result[\"content\"]\n return {\"name\": result[\"name\"], \"config\": config}\n return None\n\n\ndef get_author_profile(profile_id):\n \"\"\"Load author profile from database.\"\"\"\n return get_config_item(profile_id, \"author_profile\")\n\n\ndef get_contract(contract_id):\n \"\"\"Load content contract from database.\"\"\"\n return get_config_item(contract_id, \"contract\")\n\n\ndef get_structure(structure_id):\n \"\"\"Load content structure from database.\"\"\"\n result = get_config_item(structure_id, \"structure\")\n if result:\n # Structure has additional 'type' field in config\n result[\"type\"] = result[\"config\"].get(\"type\", \"article\")\n return result\n\n\ndef get_order(order_id):\n \"\"\"Load content order with all related data.\"\"\"\n cursor = db.execute(\n \"\"\"SELECT co.*,\n ap.name as profile_name, ap.content as profile_config,\n cc.name as contract_name, cc.content as contract_config,\n cs.name as structure_name, cs.content as structure_config\n FROM content_orders co\n LEFT JOIN content_config ap ON co.author_profile_id = ap.id AND ap.type = 'author_profile'\n LEFT JOIN content_config cc ON co.contract_id = cc.id AND cc.type = 'contract'\n LEFT JOIN content_config cs ON co.structure_id = cs.id AND cs.type = 'structure'\n WHERE co.id = %s\"\"\",\n (order_id,),\n )\n result = cursor.fetchone()\n cursor.close()\n return result\n\n\ndef build_generation_prompt(briefing, context, profile, contract, structure=None):\n \"\"\"Build the content generation prompt.\"\"\"\n\n # Format context\n context_text = \"\"\n for i, ctx in enumerate(context, 1):\n context_text += f\"\\n[Quelle {i}: {ctx['source']}]\\n{ctx['content']}\\n\"\n\n # Build rich profile instructions\n profile_text = \"\"\n if profile:\n config = profile.get(\"config\", {})\n autorenprofil = config.get(\"autorenprofil\", config) # Support both structures\n\n # Extract voice\/stimme\n stimme = autorenprofil.get(\"stimme\", {})\n stimme_text = \"\"\n if stimme:\n stimme_text = f\"\"\"\n### Stimme\/Tonalität:\n- Ton: {stimme.get('ton', 'neutral')}\n- Perspektive: {stimme.get('perspektive', 'neutral')}\n- Komplexität: {stimme.get('komplexitaet', 'mittel')}\n- Autorität: {stimme.get('autoritaet', 'Experte')}\"\"\"\n\n # Extract style\/stil\n stil = autorenprofil.get(\"stil\", {})\n stil_text = \"\"\n if stil:\n fachsprache_beispiele = stil.get(\"fachsprache_beispiele\", [])\n fachsprache_str = \", \".join(fachsprache_beispiele[:5]) if fachsprache_beispiele else \"\"\n stil_text = f\"\"\"\n### Stil:\n- Fachsprache: {'Ja' if stil.get('fachsprache', False) else 'Nein'}\n- Fachbegriffe: {fachsprache_str}\n- Satzlänge: {stil.get('satzlaenge', 'mittel')}\n- Absatzstruktur: {stil.get('absatzstruktur', 'klar gegliedert')}\"\"\"\n\n # Extract language patterns\/sprachliche_muster\n muster = autorenprofil.get(\"sprachliche_muster\", {})\n muster_text = \"\"\n if muster:\n phrasen = muster.get(\"phrasen\", [])\n phrasen_str = \", \".join([f'\"{p}\"' for p in phrasen[:4]]) if phrasen else \"\"\n uebergaenge = muster.get(\"uebergaenge\", [])\n uebergaenge_str = \", \".join([f'\"{u}\"' for u in uebergaenge[:3]]) if uebergaenge else \"\"\n muster_text = f\"\"\"\n### Sprachliche Muster:\n- Typische Phrasen: {phrasen_str}\n- Übergangsformulierungen: {uebergaenge_str}\"\"\"\n\n # Extract taboos\/tabus\n tabus = autorenprofil.get(\"tabus\", [])\n tabus_text = \"\"\n if tabus:\n tabus_str = \", \".join(tabus[:5])\n tabus_text = f\"\"\"\n### Zu vermeiden (Tabus):\n{tabus_str}\"\"\"\n\n # Extract core messages\/kernbotschaften\n kernbotschaften = autorenprofil.get(\"kernbotschaften\", [])\n kern_text = \"\"\n if kernbotschaften:\n kern_str = \"\\n\".join([f\"- {k}\" for k in kernbotschaften[:4]])\n kern_text = f\"\"\"\n### Kernbotschaften (implizit vermitteln):\n{kern_str}\"\"\"\n\n # Extract output format (HTML for web profiles)\n output_format = autorenprofil.get(\"output_format\", {})\n format_text = \"\"\n if output_format and output_format.get(\"typ\") == \"html\":\n erlaubte = output_format.get(\"erlaubte_elemente\", {})\n verboten = output_format.get(\"verbotene_elemente\", [])\n regeln = output_format.get(\"struktur_regeln\", [])\n\n erlaubte_str = []\n for kategorie, elemente in erlaubte.items():\n erlaubte_str.extend(elemente[:3])\n\n format_text = f\"\"\"\n### Output-Format: HTML\n**Erlaubte Elemente:**\n{', '.join(erlaubte_str[:10])}\n\n**Struktur-Regeln:**\n- {chr(10).join('- ' + r for r in regeln[:4]) if regeln else 'Semantisches HTML verwenden'}\n\n**Verboten:** {', '.join(verboten[:4]) if verboten else 'div, span, br, style'}\"\"\"\n\n # Combine all profile sections\n profile_text = f\"\"\"\n## Autorenprofil: {profile.get(\"name\", \"Standard\")}\n{stimme_text}\n{stil_text}\n{muster_text}\n{tabus_text}\n{kern_text}\n{format_text}\n\"\"\"\n\n # Build contract requirements\n contract_text = \"\"\n if contract:\n config = contract.get(\"config\", {})\n req = config.get(\"requirements\", {})\n contract_text = f\"\"\"\nContract: {contract.get(\"name\", \"Standard\")}\n- Wortanzahl: {req.get(\"min_words\", 500)} - {req.get(\"max_words\", 5000)} Wörter\n- Struktur-Validierung: {req.get(\"structure_validation\", True)}\n\"\"\"\n\n # Build structure instructions\n structure_text = \"\"\n if structure:\n config = structure.get(\"config\", {})\n structure_text = f\"\"\"\nStruktur-Template: {structure.get(\"name\", \"\")}\n- Typ: {structure.get(\"type\", \"\")}\n- Abschnitte: {json.dumps(config.get(\"sections\", []), ensure_ascii=False)}\n\"\"\"\n\n # Determine if HTML output is required\n html_output = False\n if profile:\n config = profile.get(\"config\", {})\n autorenprofil = config.get(\"autorenprofil\", config)\n output_format = autorenprofil.get(\"output_format\", {})\n html_output = output_format.get(\"typ\") == \"html\"\n\n format_instruction = \"\"\n if html_output:\n format_instruction = \"\"\"7. **WICHTIG: Ausgabe als wohlformatiertes HTML**\n - Verwende nur: h1, h2, h3, h4, p, ul, ol, li, strong, a, section, table, tr, td\n - Jeder h2-Abschnitt muss in einer eigenen <section> stehen\n - Kein Markdown, kein div\/span\/br\n - Fließtext immer in <p>-Tags\"\"\"\n\n prompt = f\"\"\"Du bist ein professioneller Content-Autor. Erstelle basierend auf dem Briefing und dem bereitgestellten Kontext einen hochwertigen Text.\n\n{profile_text}\n{contract_text}\n{structure_text}\n\n## Kontex... [TRUNCATED-10f16e95a07faddc]",
"structuredPatch": [
{
"oldStart": 535,
"oldLines": 6,
"newStart": 535,
"newLines": 17,
"lines": [
" return result",
" ",
" ",
"+def get_prompt(name):",
"+ \"\"\"Load prompt from database by name.\"\"\"",
"+ cursor = db.execute(",
"+ \"\"\"SELECT content FROM prompts WHERE name = %s AND is_active = 1 ORDER BY version DESC LIMIT 1\"\"\",",
"+ (name,),",
"+ )",
"+ result = cursor.fetchone()",
"+ cursor.close()",
"+ return result[\"content\"] if result else None",
"+",
"+",
" def run_critic(content, critic_id, model=\"anthropic\"):",
" \"\"\"",
" Run a single critic on content."
]
},
{
"oldStart": 550,
"oldLines": 8,
"newStart": 561,
"newLines": 15,
"lines": [
" return {\"error\": f\"Critic {critic_id} not found\"}",
" ",
" fokus = json.loads(critic[\"fokus\"]) if isinstance(critic[\"fokus\"], str) else critic[\"fokus\"]",
"+ fokus_str = \", \".join(fokus)",
" ",
"- prompt = f\"\"\"Du bist ein kritischer Lektor mit dem Fokus auf: {\", \".join(fokus)}",
"+ # Load prompt from database (via critic.prompt_id or fallback to generic)",
"+ prompt_template = critic.get(\"prompt_content\")",
"+ if not prompt_template:",
"+ prompt_template = get_prompt(\"critic-generic\")",
"+ if not prompt_template:",
"+ # Ultimate fallback - should never happen if DB is properly set up",
"+ prompt_template = \"\"\"Du bist ein kritischer Lektor mit dem Fokus auf: {fokus}",
" ",
" Analysiere den folgenden Text und gib strukturiertes Feedback:",
" "
]
},
{
"oldStart": 559,
"oldLines": 7,
"newStart": 577,
"newLines": 7,
"lines": [
" {content}",
" ",
" ## Deine Aufgabe:",
"-1. Prüfe den Text auf die Aspekte: {\", \".join(fokus)}",
"+1. Prüfe den Text auf die Aspekte: {fokus}",
" 2. Identifiziere konkrete Verbesserungspunkte",
" 3. Bewerte die Qualität (1-10)",
" "
]
},
{
"oldStart": 572,
"oldLines": 6,
"newStart": 590,
"newLines": 9,
"lines": [
" \"summary\": \"Kurze Zusammenfassung\"",
" }}\"\"\"",
" ",
"+ # Format prompt with variables",
"+ prompt = prompt_template.format(fokus=fokus_str, content=content)",
"+",
" response = call_llm(prompt, model, client_name=\"content-studio-critique\")",
" ",
" # Parse JSON from response with robust error handling"
]
}
],
"userModified": false,
"replaceAll": false
}
}