Protokoll #15136
| ID | 15136 |
|---|---|
| Zeitstempel | 2025-12-25 13:57:05.182449 |
| 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 | 5,411 (Input: 0, Output: 0) |
| Dauer | 129 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\/generators\/content_generator.py",
"content": "\"\"\"\nContent Generator - Core content generation with LLM calls.\n\"\"\"\n\nimport json\nimport sys\nimport time\n\nsys.path.insert(0, \"\/var\/www\/scripts\/pipeline\")\n\nfrom config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL, OLLAMA_CHAT_MODEL, OLLAMA_HOST\nfrom db import db\n\nfrom .config_loader import get_prompt, get_order, parse_author_profile, parse_structure\nfrom .rag_context import get_rag_context, get_semantic_context, get_taxonomy_context\nfrom .persistence import save_version, save_sources, update_order_status\n\n\ndef build_generation_prompt(\n briefing: str,\n context: list[dict],\n profile: dict | None,\n contract: dict | None,\n structure: dict | None = None,\n semantic: dict | None = None,\n taxonomy: list | None = None,\n) -> str:\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 semantic context (entities and relations)\n semantic_text = \"\"\n if semantic:\n if semantic.get(\"entities\"):\n semantic_text += \"\\n## Relevante Konzepte\\n\"\n for e in semantic[\"entities\"][:10]:\n desc = e.get(\"description\") or \"\"\n if desc:\n semantic_text += f\"- **{e['name']}** ({e['type']}): {desc[:100]}\\n\"\n else:\n semantic_text += f\"- **{e['name']}** ({e['type']})\\n\"\n\n if semantic.get(\"relations\"):\n semantic_text += \"\\n## Konzept-Beziehungen\\n\"\n for r in semantic[\"relations\"][:10]:\n semantic_text += f\"- {r['source']} → {r['relation_type']} → {r['target']}\\n\"\n\n # Build taxonomy context\n taxonomy_text = \"\"\n if taxonomy:\n taxonomy_text = \"\\n## Thematische Einordnung\\n\"\n taxonomy_text += \", \".join([t[\"name\"] for t in taxonomy])\n\n # Build profile instructions - detect new vs old format\n profile_text = \"\"\n if profile:\n config = profile.get(\"config\", {})\n\n # Detect new format (has \"haltung\" or \"tonalitaet\" at top level)\n if \"haltung\" in config or \"tonalitaet\" in config or \"grammatik_und_satzbau\" in config:\n # New Cary-style profile\n profile_text = f\"\"\"\n## Autorenprofil: {profile.get(\"name\", \"Standard\")}\n\n{parse_author_profile(config)}\n\"\"\"\n else:\n # Old format - keep backwards compatibility\n autorenprofil = config.get(\"autorenprofil\", config)\n\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\n stil = autorenprofil.get(\"stil\", {})\n stil_text = \"\"\n if stil:\n stil_text = f\"\"\"\n### Stil:\n- Fachsprache: {\"Ja\" if stil.get(\"fachsprache\", False) else \"Nein\"}\n- Satzlänge: {stil.get(\"satzlaenge\", \"mittel\")}\"\"\"\n\n tabus = autorenprofil.get(\"tabus\", [])\n tabus_text = \"\"\n if tabus:\n tabus_text = f\"\"\"\n### Zu vermeiden:\n{\", \".join(tabus[:5])}\"\"\"\n\n profile_text = f\"\"\"\n## Autorenprofil: {profile.get(\"name\", \"Standard\")}\n{stimme_text}\n{stil_text}\n{tabus_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\"\"\"\n\n # Build structure instructions - detect new vs old format\n structure_text = \"\"\n output_format = \"markdown\"\n erlaubte_tags = []\n\n if structure:\n config = structure.get(\"config\", {})\n\n # Detect new format (has \"ausgabe\" at top level)\n if \"ausgabe\" in config or \"gesamtaufbau\" in config:\n # New Blog-Struktur format\n parsed_text, output_format, erlaubte_tags = parse_structure(config)\n structure_text = f\"\"\"\n## Struktur: {structure.get(\"name\", \"\")}\n\n{parsed_text}\n\"\"\"\n else:\n # Old format\n structure_text = f\"\"\"\nStruktur-Template: {structure.get(\"name\", \"\")}\n- Abschnitte: {json.dumps(config.get(\"sections\", []), ensure_ascii=False)}\n\"\"\"\n\n # Build format instruction based on structure's ausgabe\n format_instruction = \"\"\n if output_format == \"body-html\":\n tags_str = \", \".join(erlaubte_tags) if erlaubte_tags else \"h1, h2, h3, h4, p, a, ol, ul, li, strong, table, hr\"\n format_instruction = f\"\"\"7. **KRITISCH - Ausgabe als sauberes HTML:**\n - NUR diese Tags: {tags_str}\n - KEIN Markdown (keine ##, keine **, keine -)\n - KEIN div, span, br, img, script, style\n - Jeder Absatz in <p>-Tags\n - Überschriften als <h2>, <h3>, <h4>\n - Listen als <ul>\/<ol> mit <li>\"\"\"\n\n # Load generate prompt template from database\n prompt_template = get_prompt(\"content-generate\")\n\n if prompt_template:\n prompt = prompt_template.format(\n profile_text=profile_text,\n contract_text=contract_text,\n structure_text=structure_text,\n context=context_text,\n briefing=briefing,\n format_instruction=format_instruction,\n semantic_text=semantic_text,\n taxonomy_text=taxonomy_text,\n )\n else:\n # Fallback if prompt not in DB\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{semantic_text}\n{taxonomy_text}\n\n## Kontext aus der Wissensbasis:\n{context_text}\n\n## Briefing:\n{briefing}\n\n## Anweisungen:\n1. Nutze die Informationen aus dem Kontext als Grundlage\n2. Halte dich an das Autorenprofil und den Schreibstil\n3. Beachte die Vorgaben aus dem Contract\n4. Strukturiere den Text gemäß dem Template (falls angegeben)\n5. Schreibe auf Deutsch\n6. Kennzeichne verwendete Quellen\n7. Berücksichtige die relevanten Konzepte und deren Beziehungen\n{format_instruction}\n\nErstelle nun den Content:\"\"\"\n\n return prompt\n\n\ndef call_llm(prompt: str, model: str = \"anthropic\", client_name: str = \"content-studio\") -> str:\n \"\"\"\n Call LLM to generate content with protokoll logging.\n\n Args:\n prompt: The prompt to send\n model: 'anthropic' or 'ollama'\n client_name: Identifier for protokoll logging\n\n Returns:\n Generated text content\n \"\"\"\n start_time = time.time()\n response_text = \"\"\n tokens_input = 0\n tokens_output = 0\n model_name = \"\"\n error_message = None\n status = \"completed\"\n\n try:\n if model == \"anthropic\" and ANTHROPIC_API_KEY:\n import anthropic\n\n client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)\n model_name = ANTHROPIC_MODEL\n\n message = client.messages.create(\n model=ANTHROPIC_MODEL, max_tokens=4000, messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n response_text = message.content[0].text\n\n # Extract token usage from Anthropic response\n if hasattr(message, \"usage\"):\n tokens_input = getattr(message.usage, \"input_tokens\", 0)\n tokens_output = getattr(message.usage, \"output_tokens\", 0)\n else:\n # Fallback to Ollama\n import requests\n\n model_name = OLLAMA_CHAT_MODEL\n\n response = requests.post(\n f\"{OLLAMA_HOST}\/api\/generate\",\n json={\"model\": OLLAMA_CHAT_MODEL, \"prompt\": prompt, \"stream\": False},\n timeout=900, # 15 min for large models\n )\n response.raise_for_status()\n result = response.json()\n response_text = result.get(\"response\", \"\")\n\n # Extract token counts from Ollama response\n tokens_input = result.get(\"prompt_eval_count\", 0)\n tokens_output = result.get(\"eval_count\", 0)\n\n # Clean up model artifacts (Gemma, Llama, etc.)\n artifacts = [\n \"<start_of_turn>\",\n \"<\/start_of_turn>\",\n \"<end_of_turn>\",\n \"<\/end_of_turn>\",\n \"<\/s>\",\n \"<|eot_id|>\",\n \"<|im_end|>\",\n ]\n for artifact in artifacts:\n response_text = response_text.replace(artifact, \"\").strip()\n\n except Exception as e:\n status = \"error\"\n error_message = str(e)\n raise\n\n finally:\n # Calculate duration\n duration_ms = int((time.time() - start_time) * 1000)\n\n # Log to protokoll\n db.log_to_protokoll(\n client_name=client_name,\n request=prompt,\n response=response_text if status == \"completed\" else None,\n model_name=model_name,\n tokens_input=tokens_input,\n tokens_output=tokens_output,\n duration_ms=duration_ms,\n status=status,\n error_message=error_message,\n )\n\n return response_text\n\n\ndef generate_content(\n order_id: int, model: str = \"anthropic\", collection: str = \"documents\", context_limit: int = 5\n) -> dict:\n \"\"\"\n Main content generation function.\n\n Args:\n order_id: Content order ID\n model: 'anthropic' or 'ollama'\n collection: Qdrant collection to search\n context_limit: Number of context chunks\n\n Returns:\n dict with version_id, content, sources\n \"\"\"\n db.connect()\n\n try:\n # Load order\n order = get_order(order_id)\n if not order:\n return {\"error\": f\"Order {order_id} not found\"}\n\n # Update status\n update_order_status(order_id, \"generating\")\n\n # Get RAG context\n context = get_rag_context(order[\"briefing\"], collection, context_limit)\n\n # Extract chunk_ids and document_ids for sema... [TRUNCATED-521ea97b163e2b4b]"
}
}
Response
{
"tool_response": {
"type": "create",
"filePath": "\/var\/www\/scripts\/pipeline\/generators\/content_generator.py",
"content": "\"\"\"\nContent Generator - Core content generation with LLM calls.\n\"\"\"\n\nimport json\nimport sys\nimport time\n\nsys.path.insert(0, \"\/var\/www\/scripts\/pipeline\")\n\nfrom config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL, OLLAMA_CHAT_MODEL, OLLAMA_HOST\nfrom db import db\n\nfrom .config_loader import get_prompt, get_order, parse_author_profile, parse_structure\nfrom .rag_context import get_rag_context, get_semantic_context, get_taxonomy_context\nfrom .persistence import save_version, save_sources, update_order_status\n\n\ndef build_generation_prompt(\n briefing: str,\n context: list[dict],\n profile: dict | None,\n contract: dict | None,\n structure: dict | None = None,\n semantic: dict | None = None,\n taxonomy: list | None = None,\n) -> str:\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 semantic context (entities and relations)\n semantic_text = \"\"\n if semantic:\n if semantic.get(\"entities\"):\n semantic_text += \"\\n## Relevante Konzepte\\n\"\n for e in semantic[\"entities\"][:10]:\n desc = e.get(\"description\") or \"\"\n if desc:\n semantic_text += f\"- **{e['name']}** ({e['type']}): {desc[:100]}\\n\"\n else:\n semantic_text += f\"- **{e['name']}** ({e['type']})\\n\"\n\n if semantic.get(\"relations\"):\n semantic_text += \"\\n## Konzept-Beziehungen\\n\"\n for r in semantic[\"relations\"][:10]:\n semantic_text += f\"- {r['source']} → {r['relation_type']} → {r['target']}\\n\"\n\n # Build taxonomy context\n taxonomy_text = \"\"\n if taxonomy:\n taxonomy_text = \"\\n## Thematische Einordnung\\n\"\n taxonomy_text += \", \".join([t[\"name\"] for t in taxonomy])\n\n # Build profile instructions - detect new vs old format\n profile_text = \"\"\n if profile:\n config = profile.get(\"config\", {})\n\n # Detect new format (has \"haltung\" or \"tonalitaet\" at top level)\n if \"haltung\" in config or \"tonalitaet\" in config or \"grammatik_und_satzbau\" in config:\n # New Cary-style profile\n profile_text = f\"\"\"\n## Autorenprofil: {profile.get(\"name\", \"Standard\")}\n\n{parse_author_profile(config)}\n\"\"\"\n else:\n # Old format - keep backwards compatibility\n autorenprofil = config.get(\"autorenprofil\", config)\n\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\n stil = autorenprofil.get(\"stil\", {})\n stil_text = \"\"\n if stil:\n stil_text = f\"\"\"\n### Stil:\n- Fachsprache: {\"Ja\" if stil.get(\"fachsprache\", False) else \"Nein\"}\n- Satzlänge: {stil.get(\"satzlaenge\", \"mittel\")}\"\"\"\n\n tabus = autorenprofil.get(\"tabus\", [])\n tabus_text = \"\"\n if tabus:\n tabus_text = f\"\"\"\n### Zu vermeiden:\n{\", \".join(tabus[:5])}\"\"\"\n\n profile_text = f\"\"\"\n## Autorenprofil: {profile.get(\"name\", \"Standard\")}\n{stimme_text}\n{stil_text}\n{tabus_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\"\"\"\n\n # Build structure instructions - detect new vs old format\n structure_text = \"\"\n output_format = \"markdown\"\n erlaubte_tags = []\n\n if structure:\n config = structure.get(\"config\", {})\n\n # Detect new format (has \"ausgabe\" at top level)\n if \"ausgabe\" in config or \"gesamtaufbau\" in config:\n # New Blog-Struktur format\n parsed_text, output_format, erlaubte_tags = parse_structure(config)\n structure_text = f\"\"\"\n## Struktur: {structure.get(\"name\", \"\")}\n\n{parsed_text}\n\"\"\"\n else:\n # Old format\n structure_text = f\"\"\"\nStruktur-Template: {structure.get(\"name\", \"\")}\n- Abschnitte: {json.dumps(config.get(\"sections\", []), ensure_ascii=False)}\n\"\"\"\n\n # Build format instruction based on structure's ausgabe\n format_instruction = \"\"\n if output_format == \"body-html\":\n tags_str = \", \".join(erlaubte_tags) if erlaubte_tags else \"h1, h2, h3, h4, p, a, ol, ul, li, strong, table, hr\"\n format_instruction = f\"\"\"7. **KRITISCH - Ausgabe als sauberes HTML:**\n - NUR diese Tags: {tags_str}\n - KEIN Markdown (keine ##, keine **, keine -)\n - KEIN div, span, br, img, script, style\n - Jeder Absatz in <p>-Tags\n - Überschriften als <h2>, <h3>, <h4>\n - Listen als <ul>\/<ol> mit <li>\"\"\"\n\n # Load generate prompt template from database\n prompt_template = get_prompt(\"content-generate\")\n\n if prompt_template:\n prompt = prompt_template.format(\n profile_text=profile_text,\n contract_text=contract_text,\n structure_text=structure_text,\n context=context_text,\n briefing=briefing,\n format_instruction=format_instruction,\n semantic_text=semantic_text,\n taxonomy_text=taxonomy_text,\n )\n else:\n # Fallback if prompt not in DB\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{semantic_text}\n{taxonomy_text}\n\n## Kontext aus der Wissensbasis:\n{context_text}\n\n## Briefing:\n{briefing}\n\n## Anweisungen:\n1. Nutze die Informationen aus dem Kontext als Grundlage\n2. Halte dich an das Autorenprofil und den Schreibstil\n3. Beachte die Vorgaben aus dem Contract\n4. Strukturiere den Text gemäß dem Template (falls angegeben)\n5. Schreibe auf Deutsch\n6. Kennzeichne verwendete Quellen\n7. Berücksichtige die relevanten Konzepte und deren Beziehungen\n{format_instruction}\n\nErstelle nun den Content:\"\"\"\n\n return prompt\n\n\ndef call_llm(prompt: str, model: str = \"anthropic\", client_name: str = \"content-studio\") -> str:\n \"\"\"\n Call LLM to generate content with protokoll logging.\n\n Args:\n prompt: The prompt to send\n model: 'anthropic' or 'ollama'\n client_name: Identifier for protokoll logging\n\n Returns:\n Generated text content\n \"\"\"\n start_time = time.time()\n response_text = \"\"\n tokens_input = 0\n tokens_output = 0\n model_name = \"\"\n error_message = None\n status = \"completed\"\n\n try:\n if model == \"anthropic\" and ANTHROPIC_API_KEY:\n import anthropic\n\n client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)\n model_name = ANTHROPIC_MODEL\n\n message = client.messages.create(\n model=ANTHROPIC_MODEL, max_tokens=4000, messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n response_text = message.content[0].text\n\n # Extract token usage from Anthropic response\n if hasattr(message, \"usage\"):\n tokens_input = getattr(message.usage, \"input_tokens\", 0)\n tokens_output = getattr(message.usage, \"output_tokens\", 0)\n else:\n # Fallback to Ollama\n import requests\n\n model_name = OLLAMA_CHAT_MODEL\n\n response = requests.post(\n f\"{OLLAMA_HOST}\/api\/generate\",\n json={\"model\": OLLAMA_CHAT_MODEL, \"prompt\": prompt, \"stream\": False},\n timeout=900, # 15 min for large models\n )\n response.raise_for_status()\n result = response.json()\n response_text = result.get(\"response\", \"\")\n\n # Extract token counts from Ollama response\n tokens_input = result.get(\"prompt_eval_count\", 0)\n tokens_output = result.get(\"eval_count\", 0)\n\n # Clean up model artifacts (Gemma, Llama, etc.)\n artifacts = [\n \"<start_of_turn>\",\n \"<\/start_of_turn>\",\n \"<end_of_turn>\",\n \"<\/end_of_turn>\",\n \"<\/s>\",\n \"<|eot_id|>\",\n \"<|im_end|>\",\n ]\n for artifact in artifacts:\n response_text = response_text.replace(artifact, \"\").strip()\n\n except Exception as e:\n status = \"error\"\n error_message = str(e)\n raise\n\n finally:\n # Calculate duration\n duration_ms = int((time.time() - start_time) * 1000)\n\n # Log to protokoll\n db.log_to_protokoll(\n client_name=client_name,\n request=prompt,\n response=response_text if status == \"completed\" else None,\n model_name=model_name,\n tokens_input=tokens_input,\n tokens_output=tokens_output,\n duration_ms=duration_ms,\n status=status,\n error_message=error_message,\n )\n\n return response_text\n\n\ndef generate_content(\n order_id: int, model: str = \"anthropic\", collection: str = \"documents\", context_limit: int = 5\n) -> dict:\n \"\"\"\n Main content generation function.\n\n Args:\n order_id: Content order ID\n model: 'anthropic' or 'ollama'\n collection: Qdrant collection to search\n context_limit: Number of context chunks\n\n Returns:\n dict with version_id, content, sources\n \"\"\"\n db.connect()\n\n try:\n # Load order\n order = get_order(order_id)\n if not order:\n return {\"error\": f\"Order {order_id} not found\"}\n\n # Update status\n update_order_status(order_id, \"generating\")\n\n # Get RAG context\n context = get_rag_context(order[\"briefing\"], collection, context_limit)\n\n # Extract chunk_ids and document_ids for sema... [TRUNCATED-521ea97b163e2b4b]",
"structuredPatch": [],
"originalFile": null
}
}