feat(router): B1a — lokale Fast-Lane (reden-only) mit Schaltern, gated (Default aus)
Zwischen Skill-Fast-Path und Claude-Loop: einfache Plauder-Turns → lokales Qwen (schlanker Prompt, KEINE Tools) in <1 s; sonst Claude wie bisher. - router.py: load_config() liest /shared/config/local_llm.json (enabled/localOnly/ toolVariant; Default enabled=false → alles Claude). should_try_local() Heuristik (kurz + keine Tool-/Technik-Marker; localOnly erzwingt lokal). build_local_ system_prompt() = schlank (IDENTITY_ANCHOR + Schnell-Modus + Awareness-Liste + <<ESCALATE>>-Regel, keine Tool-Schemas). - agent.py: _try_local_fast_lane() — baut schlanken Prompt + Window, ruft local_llm_chat; leer/ESCALATE → None (→ Claude), sonst Antwort + persistiert. localOnly: kein Claude-Fallback (Eval), ehrliche Fehlermeldung bei Nichterreich. In chat() nach User-Turn gated aufgerufen. Heuristik lokal verifiziert (alle Testfaelle korrekt). Gated off → laufendes Verhalten unveraendert bis Master-Schalter an. Diagnostic-Toggles (B1a-2) + lokale Tools (B1b) folgen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+61
-1
@@ -26,8 +26,10 @@ from typing import Optional
|
||||
|
||||
from conversation import Conversation, Turn
|
||||
from memory import Embedder, VectorStore, MemoryPoint
|
||||
from prompts import build_system_prompt, IDENTITY_SEED
|
||||
from prompts import build_system_prompt, IDENTITY_SEED, IDENTITY_ANCHOR
|
||||
from proxy_client import ProxyClient, Message as ProxyMessage
|
||||
import router as router_mod
|
||||
from local_llm import local_llm_chat
|
||||
import skills as skills_mod
|
||||
import triggers as triggers_mod
|
||||
import watcher as watcher_mod
|
||||
@@ -1053,6 +1055,56 @@ class Agent:
|
||||
return reply
|
||||
return None
|
||||
|
||||
# ── Fast-Lane: lokales schnelles LLM (Plan B, B1a) ──
|
||||
#
|
||||
# Zwischen Skill-Fast-Path und Claude-Loop: einfache Plauder-Turns beantwortet
|
||||
# das lokale Qwen (schlanker Prompt, KEINE Tools) in <1 s. Gated ueber
|
||||
# /shared/config/local_llm.json (Default aus → alles Claude wie bisher).
|
||||
# Rueckgabe: fertige Antwort (str) wenn lokal erledigt, sonst None → Claude.
|
||||
|
||||
def _try_local_fast_lane(self, user_message: str,
|
||||
active_project_id: str) -> Optional[str]:
|
||||
cfg = router_mod.load_config()
|
||||
if not router_mod.should_try_local(user_message, cfg):
|
||||
return None
|
||||
|
||||
sys_prompt = router_mod.build_local_system_prompt(IDENTITY_ANCHOR)
|
||||
window = self.conversation.window(project_id=active_project_id)
|
||||
messages = [{"role": "system", "content": sys_prompt}]
|
||||
messages += [{"role": t.role, "content": t.content} for t in window]
|
||||
|
||||
res = local_llm_chat(messages, max_tokens=400, temperature=0.5)
|
||||
local_only = bool(cfg.get("localOnly"))
|
||||
|
||||
if not res.get("ok"):
|
||||
logger.info("[router] lokal fehlgeschlagen (%s) — %s",
|
||||
res.get("error"), "kein Fallback (localOnly)" if local_only else "→ Claude")
|
||||
if local_only:
|
||||
# Eval-Modus: KEIN Claude. Ehrliche Fehlermeldung statt Stille.
|
||||
return f"[Lokales LLM nicht erreichbar: {res.get('error', 'unbekannt')}]"
|
||||
return None
|
||||
|
||||
content = (res.get("content") or "").strip()
|
||||
|
||||
if local_only:
|
||||
# Erzwungen lokal: Escalation-Marker ignorieren, Antwort so nehmen.
|
||||
content = content.replace(router_mod.ESCALATE_MARKER, "").strip()
|
||||
if not content:
|
||||
return "[Lokales LLM lieferte keine Antwort.]"
|
||||
logger.info("[router] lokal (localOnly) %sms", res.get("elapsedMs"))
|
||||
self.conversation.add("assistant", content, project_id=active_project_id)
|
||||
return content
|
||||
|
||||
# Normalbetrieb: leere Antwort oder Escalation-Marker → Claude.
|
||||
if not content or router_mod.ESCALATE_MARKER in content:
|
||||
logger.info("[router] lokal eskaliert → Claude")
|
||||
return None
|
||||
|
||||
logger.info("[router] lokal beantwortet in %sms (%d Zeichen)",
|
||||
res.get("elapsedMs"), len(content))
|
||||
self.conversation.add("assistant", content, project_id=active_project_id)
|
||||
return content
|
||||
|
||||
# ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ──
|
||||
|
||||
MAX_TOOL_ITERATIONS = 8 # Schutz vor Endlos-Loops
|
||||
@@ -1101,6 +1153,14 @@ class Agent:
|
||||
if active_project_id:
|
||||
projects_mod.touch_project(active_project_id)
|
||||
|
||||
# Fast-Lane: lokales schnelles LLM (Plan B, B1a). Gated ueber
|
||||
# /shared/config/local_llm.json (Default aus → alles laeuft wie bisher
|
||||
# ueber Claude). Erledigt es den Turn: fertige Antwort zurueck, der
|
||||
# teure Claude-Aufbau + Tool-Loop wird uebersprungen. Sonst None → Claude.
|
||||
local_reply = self._try_local_fast_lane(user_message, active_project_id)
|
||||
if local_reply is not None:
|
||||
return local_reply
|
||||
|
||||
# 2. Hot Memory (alle pinned Punkte)
|
||||
hot = self.store.list_pinned()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user