574 lines
23 KiB
Python
574 lines
23 KiB
Python
"""OpenRouter LLM-Wrapper mit Tool-Calling.
|
|
|
|
Das LLM entscheidet selbst welche Datenquellen es abfragt.
|
|
Neue Datenquelle = Tool-Definition hier + Handler in context.py.
|
|
"""
|
|
|
|
import json
|
|
import requests
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from core import config
|
|
|
|
MODEL = "openai/gpt-4o-mini"
|
|
MAX_TOOL_ROUNDS = 3
|
|
|
|
SYSTEM_PROMPT = """Du bist der Hausmeister-Bot fuer ein Homelab. Deutsch, kurz, direkt, operativ.
|
|
|
|
STIL:
|
|
- So wenig Worte wie moeglich, solange nichts Wichtiges fehlt.
|
|
- KEINE Abschlussformeln ("Wenn du weitere Informationen benoetigst...").
|
|
- KEINE kuenstlichen Wuensche ("Guten Flug!", "Viel Erfolg!").
|
|
- KEINE Rueckfragen ob der User mehr wissen will.
|
|
- Emojis nur wenn sie Information tragen. Telegram-Format (kein Markdown).
|
|
|
|
GEDAECHTNIS LESEN:
|
|
Am Ende dieses System-Prompts findest du eine Sektion "=== GEDAECHTNIS ===" mit Fakten die du dir ueber den User und die Umgebung gemerkt hast.
|
|
WICHTIG: Beantworte Fragen IMMER anhand dieser Fakten! Wenn der User z.B. fragt "Wo fliege ich hin?" und im Gedaechtnis steht "Ist naechste Woche in Frankfurt", dann antworte direkt mit dieser Info.
|
|
Das Gedaechtnis ist deine primaere Wissensquelle ueber den User.
|
|
|
|
GEDAECHTNIS — memory_suggest:
|
|
Du MUSST memory_suggest aufrufen wenn der User etwas Nuetzliches sagt.
|
|
memory_suggest speichert DIREKT. Duplikate werden automatisch erkannt. Widersprueche werden automatisch aufgeloest (altes Item wird ersetzt).
|
|
|
|
MEMORY_TYPE — immer angeben:
|
|
- "preference" → Vorlieben ("Lieblingskaffee ist Flat White", "bevorzuge dunkles Theme")
|
|
- "relationship" → Beziehungen/Rollen ("Ali ist Ansprechpartner fuer Wohnung")
|
|
- "fact" → Stabile Fakten ("Server IP geaendert", "Jarvis aktiv seit...")
|
|
- "plan" → Konkrete Vorhaben mit Zeitbezug ("Reise nach Frankfurt am 18.03.")
|
|
- "temporary" → Kurzfristige Zustaende ("bin bis Mittwoch in Berlin", "morgen Zahnarzt")
|
|
- "uncertain" → Vage Aussagen ("vielleicht fliege ich frueher", "glaube Ali kuemmert sich")
|
|
|
|
CONFIDENCE — immer angeben:
|
|
- "high" → Klare Aussage ("Mein Lieblingskaffee ist Flat White")
|
|
- "medium" → Wahrscheinlich aber nicht 100% ("Ali kuemmert sich wohl darum")
|
|
- "low" → Sehr vage ("vielleicht aendere ich das noch")
|
|
|
|
REGELN:
|
|
- Bei plan/temporary: IMMER expires_at mit Zeitausdruck angeben ("naechste Woche", "morgen")
|
|
- Bei uncertain mit low confidence: NUR speichern wenn es trotzdem nuetzlich sein koennte
|
|
- Vages Gerede, Smalltalk, emotionale Kurzaeusserungen: NICHT speichern
|
|
- Passwoerter, Tokens: NIE speichern
|
|
- Bei Widerspruch zu bestehendem Wissen: NUR den neuen Fakt speichern (z.B. "Lieblingskaffee ist Americano"). NICHT den alten Fakt umformulieren oder als "war..." speichern. Das System erkennt Widersprueche automatisch und ersetzt den alten Eintrag.
|
|
|
|
SESSION-RUECKBLICK:
|
|
- "Was haben wir besprochen?" → session_summary OHNE topic
|
|
- "Was haben wir ueber X besprochen?" → session_summary MIT topic="X"
|
|
- NUR echte Gespraechsinhalte aus den Treffern wiedergeben.
|
|
- KEINE Negativ-Aussagen hinzufuegen. Nichts ueber Dinge sagen die NICHT besprochen wurden.
|
|
Verboten: "wurde nicht thematisiert", "keine weiteren Details", "nicht besprochen", "nicht erwaehnt".
|
|
- Wenn es nur 1 Treffer gibt, gib nur diesen 1 Treffer wieder. Fuege nichts hinzu.
|
|
- Optional kurz erwaehnen was sonst noch Thema war.
|
|
- session_search nur fuer Stichwort-Suche in ALTEN Sessions (nicht aktuelle).
|
|
|
|
BILDERKENNUNG:
|
|
Wenn der User ein Bild oder PDF schickt:
|
|
- Beschreibe STRUKTURIERT was du siehst.
|
|
- Bei Flugplaenen/Buchungen: Extrahiere ALLE Daten (Flugnummer, Datum, Abflug/Ankunft Uhrzeit, Airports mit IATA-Codes, Preis, Buchungscode, Airline, Sitzplatz, Gepaeck).
|
|
- Bei Screenshots von Fehlern/Logs: Identifiziere das Problem, ordne es einem Container/Service zu, schlage Loesung vor.
|
|
- Bei Rechnungen/Dokumenten: Extrahiere Betrag, Datum, Absender, Faelligkeit.
|
|
- WICHTIG: Speichere erkannte Reiseplaene, Termine, Buchungen IMMER via memory_suggest (memory_type="plan", mit expires_at).
|
|
- Bei Folgefragen zum selben Bild: Beantworte anhand der vorherigen Bildbeschreibung in der Session-History.
|
|
|
|
TOOLS:
|
|
Nutze Tools fuer Live-Daten. Wenn alles OK: kurz sagen. Bei Problemen: erklaeren + Loesung."""
|
|
|
|
TOOLS = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_all_containers",
|
|
"description": "Status aller Container auf allen Proxmox-Servern (running/stopped, RAM, Uptime)",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_container_detail",
|
|
"description": "Detail-Status eines einzelnen Containers. Suche per VMID (z.B. 101) oder Name (z.B. wordpress, rss-manager, forgejo)",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string", "description": "VMID (z.B. '109') oder Container-Name (z.B. 'wordpress')"}
|
|
},
|
|
"required": ["query"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "count_errors",
|
|
"description": "Zählt Fehler-Logs aus Loki und gibt ANZAHL pro Host zurück. Nutze dieses Tool wenn nach der ANZAHL von Fehlern gefragt wird (z.B. 'wieviele Fehler', 'wie oft', 'Fehleranzahl').",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"hours": {"type": "number", "description": "Zeitraum in Stunden (z.B. 24 = heute, 72 = 3 Tage, 168 = 1 Woche)", "default": 24}
|
|
},
|
|
"required": [],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_errors",
|
|
"description": "Zeigt Fehler-Logs aus Loki mit Beispielen (Inhalt der Fehlermeldungen). Nutze dieses Tool wenn nach dem INHALT oder DETAILS von Fehlern gefragt wird.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"hours": {"type": "number", "description": "Zeitraum in Stunden (default: 2)", "default": 2}
|
|
},
|
|
"required": [],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_container_logs",
|
|
"description": "Letzte Logs eines bestimmten Containers aus Loki",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"container": {"type": "string", "description": "Hostname des Containers (z.B. 'rss-manager', 'wordpress-v2')"},
|
|
"hours": {"type": "number", "description": "Zeitraum in Stunden (default: 1)", "default": 1},
|
|
},
|
|
"required": ["container"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_silent_hosts",
|
|
"description": "Welche Hosts senden keine Logs mehr? (Stille-Check)",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_server_metrics",
|
|
"description": "CPU, RAM, Disk, Load, Uptime von Proxmox-Servern via Prometheus. Ohne host = alle Server.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"host": {
|
|
"type": "string",
|
|
"description": "Hostname (pve-hetzner, pve-ka-1, pve-ka-2, pve-ka-3, pve-mu-2, pve-mu-3, pve-he, pbs-mu). Leer = alle.",
|
|
}
|
|
},
|
|
"required": [],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_server_warnings",
|
|
"description": "Nur Warnungen: Server mit CPU>80%, RAM>85% oder Disk>85%",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_wordpress_stats",
|
|
"description": "WordPress/Blog-Statistiken: Posts heute/gestern/Woche, offene Kommentare, letzte Artikel, Plugin-Status",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_feed_stats",
|
|
"description": "RSS-Feed-Status: Aktive Feeds, Artikel heute/gestern, Fehler",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_forgejo_status",
|
|
"description": "Forgejo Git-Server: Offene Issues/TODOs, letzte Commits, Repos",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "create_issue",
|
|
"description": "Neues TODO/Issue in Forgejo erstellen",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {"type": "string", "description": "Titel des Issues"},
|
|
"body": {"type": "string", "description": "Beschreibung (optional)", "default": ""},
|
|
},
|
|
"required": ["title"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "close_issue",
|
|
"description": "Ein bestehendes Issue/TODO in Forgejo schliessen (als erledigt markieren)",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"number": {"type": "integer", "description": "Issue-Nummer (z.B. 3)"},
|
|
},
|
|
"required": ["number"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_seafile_status",
|
|
"description": "Seafile Cloud-Speicher: Belegter Speicherplatz, Bibliotheken",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_backup_status",
|
|
"description": "Proxmox Backup Server (PBS): Datastore-Belegung, letzte Backups, Snapshot-Anzahl",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_mail_summary",
|
|
"description": "E-Mail Übersicht: Anzahl Mails, ungelesene, letzte Mails, wichtige Absender (Bank, Hoster, etc.)",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_mail_count",
|
|
"description": "Anzahl E-Mails (gesamt und ungelesen)",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "search_mail",
|
|
"description": "E-Mails durchsuchen nach Absender oder Betreff",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string", "description": "Suchbegriff (Absender oder Betreff, z.B. 'PayPal', 'Rechnung', 'Hetzner')"},
|
|
"days": {"type": "integer", "description": "Zeitraum in Tagen (default: 30)", "default": 30},
|
|
},
|
|
"required": ["query"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_todays_mails",
|
|
"description": "Alle E-Mails von heute",
|
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_smart_mail_digest",
|
|
"description": "Intelligente Mail-Zusammenfassung: KI klassifiziert Mails in Wichtig/Aktion/Info/Newsletter/Spam. Nutze dies wenn der User nach 'wichtigen Mails' fragt oder wissen will ob etwas Relevantes dabei ist.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"hours": {"type": "integer", "description": "Zeitraum in Stunden (default: 24)", "default": 24},
|
|
},
|
|
"required": [],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "memory_read",
|
|
"description": "Liest persistente Gedaechtnis-Eintraege (Fakten ueber User, Umgebung, Projekte). Nutze dieses Tool wenn du wissen willst was du dir gemerkt hast.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"scope": {"type": "string", "description": "Filter: user, environment, project (leer = alle)", "default": ""},
|
|
},
|
|
"required": [],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "memory_suggest",
|
|
"description": "Speichert einen Fakt direkt im Langzeitgedaechtnis. IMMER aufrufen wenn der User Reiseplaene, Termine, Vorlieben, Beziehungen, Projektstatus oder stabile Fakten mitteilt. System erkennt Duplikate und ersetzt Widersprueche automatisch.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"scope": {"type": "string", "enum": ["user", "environment", "project"], "description": "user=persoenlich, environment=Infrastruktur, project=Projekt"},
|
|
"kind": {"type": "string", "enum": ["fact", "preference", "rule", "note"], "description": "Grobe Kategorie"},
|
|
"content": {"type": "string", "description": "Der Fakt (kurz, 3. Person, z.B. 'Lieblingskaffee ist Flat White')"},
|
|
"memory_type": {"type": "string", "enum": ["fact", "preference", "relationship", "plan", "temporary", "uncertain"], "description": "fact=stabiler Fakt, preference=Vorliebe, relationship=Beziehung/Rolle, plan=Vorhaben mit Zeitbezug, temporary=kurzfristiger Zustand, uncertain=vage Aussage"},
|
|
"confidence": {"type": "string", "enum": ["high", "medium", "low"], "description": "high=klare Aussage, medium=wahrscheinlich, low=vage"},
|
|
"expires_at": {"type": "string", "description": "Bei plan/temporary: Zeitangabe wann es ablaeuft (z.B. 'naechste Woche', 'morgen', '22.03.2026'). Leer bei dauerhaften Fakten."},
|
|
},
|
|
"required": ["scope", "kind", "content", "memory_type", "confidence"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "session_search",
|
|
"description": "Volltextsuche in vergangenen Sessions nach konkreten Stichworten. Fuer gezielte Suche wie 'Was habe ich ueber Backup gesagt?' oder 'Wann war das mit Seafile?'.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string", "description": "Suchbegriffe"},
|
|
},
|
|
"required": ["query"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "session_summary",
|
|
"description": "Zusammenfassung der aktuellen Session. Ohne topic = alle Themen. Mit topic = nur thematisch passende Punkte. Nutze topic wenn die Frage ein klares Thema enthaelt (z.B. 'Container', 'Jarvis', 'Backup').",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"topic": {"type": "string", "description": "Themenbegriff zum Filtern (z.B. 'Container', 'Backup'). Leer lassen fuer allgemeine Zusammenfassung."},
|
|
},
|
|
"required": [],
|
|
},
|
|
},
|
|
},
|
|
]
|
|
|
|
|
|
def _get_api_key() -> str:
|
|
cfg = config.parse_config()
|
|
return cfg.api_keys.get("openrouter_key", "")
|
|
|
|
|
|
def _call_openrouter(messages: list, api_key: str, use_tools: bool = True) -> dict:
|
|
payload = {
|
|
"model": MODEL,
|
|
"messages": messages,
|
|
"max_tokens": 600,
|
|
}
|
|
if use_tools:
|
|
payload["tools"] = TOOLS
|
|
payload["tool_choice"] = "auto"
|
|
|
|
r = requests.post(
|
|
"https://openrouter.ai/api/v1/chat/completions",
|
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
json=payload,
|
|
timeout=60,
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def ask(question: str, context: str) -> str:
|
|
"""Legacy-Funktion fuer /commands die bereits Kontext mitbringen."""
|
|
api_key = _get_api_key()
|
|
if not api_key:
|
|
return "OpenRouter API Key fehlt in homelab.conf"
|
|
|
|
messages = [
|
|
{"role": "system", "content": SYSTEM_PROMPT},
|
|
{"role": "user", "content": f"Kontext (Live-Daten):\n{context}\n\nFrage: {question}"},
|
|
]
|
|
try:
|
|
data = _call_openrouter(messages, api_key, use_tools=False)
|
|
return data["choices"][0]["message"]["content"]
|
|
except Exception as e:
|
|
return f"LLM-Fehler: {e}"
|
|
|
|
|
|
def ask_with_tools(question: str, tool_handlers: dict, session_id: str = None) -> str:
|
|
"""Freitext-Frage mit automatischem Tool-Calling.
|
|
|
|
tool_handlers: dict von tool_name -> callable(**kwargs) -> str
|
|
session_id: aktive Session fuer Konversations-History
|
|
"""
|
|
api_key = _get_api_key()
|
|
if not api_key:
|
|
return "OpenRouter API Key fehlt in homelab.conf"
|
|
|
|
try:
|
|
import memory_client
|
|
memory_items = memory_client.get_relevant_memory(question, top_k=10)
|
|
memory_block = memory_client.format_memory_for_prompt(memory_items)
|
|
except Exception:
|
|
memory_block = ""
|
|
|
|
messages = [
|
|
{"role": "system", "content": SYSTEM_PROMPT + memory_block},
|
|
]
|
|
|
|
_RECAP_MARKERS = ["was haben wir", "worüber haben wir", "worüber hatten wir",
|
|
"worueber haben wir", "was hatten wir besprochen", "was war heute thema"]
|
|
|
|
def _is_recap(text):
|
|
t = text.lower()
|
|
return any(m in t for m in _RECAP_MARKERS)
|
|
|
|
if session_id:
|
|
try:
|
|
import memory_client
|
|
history = memory_client.get_session_messages(session_id, limit=10)
|
|
skip_next_assistant = False
|
|
for msg in history:
|
|
role = msg.get("role", "")
|
|
content = msg.get("content", "")
|
|
if not content:
|
|
continue
|
|
if role == "user" and _is_recap(content):
|
|
skip_next_assistant = True
|
|
continue
|
|
if role == "assistant" and skip_next_assistant:
|
|
skip_next_assistant = False
|
|
continue
|
|
skip_next_assistant = False
|
|
if role in ("user", "assistant"):
|
|
messages.append({"role": role, "content": content})
|
|
except Exception:
|
|
pass
|
|
|
|
messages.append({"role": "user", "content": question})
|
|
|
|
try:
|
|
for _round in range(MAX_TOOL_ROUNDS):
|
|
data = _call_openrouter(messages, api_key, use_tools=True)
|
|
choice = data["choices"][0]
|
|
msg = choice["message"]
|
|
|
|
tool_calls = msg.get("tool_calls")
|
|
if not tool_calls:
|
|
return msg.get("content", "Keine Antwort vom LLM.")
|
|
|
|
messages.append(msg)
|
|
|
|
for tc in tool_calls:
|
|
fn_name = tc["function"]["name"]
|
|
try:
|
|
fn_args = json.loads(tc["function"]["arguments"])
|
|
except (json.JSONDecodeError, KeyError):
|
|
fn_args = {}
|
|
|
|
handler = tool_handlers.get(fn_name)
|
|
if handler:
|
|
try:
|
|
result = handler(**fn_args)
|
|
except Exception as e:
|
|
result = f"Fehler bei {fn_name}: {e}"
|
|
else:
|
|
result = f"Unbekanntes Tool: {fn_name}"
|
|
|
|
messages.append({
|
|
"role": "tool",
|
|
"tool_call_id": tc["id"],
|
|
"content": str(result)[:3000],
|
|
})
|
|
|
|
data = _call_openrouter(messages, api_key, use_tools=False)
|
|
return data["choices"][0]["message"]["content"]
|
|
|
|
except Exception as e:
|
|
return f"LLM-Fehler: {e}"
|
|
|
|
|
|
def ask_with_image(image_base64: str, caption: str, tool_handlers: dict, session_id: str = None) -> str:
|
|
"""Bild-Analyse mit optionalem Text und Tool-Calling via Vision-faehigem Modell."""
|
|
api_key = _get_api_key()
|
|
if not api_key:
|
|
return "OpenRouter API Key fehlt in homelab.conf"
|
|
|
|
try:
|
|
import memory_client
|
|
query = caption if caption else "Bild-Analyse"
|
|
memory_items = memory_client.get_relevant_memory(query, top_k=10)
|
|
memory_block = memory_client.format_memory_for_prompt(memory_items)
|
|
except Exception:
|
|
memory_block = ""
|
|
|
|
prompt_text = caption if caption else "Was siehst du auf diesem Bild? Beschreibe was du erkennst."
|
|
user_content = [
|
|
{"type": "text", "text": prompt_text},
|
|
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}},
|
|
]
|
|
|
|
messages = [
|
|
{"role": "system", "content": SYSTEM_PROMPT + memory_block},
|
|
]
|
|
|
|
if session_id:
|
|
try:
|
|
import memory_client
|
|
history = memory_client.get_session_messages(session_id, limit=6)
|
|
for msg in history:
|
|
role = msg.get("role", "")
|
|
content = msg.get("content", "")
|
|
if content and role in ("user", "assistant"):
|
|
messages.append({"role": role, "content": content})
|
|
except Exception:
|
|
pass
|
|
|
|
messages.append({"role": "user", "content": user_content})
|
|
|
|
try:
|
|
for _round in range(MAX_TOOL_ROUNDS):
|
|
data = _call_openrouter(messages, api_key, use_tools=True)
|
|
choice = data["choices"][0]
|
|
msg = choice["message"]
|
|
|
|
tool_calls = msg.get("tool_calls")
|
|
if not tool_calls:
|
|
return msg.get("content", "Keine Antwort vom LLM.")
|
|
|
|
messages.append(msg)
|
|
|
|
for tc in tool_calls:
|
|
fn_name = tc["function"]["name"]
|
|
try:
|
|
fn_args = json.loads(tc["function"]["arguments"])
|
|
except (json.JSONDecodeError, KeyError):
|
|
fn_args = {}
|
|
|
|
handler = tool_handlers.get(fn_name)
|
|
if handler:
|
|
try:
|
|
result = handler(**fn_args)
|
|
except Exception as e:
|
|
result = f"Fehler bei {fn_name}: {e}"
|
|
else:
|
|
result = f"Unbekanntes Tool: {fn_name}"
|
|
|
|
messages.append({
|
|
"role": "tool",
|
|
"tool_call_id": tc["id"],
|
|
"content": str(result)[:3000],
|
|
})
|
|
|
|
data = _call_openrouter(messages, api_key, use_tools=False)
|
|
return data["choices"][0]["message"]["content"]
|
|
|
|
except Exception as e:
|
|
return f"LLM-Fehler: {e}"
|