Files
Onyscribe-Admin/app/agent/tools.py
RubenRWU 7d8c226da2 Onyscribe Admin: Initialer Stand
Admin-Plattform für die Tenants von Onyscribe: Editor mit Live-Vorschau (echter Core in Sandbox), Versionen, KI-Agent (Cortecs),
OIDC-Abgleich über die Login-Developer-API, Deploy-Engine (Mittwald, gesperrt ohne DEPLOY_LIVE, Freigabe pro Tenant),
Core-Vertrag (Schema/Manifest), Core-Releases mit Kompatibilitätsprüfung, Canary-Rollout und Rollback, Zustandsabgleich.
Runbook: docs/RUNBOOK_PARALLELTEST.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 15:30:26 +02:00

111 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Werkzeuge des Agenten (neutrale Definition + Ausführung auf dem Workspace)."""
from __future__ import annotations
import io
import json
from app.agent.workspace import Workspace, is_personal
from app.services import llm, preview
from app.settings import PLATFORM_DIR
TOOLS = [
{"name": "list_files", "description": "Listet alle Dateien des Tenants (inkl. tenant.json) mit Größe und Änderungsstatus.",
"schema": {"type": "object", "properties": {}}},
{"name": "read_file", "description": "Liest eine Textdatei des Tenants (prompt.txt, protocol.html, protocol.css, overrides.py, tenant.json …). "
"Fehlt eine Datei, gilt der Core-Standard – nutze dann read_core_default.",
"schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "read_core_default", "description": "Liest eine Standarddatei des Cores als Vorlage: prompt.txt, protocol.html, protocol.css oder hooks.py (Hook-Dokumentation).",
"schema": {"type": "object", "properties": {"name": {"type": "string", "enum": ["prompt.txt", "protocol.html", "protocol.css", "hooks.py"]}}, "required": ["name"]}},
{"name": "write_file", "description": "Schreibt/ersetzt eine Textdatei des Tenants vollständig (z. B. um eine eigene prompt.txt oder protocol.css anzulegen). Nur im Entwurf – ein Mensch gibt frei.",
"schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Ersetzt in einer Datei genau eine Stelle: old_str muss exakt 1× vorkommen. Bevorzuge das gegenüber write_file bei kleinen Änderungen.",
"schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_str": {"type": "string"}, "new_str": {"type": "string"}}, "required": ["path", "old_str", "new_str"]}},
{"name": "delete_file", "description": "Löscht eine Tenant-Datei (z. B. um auf den Core-Standard zurückzufallen).",
"schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "set_config", "description": "Setzt einen Wert in tenant.json per Punkt-Pfad, z. B. key='colors.accent', value='#0B6E4F' oder key='flags.ui_kontakt_field', value=false.",
"schema": {"type": "object", "properties": {"key": {"type": "string"}, "value": {}}, "required": ["key", "value"]}},
{"name": "validate", "description": "Prüft den aktuellen Workspace mit dem ECHTEN Core (Web-UI + PDF rendern, Prompt laden). Gibt Fehler/Warnungen zurück. Nach Änderungen immer aufrufen.",
"schema": {"type": "object", "properties": {}}},
{"name": "render_pdf", "description": "Rendert das Beispielprotokoll als PDF und gibt Seitenzahl und extrahierten Text zurück, damit du Aufbau und Inhalt prüfen kannst.",
"schema": {"type": "object", "properties": {}}},
{"name": "show_final_prompt", "description": "Zeigt den finalen System-Prompt, wie der Core ihn an das LLM schickt (nach Platzhalter-Ersetzung und Hooks). Namenslisten werden gekürzt.",
"schema": {"type": "object", "properties": {}}},
{"name": "run_prompt_test", "description": "Führt das Protokoll-LLM mit dem Workspace-Prompt auf einem Beispieltranskript aus (kostet Geld – höchstens 1× pro Antwort). Liefert den Protokolltext.",
"schema": {"type": "object", "properties": {"transcript": {"type": "string", "description": "Optional; sonst das Standard-Beispieltranskript."}}}},
]
class ToolRunner:
def __init__(self, ws: Workspace):
self.ws, self.db, self.tenant = ws, ws.db, ws.tenant
self.llm_calls = 0
def _preview(self, kind, **kw):
return preview.run_content(self.db, self.tenant, self.ws.cfg, self.ws.files, kind, **kw)
def run(self, name: str, a: dict) -> tuple[str, str]:
"""→ (Kurzbeschreibung des Arguments für die Anzeige, Ergebnistext)."""
try:
return getattr(self, f"t_{name}")(a)
except (KeyError, ValueError, PermissionError, SyntaxError, json.JSONDecodeError) as exc:
return a.get("path") or a.get("key") or "", f"FEHLER: {exc}"
except preview.PreviewError as exc:
return "", f"FEHLER (Core-Rendering): {str(exc)[-900:]}"
def t_list_files(self, a):
rows = [{"path": "tenant.json", "text": True}] + self.ws.list_files()
return "", json.dumps(rows, ensure_ascii=False)
def t_read_file(self, a):
return a["path"], self.ws.read(a["path"])
def t_read_core_default(self, a):
n = a["name"]
p = (PLATFORM_DIR / "core" / ("hooks.py" if n == "hooks.py" else ("defaults" if n == "prompt.txt" else "templates")) / n) \
if n != "hooks.py" else PLATFORM_DIR / "core" / "hooks.py"
return n, p.read_text(encoding="utf-8")[:60_000]
def t_write_file(self, a):
self.ws.write(a["path"], a["content"])
return a["path"], f"geschrieben ({len(a['content'])} Zeichen)"
def t_edit_file(self, a):
self.ws.edit(a["path"], a["old_str"], a["new_str"])
return a["path"], "ersetzt"
def t_delete_file(self, a):
self.ws.delete(a["path"])
return a["path"], "gelöscht"
def t_set_config(self, a):
self.ws.set_config_value(a["key"], a["value"])
return a["key"], f"gesetzt = {json.dumps(a['value'], ensure_ascii=False)}"
def t_validate(self, a):
r = self._preview("validate")
return "Core-Rendering", json.dumps(r, ensure_ascii=False)
def t_render_pdf(self, a):
pdf = self._preview("pdf")
from pypdf import PdfReader
rd = PdfReader(io.BytesIO(pdf))
text = "\n".join((pg.extract_text() or "") for pg in rd.pages)
return "Beispielprotokoll", f"{len(rd.pages)} Seite(n)\n---\n{text[:3500]}"
def t_show_final_prompt(self, a):
r = self._preview("prompt", args={"title": "Beispiel", "terminart": "", "projekt": "", "kunde": ""})
p = r["prompt"]
# Namenslisten (personenbezogen) nicht an das Agenten-Modell geben
marker = "Namensregister"
if marker in p:
p = p[:p.index(marker)] + "[Namensregister mit personenbezogenen Daten ausgeblendet]"
return f"{r['chars']} Zeichen", p[:40_000]
def t_run_prompt_test(self, a):
if self.llm_calls >= 1:
return "", "FEHLER: Pro Antwort ist nur ein Prompt-Test erlaubt."
self.llm_calls += 1
from app.agent.testrun import prompt_test
res = prompt_test(self.db, self.tenant, self.ws.cfg, self.ws.files, a.get("transcript"))
return "Beispieltranskript", f"[{res['model']}, {res['input_tokens']}→{res['output_tokens']} Tokens]\n{res['text'][:6000]}"