feat: couple broadcast to voice + voice-controlled broadcast toggle
Completes the STREAM_BROWSER=true behaviour:
- handleJoin auto-starts the broadcast on voice join and wires the session to
the guild streamer; each turn reports the live state to the brain so search
routes Chrome (live) vs Gemini (off).
- New setBroadcast tool lets the user toggle the broadcast by voice ("방송
켜줘/꺼줘") via the LLM (no hardcoded phrases); it refuses when
STREAM_BROWSER=false. The directive flows brain -> bridge (broadcast_action)
-> bot streamer.start/stop, guarded by isActive() so it's idempotent.
- Per-turn IPC uses a thread-local (reply/turn_state.py) instead of threading
params through the whole engine chain: bridge sets broadcasting in, tool
records the directive out; Tool.execute exposes broadcasting on ToolContext.
Bot typecheck clean; brain covered by tests/test_set_broadcast.py (+ existing
routing tests). Specs + docs updated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
75
src/jarvis/tools/builtin/set_broadcast.py
Normal file
75
src/jarvis/tools/builtin/set_broadcast.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Start or stop the live screen-share broadcast (Go-Live) on request.
|
||||
|
||||
The brain cannot drive the Discord broadcast itself — the bot owns it. This
|
||||
tool records a directive ("start"/"stop") in the per-turn state; the bridge
|
||||
returns it to the bot, which performs the actual start/stop. Only meaningful in
|
||||
screen-share mode (``STREAM_BROWSER`` true); when broadcasting is disabled the
|
||||
tool refuses so the user is told it cannot be turned on.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from ..base import Tool, ToolContext
|
||||
from ..types import ToolExecutionResult
|
||||
from ...debug import debug_log
|
||||
|
||||
|
||||
class SetBroadcastTool(Tool):
|
||||
"""Turn the live screen-share broadcast on or off."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "setBroadcast"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return (
|
||||
"Start or stop the live screen-share broadcast (Go-Live / showing your "
|
||||
"screen on the call). Use when the user asks to turn the broadcast or "
|
||||
"screen on or off (show/hide your screen, go live, stop streaming). "
|
||||
"Only available in screen-share mode."
|
||||
)
|
||||
|
||||
@property
|
||||
def inputSchema(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["start", "stop"],
|
||||
"description": "'start' to begin the broadcast, 'stop' to end it.",
|
||||
}
|
||||
},
|
||||
"required": ["action"],
|
||||
}
|
||||
|
||||
def run(self, args: Optional[Dict[str, Any]], context: ToolContext) -> ToolExecutionResult:
|
||||
cfg = context.cfg
|
||||
if not getattr(cfg, "stream_browser", True):
|
||||
# Broadcast capability is disabled (STREAM_BROWSER=false): refuse so
|
||||
# the user is clearly told it can't be turned on in this mode.
|
||||
return ToolExecutionResult(
|
||||
success=False,
|
||||
reply_text="방송(화면 공유) 기능이 꺼져 있어 켤 수 없습니다 (STREAM_BROWSER=false).",
|
||||
)
|
||||
|
||||
action = ""
|
||||
if args and isinstance(args, dict):
|
||||
action = str(args.get("action", "")).strip().lower()
|
||||
if action not in ("start", "stop"):
|
||||
return ToolExecutionResult(
|
||||
success=False,
|
||||
reply_text="Specify action='start' or action='stop'.",
|
||||
)
|
||||
|
||||
# Record the directive for the bridge to return to the bot.
|
||||
from ...reply.turn_state import request_broadcast
|
||||
request_broadcast(action)
|
||||
debug_log(f" 📡 setBroadcast requested: {action}", "stream")
|
||||
return ToolExecutionResult(
|
||||
success=True,
|
||||
reply_text="방송을 시작합니다." if action == "start" else "방송을 종료합니다.",
|
||||
)
|
||||
30
src/jarvis/tools/builtin/set_broadcast.spec.md
Normal file
30
src/jarvis/tools/builtin/set_broadcast.spec.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# setBroadcast tool
|
||||
|
||||
Lets the user turn the live screen-share broadcast (Go-Live) on or off by voice
|
||||
or text, e.g. "방송 켜줘 / 화면 보여줘" or "방송 꺼줘". The LLM picks the intent
|
||||
and calls the tool (no hardcoded phrase matching), so it works in any language.
|
||||
|
||||
## Contract
|
||||
|
||||
- Input: `{ "action": "start" | "stop" }`.
|
||||
- The brain cannot drive the Discord broadcast itself (the bot owns it). The
|
||||
tool records the directive in request-scoped state
|
||||
(`jarvis/reply/turn_state.request_broadcast`); `bridge/server.py` returns it as
|
||||
`broadcast_action` in the `/converse` (and `/text`) response, and the bot
|
||||
(`voice.ts` -> `index.ts` `onBroadcastAction`) calls `streamer.start/stop`.
|
||||
- Mode gate: when `cfg.stream_browser` is false (`STREAM_BROWSER=false`)
|
||||
broadcasting is disabled — the tool refuses with a spoken explanation and
|
||||
records no directive. This is the same capability flag that makes `/stream`
|
||||
refuse and forces real-time search to Gemini.
|
||||
|
||||
## Principles
|
||||
|
||||
- Tool returns raw intent/result; it performs no broadcast itself and makes no
|
||||
LLM call.
|
||||
- Idempotent at the bot edge: `start` is a no-op if already live, `stop` a no-op
|
||||
if already off (`streamer.isActive()` guard).
|
||||
- Fail-safe: a missing/invalid action records nothing; the bot only acts on an
|
||||
explicit `"start"`/`"stop"` directive.
|
||||
|
||||
See `docs/stream_browser_modes.md` for how this fits the broadcast-coupled
|
||||
search routing.
|
||||
Reference in New Issue
Block a user