diff --git a/bot/scripts/stream-test/chrome-control.mjs b/bot/scripts/stream-test/chrome-control.mjs index e6031d5..5327ec2 100644 --- a/bot/scripts/stream-test/chrome-control.mjs +++ b/bot/scripts/stream-test/chrome-control.mjs @@ -97,6 +97,32 @@ try { break; } + case 'search': { + // One-shot "search on a site": build the engine's results URL so a small + // model doesn't have to chain navigate->type->enter. Visible on screen. + const q = String(cmd.query || '').trim(); + if (!q) throw new Error('search: no query'); + const site = String(cmd.site || 'google').toLowerCase(); + const engines = { + naver: 'https://search.naver.com/search.naver?query=', + google: 'https://www.google.com/search?q=', + daum: 'https://search.daum.net/search?q=', + youtube: 'https://www.youtube.com/results?search_query=', + bing: 'https://www.bing.com/search?q=', + }; + const base = engines[site] || engines.google; + const target = base + encodeURIComponent(q); + await front(page); + if (HAS_XDOTOOL && cmd.human !== false) { + try { await human.navigateOmnibox(target); await page.waitForLoadState('domcontentloaded').catch(() => {}); } + catch { await page.goto(target, { waitUntil: 'domcontentloaded' }); } + } else { + await page.goto(target, { waitUntil: 'domcontentloaded' }); + } + out({ ok: true, site: engines[site] ? site : 'google', query: q, url: page.url(), title: await page.title().catch(() => '') }); + break; + } + case 'back': await front(page); await page.goBack({ waitUntil: 'domcontentloaded' }).catch(() => {}); out({ ok: true, url: page.url() }); break; case 'forward': await front(page); await page.goForward({ waitUntil: 'domcontentloaded' }).catch(() => {}); out({ ok: true, url: page.url() }); break; case 'refresh': await front(page); await reload(); out({ ok: true, url: page.url() }); break; diff --git a/src/jarvis/system_prompt.py b/src/jarvis/system_prompt.py index a6d10f6..5565f2a 100644 --- a/src/jarvis/system_prompt.py +++ b/src/jarvis/system_prompt.py @@ -36,6 +36,8 @@ _SYSTEM_PROMPT_TEMPLATE: str = ( "butler clichés, and never address the user as 'sir', 'madam', 'my liege', or similar. " "Never stack multiple jokes in one reply. " "Be concise, conversational, and actionable. " + "This is a spoken voice assistant: answer in ONE short sentence whenever possible " + "(two at the very most). No lists, no preamble, no 'is there anything else' offers. " "Never claim you performed an action — opening a website, navigating the browser, " "playing or showing something on screen, changing a setting, sending a message — unless a " "tool actually did it this turn and reported success. If you have no tool for what was asked, " diff --git a/src/jarvis/tools/builtin/control_browser.py b/src/jarvis/tools/builtin/control_browser.py index 5e244f9..9c13498 100644 --- a/src/jarvis/tools/builtin/control_browser.py +++ b/src/jarvis/tools/builtin/control_browser.py @@ -41,12 +41,14 @@ class ControlBrowserTool(Tool): @property def description(self) -> str: return ( - "Operate the on-screen Chrome shown on the broadcast like a person: open a " - "website/URL, list/open/close/switch tabs, close popups, click an element, " - "type text, scroll, or screenshot. Use whenever the user asks to go to a site " - "(e.g. 'open Naver'), check what's on screen, or interact with the page. " - "Only available in screen-share mode. Do NOT claim you did any of this unless " - "this tool returns success." + "Drive the on-screen Chrome that is shown on the broadcast, like a person would. " + "Use this (NOT webSearch) whenever the user wants something done or shown IN the " + "browser on screen: open a website or URL, search on a specific site (action " + "'search' with site=naver/google/daum/youtube/bing), go back/forward, refresh, " + "manage tabs (list/new/close/switch), close popups, click, type, scroll, or " + "screenshot. webSearch only returns text and shows nothing on screen; this tool " + "actually navigates the visible browser. Only available in screen-share mode. " + "Never claim you did any of this unless this tool returns success." ) @property @@ -57,14 +59,16 @@ class ControlBrowserTool(Tool): "action": { "type": "string", "enum": [ - "status", "listTabs", "navigate", "back", "forward", - "refresh", "newTab", "closeTab", "activateTab", + "status", "listTabs", "navigate", "search", "back", + "forward", "refresh", "newTab", "closeTab", "activateTab", "closePopups", "click", "type", "scroll", "pressKey", "screenshot", ], "description": "What to do in the browser.", }, "url": {"type": "string", "description": "Target URL/site for navigate/newTab (e.g. 'naver.com')."}, + "query": {"type": "string", "description": "Search text for action 'search'."}, + "site": {"type": "string", "description": "Search site for action 'search': naver, google, daum, youtube, bing."}, "index": {"type": "integer", "description": "Tab index for closeTab/activateTab (from listTabs)."}, "selector": {"type": "string", "description": "CSS selector for click/type."}, "text": {"type": "string", "description": "Text to type."}, @@ -129,6 +133,10 @@ class ControlBrowserTool(Tool): def _summarise(action: str, args: Dict[str, Any], data: Dict[str, Any]) -> str: if action == "navigate": return f"브라우저에서 {data.get('url', args.get('url'))} 로 이동했습니다." + if action == "search": + return f"{data.get('site', '')}에서 '{data.get('query', args.get('query'))}'를 검색해 화면에 띄웠습니다." + if action in ("back", "forward", "refresh"): + return f"브라우저: {action} 완료 ({data.get('url', '')})." if action in ("status", "listTabs"): tabs = data.get("tabs", []) lines = "\n".join(f" [{t['index']}] {t.get('title') or t['url']}" for t in tabs) or " (탭 없음)"