Protokoll #15138
| ID | 15138 |
|---|---|
| Zeitstempel | 2025-12-25 13:58:13.673264 |
| Client | root |
| IP | Warning: Undefined array key "request_ip" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 |
| Modell | claude-sonnet-4-20250514 |
| Status | completed |
| Tokens | 15,175 (Input: 0, Output: 0) |
| Dauer | 117 ms |
| Request-Zeit | Warning: Undefined array key "request_timestamp" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 18 |
| Response-Zeit | - |
Warning: Undefined array key "error_message" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 22
Request
{
"event": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "\/var\/www\/scripts\/pipeline\/generate.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nContent Generation for KI-System - Wrapper Module.\n\nThis module provides backward compatibility by re-exporting all functions\nfrom the generators subpackage. New code should import directly from generators.\n\nRefactored for SRP compliance - see generators\/ for implementation.\n\"\"\"\n\nimport json\nimport sys\n\n# Re-export all public functions from generators package\nfrom generators import (\n repair_json as _repair_json,\n get_rag_context,\n get_semantic_context,\n get_taxonomy_context,\n get_prompt,\n get_config_item,\n get_author_profile,\n get_contract,\n get_structure,\n get_order,\n build_generation_prompt,\n call_llm,\n generate_content,\n save_version,\n save_sources,\n update_order_status,\n get_critic,\n run_critic,\n run_critique_round,\n revise_content,\n)\n\n# Also export config_loader parsers with original names for backward compatibility\nfrom generators.config_loader import parse_author_profile as _parse_new_author_profile\nfrom generators.config_loader import parse_structure as _parse_new_structure\n\n__all__ = [\n \"_repair_json\",\n \"get_rag_context\",\n \"get_semantic_context\",\n \"get_taxonomy_context\",\n \"get_prompt\",\n \"get_config_item\",\n \"get_author_profile\",\n \"get_contract\",\n \"get_structure\",\n \"get_order\",\n \"_parse_new_author_profile\",\n \"_parse_new_structure\",\n \"build_generation_prompt\",\n \"call_llm\",\n \"generate_content\",\n \"save_version\",\n \"save_sources\",\n \"update_order_status\",\n \"get_critic\",\n \"run_critic\",\n \"run_critique_round\",\n \"revise_content\",\n]\n\n\nif __name__ == \"__main__\":\n if len(sys.argv) < 3:\n print(\"Usage: python generate.py <command> <id> [options]\")\n print(\"Commands: generate, critique, revise\")\n sys.exit(1)\n\n command = sys.argv[1]\n entity_id = int(sys.argv[2])\n model = sys.argv[3] if len(sys.argv) > 3 else \"anthropic\"\n\n if command == \"generate\":\n result = generate_content(entity_id, model=model)\n elif command == \"critique\":\n result = run_critique_round(entity_id, model=model)\n elif command == \"revise\":\n result = revise_content(entity_id, model=model)\n else:\n print(f\"Unknown command: {command}\")\n sys.exit(1)\n\n print(json.dumps(result, ensure_ascii=False, indent=2))\n"
}
}
Response
{
"tool_response": {
"type": "update",
"filePath": "\/var\/www\/scripts\/pipeline\/generate.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nContent Generation for KI-System - Wrapper Module.\n\nThis module provides backward compatibility by re-exporting all functions\nfrom the generators subpackage. New code should import directly from generators.\n\nRefactored for SRP compliance - see generators\/ for implementation.\n\"\"\"\n\nimport json\nimport sys\n\n# Re-export all public functions from generators package\nfrom generators import (\n repair_json as _repair_json,\n get_rag_context,\n get_semantic_context,\n get_taxonomy_context,\n get_prompt,\n get_config_item,\n get_author_profile,\n get_contract,\n get_structure,\n get_order,\n build_generation_prompt,\n call_llm,\n generate_content,\n save_version,\n save_sources,\n update_order_status,\n get_critic,\n run_critic,\n run_critique_round,\n revise_content,\n)\n\n# Also export config_loader parsers with original names for backward compatibility\nfrom generators.config_loader import parse_author_profile as _parse_new_author_profile\nfrom generators.config_loader import parse_structure as _parse_new_structure\n\n__all__ = [\n \"_repair_json\",\n \"get_rag_context\",\n \"get_semantic_context\",\n \"get_taxonomy_context\",\n \"get_prompt\",\n \"get_config_item\",\n \"get_author_profile\",\n \"get_contract\",\n \"get_structure\",\n \"get_order\",\n \"_parse_new_author_profile\",\n \"_parse_new_structure\",\n \"build_generation_prompt\",\n \"call_llm\",\n \"generate_content\",\n \"save_version\",\n \"save_sources\",\n \"update_order_status\",\n \"get_critic\",\n \"run_critic\",\n \"run_critique_round\",\n \"revise_content\",\n]\n\n\nif __name__ == \"__main__\":\n if len(sys.argv) < 3:\n print(\"Usage: python generate.py <command> <id> [options]\")\n print(\"Commands: generate, critique, revise\")\n sys.exit(1)\n\n command = sys.argv[1]\n entity_id = int(sys.argv[2])\n model = sys.argv[3] if len(sys.argv) > 3 else \"anthropic\"\n\n if command == \"generate\":\n result = generate_content(entity_id, model=model)\n elif command == \"critique\":\n result = run_critique_round(entity_id, model=model)\n elif command == \"revise\":\n result = revise_content(entity_id, model=model)\n else:\n print(f\"Unknown command: {command}\")\n sys.exit(1)\n\n print(json.dumps(result, ensure_ascii=False, indent=2))\n",
"structuredPatch": [
{
"oldStart": 1,
"oldLines": 1115,
"newStart": 1,
"newLines": 71,
"lines": [
" #!\/usr\/bin\/env python3",
" \"\"\"",
"-Content Generation for KI-System",
"-Generates content using RAG context, author profiles, and contracts.",
"+Content Generation for KI-System - Wrapper Module.",
"+",
"+This module provides backward compatibility by re-exporting all functions",
"+from the generators subpackage. New code should import directly from generators.",
"+",
"+Refactored for SRP compliance - see generators\/ for implementation.",
" \"\"\"",
" ",
" import json",
" import sys",
" ",
"-sys.path.insert(0, \"\/var\/www\/scripts\/pipeline\")",
"+# Re-export all public functions from generators package",
"+from generators import (",
"+ repair_json as _repair_json,",
"+ get_rag_context,",
"+ get_semantic_context,",
"+ get_taxonomy_context,",
"+ get_prompt,",
"+ get_config_item,",
"+ get_author_profile,",
"+ get_contract,",
"+ get_structure,",
"+ get_order,",
"+ build_generation_prompt,",
"+ call_llm,",
"+ generate_content,",
"+ save_version,",
"+ save_sources,",
"+ update_order_status,",
"+ get_critic,",
"+ run_critic,",
"+ run_critique_round,",
"+ revise_content,",
"+)",
" ",
"-from config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL, OLLAMA_CHAT_MODEL, OLLAMA_HOST # noqa: I001, E402",
"-from db import db # noqa: E402",
"-from embed import search_similar # noqa: E402",
"+# Also export config_loader parsers with original names for backward compatibility",
"+from generators.config_loader import parse_author_profile as _parse_new_author_profile",
"+from generators.config_loader import parse_structure as _parse_new_structure",
" ",
"+__all__ = [",
"+ \"_repair_json\",",
"+ \"get_rag_context\",",
"+ \"get_semantic_context\",",
"+ \"get_taxonomy_context\",",
"+ \"get_prompt\",",
"+ \"get_config_item\",",
"+ \"get_author_profile\",",
"+ \"get_contract\",",
"+ \"get_structure\",",
"+ \"get_order\",",
"+ \"_parse_new_author_profile\",",
"+ \"_parse_new_structure\",",
"+ \"build_generation_prompt\",",
"+ \"call_llm\",",
"+ \"generate_content\",",
"+ \"save_version\",",
"+ \"save_sources\",",
"+ \"update_order_status\",",
"+ \"get_critic\",",
"+ \"run_critic\",",
"+ \"run_critique_round\",",
"+ \"revise_content\",",
"+]",
" ",
"-def _repair_json(json_str):",
"- \"\"\"",
"- Attempt to repair common JSON issues from LLM output.",
" ",
"- Fixes:",
"- - Unescaped quotes in strings",
"- - Missing commas between array elements",
"- - Trailing commas",
"- - Control characters in strings",
"- \"\"\"",
"- import re",
"-",
"- # Remove control characters except newlines and tabs",
"- json_str = re.sub(r\"[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]\", \"\", json_str)",
"-",
"- # Fix common issue: missing comma before closing bracket in arrays",
"- json_str = re.sub(r'\"\\s*\\n\\s*]', '\"\\n]', json_str)",
"-",
"- # Fix trailing commas before closing brackets\/braces",
"- json_str = re.sub(r\",\\s*}\", \"}\", json_str)",
"- json_str = re.sub(r\",\\s*]\", \"]\", json_str)",
"-",
"- # Fix missing commas between array elements (string followed by string)",
"- json_str = re.sub(r'\"\\s*\\n\\s*\"', '\",\\n\"', json_str)",
"-",
"- # Fix unescaped quotes within strings (heuristic: quotes not at boundaries)",
"- # This is tricky, so we do a simple fix for common patterns",
"- lines = json_str.split(\"\\n\")",
"- fixed_lines = []",
"- for line in lines:",
"- # Count quotes - if odd number and line has content, try to fix",
"- quote_count = line.count('\"') - line.count('\\\\\"')",
"- if quote_count % 2 != 0 and \":\" in line:",
"- # Try to escape internal quotes (very basic heuristic)",
"- parts = line.split(\":\", 1)",
"- if len(parts) == 2:",
"- key_part = parts[0]",
"- value_part = parts[1]",
"- # If value has odd quotes, try to balance",
"- if value_part.count('\"') % 2 != 0:",
"- # Add escaped quote or remove problematic char",
"- value_part = value_part.rstrip().rstrip(\",\")",
"- if not value_part.endswith('\"'):",
"- value_part += '\"'",
"- line = key_part + \":\" + value_part",
"- fixed_lines.append(line)",
"-",
"- return \"\\n\".join(fixed_lines)",
"-",
"-",
"-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 get_rag_context(briefing, collection=\"documents\", limit=5):",
"- \"\"\"",
"- Get relevant context from Qdrant based on briefing.",
"- Returns list of chunks with content and metadata.",
"- \"\"\"",
"- results = search_similar(briefing, collection=collection, limit=limit)",
"-",
"- context_items = []",
"- for result in results:",
"- context_items.append(",
"- {",
"- \"content\": result[\"payload\"].get(\"content\", \"\"),",
"- \"source\": result[\"payload\"].get(\"document_title\", \"Unknown\"),",
"- \"score\": round(result[\"score\"], 4),",
"- \"chunk_id\": result[\"payload\"].get(\"chunk_id\"),",
"- \"document_id\": result[\"payload\"].get(\"document_id\"),",
"- }",
"- )",
"-",
"- return context_items",
"-",
"-",
"-def get_config_item(item_id, item_type):",
"- \"\"\"Load configuration item from content_config table.\"\"\"",
"- if not item_id:",
"- return None",
"-",
"- cursor = db.execute(",
"- \"SELECT name, content FROM content_config WHERE id = %s AND type = %s AND status = 'active'\",",
"- (item_id, item_type),",
"- )",
"- result = cursor.fetchone()",
"- cursor.close()",
"-",
"- if result:",
"- config = json.loads(result[\"content\"]) if isinstance(result[\"content\"], str) else result[\"content\"]",
"- return {\"name\": result[\"name\"], \"config\": config}",
"- return None",
"-",
"-",
"-def get_semantic_context(chunk_ids):",
"- \"\"\"",
"- Load entities and relations based on chunk_ids.",
"-",
"- Uses the chunk_entities junction table to find relevant entities,",
"- then loads relations between those entities.",
"-",
"- Args:",
"- chunk_ids: List of chunk IDs from RAG context",
"-",
"- Returns:",
"- dict with 'entities' and 'relations' lists",
"- \"\"\"",
"- if not chunk_ids:",
"- return {\"entities\": [], \"relations\": []}",
"-",
"- # Filter out None values",
"- chunk_ids = [cid for cid in chunk_ids if cid is not None]",
"- if not chunk_ids:",
"- return {\"entities\": [], \"relations\": []}",
"-",
"- placeholders = \", \".join([\"%s\"] * len(chunk_ids))",
"-",
"- # Load entities via chunk_entities",
"- cursor = db.execute(",
"- f\"\"\"SELECT DISTINCT e.id, e.name, e.type, e.description,",
"- AVG(ce.relevance_score) as relevance",
"- FROM chunk_entities ce",
"- JOIN entities e ON ce.entity_id = e.id",
"- WHERE ce.chunk_id IN ({placeholders})",
"- GROUP BY e.id, e.name, e.type, e.description",
"- ORDER BY relevance DESC",
"- LIMIT 10\"\"\",",
"- tuple(chunk_ids),",
"- )",
"- entities = cursor.fetchall()",
"- cursor.close()",
"-",
"- if not entities:",
"- return {\"entities\": [], \"relations\": []}",
"-",
"- # Get entity IDs for relation lookup",
"- entity_ids = [e[\"id\"] for e in entities]",
"- entity_placeholders = \", \".join([\"%s\"] * len(entity_ids))",
"-",
"- # Load relations between found entities",
"- cursor = db.execute(",
"- f\"\"\"SELECT e1.name as source, er.relation_type, e2.name as target",
"- FROM entity_relations er",
"- JOIN entities e1 ON er.source_entity_id = e1.id",
"- JOIN entities e2 ON er.target_entity_id = e2.id",
"- WHERE e1.id IN ({entity_placeholders}) AND e2.id IN ({entity_placeholders})",
"- LIMIT 15\"\"\",",
"- tuple(entity_ids) + tuple(entity_ids),",
"- )",
"- relations = cursor.fetchall()",
"- cursor.close()",
"-",
"- return {\"entities\": entities, \"relations\": relations}",
"-",
"-",
"-def get_taxonomy_context(document_ids):",
"- \"\"\"",
"- Load taxonomy terms for documents.",
"-",
"- Args:",
"- document_ids: List of document IDs from RAG context",
"-",
"- Returns:",
"- List of taxonomy term dicts with name, slug, confidence",
"- \"\"\"",
"- if not document_ids:",
"- return []",
"-",
"- # Filter out None values",
"- document_ids = [did for did in document_ids if did is not None]",
"- if not document_ids:",
"- return []",
"-",
"- placeholders = \", \".join([\"%s\"] * len(document_ids))",
"-",
"- cursor = db.execute(",
"- f\"\"\"SELECT DISTINCT tt.name, tt.slug, MAX(dt.confidence) as confidence",
"- FROM document_taxonomy dt",
"- JOIN taxonomy_terms tt ON dt.taxonomy_term_id = tt.id",
"- WHERE dt.document_id IN ({placeholders})",
"- GROUP BY tt.id, tt.name, tt.slug",
"- ORDER BY confidence DESC\"\"\",",
"- tuple(document_ids),",
"- )",
"- taxonomy = cursor.fetchall()",
"- cursor.close()",
"-",
"- return taxonomy",
"-",
"-",
"-def get_author_profile(profile_id):",
"- \"\"\"Load author profile from database.\"\"\"",
"- return get_config_item(profile_id, \"author_profile\")",
"-",
"-",
"-def get_contract(contract_id):",
"- \"\"\"Load content contract from database.\"\"\"",
"- return get_config_item(contract_id, \"contract\")",
"-",
"-",
"-def get_structure(structure_id):",
"- \"\"\"Load content structure from database.\"\"\"",
"- result = get_config_item(structure_id, \"structure\")",
"- if result:",
"- # Structure has additional 'type' field in config",
"- result[\"type\"] = result[\"config\"].get(\"type\", \"article\")",
"- return result",
"-",
"-",
"-def get_order(order_id):",
"- \"\"\"Load content order with all related data.\"\"\"",
"- cursor = db.execute(",
"- \"\"\"SELECT co.*,",
"- ap.name as profile_name, ap.content as profile_config,",
"- cc.name as contract_name, cc.content as contract_config,",
"- cs.name as structure_name, cs.content as structure_config",
"- FROM content_orders co",
"- LEFT JOIN content_config ap ON co.author_profile_id = ap.id AND ap.type = 'author_profile'",
"- LEFT JOIN content_config cc ON co.contract_id = cc.id AND cc.type = 'contract'",
"- LEFT JOIN content_config cs ON co.structure_id = cs.id AND cs.type = 'structure'",
"- WHERE co.id = %s\"\"\",",
"- (order_id,),",
"- )",
"- result = cursor.fetchone()",
"- cursor.close()",
"- return result",
"-",
"-",
"-def _parse_new_author_profile(config):",
"- \"\"\"Parse new-style author profile (Cary format) into prompt text.\"\"\"",
"- sections = []",
"-",
"- # Haltung",
"- haltung = config.get(\"haltung\", {})",
"- if haltung:",
"- sections.append(f\"\"\"### Haltung:",
"-- Grundhaltung: {haltung.get(\"grundhaltung\", \"\")}",
"-- Ausrichtung: {haltung.get(\"ausrichtung\", \"\")}",
"-- Spannungstoleranz: {haltung.get(\"spannungstoleranz\", \"\")}",
"-- Vereinfachung: {haltung.get(\"vereinfachung\", \"\")}\"\"\")",
"-",
"- # Tonalität",
"- tonalitaet = config.get(\"tonalitaet\", {})",
"- if tonalitaet:",
"- sections.append(f\"\"\"### Tonalität:",
"-- Charakter: {tonalitaet.get(\"charakter\", \"\")}",
"-- Stil: {tonalitaet.get(\"stil\", \"\")}",
"-- Wirkung: {tonalitaet.get(\"wirkung\", \"\")}",
"-- Abgrenzung: {tonalitaet.get(\"abgrenzung\", \"\")}\"\"\")",
"-",
"- # Sprachmodus",
"- sprachmodus = config.get(\"sprachmodus\", {})",
"- if sprachmodus:",
"- sections.append(f\"\"\"### Sprachmodus:",
"-- Denkstil: {sprachmodus.get(\"denkstil\", \"\")}",
"-- Aussagenform: {sprachmodus.get(\"aussagenform\", \"\")}",
"-- Fragenfunktion: {sprachmodus.get(\"fragenfunktion\", \"\")}\"\"\")",
"-",
"- # Grammatik und Satzbau - WICHTIG für Verbote",
"- grammatik = config.get(\"grammatik_und_satzbau\", {})",
"- if grammatik:",
"- verbote = []",
"- if grammatik.get(\"stakkato\") == \"ausgeschlossen\":",
"- verbote.append(\"Stakkato-Sätze\")",
"- if grammatik.get(\"einschuebe\") == \"keine\":",
"- verbote.append(\"Einschübe\")",
"- if grammatik.get(\"gedankenstriche\") == \"verboten\":",
"- verbote.append(\"Gedankenstriche (–)\")",
"-",
"- sections.append(f\"\"\"### Grammatik und Satzbau:",
"-- Sätze: {grammatik.get(\"saetze\", \"\")}",
"-- Rhythmus: {grammatik.get(\"rhythmus\", \"\")}",
"-- **VERBOTEN:** {\", \".join(verbote) if verbote else \"keine\"}\"\"\")",
"-",
"- # Wortwahl",
"- wortwahl = config.get(\"wortwahl\", {})",
"- if wortwahl:",
"- verboten = []",
"- if wortwahl.get(\"buzzwords\") == \"ausgeschlossen\":",
"- verboten.append(\"Buzzwords\")",
"- if wortwahl.get(\"methodennamen\") == \"ausgeschlossen\":",
"- verboten.append(\"Methodennamen\")",
"-",
"- sections.append(f\"\"\"### Wortwahl:",
"-- Niveau: {wortwahl.get(\"niveau\", \"\")}",
"-- Begriffe: {wortwahl.get(\"begriffe\", \"\")}",
"-- **VERBOTEN:** {\", \".join(verboten) if verboten else \"keine\"}\"\"\")",
"-",
"- # Adressierung",
"- adressierung = config.get(\"adressierung\", {})",
"- if adressierung:",
"- sections.append(f\"\"\"### Adressierung:",
"-- Form: {adressierung.get(\"form\", \"Sie\")}",
"-- Beziehung: {adressierung.get(\"beziehung\", \"\")}",
"-- Einladung: {adressierung.get(\"einladung\", \"\")}\"\"\")",
"-",
"- # Metaphern",
"- metaphern = config.get(\"metaphern\", {})",
"- if metaphern:",
"- sections.append(f\"\"\"### Metaphern:",
"-- Einsatz: {metaphern.get(\"einsatz\", \"\")}",
"-- Herkunft: {metaphern.get(\"herkunft\", \"\")}",
"-- Konsistenz: {metaphern.get(\"konsistenz\", \"\")}\"\"\")",
"-",
"- return \"\\n\\n\".join(sections)",
"-",
"-",
"-def _parse_new_structure(config):",
"- \"\"\"Parse new-style structure profile into prompt text and format info.\"\"\"",
"- sections = []",
"- output_format = None",
"- erlaubte_tags = []",
"-",
"- # Ausgabe-Format",
"- ausgabe = config.get(\"ausgabe\", {})",
"- if ausgabe:",
"- output_format = ausgabe.get(\"format\", \"markdown\")",
"- erlaubte_tags = ausgabe.get(\"erlaubte_tags\", [])",
"- verbotene_tags = ausgabe.get(\"verbotene_tags\", [])",
"-",
"- if output_format == \"body-html\":",
"- sections.append(f\"\"\"### Ausgabe-Format: HTML",
"-- **Nur diese Tags verwenden:** {\", \".join(erlaubte_tags)}",
"-- **Verboten:** {\", \".join(verbotene_tags)}",
"-- {ausgabe.get(\"hinweis\", \"Sauberes semantisches HTML\")}\"\"\")",
"-",
"- # Gesamtaufbau",
"- aufbau = config.get(\"gesamtaufbau\", {})",
"- if aufbau:",
"- sections.append(f\"\"\"### Gesamtaufbau:",
"-- Form: {aufbau.get(\"form\", \"\")}",
"-- Dramaturgie: {aufbau.get(\"dramaturgie\", \"\")}",
"-- Linearität: {aufbau.get(\"linearitaet\", \"\")}",
"-- Themensprünge: {aufbau.get(\"themenspruenge\", \"\")}\"\"\")",
"-",
"- # Einstieg",
"- einstieg = config.get(\"einstieg\", {})",
"- if einstieg:",
"- sections.append(f\"\"\"### Einstieg:",
"-- Funktion: {einstieg.get(\"funktion\", \"\")}",
"-- Inhaltstyp: {einstieg.get(\"inhaltstyp\", \"\")}",
"-- Ausschluss: {einstieg.get(\"ausschluss\", \"\")}\"\"\")",
"-",
"- # Hauptteil-Blöcke",
"- hauptteil = config.get(\"hauptteil\", {})",
"- bloecke = hauptteil.get(\"bloecke\", [])",
"- if bloecke:",
"- bloecke_text = \"\\n\".join(",
"- [f\" {i + 1}. {b.get('fokus', '')} → {b.get('ziel', '')}\" for i, b in enumerate(bloecke)]",
"- )",
"- sections.append(f\"\"\"### Hauptteil-Struktur:",
"-{bloecke_text}\"\"\")",
"-",
"- # Schluss",
"- schluss = config.get(\"schluss\", {})",
"- if schluss:",
"- sections.append(f\"\"\"### Schluss:",
"-- Typ: {schluss.get(\"typ\", \"\")}",
"-- Funktion: {schluss.get(\"funktion\", \"\")}",
"-- Abgrenzung: {schluss.get(\"abgrenzung\", \"\")}\"\"\")",
"-",
"- # Formatierung",
"- formatierung = config.get(\"formatierung\", {})",
"- if formatierung:",
"- ausschluss = formatierung.get(\"ausschluss\", [])",
"- if ausschluss:",
"- sections.append(f\"\"\"### Formatierung verboten:",
"-{\", \".join(ausschluss)}\"\"\")",
"-",
"- return \"\\n\\n\".join(sections), output_format, erlaubte_tags",
"-",
"-",
"-def build_generation_prompt(briefing, context, profile, contract, structure=None, semantic=None, taxonomy=None):",
"- \"\"\"Build the content generation prompt.\"\"\"",
"-",
"- # Format context",
"- context_text = \"\"",
"- for i, ctx in enumerate(context, 1):",
"- context_text += f\"\\n[Quelle {i}: {ctx['source']}]\\n{ctx['content']}\\n\"",
"-",
"- # Build semantic context (entities and relations)",
"- semantic_text = \"\"",
"- if semantic:",
"- if semantic.get(\"entities\"):",
"- semantic_text += \"\\n## Relevante Konzepte\\n\"",
"- for e in semantic[\"entities\"][:10]:",
"- desc = e.get(\"description\") or \"\"",
"- if desc:",
"- semantic_text += f\"- **{e['name']}** ({e['type']}): {desc[:100]}\\n\"",
"- else:",
"- semantic_text += f\"- **{e['name']}** ({e['type']})\\n\"",
"-",
"- if semantic.get(\"relations\"):",
"- semantic_text += \"\\n## Konzept-Beziehungen\\n\"",
"- for r in semantic[\"relations\"][:10]:",
"- semantic_text += f\"- {r['source']} → {r['relation_type']} → {r['target']}\\n\"",
"-",
"- # Build taxonomy context",
"- taxonomy_text = \"\"",
"- if taxonomy:",
"- taxonomy_text = \"\\n## Thematische Einordnung\\n\"",
"- taxonomy_text += \", \".join([t[\"name\"] for t in taxonomy])",
"-",
"- # Build profile instructions - detect new vs old format",
"- profile_text = \"\"",
"- if profile:",
"- config = profile.get(\"config\", {})",
"-",
"- # Detect new format (has \"haltung\" or \"tonalitaet\" at top level)",
"- if \"haltung\" in config or \"tonalitaet\" in config or \"grammatik_und_satzbau\" in config:",
"- # New Cary-style profile",
"- profile_text = f\"\"\"",
"-## Autorenprofil: {profile.get(\"name\", \"Standard\")}",
"-",
"-{_parse_new_author_profile(config)}",
"-\"\"\"",
"- else:",
"- # Old format - keep backwards compatibility",
"- autorenprofil = config.get(\"autorenprofil\", config)",
"-",
"- stimme = autorenprofil.get(\"stimme\", {})",
"- stimme_text = \"\"",
"- if stimme:",
"- stimme_text = f\"\"\"",
"-### Stimme\/Tonalität:",
"-- Ton: {stimme.get(\"ton\", \"neutral\")}",
"-- Perspektive: {stimme.get(\"perspektive\", \"neutral\")}",
"-- Komplexität: {stimme.get(\"komplexitaet\", \"mittel\")}\"\"\"",
"-",
"- stil = autorenprofil.get(\"stil\", {})",
"- stil_text = \"\"",
"- if stil:",
"- stil_text = f\"\"\"",
"-### Stil:",
"-- Fachsprache: {\"Ja\" if stil.get(\"fachsprache\", False) else \"Nein\"}",
"-- Satzlänge: {stil.get(\"satzlaenge\", \"mittel\")}\"\"\"",
"-",
"- tabus = autorenprofil.get(\"tabus\", [])",
"- tabus_text = \"\"",
"- if tabus:",
"- tabus_text = f\"\"\"",
"-### Zu vermeiden:",
"-{\", \".join(tabus[:5])}\"\"\"",
"-",
"- profile_text = f\"\"\"",
"-## Autorenprofil: {profile.get(\"name\", \"Standard\")}",
"-{stimme_text}",
"-{stil_text}",
"-{tabus_text}",
"-\"\"\"",
"-",
"- # Build contract requirements",
"- contract_text = \"\"",
"- if contract:",
"- config = contract.get(\"config\", {})",
"- req = config.get(\"requirements\", {})",
"- contract_text = f\"\"\"",
"-Contract: {contract.get(\"name\", \"Standard\")}",
"-- Wortanzahl: {req.get(\"min_words\", 500)} - {req.get(\"max_words\", 5000)} Wörter",
"-\"\"\"",
"-",
"- # Build structure instructions - detect new vs old format",
"- structure_text = \"\"",
"- output_format = \"markdown\"",
"- erlaubte_tags = []",
"-",
"- if structure:",
"- config = structure.get(\"config\", {})",
"-",
"- # Detect new format (has \"ausgabe\" at top level)",
"- if \"ausgabe\" in config or \"gesamtaufbau\" in config:",
"- # New Blog-Struktur format",
"- parsed_text, output_format, erlaubte_tags = _parse_new_structure(config)",
"- structure_text = f\"\"\"",
"-## Struktur: {structure.get(\"name\", \"\")}",
"-",
"-{parsed_text}",
"-\"\"\"",
"- else:",
"- # Old format",
"- structure_text = f\"\"\"",
"-Struktur-Template: {structure.get(\"name\", \"\")}",
"-- Abschnitte: {json.dumps(config.get(\"sections\", []), ensure_ascii=False)}",
"-\"\"\"",
"-",
"- # Build format instruction based on structure's ausgabe",
"- format_instruction = \"\"",
"- if output_format == \"body-html\":",
"- tags_str = \", \".join(erlaubte_tags) if erlaubte_tags else \"h1, h2, h3, h4, p, a, ol, ul, li, strong, table, hr\"",
"- format_instruction = f\"\"\"7. **KRITISCH - Ausgabe als sauberes HTML:**",
"- - NUR diese Tags: {tags_str}",
"- - KEIN Markdown (keine ##, keine **, keine -)",
"- - KEIN div, span, br, img, script, style",
"- - Jeder Absatz in <p>-Tags",
"- - Überschriften als <h2>, <h3>, <h4>",
"- - Listen als <ul>\/<ol> mit <li>\"\"\"",
"-",
"- # Load generate prompt template from database",
"- # Note: Assumes db.connect() was called by the caller (generate_content)",
"- prompt_template = get_prompt(\"content-generate\")",
"-",
"- if prompt_template:",
"- prompt = prompt_template.format(",
"- profile_text=profile_text,",
"- contract_text=contract_text,",
"- structure_text=structure_text,",
"- context=context_text,",
"- briefing=briefing,",
"- format_instruction=format_instruction,",
"- semantic_text=semantic_text,",
"- taxonomy_text=taxonomy_text,",
"- )",
"- else:",
"- # Fallback if prompt not in DB",
"- prompt = f\"\"\"Du bist ein professioneller Content-Autor. Erstelle basierend auf dem Briefing und dem bereitgestellten Kontext einen hochwertigen Text.",
"-",
"-{profile_text}",
"-{contract_text}",
"-{structure_text}",
"-{semantic_text}",
"-{taxonomy_text}",
"-",
"-## Kontext aus der Wissensbasis:",
"-{context_text}",
"-",
"-## Briefing:",
"-{briefing}",
"-",
"-## Anweisungen:",
"-1. Nutze die Informationen aus dem Kontext als Grundlage",
"-2. Halte dich an das Autorenprofil und den Schreibstil",
"-3. Beachte die Vorgaben aus dem Contract",
"-4. Strukturiere den Text gemäß dem Template (falls angegeben)",
"-5. Schreibe auf Deutsch",
"-6. Kennzeichne verwendete Quellen",
"-7. Berücksichtige die relevanten Konzepte und deren Beziehungen",
"-{format_instruction}",
"-",
"-Erstelle nun den Content:\"\"\"",
"-",
"- return prompt",
"-",
"-",
"-def call_llm(prompt, model=\"anthropic\", client_name=\"content-studio\"):",
"- \"\"\"",
"- Call LLM to generate content with protokoll logging.",
"-",
"- Args:",
"- prompt: The prompt to send",
"- model: 'anthropic' or 'ollama'",
"- client_name: Identifier for protokoll logging",
"-",
"- Returns:",
"- Generated text content",
"- \"\"\"",
"- import time",
"-",
"- start_time = time.time()",
"- response_text = \"\"",
"- tokens_input = 0",
"- tokens_output = 0",
"- model_name = \"\"",
"- error_message = None",
"- status = \"completed\"",
"-",
"- try:",
"- if model == \"anthropic\" and ANTHROPIC_API_KEY:",
"- import anthropic",
"-",
"- client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)",
"- model_name = ANTHROPIC_MODEL",
"-",
"- message = client.messages.create(",
"- model=ANTHROPIC_MODEL, max_tokens=4000, messages=[{\"role\": \"user\", \"content\": prompt}]",
"- )",
"- response_text = message.content[0].text",
"-",
"- # Extract token usage from Anthropic response",
"- if hasattr(message, \"usage\"):",
"- tokens_input = getattr(message.usage, \"input_tokens\", 0)",
"- tokens_output = getattr(message.usage, \"output_tokens\", 0)",
"- else:",
"- # Fallback to Ollama",
"- import requests",
"-",
"- model_name = OLLAMA_CHAT_MODEL",
"-",
"- response = requests.post(",
"- f\"{OLLAMA_HOST}\/api\/generate\",",
"- json={\"model\": OLLAMA_CHAT_MODEL, \"prompt\": prompt, \"stream\": False},",
"- timeout=900, # 15 min for large models",
"- )",
"- response.raise_for_status()",
"- result = response.json()",
"- response_text = result.get(\"response\", \"\")",
"-",
"- # Extract token counts from Ollama response",
"- tokens_input = result.get(\"prompt_eval_count\", 0)",
"- tokens_output = result.get(\"eval_count\", 0)",
"-",
"- # Clean up model artifacts (Gemma, Llama, etc.)",
"- artifacts = [",
"- \"<start_of_turn>\",",
"- \"<\/start_of_turn>\",",
"- \"<end_of_turn>\",",
"- \"<\/end_of_turn>\",",
"- \"<\/s>\",",
"- \"<|eot_id|>\",",
"- \"<|im_end|>\",",
"- ]",
"- for artifact in artifacts:",
"- response_text = response_text.replace(artifact, \"\").strip()",
"-",
"- except Exception as e:",
"- status = \"error\"",
"- error_message = str(e)",
"- raise",
"-",
"- finally:",
"- # Calculate duration",
"- duration_ms = int((time.time() - start_time) * 1000)",
"-",
"- # Log to protokoll",
"- db.log_to_protokoll(",
"- client_name=client_name,",
"- request=prompt,",
"- response=response_text if status == \"completed\" else None,",
"- model_name=model_name,",
"- tokens_input=tokens_input,",
"- tokens_output=tokens_output,",
"- duration_ms=duration_ms,",
"- status=status,",
"- error_message=error_message,",
"- )",
"-",
"- return response_text",
"-",
"-",
"-def save_version(order_id, content, version_number=1, output_format=\"markdown\"):",
"- \"\"\"Save content version to database.\"\"\"",
"- content_json = json.dumps({\"text\": content, \"format\": output_format})",
"-",
"- cursor = db.execute(",
"- \"\"\"INSERT INTO content_versions (order_id, version_number, content)",
"- VALUES (%s, %s, %s)\"\"\",",
"- (order_id, version_number, content_json),",
"- )",
"- db.commit()",
"- version_id = cursor.lastrowid",
"- cursor.close()",
"- return version_id",
"-",
"-",
"-def save_sources(order_id, context):",
"- \"\"\"Save RAG sources to content_sources.\"\"\"",
"- for ctx in context:",
"- # Try to find chunk_id by content match",
"- cursor = db.execute(\"SELECT id FROM chunks WHERE content LIKE %s LIMIT 1\", (ctx[\"content\"][:100] + \"%\",))",
"- chunk = cursor.fetchone()",
"- cursor.close()",
"-",
"- if chunk:",
"- cursor = db.execute(",
"- \"\"\"INSERT IGNORE INTO content_sources (order_id, chunk_id, relevance_score)",
"- VALUES (%s, %s, %s)\"\"\",",
"- (order_id, chunk[\"id\"], ctx[\"score\"]),",
"- )",
"- db.commit()",
"- cursor.close()",
"-",
"-",
"-def update_order_status(order_id, status):",
"- \"\"\"Update order status.\"\"\"",
"- cursor = db.execute(\"UPDATE content_orders SET status = %s, updated_at = NOW() WHERE id = %s\", (status, order_id))",
"- db.commit()",
"- cursor.close()",
"-",
"-",
"-def generate_content(order_id, model=\"anthropic\", collection=\"documents\", context_limit=5):",
"- \"\"\"",
"- Main content generation function.",
"-",
"- Args:",
"- order_id: Content order ID",
"- model: 'anthropic' or 'ollama'",
"- collection: Qdrant collection to search",
"- context_limit: Number of context chunks",
"-",
"- Returns:",
"- dict with version_id, content, sources",
"- \"\"\"",
"- db.connect()",
"-",
"- try:",
"- # Load order",
"- order = get_order(order_id)",
"- if not order:",
"- return {\"error\": f\"Order {order_id} not found\"}",
"-",
"- # Update status",
"- update_order_status(order_id, \"generating\")",
"-",
"- # Get RAG context",
"- context = get_rag_context(order[\"briefing\"], collection, context_limit)",
"-",
"- # Extract chunk_ids and document_ids for semantic context",
"- chunk_ids = [c.get(\"chunk_id\") for c in context if c.get(\"chunk_id\")]",
"- doc_ids = list({c.get(\"document_id\") for c in context if c.get(\"document_id\")})",
"-",
"- # Load semantic context (entities and relations)",
"- semantic = get_semantic_context(chunk_ids) if chunk_ids else None",
"-",
"- # Load taxonomy context",
"- taxonomy = get_taxonomy_context(doc_ids) if doc_ids else None",
"-",
"- # Build profile\/contract\/structure",
"- profile = None",
"- if order.get(\"profile_config\"):",
"- config = (",
"- json.loads(order[\"profile_config\"])",
"- if isinstance(order[\"profile_config\"], str)",
"- else order[\"profile_config\"]",
"- )",
"- profile = {\"name\": order[\"profile_name\"], \"config\": config}",
"-",
"- contract = None",
"- if order.get(\"contract_config\"):",
"- config = (",
"- json.loads(order[\"contract_config\"])",
"- if isinstance(order[\"contract_config\"], str)",
"- else order[\"contract_config\"]",
"- )",
"- contract = {\"name\": order[\"contract_name\"], \"config\": config}",
"-",
"- structure = None",
"- output_format = \"markdown\" # Default",
"- if order.get(\"structure_config\"):",
"- config = (",
"- json.loads(order[\"structure_config\"])",
"- if isinstance(order[\"structure_config\"], str)",
"- else order[\"structure_config\"]",
"- )",
"- structure = {\"name\": order[\"structure_name\"], \"config\": config}",
"- # Determine output format from structure",
"- ausgabe = config.get(\"ausgabe\", {})",
"- output_format = ausgabe.get(\"format\", \"markdown\")",
"-",
"- # Build prompt",
"- prompt = build_generation_prompt(",
"- order[\"briefing\"], context, profile, contract, structure, semantic=semantic, taxonomy=taxonomy",
"- )",
"-",
"- # Generate content",
"- content = call_llm(prompt, model, client_name=\"content-studio-generate\")",
"-",
"- # Get current version number",
"- cursor = db.execute(",
"- \"SELECT MAX(version_number) as max_v FROM content_versions WHERE order_id = %s\", (order_id,)",
"- )",
"- result = cursor.fetchone()",
"- cursor.close()",
"- version_number = (result[\"max_v\"] or 0) + 1",
"-",
"- # Save version with correct format",
"- version_id = save_version(order_id, content, version_number, output_format)",
"-",
"- # Save sources",
"- save_sources(order_id, context)",
"-",
"- # Update status",
"- update_order_status(order_id, \"critique\")",
"-",
"- return {",
"- \"success\": True,",
"- \"order_id\": order_id,",
"- \"version_id\": version_id,",
"- \"version_number\": version_number,",
"- \"content\": content,",
"- \"sources\": [{\"source\": c[\"source\"], \"score\": c[\"score\"]} for c in context],",
"- }",
"-",
"- except Exception as e:",
"- update_order_status(order_id, \"draft\")",
"- return {\"error\": str(e)}",
"- finally:",
"- db.disconnect()",
"-",
"-",
"-def get_critic(critic_id):",
"- \"\"\"Load critic from database.\"\"\"",
"- cursor = db.execute(",
"- \"\"\"SELECT c.*, p.content as prompt_content",
"- FROM critics c",
"- LEFT JOIN prompts p ON c.prompt_id = p.id",
"- WHERE c.id = %s AND c.is_active = 1\"\"\",",
"- (critic_id,),",
"- )",
"- result = cursor.fetchone()",
"- cursor.close()",
"- return result",
"-",
"-",
"-def run_critic(content, critic_id, model=\"anthropic\"):",
"- \"\"\"",
"- Run a single critic on content.",
"-",
"- Returns:",
"- dict with feedback and rating",
"- \"\"\"",
"- db.connect()",
"-",
"- try:",
"- critic = get_critic(critic_id)",
"- if not critic:",
"- 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)",
"-",
"- # 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:",
"-",
"-## Text:",
"-{content}",
"-",
"-## Deine Aufgabe:",
"-1. Prüfe den Text auf die Aspekte: {fokus}",
"-2. Identifiziere konkrete Verbesserungspunkte",
"-3. Bewerte die Qualität (1-10)",
"-",
"-Antworte im JSON-Format:",
"-{{",
"- \"rating\": 8,",
"- \"passed\": true,",
"- \"issues\": [\"Issue 1\", \"Issue 2\"],",
"- \"suggestions\": [\"Suggestion 1\"],",
"- \"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",
"- import re",
"-",
"- json_match = re.search(r\"\\{[\\s\\S]*\\}\", response)",
"- if json_match:",
"- json_str = json_match.group()",
"- try:",
"- feedback = json.loads(json_str)",
"- feedback[\"critic_name\"] = critic[\"name\"]",
"- return feedback",
"- except json.JSONDecodeError:",
"- # Try to repair common JSON issues",
"- repaired = _repair_json(json_str)",
"- try:",
"- feedback = json.loads(repaired)",
"- feedback[\"critic_name\"] = critic[\"name\"]",
"- return feedback",
"- except json.JSONDecodeError:",
"- pass",
"-",
"- return {",
"- \"critic_name\": critic[\"name\"],",
"- \"rating\": 5,",
"- \"passed\": False,",
"- \"issues\": [\"Konnte Feedback nicht parsen\"],",
"- \"suggestions\": [],",
"- \"summary\": response[:500],",
"- }",
"-",
"- except Exception as e:",
"- return {\"error\": str(e)}",
"- finally:",
"- db.disconnect()",
"-",
"-",
"-def run_critique_round(version_id, model=\"anthropic\"):",
"- \"\"\"",
"- Run all active critics on a content version.",
"-",
"- Returns:",
"- dict with all critique results",
"- \"\"\"",
"- db.connect()",
"-",
"- try:",
"- # Get version content",
"- cursor = db.execute(",
"- \"SELECT cv.*, co.id as order_id, co.current_critique_round FROM content_versions cv JOIN content_orders co ON cv.order_id = co.id WHERE cv.id = %s\",",
"- (version_id,),",
"- )",
"- version = cursor.fetchone()",
"- cursor.close()",
"-",
"- if not version:",
"- return {\"error\": \"Version not found\"}",
"-",
"- content_data = json.loads(version[\"content\"]) if isinstance(version[\"content\"], str) else version[\"content\"]",
"- content_text = content_data.get(\"text\", \"\")",
"-",
"- # Get all active critics",
"- cursor = db.execute(\"SELECT id, name FROM critics WHERE is_active = 1 ORDER BY sort_order\")",
"- critics = cursor.fetchall()",
"- cursor.close()",
"-",
"- # Increment critique round",
"- new_round = (version[\"current_critique_round\"] or 0) + 1",
"- cursor = db.execute(",
"- \"UPDATE content_orders SET current_critique_round = %s WHERE id = %s\", (new_round, version[\"order_id\"])",
"- )",
"- db.commit()",
"- cursor.close()",
"-",
"- # Run each critic",
"- results = []",
"- all_passed = True",
"-",
"- for critic in critics:",
"- db.disconnect() # Disconnect before calling run_critic",
"- feedback = run_critic(content_text, critic[\"id\"], model)",
"- db.connect() # Reconnect",
"-",
"- if \"error\" not in feedback:",
"- # Save critique",
"- cursor = db.execute(",
"- \"\"\"INSERT INTO content_critiques (version_id, critic_id, round, feedback)",
"- VALUES (%s, %s, %s, %s)\"\"\",",
"- (version_id, critic[\"id\"], new_round, json.dumps(feedback)),",
"- )",
"- db.commit()",
"- cursor.close()",
"-",
"- if not feedback.get(\"passed\", True):",
"- all_passed = False",
"-",
"- results.append(feedback)",
"-",
"- # Update order status based on results",
"- if all_passed:",
"- update_order_status(version[\"order_id\"], \"validate\")",
"- else:",
"- update_order_status(version[\"order_id\"], \"revision\")",
"-",
"- return {\"success\": True, \"round\": new_round, \"critiques\": results, \"all_passed\": all_passed}",
"-",
"- except Exception as e:",
"- return {\"error\": str(e)}",
"- finally:",
"- db.disconnect()",
"-",
"-",
"-def revise_content(version_id, model=\"anthropic\"):",
"- \"\"\"",
"- Create a revision based on critique feedback.",
"-",
"- Returns:",
"- dict with new version info",
"- \"\"\"",
"- db.connect()",
"-",
"- try:",
"- # Get version and critiques",
"- cursor = db.execute(",
"- \"\"\"SELECT cv.*, co.id as order_id, co.briefing, co.current_critique_round,",
"- ap.content as profile_config,",
"- cs.content as structure_config",
"- FROM content_versions cv",
"- JOIN content_orders co ON cv.order_id = co.id",
"- LEFT JOIN content_config ap ON co.author_profile_id = ap.id AND ap.type = 'author_profile'",
"- LEFT JOIN content_config cs ON co.structure_id = cs.id AND cs.type = 'structure'",
"- WHERE cv.id = %s\"\"\",",
"- (version_id,),",
"- )",
"- version = cursor.fetchone()",
"- cursor.close()",
"-",
"- if not version:",
"- return {\"error\": \"Version not found\"}",
"-",
"- content_data = json.loads(version[\"content\"]) if isinstance(version[\"content\"], str) else version[\"content\"]",
"- content_text = content_data.get(\"text\", \"\")",
"-",
"- # Get latest critiques",
"- cursor = db.execute(",
"- \"\"\"SELECT c.name, cc.feedback",
"- FROM content_critiques cc",
"- JOIN critics c ON cc.critic_id = c.id",
"- WHERE cc.version_id = %s AND cc.round = %s\"\"\",",
"- (version_id, version[\"current_critique_round\"]),",
"- )",
"- critiques = cursor.fetchall()",
"- cursor.close()",
"-",
"- # Build revision prompt",
"- feedback_text = \"\"",
"- for critique in critiques:",
"- fb = json.loads(critique[\"feedback\"]) if isinstance(critique[\"feedback\"], str) else critique[\"feedback\"]",
"- feedback_text += f\"\\n### {critique['name']}:\\n\"",
"- feedback_text += f\"- Bewertung: {fb.get('rating', 'N\/A')}\/10\\n\"",
"- feedback_text += f\"- Probleme: {', '.join(fb.get('issues', []))}\\n\"",
"- feedback_text += f\"- Vorschläge: {', '.join(fb.get('suggestions', []))}\\n\"",
"-",
"- # Determine output format from structure",
"- output_format = \"markdown\" # Default",
"- html_instruction = \"\"",
"- if version.get(\"structure_config\"):",
"- structure_config = (",
"- json.loads(version[\"structure_config\"])",
"- if isinstance(version[\"structure_config\"], str)",
"- else version[\"structure_config\"]",
"- )",
"- ausgabe = structure_config.get(\"ausgabe\", {})",
"- output_format = ausgabe.get(\"format\", \"markdown\")",
"- erlaubte_tags = ausgabe.get(",
"- \"erlaubte_tags\", [\"h1\", \"h2\", \"h3\", \"h4\", \"p\", \"ul\", \"ol\", \"li\", \"strong\", \"a\", \"table\", \"hr\"]",
"- )",
"-",
"- if output_format == \"body-html\":",
"- tags_str = \", \".join(erlaubte_tags)",
"- html_instruction = f\"\"\"",
"-5. **KRITISCH - Behalte das HTML-Format bei!**",
"- - Nur diese Tags: {tags_str}",
"- - KEIN Markdown, KEINE ## oder ** oder -",
"- - KEIN div, span, br, img, script, style",
"- - Fließtext immer in <p>-Tags\"\"\"",
"-",
"- # Load revise prompt from database",
"- prompt_template = get_prompt(\"content-revise\")",
"- if prompt_template:",
"- prompt = prompt_template.format(",
"- content=content_text, feedback=feedback_text, html_instruction=html_instruction",
"- )",
"- else:",
"- # Fallback if prompt not in DB",
"- prompt = f\"\"\"Du bist ein professioneller Content-Editor. Überarbeite den folgenden Text basierend auf dem Feedback der Kritiker.",
"-",
"-## Originaler Text:",
"-{content_text}",
"-",
"-## Feedback der Kritiker:",
"-{feedback_text}",
"-",
"-## Anweisungen:",
"-1. Behebe alle genannten Probleme",
"-2. Setze die Verbesserungsvorschläge um",
"-3. Behalte den Grundton und Stil bei",
"-4. Achte auf eine kohärente Überarbeitung",
"-{html_instruction}",
"-",
"-Erstelle nun die überarbeitete Version:\"\"\"",
"-",
"- # Generate revision",
"- update_order_status(version[\"order_id\"], \"generating\")",
"- revised_content = call_llm(prompt, model, client_name=\"content-studio-revise\")",
"-",
"- # Save new version with correct format",
"- new_version_number = version[\"version_number\"] + 1",
"- new_version_id = save_version(version[\"order_id\"], revised_content, new_version_number, output_format)",
"-",
"- # Update status",
"- update_order_status(version[\"order_id\"], \"critique\")",
"-",
"- return {",
"- \"success\": True,",
"- \"order_id\": version[\"order_id\"],",
"- \"version_id\": new_version_id,",
"- \"version_number\": new_version_number,",
"- \"content\": revised_content,",
"- }",
"-",
"- except Exception as e:",
"- return {\"error\": str(e)}",
"- finally:",
"- db.disconnect()",
"-",
"-",
" if __name__ == \"__main__\":",
"- import sys",
"-",
" if len(sys.argv) < 3:",
" print(\"Usage: python generate.py <command> <id> [options]\")",
" print(\"Commands: generate, critique, revise\")"
]
}
],
"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, \"\/var\/www\/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_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 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 \"chunk_id\": result[\"payload\"].get(\"chunk_id\"),\n \"document_id\": result[\"payload\"].get(\"document_id\"),\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_semantic_context(chunk_ids):\n \"\"\"\n Load entities and relations based on chunk_ids.\n\n Uses the chunk_entities junction table to find relevant entities,\n then loads relations between those entities.\n\n Args:\n chunk_ids: List of chunk IDs from RAG context\n\n Returns:\n dict with 'entities' and 'relations' lists\n \"\"\"\n if not chunk_ids:\n return {\"entities\": [], \"relations\": []}\n\n # Filter out None values\n chunk_ids = [cid for cid in chunk_ids if cid is not None]\n if not chunk_ids:\n return {\"entities\": [], \"relations\": []}\n\n placeholders = \", \".join([\"%s\"] * len(chunk_ids))\n\n # Load entities via chunk_entities\n cursor = db.execute(\n f\"\"\"SELECT DISTINCT e.id, e.name, e.type, e.description,\n AVG(ce.relevance_score) as relevance\n FROM chunk_entities ce\n JOIN entities e ON ce.entity_id = e.id\n WHERE ce.chunk_id IN ({placeholders})\n GROUP BY e.id, e.name, e.type, e.description\n ORDER BY relevance DESC\n LIMIT 10\"\"\",\n tuple(chunk_ids),\n )\n entities = cursor.fetchall()\n cursor.close()\n\n if not entities:\n return {\"entities\": [], \"relations\": []}\n\n # Get entity IDs for relation lookup\n entity_ids = [e[\"id\"] for e in entities]\n entity_placeholders = \", \".join([\"%s\"] * len(entity_ids))\n\n # Load relations between found entities\n cursor = db.execute(\n f\"\"\"SELECT e1.name as source, er.relation_type, e2.name as target\n FROM entity_relations er\n JOIN entities e1 ON er.source_entity_id = e1.id\n JOIN entities e2 ON er.target_entity_id = e2.id\n WHERE e1.id IN ({entity_placeholders}) AND e2.id IN ({entity_placeholders})\n LIMIT 15\"\"\",\n tuple(entity_ids) + tuple(entity_ids),\n )\n relations = cursor.fetchall()\n cursor.close()\n\n return {\"entities\": entities, \"relations\": relations}\n\n\ndef get_taxonomy_context(document_ids):\n \"\"\"\n Load taxonomy terms for documents.\n\n Args:\n document_ids: List of document IDs from RAG context\n\n Returns:\n List of taxonomy term dicts with name, slug, confidence\n \"\"\"\n if not document_ids:\n return []\n\n # Filter out None values\n document_ids = [did for did in document_ids if did is not None]\n if not document_ids:\n return []\n\n placeholders = \", \".join([\"%s\"] * len(document_ids))\n\n cursor = db.execute(\n f\"\"\"SELECT DISTINCT tt.name, tt.slug, MAX(dt.confidence) as confidence\n FROM document_taxonomy dt\n JOIN taxonomy_terms tt ON dt.taxonomy_term_id = tt.id\n WHERE dt.document_id IN ({placeholders})\n GROUP BY tt.id, tt.name, tt.slug\n ORDER BY confidence DESC\"\"\",\n tuple(document_ids),\n )\n taxonomy = cursor.fetchall()\n cursor.close()\n\n return taxonomy\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 _parse_new_author_profile(config):\n \"\"\"Parse new-style author profile (Cary format) into prompt text.\"\"\"\n sections = []\n\n # Haltung\n haltung = config.get(\"haltung\", {})\n if haltung:\n sections.append(f\"\"\"### Haltung:\n- Grundhaltung: {haltung.get(\"grundhaltung\", \"\")}\n- Ausrichtung: {haltung.get(\"ausrichtung\", \"\")}\n- Spannungstoleranz: {haltung.get(\"spannungstoleranz\", \"\")}\n- Vereinfachung: {haltung.get(\"vereinfachung\", \"\")}\"\"\")\n\n # Tonalität\n tonalitaet = config.get(\"tonalitaet\", {})\n if tonalitaet:\n sections.append(f\"\"\"### Tonalität:\n- Charakter: {tonalitaet.get(\"charakter\", \"\")}\n- Stil: {tonalitaet.get(\"stil\", \"\")}\n- Wirkung: {tonalitaet.get(\"wirkung\", \"\")}\n- Abgrenzung: {tonalitaet.get(\"abgrenzung\", \"\")}\"\"\")\n\n # Sprachmodus\n sprachmodus = config.get(\"sprachmodus\", {})\n if sprachmodus:\n sections.append(f\"\"\"### Sprachmodus:\n- Denkstil: {sprachmodus.get(\"denkstil\", \"\")}\n- Aussagenform: {sprachmodus.get(\"aussagenform\", \"\")}\n- Fragenfunktion: {sprachmodus.get(\"fragenfunktion\", \"\")}\"\"\")\n\n # Grammatik und Satzbau - WICHTIG für Verbote\n grammatik = config.get(\"grammatik_und_satzbau\", {})\n if grammatik:\n verbote = []\n if grammatik.get(\"stakkato\") == \"ausgeschlossen\":\n verbote.append(\"Stakkato-Sätze\")\n if grammatik.get(\"einschuebe\") == \"keine\":\n verbote.append(\"Einschübe\")\n if grammatik.get(\"gedankenstriche\") == \"verboten\":\n verbote.append(\"Gedankenstriche (–)\")\n\n sections.append(f\"\"\"### Grammatik und Satzbau:\n- Sätze: {grammatik.get(\"saetze\", \"\")}\n- Rhythmus: {grammatik.get(\"rhythmus\", \"\")}\n- **VERBOTEN:** {\", \".join(verbote) if verbote else \"keine\"}\"\"\")\n\n # Wortwahl\n wortwahl = config.get(\"wortwahl\", {})\n if wortwahl:\n verboten = []\n if wortwahl.get(\"buzzwords\") == \"ausge... [TRUNCATED-2e4bdc5f812ef488]"
}
}