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:
@@ -71,7 +71,7 @@ services:
|
||||
# serialised under load and pushed TTS to 7-8s; GPU does ~0.3s/sentence.
|
||||
MELO_DEVICE: ${MELO_DEVICE:-cuda}
|
||||
# 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
|
||||
# hardware (the planner adds a full model round-trip per turn).
|
||||
PLANNER_ENABLED: ${PLANNER_ENABLED:-0}
|
||||
|
||||
@@ -825,6 +825,70 @@ def _build_enrichment_context_hint(cfg, recent_messages: list) -> Optional[str]:
|
||||
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],
|
||||
text: str, dialogue_memory: "DialogueMemory",
|
||||
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
|
||||
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
|
||||
recent_messages = []
|
||||
is_new_conversation = True
|
||||
|
||||
Reference in New Issue
Block a user