feat(local-llm): B1b — lokale Tools (web_search via SearXNG, Timer, Spotify, Memory)
Das lokale Tier kann jetzt Werkzeuge nutzen — Wetter/News/Fakten laufen lokal
statt ueber Claudes langsamen Web-Fetch.
- SearXNG-Container (aria VM, aria-net): self-hosted Meta-Suche, keyless,
JSON-API aktiviert (aria-data/searxng/settings.yml), Rate-Limiter aus.
Brain-Env SEARXNG_URL.
- web_search-Tool in META_TOOLS + _dispatch_tool (_web_search fragt SearXNG,
gibt Titel/Snippet/URL der Top-Treffer). Steht auch Claude zur Verfuegung.
- Lokaler Tool-Loop (_try_local_fast_lane): kuratiertes Set web_search /
memory_search / trigger_timer / run_spotify; max 3 Runden, sonst → Claude.
Break-/Escalate-Guard bleibt.
- Router: should_try_local schliesst nur noch CLAUDE-ONLY-Themen aus (Bild,
Skill, Projekt, OAuth, Licht, Kalender/Mail). Wetter/Timer/News/Musik/Memory
gehen jetzt lokal. Verifiziert (alle Testfaelle korrekt).
- Schlanker Local-Prompt mit Tool-Guidance ("nur nutzen wenn noetig, sonst
Smalltalk direkt beantworten"). Skill-BAUEN bleibt Claude-only.
Deploy: ARIA-VM brain+bridge+searxng, Gamebox llm-adapter (tool-passthrough).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+136
-27
@@ -21,6 +21,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ import oauth as oauth_mod
|
|||||||
import projects as projects_mod
|
import projects as projects_mod
|
||||||
|
|
||||||
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
|
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
|
||||||
|
# SearXNG (self-hosted Meta-Suche) — Backend fuers web_search-Tool (B1b).
|
||||||
|
SEARXNG_URL = os.environ.get("SEARXNG_URL", "http://searxng:8080").rstrip("/")
|
||||||
# FLUX-Render kann bis ~90s dauern, beim ersten Render nach Container-Start
|
# FLUX-Render kann bis ~90s dauern, beim ersten Render nach Container-Start
|
||||||
# laedt die flux-bridge zudem ~24 GB Modell von HF (~5-10 min). Brain wartet
|
# laedt die flux-bridge zudem ~24 GB Modell von HF (~5-10 min). Brain wartet
|
||||||
# synchron — Stefan kuendigt es vorher an wenn er weiss dass es feuert.
|
# synchron — Stefan kuendigt es vorher an wenn er weiss dass es feuert.
|
||||||
@@ -66,6 +69,38 @@ def _load_flux_config() -> dict:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _web_search(query: str, max_results: int = 5) -> str:
|
||||||
|
"""Fragt die self-hosted SearXNG-Instanz (JSON-API) und gibt die Top-Treffer
|
||||||
|
als kompakten Text zurueck (Titel + Snippet + URL). Nie werfen — Fehler als
|
||||||
|
Text-Resultat, damit der Tool-Loop weitermachen kann."""
|
||||||
|
try:
|
||||||
|
params = urllib.parse.urlencode({
|
||||||
|
"q": query, "format": "json", "language": "de", "safesearch": "0",
|
||||||
|
})
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{SEARXNG_URL}/search?{params}",
|
||||||
|
headers={"User-Agent": "ARIA/1.0", "Accept": "application/json"},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
|
data = json.loads(resp.read().decode("utf-8", "ignore"))
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("web_search (SearXNG) fehlgeschlagen: %s", exc)
|
||||||
|
return f"FEHLER: Websuche nicht verfuegbar ({exc})."
|
||||||
|
results = (data.get("results") or [])[:max_results]
|
||||||
|
if not results:
|
||||||
|
answers = data.get("answers") or []
|
||||||
|
if answers:
|
||||||
|
return "Direkte Antwort: " + " | ".join(str(a) for a in answers[:3])
|
||||||
|
return f"Keine Web-Treffer fuer '{query}'."
|
||||||
|
lines = [f"{len(results)} Web-Treffer fuer '{query}':"]
|
||||||
|
for i, r in enumerate(results, 1):
|
||||||
|
title = (r.get("title") or "").strip()
|
||||||
|
url = (r.get("url") or "").strip()
|
||||||
|
snippet = (r.get("content") or "").strip()
|
||||||
|
lines.append(f"\n{i}. {title}\n {snippet[:300]}\n Quelle: {url}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
# Meta-Tool: ARIA kann selbst neue Skills bauen
|
# Meta-Tool: ARIA kann selbst neue Skills bauen
|
||||||
META_TOOLS = [
|
META_TOOLS = [
|
||||||
{
|
{
|
||||||
@@ -811,6 +846,29 @@ META_TOOLS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "web_search",
|
||||||
|
"description": (
|
||||||
|
"Durchsuche das Web (via SearXNG) nach AKTUELLEN, nachschlagbaren "
|
||||||
|
"Infos: Wetter, News, Fakten, Oeffnungszeiten, Preise, Definitionen "
|
||||||
|
"usw. Nutze das immer, wenn die Antwort aktuelles Wissen braucht, "
|
||||||
|
"das weder im Gedaechtnis noch in deinem Training steht (oder "
|
||||||
|
"veraltet sein koennte). Gib eine praezise Suchanfrage wie bei "
|
||||||
|
"Google. Ergebnis = Titel + Snippet + URL der Top-Treffer; fasse "
|
||||||
|
"daraus knapp die Antwort zusammen und nenne bei Bedarf die Quelle."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {"type": "string", "description": "Suchanfrage (praezise, Suchmaschinen-Stil)"},
|
||||||
|
"max_results": {"type": "integer", "description": "Anzahl Treffer (Default 5, max 10)"},
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
# ── Projekte (Stefan-Konzept: Threads im Hauptchat verankert) ──
|
# ── Projekte (Stefan-Konzept: Threads im Hauptchat verankert) ──
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
@@ -1055,62 +1113,104 @@ class Agent:
|
|||||||
return reply
|
return reply
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ── Fast-Lane: lokales schnelles LLM (Plan B, B1a) ──
|
# ── Fast-Lane: lokales schnelles LLM (Plan B, B1a/B1b) ──
|
||||||
#
|
#
|
||||||
# Zwischen Skill-Fast-Path und Claude-Loop: einfache Plauder-Turns beantwortet
|
# Zwischen Skill-Fast-Path und Claude-Loop: einfache Turns beantwortet das
|
||||||
# das lokale Qwen (schlanker Prompt, KEINE Tools) in <1 s. Gated ueber
|
# lokale Qwen in <1 s. Seit B1b mit kuratierten Tools (web_search,
|
||||||
|
# memory_search, trigger_timer, Spotify). Gated ueber
|
||||||
# /shared/config/local_llm.json (Default aus → alles Claude wie bisher).
|
# /shared/config/local_llm.json (Default aus → alles Claude wie bisher).
|
||||||
# Rueckgabe: fertige Antwort (str) wenn lokal erledigt, sonst None → Claude.
|
# Rueckgabe: fertige Antwort (str) wenn lokal erledigt, sonst None → Claude.
|
||||||
|
|
||||||
_LOCAL_WINDOW_TURNS = 8 # nur die letzten N Turns ans lokale Modell (Speed)
|
_LOCAL_WINDOW_TURNS = 8 # nur die letzten N Turns ans lokale Modell (Speed)
|
||||||
|
_LOCAL_TOOL_ITERATIONS = 3 # max Tool-Runden lokal, sonst → Claude
|
||||||
|
|
||||||
|
# Kuratierte Tool-Auswahl fuers lokale Tier (B1b). Namen aus META_TOOLS +
|
||||||
|
# der Spotify-Skill. Bewusst klein (Speed + Sicherheit); alles andere → Claude.
|
||||||
|
_LOCAL_TOOL_NAMES = {"web_search", "memory_search", "trigger_timer"}
|
||||||
|
|
||||||
|
def _build_local_tools(self) -> list:
|
||||||
|
tools = [t for t in META_TOOLS
|
||||||
|
if t.get("function", {}).get("name") in self._LOCAL_TOOL_NAMES]
|
||||||
|
for s in skills_mod.list_skills(active_only=False):
|
||||||
|
if s.get("name") == "spotify" and s.get("active", True):
|
||||||
|
tools.append(_skill_to_tool(s))
|
||||||
|
break
|
||||||
|
return tools
|
||||||
|
|
||||||
def _try_local_fast_lane(self, user_message: str,
|
def _try_local_fast_lane(self, user_message: str,
|
||||||
active_project_id: str) -> Optional[str]:
|
active_project_id: str) -> Optional[str]:
|
||||||
cfg = router_mod.load_config()
|
cfg = router_mod.load_config()
|
||||||
if not router_mod.should_try_local(user_message, cfg):
|
if not router_mod.should_try_local(user_message, cfg):
|
||||||
return None
|
return None
|
||||||
|
local_only = bool(cfg.get("localOnly"))
|
||||||
|
tools = self._build_local_tools() # B1b: kuratierte Tools
|
||||||
|
|
||||||
sys_prompt = router_mod.build_local_system_prompt(IDENTITY_ANCHOR)
|
sys_prompt = router_mod.build_local_system_prompt(IDENTITY_ANCHOR,
|
||||||
# Nur die letzten paar Turns ans lokale Modell — es ist fuer kurze
|
has_tools=bool(tools))
|
||||||
# Plauder-Turns da. Volles Fenster (bis 50) wuerde das Prefill aufblaehen
|
# Nur die letzten paar Turns ans lokale Modell (Speed — volles Fenster
|
||||||
# und den Speed-Vorteil auffressen (gemessen: 12 Turns → ~2,6s statt ~0,8s).
|
# wuerde das Prefill aufblaehen).
|
||||||
window = self.conversation.window(project_id=active_project_id)[-self._LOCAL_WINDOW_TURNS:]
|
window = self.conversation.window(project_id=active_project_id)[-self._LOCAL_WINDOW_TURNS:]
|
||||||
messages = [{"role": "system", "content": sys_prompt}]
|
messages = [{"role": "system", "content": sys_prompt}]
|
||||||
messages += [{"role": t.role, "content": t.content} for t in window]
|
messages += [{"role": t.role, "content": t.content} for t in window]
|
||||||
|
|
||||||
res = local_llm_chat(messages, max_tokens=400, temperature=0.5)
|
# Tool-Loop: lokales Modell darf web_search/memory_search/trigger_timer/
|
||||||
local_only = bool(cfg.get("localOnly"))
|
# Spotify aufrufen. Ergebnisse zurueck, bis es final (ohne tool_calls) antwortet.
|
||||||
|
final = ""
|
||||||
|
for _ in range(self._LOCAL_TOOL_ITERATIONS):
|
||||||
|
res = local_llm_chat(messages, max_tokens=500, temperature=0.5, tools=tools)
|
||||||
if not res.get("ok"):
|
if not res.get("ok"):
|
||||||
logger.info("[router] lokal fehlgeschlagen (%s) — %s",
|
logger.info("[router] lokal fehlgeschlagen (%s) — %s", res.get("error"),
|
||||||
res.get("error"), "kein Fallback (localOnly)" if local_only else "→ Claude")
|
"kein Fallback (localOnly)" if local_only else "→ Claude")
|
||||||
if local_only:
|
if local_only:
|
||||||
# Eval-Modus: KEIN Claude. Ehrliche Fehlermeldung statt Stille.
|
|
||||||
return f"[Lokales LLM nicht erreichbar: {res.get('error', 'unbekannt')}]"
|
return f"[Lokales LLM nicht erreichbar: {res.get('error', 'unbekannt')}]"
|
||||||
return None
|
return None
|
||||||
|
tcs = res.get("tool_calls")
|
||||||
content = (res.get("content") or "").strip()
|
if tcs:
|
||||||
|
messages.append({"role": "assistant",
|
||||||
|
"content": res.get("content") or "",
|
||||||
|
"tool_calls": tcs})
|
||||||
|
for tc in tcs:
|
||||||
|
fn = tc.get("function") or {}
|
||||||
|
tname = fn.get("name") or ""
|
||||||
|
try:
|
||||||
|
targs = json.loads(fn.get("arguments") or "{}")
|
||||||
|
except Exception:
|
||||||
|
targs = {}
|
||||||
|
logger.info("[router] lokal Tool-Call: %s(%s)", tname,
|
||||||
|
", ".join(targs.keys()))
|
||||||
|
tresult = self._dispatch_tool(tname, targs)
|
||||||
|
messages.append({"role": "tool",
|
||||||
|
"tool_call_id": tc.get("id") or "",
|
||||||
|
"name": tname,
|
||||||
|
"content": (tresult or "")[:6000]})
|
||||||
|
continue # naechste Runde mit Tool-Ergebnissen
|
||||||
|
final = (res.get("content") or "").strip()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.info("[router] lokal Tool-Loop-Limit → %s",
|
||||||
|
"Fallback (localOnly)" if local_only else "Claude")
|
||||||
|
if not local_only:
|
||||||
|
return None
|
||||||
|
final = final or "[Lokales LLM: Tool-Loop-Limit erreicht.]"
|
||||||
|
|
||||||
if local_only:
|
if local_only:
|
||||||
# Erzwungen lokal: Escalation-Marker ignorieren, Antwort so nehmen.
|
final = final.replace(router_mod.ESCALATE_MARKER, "").strip()
|
||||||
content = content.replace(router_mod.ESCALATE_MARKER, "").strip()
|
if not final:
|
||||||
if not content:
|
|
||||||
return "[Lokales LLM lieferte keine Antwort.]"
|
return "[Lokales LLM lieferte keine Antwort.]"
|
||||||
logger.info("[router] lokal (localOnly) %sms", res.get("elapsedMs"))
|
logger.info("[router] lokal (localOnly) beantwortet")
|
||||||
self.conversation.add("assistant", content, project_id=active_project_id)
|
self.conversation.add("assistant", final, project_id=active_project_id)
|
||||||
return content
|
return final
|
||||||
|
|
||||||
# Normalbetrieb: leer, Escalation-Marker oder (unwahrscheinlich) ein
|
# Normalbetrieb: leer, Escalation-Marker oder (unwahrscheinlich) ein
|
||||||
# Identity-Break → Claude uebernehmen.
|
# Identity-Break → Claude uebernehmen.
|
||||||
if (not content or router_mod.ESCALATE_MARKER in content
|
if (not final or router_mod.ESCALATE_MARKER in final
|
||||||
or looks_like_identity_break(content)):
|
or looks_like_identity_break(final)):
|
||||||
logger.info("[router] lokal eskaliert → Claude")
|
logger.info("[router] lokal eskaliert → Claude")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.info("[router] lokal beantwortet in %sms (%d Zeichen)",
|
logger.info("[router] lokal beantwortet (%d Zeichen)", len(final))
|
||||||
res.get("elapsedMs"), len(content))
|
self.conversation.add("assistant", final, project_id=active_project_id)
|
||||||
self.conversation.add("assistant", content, project_id=active_project_id)
|
return final
|
||||||
return content
|
|
||||||
|
|
||||||
# ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ──
|
# ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ──
|
||||||
|
|
||||||
@@ -1808,6 +1908,15 @@ class Agent:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("memory_search fehlgeschlagen")
|
logger.exception("memory_search fehlgeschlagen")
|
||||||
return f"FEHLER: {e}"
|
return f"FEHLER: {e}"
|
||||||
|
if name == "web_search":
|
||||||
|
query = (arguments.get("query") or "").strip()
|
||||||
|
if not query:
|
||||||
|
return "FEHLER: query ist Pflicht."
|
||||||
|
try:
|
||||||
|
n = int(arguments.get("max_results", 5))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
n = 5
|
||||||
|
return _web_search(query, max(1, min(n, 10)))
|
||||||
if name == "memory_update":
|
if name == "memory_update":
|
||||||
pid = (arguments.get("id") or "").strip()
|
pid = (arguments.get("id") or "").strip()
|
||||||
if not pid:
|
if not pid:
|
||||||
|
|||||||
+40
-22
@@ -57,15 +57,17 @@ def load_config() -> dict:
|
|||||||
# lokale Tier soll nur die klaren Plauder-Turns abgreifen; Fehlklassifikation
|
# lokale Tier soll nur die klaren Plauder-Turns abgreifen; Fehlklassifikation
|
||||||
# faengt zusaetzlich das <<ESCALATE>> im Modell selbst ab.
|
# faengt zusaetzlich das <<ESCALATE>> im Modell selbst ab.
|
||||||
|
|
||||||
# Wortstämme, die auf Tool-/Aktionsbedarf hindeuten → nicht lokal (in B1a).
|
# CLAUDE-ONLY-Themen → nicht lokal. Seit B1b hat das lokale Tier Werkzeuge
|
||||||
|
# (web_search, memory_search, trigger_timer, Spotify), daher gehen Wetter, News,
|
||||||
|
# Fakten, Timer, Musik, Gedaechtnis-Suche jetzt LOKAL. Nur was das lokale Tier
|
||||||
|
# nicht kann bleibt hier: Bilder, Skills, Projekte, OAuth, Smart-Home (keine
|
||||||
|
# Anbindung), Kalender/Mail (kein Tool).
|
||||||
_TOOL_HINTS = re.compile(
|
_TOOL_HINTS = re.compile(
|
||||||
r"\b(wetter|temperatur|regne|timer|erinner|weck|licht|lampe|steckdose|"
|
r"\b(bild|generier|male?\b|malen|zeichne|foto|"
|
||||||
r"spotify|musik|lied|song|playlist|lauter|leiser|"
|
r"skill|projekt|oauth|"
|
||||||
r"bild|generier|male?\b|zeichne|foto|"
|
r"licht|lampe|steckdose|rollade|heizung|"
|
||||||
r"merk dir|memory|gedächtnis|erinnere dich|"
|
r"kalender|termin|"
|
||||||
r"skill|trigger|projekt|oauth|spotify|kalender|termin|"
|
r"maild?|e-?mail|nachricht schreiben)\b",
|
||||||
r"such(e|st)?\b|google|internet|news|nachrichten|aktie|kurs|"
|
|
||||||
r"maild?|email|nachricht schreiben|sende)\b",
|
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,31 +106,47 @@ def should_try_local(user_message: str, cfg: dict) -> bool:
|
|||||||
# (WAS ARIA kann, ohne volle Schemas) + Escalation-Regel. KEINE Tool-Schemas,
|
# (WAS ARIA kann, ohne volle Schemas) + Escalation-Regel. KEINE Tool-Schemas,
|
||||||
# kein volles Memory (B1a).
|
# kein volles Memory (B1a).
|
||||||
|
|
||||||
|
# Was das lokale Tier NICHT selbst kann → dafuer eskaliert es an Claude.
|
||||||
_AWARENESS = (
|
_AWARENESS = (
|
||||||
"ARIA kann ausserdem (ueber ihr grosses Backend, nicht in diesem schnellen "
|
"Nur das grosse Modell (nicht du im Schnell-Modus) kann: Bilder generieren, "
|
||||||
"Modus): Wetter/Zeit/Websuche, Timer & Erinnerungen, Musik (Spotify) & Licht "
|
"Skills bauen/aendern, Projekte & OAuth verwalten, ins Gedaechtnis SCHREIBEN, "
|
||||||
"steuern, Bilder generieren, ins Gedaechtnis schreiben/suchen, Skills bauen, "
|
"sowie tiefe/technische Analysen und langen Code."
|
||||||
"Projekte & OAuth verwalten."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_local_system_prompt(identity_anchor: str, pinned_persona: str = "") -> str:
|
def build_local_system_prompt(identity_anchor: str, has_tools: bool = False,
|
||||||
|
pinned_persona: str = "") -> str:
|
||||||
"""Schlanker System-Prompt fuers lokale LLM. identity_anchor = derselbe
|
"""Schlanker System-Prompt fuers lokale LLM. identity_anchor = derselbe
|
||||||
Anker wie bei Claude (Rolle haelt); pinned_persona optional kurz."""
|
Anker wie bei Claude (Rolle haelt)."""
|
||||||
parts = [
|
parts = [
|
||||||
identity_anchor.strip(),
|
identity_anchor.strip(),
|
||||||
"",
|
"",
|
||||||
"## SCHNELL-MODUS",
|
"## SCHNELL-MODUS",
|
||||||
"Du laeufst gerade als schnelles lokales Modell fuer einfache, kurze "
|
"Du laeufst gerade als schnelles lokales Modell fuer Alltags-Konversation "
|
||||||
"Alltags-Konversation. Antworte knapp, freundlich, auf Deutsch, als ARIA.",
|
"und einfache Aufgaben. Antworte knapp, freundlich, auf Deutsch, als ARIA.",
|
||||||
|
]
|
||||||
|
if has_tools:
|
||||||
|
parts += [
|
||||||
"",
|
"",
|
||||||
"## WAS DU HIER NICHT TUST",
|
"## DEINE WERKZEUGE — nur nutzen wenn die Frage es WIRKLICH braucht",
|
||||||
|
"- `web_search`: aktuelle Infos aus dem Netz — Wetter, News, Fakten, "
|
||||||
|
"Preise, Oeffnungszeiten. Bei Wetter: nimm Stefans Ort aus dem "
|
||||||
|
"GPS-Hinweis in der Nachricht.",
|
||||||
|
"- `memory_search`: in ARIAs Gedaechtnis nachsehen (lesen).",
|
||||||
|
"- `trigger_timer`: Timer/Erinnerung setzen ('in 10 Minuten…').",
|
||||||
|
"- `run_spotify`: Musik steuern (naechster Titel, Pause, weiter).",
|
||||||
|
"WICHTIG: Bei reinem Smalltalk ('wie gehts', Begruessung, Meinung) "
|
||||||
|
"KEIN Werkzeug — einfach direkt antworten. Werkzeuge nur bei echtem "
|
||||||
|
"Bedarf; erfinde keine.",
|
||||||
|
]
|
||||||
|
parts += [
|
||||||
|
"",
|
||||||
|
"## WAS DU HIER NICHT KANNST",
|
||||||
_AWARENESS,
|
_AWARENESS,
|
||||||
"Fuer alles, was ein Werkzeug/eine Aktion braucht, tiefer/technischer wird, "
|
"Dafuer — und bei tiefen/technischen Fragen, langem Code, oder wenn du "
|
||||||
"aktuelles Wissen oder Nachschlagen verlangt, oder wo du unsicher bist: "
|
f"unsicher bist — antworte AUSSCHLIESSLICH mit exakt `{ESCALATE_MARKER}` "
|
||||||
f"antworte AUSSCHLIESSLICH mit exakt `{ESCALATE_MARKER}` (nichts sonst). "
|
"(nichts sonst). Dann uebernimmt das grosse Modell mit vollem Zugriff. "
|
||||||
"Dann uebernimmt automatisch das grosse Modell mit vollem Zugriff. "
|
"Lieber einmal eskalieren als falsch raten oder ein Werkzeug erfinden.",
|
||||||
"Lieber einmal eskalieren als falsch raten.",
|
|
||||||
]
|
]
|
||||||
if pinned_persona.strip():
|
if pinned_persona.strip():
|
||||||
parts += ["", "## PERSONA", pinned_persona.strip()]
|
parts += ["", "## PERSONA", pinned_persona.strip()]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# SearXNG-Config fuer ARIA (self-hosted Meta-Suche, Backend fuers web_search-Tool).
|
||||||
|
# Erbt alle Default-Engines; wir ueberschreiben nur das Noetige:
|
||||||
|
# - JSON-Format aktiviert (Default AUS) -> Brain kann /search?format=json rufen
|
||||||
|
# - Rate-Limiter aus -> programmatischer Brain-Zugriff wird nicht geblockt
|
||||||
|
# - eigener secret_key (interne Instanz auf aria-net, nicht oeffentlich exponiert)
|
||||||
|
use_default_settings: true
|
||||||
|
|
||||||
|
server:
|
||||||
|
# Interner Dienst auf aria-net, nicht oeffentlich. Trotzdem ein eigener Key.
|
||||||
|
# Bei Bedarf aendern (beliebiger langer Zufallsstring).
|
||||||
|
secret_key: "aria-searxng-6f2c9a1e8b7d4f30a5c1e2d9b8a7f6c3"
|
||||||
|
limiter: false
|
||||||
|
image_proxy: false
|
||||||
|
|
||||||
|
search:
|
||||||
|
formats:
|
||||||
|
- html
|
||||||
|
- json
|
||||||
|
# Deutsch bevorzugen (Brain kann per Query-Param ueberschreiben).
|
||||||
|
default_lang: "de"
|
||||||
|
|
||||||
|
# Sanftere Timeouts, damit eine langsame Engine die Suche nicht ausbremst.
|
||||||
|
outgoing:
|
||||||
|
request_timeout: 5.0
|
||||||
|
max_request_timeout: 10.0
|
||||||
@@ -53,6 +53,21 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- aria-net
|
- aria-net
|
||||||
|
|
||||||
|
# ─── SearXNG (self-hosted Meta-Suche) ────────────────────
|
||||||
|
# Backend fuer das web_search-Tool (B1b). Aggregiert Google/Bing/Brave/… ohne
|
||||||
|
# API-Key, laeuft nur intern auf aria-net. Config: aria-data/searxng/settings.yml
|
||||||
|
# (JSON-Format aktiviert, Rate-Limiter aus fuer den Brain-Zugriff).
|
||||||
|
searxng:
|
||||||
|
image: searxng/searxng:latest
|
||||||
|
container_name: aria-searxng
|
||||||
|
volumes:
|
||||||
|
- ./aria-data/searxng:/etc/searxng
|
||||||
|
environment:
|
||||||
|
- SEARXNG_BASE_URL=http://searxng:8080/
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- aria-net
|
||||||
|
|
||||||
# ─── ARIA Brain (Agent + Memory) ─────────────────────────
|
# ─── ARIA Brain (Agent + Memory) ─────────────────────────
|
||||||
# Loest das alte aria-core (OpenClaw) ab. Vector-DB-basiertes
|
# Loest das alte aria-core (OpenClaw) ab. Vector-DB-basiertes
|
||||||
# Memory, eigener Agent-Loop, SSH zur aria-wohnung-VM.
|
# Memory, eigener Agent-Loop, SSH zur aria-wohnung-VM.
|
||||||
@@ -86,6 +101,8 @@ services:
|
|||||||
- RVS_HOST=${RVS_HOST:-}
|
- RVS_HOST=${RVS_HOST:-}
|
||||||
- RVS_PORT_PUBLIC=${RVS_PORT_PUBLIC:-${RVS_PORT:-443}}
|
- RVS_PORT_PUBLIC=${RVS_PORT_PUBLIC:-${RVS_PORT:-443}}
|
||||||
- RVS_TLS=${RVS_TLS:-true}
|
- RVS_TLS=${RVS_TLS:-true}
|
||||||
|
# SearXNG (self-hosted Meta-Suche) fuer das web_search-Tool (B1b).
|
||||||
|
- SEARXNG_URL=${SEARXNG_URL:-http://searxng:8080}
|
||||||
volumes:
|
volumes:
|
||||||
- ./aria-data/brain/data:/data # Memory-Cache + Skills + Models (bind-mount fuer Export)
|
- ./aria-data/brain/data:/data # Memory-Cache + Skills + Models (bind-mount fuer Export)
|
||||||
- ./aria-data/brain-import:/import:ro # Quell-MDs fuer den initialen Memory-Import (read-only)
|
- ./aria-data/brain-import:/import:ro # Quell-MDs fuer den initialen Memory-Import (read-only)
|
||||||
|
|||||||
Reference in New Issue
Block a user