From 49061d30f08a5a9c78a52f979d129c1a3689ac20 Mon Sep 17 00:00:00 2001 From: javis-bot Date: Mon, 15 Jun 2026 00:58:53 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20deterministic=20browser=20navigate=20for?= =?UTF-8?q?=20'go=20back=20to=20'=20(=EA=B5=AC=EA=B8=80=EB=A1=9C=20?= =?UTF-8?q?=EB=8F=8C=EC=95=84=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 7B narrated '구글 메인으로 돌아갑니다' without acting, so the screen stayed on Naver. Split site intent into SEARCH vs NAV: nav words (돌아가/이동/열어/메인/go back) now drive controlBrowser.navigate to the site homepage directly, search words run controlBrowser.search — both deterministically, no LLM. --- src/jarvis/reply/engine.py | 60 +++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/jarvis/reply/engine.py b/src/jarvis/reply/engine.py index 39f4620..db576b9 100644 --- a/src/jarvis/reply/engine.py +++ b/src/jarvis/reply/engine.py @@ -833,25 +833,30 @@ _SITE_TOKEN_MAP = { "다음": "daum", "daum": "daum", "빙": "bing", "bing": "bing", } -# Search / open intent words (Korean deployment + English). Kept explicit because -# this is a DETERMINISTIC fast-path — the small chat model can't be trusted to -# emit the controlBrowser call reliably, so when the user names a site AND -# expresses a search/open intent we run it directly, no LLM judgement. -_SEARCH_INTENT_WORDS = ( - "검색해줘", "검색해", "검색", "찾아줘", "찾아봐", "찾아", "열어줘", "열어", - "들어가줘", "들어가", "띄워줘", "띄워", "보여줘", - "search for", "search", "look up", "find", "open", "go to", "navigate", +# Site homepages for the navigate (go-to / go-back) intent. +_SITE_HOME = { + "naver": "naver.com", "google": "google.com", "daum": "daum.net", + "youtube": "youtube.com", "bing": "bing.com", +} +# SEARCH intent (run a query on the site) vs NAV intent (just open / go back to +# the site). Explicit word lists because this is a DETERMINISTIC fast-path — the +# chat model narrates ("돌아갑니다") without emitting the controlBrowser call, so +# we act directly. "돌아가" (go back) is NAV, "검색" is SEARCH. +_SEARCH_WORDS = ("검색", "찾아", "search", "look up", "find") +_NAV_WORDS = ( + "돌아가", "돌아와", "이동", "가줘", "가자", "열어", "들어가", "띄워", "보여", + "메인", "홈페이지", "홈으로", "back to", "go back", "go to", "open", "navigate", +) +_ALL_INTENT_WORDS = _SEARCH_WORDS + _NAV_WORDS + ( + "검색해줘", "검색해", "찾아줘", "찾아봐", "열어줘", "들어가줘", "띄워줘", "보여줘", ) def _maybe_deterministic_site_search(text: str, db, cfg, language) -> Optional[str]: - """When broadcasting AND the user names a site AND asks to search/open it, - run controlBrowser.search directly so the result actually appears on screen. - - The 3B chat model reliably narrates ("검색하겠습니다") instead of emitting the - controlBrowser tool call, so site-specified search is executed - deterministically here rather than left to the model. Fail-open: any problem - returns None and the normal reply flow continues. + """When broadcasting AND the user names a site AND asks to search or open/go + to it, drive the on-screen browser directly (search or navigate) so it + actually happens — the chat model only narrates ("돌아갑니다") without acting. + Fail-open: any problem returns None and the normal reply flow continues. """ try: from . import turn_state @@ -865,27 +870,34 @@ def _maybe_deterministic_site_search(text: str, db, cfg, language) -> Optional[s if _t in low: site, tok = _key, _t break - if not site or not any(w in low for w in _SEARCH_INTENT_WORDS): + has_search = any(w in low for w in _SEARCH_WORDS) + has_nav = any(w in low for w in _NAV_WORDS) + if not site or not (has_search or has_nav): return None import re - q = re.sub(re.escape(tok) + r"(에서|에다가|에다|에|로|를|을)?", " ", text, flags=re.IGNORECASE) - for w in sorted(_SEARCH_INTENT_WORDS, key=len, reverse=True): + q = re.sub(re.escape(tok) + r"(에서|에다가|에다|에|로|를|을|으로)?", " ", text, flags=re.IGNORECASE) + for w in sorted(_ALL_INTENT_WORDS, key=len, reverse=True): q = re.sub(re.escape(w), " ", q, flags=re.IGNORECASE) q = re.sub(r"\s+", " ", q).strip(" .,!?。") - if not q: - q = text + from ..tools.registry import run_tool_with_retries + if has_search and len(q) >= 2: + args = {"action": "search", "site": site, "query": q} + else: + # NAV (go back / open) — go to the site's homepage. + args = {"action": "navigate", "url": _SITE_HOME.get(site, site)} res = run_tool_with_retries( - db=db, cfg=cfg, tool_name="controlBrowser", - tool_args={"action": "search", "site": site, "query": q}, + db=db, cfg=cfg, tool_name="controlBrowser", tool_args=args, system_prompt="", original_prompt="", redacted_text=redact(text), max_retries=1, language=language, ) if res and getattr(res, "success", False): - debug_log(f"deterministic site search executed: {site} '{q}'", "tools") + debug_log(f"deterministic browser: {args}", "tools") + if args["action"] == "navigate": + return res.reply_text or f"{site} 메인 페이지로 이동했습니다." return res.reply_text or f"{site}에서 '{q}'를 검색해 화면에 띄웠습니다." except Exception as e: # noqa: BLE001 - debug_log(f"deterministic site search failed (fail-open): {e}", "tools") + debug_log(f"deterministic browser failed (fail-open): {e}", "tools") return None