fix: deterministic browser navigate for 'go back to <site>' (구글로 돌아가)

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.
This commit is contained in:
javis-bot
2026-06-15 00:58:53 +09:00
parent c522e1b285
commit 49061d30f0

View File

@@ -833,25 +833,30 @@ _SITE_TOKEN_MAP = {
"다음": "daum", "daum": "daum", "다음": "daum", "daum": "daum",
"": "bing", "bing": "bing", "": "bing", "bing": "bing",
} }
# Search / open intent words (Korean deployment + English). Kept explicit because # Site homepages for the navigate (go-to / go-back) intent.
# this is a DETERMINISTIC fast-path — the small chat model can't be trusted to _SITE_HOME = {
# emit the controlBrowser call reliably, so when the user names a site AND "naver": "naver.com", "google": "google.com", "daum": "daum.net",
# expresses a search/open intent we run it directly, no LLM judgement. "youtube": "youtube.com", "bing": "bing.com",
_SEARCH_INTENT_WORDS = ( }
"검색해줘", "검색해", "검색", "찾아줘", "찾아봐", "찾아", "열어줘", "열어", # 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
"search for", "search", "look up", "find", "open", "go to", "navigate", # 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]: 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, """When broadcasting AND the user names a site AND asks to search or open/go
run controlBrowser.search directly so the result actually appears on screen. to it, drive the on-screen browser directly (search or navigate) so it
actually happens — the chat model only narrates ("돌아갑니다") without acting.
The 3B chat model reliably narrates ("검색하겠습니다") instead of emitting the Fail-open: any problem returns None and the normal reply flow continues.
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.
""" """
try: try:
from . import turn_state from . import turn_state
@@ -865,27 +870,34 @@ def _maybe_deterministic_site_search(text: str, db, cfg, language) -> Optional[s
if _t in low: if _t in low:
site, tok = _key, _t site, tok = _key, _t
break 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 return None
import re import re
q = re.sub(re.escape(tok) + r"(에서|에다가|에다|에|로|를|을)?", " ", text, flags=re.IGNORECASE) q = re.sub(re.escape(tok) + r"(에서|에다가|에다|에|로|를|을|으로)?", " ", text, flags=re.IGNORECASE)
for w in sorted(_SEARCH_INTENT_WORDS, key=len, reverse=True): for w in sorted(_ALL_INTENT_WORDS, key=len, reverse=True):
q = re.sub(re.escape(w), " ", q, flags=re.IGNORECASE) q = re.sub(re.escape(w), " ", q, flags=re.IGNORECASE)
q = re.sub(r"\s+", " ", q).strip(" .,!?。") q = re.sub(r"\s+", " ", q).strip(" .,!?。")
if not q:
q = text
from ..tools.registry import run_tool_with_retries 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( res = run_tool_with_retries(
db=db, cfg=cfg, tool_name="controlBrowser", db=db, cfg=cfg, tool_name="controlBrowser", tool_args=args,
tool_args={"action": "search", "site": site, "query": q},
system_prompt="", original_prompt="", redacted_text=redact(text), system_prompt="", original_prompt="", redacted_text=redact(text),
max_retries=1, language=language, max_retries=1, language=language,
) )
if res and getattr(res, "success", False): 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}'를 검색해 화면에 띄웠습니다." return res.reply_text or f"{site}에서 '{q}'를 검색해 화면에 띄웠습니다."
except Exception as e: # noqa: BLE001 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 return None