fix: deterministic on-screen site search + lock replies to Korean

- Site-specified search ("네이버에서 X 검색해줘") now runs controlBrowser.search
  directly in the engine when broadcasting, instead of relying on the 3B model
  to emit the tool call (it kept narrating "검색하겠습니다" without acting).
- Set OUTPUT_LANGUAGE=ko so replies are Korean-only — stops the small model
  leaking CJK/Hanja and English fragments (每, 朗, "feels like") into weather
  and other answers, and keeps them concise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-14 21:07:26 +09:00
parent 37759f2b2c
commit 11a72cb296
2 changed files with 72 additions and 1 deletions

View File

@@ -71,7 +71,7 @@ services:
# serialised under load and pushed TTS to 7-8s; GPU does ~0.3s/sentence. # serialised under load and pushed TTS to 7-8s; GPU does ~0.3s/sentence.
MELO_DEVICE: ${MELO_DEVICE:-cuda} MELO_DEVICE: ${MELO_DEVICE:-cuda}
# Optional single-language lock for replies (empty = user's own language). # Optional single-language lock for replies (empty = user's own language).
OUTPUT_LANGUAGE: ${OUTPUT_LANGUAGE:-} OUTPUT_LANGUAGE: ${OUTPUT_LANGUAGE:-ko}
# Drop the pre-loop planner LLM call to cut voice-reply latency on small # Drop the pre-loop planner LLM call to cut voice-reply latency on small
# hardware (the planner adds a full model round-trip per turn). # hardware (the planner adds a full model round-trip per turn).
PLANNER_ENABLED: ${PLANNER_ENABLED:-0} PLANNER_ENABLED: ${PLANNER_ENABLED:-0}

View File

@@ -825,6 +825,70 @@ def _build_enrichment_context_hint(cfg, recent_messages: list) -> Optional[str]:
return "\n\n".join(parts) if parts else None return "\n\n".join(parts) if parts else None
# Site tokens (proper nouns, not language patterns) → controlBrowser search site.
_SITE_TOKEN_MAP = {
"네이버": "naver", "naver": "naver",
"구글": "google", "google": "google",
"유튜브": "youtube", "유투브": "youtube", "youtube": "youtube",
"다음": "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",
)
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.
"""
try:
from . import turn_state
if not getattr(cfg, "stream_browser", True):
return None
if not turn_state.get_broadcasting():
return None
low = (text or "").lower()
site = tok = None
for _t, _key in _SITE_TOKEN_MAP.items():
if _t in low:
site, tok = _key, _t
break
if not site or not any(w in low for w in _SEARCH_INTENT_WORDS):
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(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
res = run_tool_with_retries(
db=db, cfg=cfg, tool_name="controlBrowser",
tool_args={"action": "search", "site": site, "query": q},
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")
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")
return None
def run_reply_engine(db: "Database", cfg, tts: Optional[Any], def run_reply_engine(db: "Database", cfg, tts: Optional[Any],
text: str, dialogue_memory: "DialogueMemory", text: str, dialogue_memory: "DialogueMemory",
language: Optional[str] = None) -> Optional[str]: language: Optional[str] = None) -> Optional[str]:
@@ -849,6 +913,13 @@ def run_reply_engine(db: "Database", cfg, tts: Optional[Any],
# Step 1: Redact sensitive information # Step 1: Redact sensitive information
redacted = redact(text) redacted = redact(text)
# Step 0.5: Deterministic on-screen site search. If the user named a site and
# asked to search/open it while broadcasting, do it directly — the small chat
# model otherwise just narrates without calling the browser tool.
_site_search_reply = _maybe_deterministic_site_search(text, db, cfg, language)
if _site_search_reply is not None:
return _site_search_reply
# Step 2: Check for recent dialogue context # Step 2: Check for recent dialogue context
recent_messages = [] recent_messages = []
is_new_conversation = True is_new_conversation = True