feat(brain): wire STREAM_BROWSER real-time modes into the reply engine (browser + Gemini)
Completes the two info modes in the Python brain: - config.py: read STREAM_BROWSER / GEMINI_API_KEY / GEMINI_MODEL from env into Settings (stream_browser, gemini_api_key, gemini_model). Verified load_settings reads both modes. - realtime_search.py: two fail-open backends returning the same fenced UNTRUSTED-WEB-EXTRACT envelope: browser_search() shells the Node CDP helper to drive the on-screen Chrome (visible on the broadcast); gemini_search() calls the Gemini REST API with google_search grounding. - web_search.run(): routes by mode before the DDG cascade (true->browser, false->Gemini), falling through to DDG/Brave/Wikipedia on any miss. - browse_and_play tool: plays a YouTube video on the shared screen (true mode only); registered in the tool registry. - specs + docs/llm_contexts.md updated (new Gemini LLM context); CLAUDE.md spec registry updated. Verified live against the running Chrome: true-mode webSearch returned real Google results for "오늘 서울 날씨", browseAndPlay played the IU 밤편지 MV, and false-mode degrades gracefully on a bad/absent key. A valid GEMINI_API_KEY is still needed to confirm the real Gemini grounding output. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
83
src/jarvis/tools/builtin/browse_and_play.py
Normal file
83
src/jarvis/tools/builtin/browse_and_play.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Play a YouTube video on the shared screen (browser/screen-share mode).
|
||||
|
||||
Only meaningful when ``STREAM_BROWSER`` is true: it drives the on-screen Chrome
|
||||
(via the Node CDP helper) to search YouTube and play the first result, which is
|
||||
visible on the Go-Live broadcast. In voice-only mode (false) there is nothing to
|
||||
show, so the tool reports that and does nothing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from ..base import Tool, ToolContext
|
||||
from ..types import ToolExecutionResult
|
||||
from ...debug import debug_log
|
||||
from .realtime_search import _NODE_SCRIPT
|
||||
|
||||
|
||||
class BrowseAndPlayTool(Tool):
|
||||
"""Play a YouTube video on the shared screen."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "browseAndPlay"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return (
|
||||
"Play a song / music video / clip on the shared screen by searching YouTube "
|
||||
"and playing the first result. Use when the user asks you to play or watch "
|
||||
"something. Only available in screen-share mode."
|
||||
)
|
||||
|
||||
@property
|
||||
def inputSchema(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "What to play, e.g. 'IU Good Day' or 'lofi hip hop'.",
|
||||
}
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
|
||||
def run(self, args: Optional[Dict[str, Any]], context: ToolContext) -> ToolExecutionResult:
|
||||
cfg = context.cfg
|
||||
if not getattr(cfg, "stream_browser", True):
|
||||
return ToolExecutionResult(
|
||||
success=False,
|
||||
reply_text="화면 공유 모드(STREAM_BROWSER=true)에서만 영상을 재생할 수 있습니다.",
|
||||
)
|
||||
query = ""
|
||||
if args and isinstance(args, dict):
|
||||
query = str(args.get("query", "")).strip()
|
||||
if not query:
|
||||
return ToolExecutionResult(success=False, reply_text="재생할 내용을 알려주세요.")
|
||||
if not _NODE_SCRIPT.exists():
|
||||
return ToolExecutionResult(success=False, reply_text="브라우저 재생 도구를 찾을 수 없습니다.")
|
||||
|
||||
context.user_print(f"▶️ 화면에서 '{query}' 재생 중…")
|
||||
debug_log(f" ▶️ browseAndPlay '{query}'", "tools")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["node", str(_NODE_SCRIPT), query, "youtube"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=40,
|
||||
env={**os.environ, "CDP_PORT": os.environ.get("CDP_PORT", "9222")},
|
||||
)
|
||||
data = json.loads((proc.stdout or "").strip() or "{}")
|
||||
except Exception as e:
|
||||
return ToolExecutionResult(success=False, reply_text=f"재생에 실패했습니다: {e}")
|
||||
if not data.get("ok"):
|
||||
return ToolExecutionResult(
|
||||
success=False, reply_text=f"재생에 실패했습니다: {data.get('error', 'unknown')}"
|
||||
)
|
||||
title = data.get("title") or query
|
||||
return ToolExecutionResult(success=True, reply_text=f"화면에서 '{title}' 재생을 시작했습니다.")
|
||||
Reference in New Issue
Block a user