Governor Phase 0 fertig + Phase 2: Hart-Deckel default + Sprach-Signal an Lucy
Phase 0 abgeschlossen: - Hart-Deckel jetzt Default AN (Soft+5000), da der weiche Schnitt allein bei Weiterarbeit ueber die Grenze eine Fassade erzeugt (P0-Befund). GOV_HARD_CEILING=0 schaltet ihn aus. gov-ctl reicht Arg 2 nur bei Bedarf durch. - README: Empfehlung/Doku auf Default-an aktualisiert, Commit-Msg-Restpunkt notiert (--no-auto-commits als saubere Option). Phase 2 (Voice-Hook): beim Feuern (soft/hard) POSTet der Governor best-effort + gedrosselt (Default 300 s) eine Meldung an Lucys vorhandene Announce-Pipeline (POST :9001/api/voice/announce, source=governor, priority=normal). Lucy pollt, dedupliziert und spricht sie via lokales TTS -- KEIN Lucy-Code noetig. E2E bewiesen: Feuern -> ANNOUNCE status=200 -> Eintrag id 394 source=governor in der Queue. GOV_ANNOUNCE_URL="" schaltet es ab. announce-test.sh beigelegt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,11 +21,14 @@ Konfiguration per Umgebungsvariablen (alle optional):
|
||||
GOV_HOST Listen-Adresse (Default 0.0.0.0)
|
||||
GOV_UPSTREAM Modell-Endpoint (Default http://127.0.0.1:8080)
|
||||
GOV_THRESHOLD Soft-Schwelle fuer den Einschub (Default 25000)
|
||||
GOV_HARD_CEILING Hart-Deckel; 0 = aus (Default 0)
|
||||
GOV_HARD_CEILING Hart-Deckel; 0 = aus (Default: Soft+5000, AN)
|
||||
GOV_CHARS_PER_TOKEN Heuristik Zeichen->Token (Default 3.5, kalibriert)
|
||||
GOV_LOG Logdatei (zusaetzlich zu stdout) (Default ./governor.log)
|
||||
GOV_DIRECTIVE Text des Soft-Einschubs (sonst Default unten)
|
||||
GOV_HARDSTOP_MSG Text der Hart-Stopp-Antwort (sonst Default unten)
|
||||
GOV_ANNOUNCE_URL Lucy-Sprach-Signal-Endpunkt; "" = aus (Default :9001/api/voice/announce)
|
||||
GOV_ANNOUNCE_THROTTLE Sekunden zwischen Signalen (Default 300)
|
||||
GOV_ANNOUNCE_TEXT Text des Sprach-Signals (sonst Default)
|
||||
"""
|
||||
|
||||
import http.client
|
||||
@@ -43,7 +46,11 @@ PORT = int(os.environ.get("GOV_PORT", "8100"))
|
||||
HOST = os.environ.get("GOV_HOST", "0.0.0.0")
|
||||
UPSTREAM = os.environ.get("GOV_UPSTREAM", "http://127.0.0.1:8080")
|
||||
THRESHOLD = int(os.environ.get("GOV_THRESHOLD", "25000"))
|
||||
HARD_CEILING = int(os.environ.get("GOV_HARD_CEILING", "0")) # 0 = deaktiviert
|
||||
# Hart-Deckel: Default AN (Soft + 5000 = ein Finalisier-Zug Luft), weil der weiche
|
||||
# Schnitt allein bei Weiterarbeit ueber die Grenze eine Fassade erzeugt (Befund P0).
|
||||
# Explizit setzbar; GOV_HARD_CEILING=0 schaltet ihn aus.
|
||||
_hard_env = os.environ.get("GOV_HARD_CEILING")
|
||||
HARD_CEILING = (THRESHOLD + 5000) if _hard_env is None else int(_hard_env)
|
||||
CHARS_PER_TOKEN = float(os.environ.get("GOV_CHARS_PER_TOKEN", "3.5"))
|
||||
LOG_PATH = os.environ.get("GOV_LOG", os.path.join(os.getcwd(), "governor.log"))
|
||||
|
||||
@@ -69,6 +76,18 @@ DEFAULT_HARDSTOP = (
|
||||
)
|
||||
HARDSTOP_MSG = os.environ.get("GOV_HARDSTOP_MSG", DEFAULT_HARDSTOP)
|
||||
|
||||
# Sprach-Signal an Lucy (Phase 2): beim Feuern POSTet der Governor eine Meldung an die
|
||||
# vorhandene MC2-Announce-Pipeline (:9001). Lucy pollt sie ohnehin, dedupliziert und
|
||||
# spricht sie (gated durch ihren "Box-Meldungen laut"-Schalter). Best-effort, gedrosselt
|
||||
# gegen die Pro-Runde-Feuerung. GOV_ANNOUNCE_URL="" schaltet das Signal ab.
|
||||
ANNOUNCE_URL = os.environ.get("GOV_ANNOUNCE_URL", "http://127.0.0.1:9001/api/voice/announce")
|
||||
ANNOUNCE_THROTTLE = float(os.environ.get("GOV_ANNOUNCE_THROTTLE", "300")) # Sekunden
|
||||
DEFAULT_ANNOUNCE = (
|
||||
"Commander, die Coding-Sitzung wird voll — ungefähr {est} Tokens. Ich sichere den "
|
||||
"Stand im Savepoint; am besten fangen wir gleich frisch an."
|
||||
)
|
||||
ANNOUNCE_TEXT = os.environ.get("GOV_ANNOUNCE_TEXT", DEFAULT_ANNOUNCE)
|
||||
|
||||
up = urlparse(UPSTREAM)
|
||||
UP_HOST = up.hostname or "127.0.0.1"
|
||||
UP_PORT = up.port or 80
|
||||
@@ -80,6 +99,9 @@ HOP_BY_HOP = {
|
||||
|
||||
_log_lock = threading.Lock()
|
||||
_PROMPT_TOKENS_RE = re.compile(r'"prompt_tokens"\s*:\s*(\d+)')
|
||||
_announce_lock = threading.Lock()
|
||||
_last_announce = 0.0 # Zeitstempel der letzten Meldung (Drossel)
|
||||
_an = urlparse(ANNOUNCE_URL) if ANNOUNCE_URL else None
|
||||
|
||||
|
||||
def log(line: str) -> None:
|
||||
@@ -115,6 +137,40 @@ def estimate_tokens(messages) -> int:
|
||||
return int(chars / CHARS_PER_TOKEN)
|
||||
|
||||
|
||||
def _post_announce(est) -> None:
|
||||
"""POSTet die Meldung an die MC2-Announce-Pipeline. Laeuft im Hintergrund-Thread."""
|
||||
try:
|
||||
text = (ANNOUNCE_TEXT.replace("{est}", str(est))
|
||||
.replace("{threshold}", str(THRESHOLD)))
|
||||
body = json.dumps({"text": text, "subject": "[Governor]",
|
||||
"source": "governor", "priority": "normal"}).encode("utf-8")
|
||||
conn = http.client.HTTPConnection(_an.hostname or "127.0.0.1",
|
||||
_an.port or 80, timeout=4)
|
||||
conn.request("POST", _an.path or "/api/voice/announce", body=body,
|
||||
headers={"Content-Type": "application/json",
|
||||
"Content-Length": str(len(body))})
|
||||
resp = conn.getresponse()
|
||||
resp.read()
|
||||
conn.close()
|
||||
log(f"ANNOUNCE -> Lucy status={resp.status} est={est}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log(f"ANNOUNCE fehlgeschlagen: {exc!r}")
|
||||
|
||||
|
||||
def maybe_announce(est) -> None:
|
||||
"""Sprach-Signal an Lucy ausloesen — gedrosselt, damit die Pro-Runde-Feuerung
|
||||
nicht spammt (eine Aeusserung je Ueberschreitungs-Episode). Best-effort."""
|
||||
if not _an:
|
||||
return
|
||||
global _last_announce
|
||||
now = time.time()
|
||||
with _announce_lock:
|
||||
if now - _last_announce < ANNOUNCE_THROTTLE:
|
||||
return
|
||||
_last_announce = now
|
||||
threading.Thread(target=_post_announce, args=(est,), daemon=True).start()
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
server_version = "Governor/0.2"
|
||||
@@ -161,6 +217,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
model = ""
|
||||
if is_chat and body:
|
||||
action, body, est, streaming, model = self._decide(body)
|
||||
if action in ("soft", "hard"):
|
||||
maybe_announce(est) # Sprach-Signal an Lucy (gedrosselt)
|
||||
|
||||
# HARTER STOPP: selbst antworten, Upstream nie fragen.
|
||||
if action == "hard":
|
||||
|
||||
Reference in New Issue
Block a user