diff --git a/solution/backend/services/agent/channel.py b/solution/backend/services/agent/channel.py index 28567ed..a2bacaf 100644 --- a/solution/backend/services/agent/channel.py +++ b/solution/backend/services/agent/channel.py @@ -224,6 +224,11 @@ async def handle(utterance: str, channel_user_key: str) -> dict: if user is None: return _say("계정을 찾지 못했어요. 관리자 화면에서 다시 연결해 주세요.") + # ★ 이미 연결된 사람이 코드를 또 보내는 일이 실제로 있었다(2026-09-22). 그대로 두면 + # 6자리가 그냥 발화로 모델에 넘어가 유료 호출 + 대기만 쌓인다 — 여기서 끊는다. + if CODE_PATTERN.fullmatch(utterance.upper()): + return _say("이미 연결되어 있어요. 바로 말씀하시면 됩니다.\n예) 체크인 시간 3시로 바꿔줘") + # ── 확인 이어받기 ──────────────────────────────────────────────────── pending = None if row.pending_tool and row.pending_expires_at and row.pending_expires_at > _now(): diff --git a/solution/backend/services/agent/runtime.py b/solution/backend/services/agent/runtime.py index 1d53f68..ee1e352 100644 --- a/solution/backend/services/agent/runtime.py +++ b/solution/backend/services/agent/runtime.py @@ -71,14 +71,18 @@ async def _context_facts(user: UserInfo, place_id: str, place) -> list[dict]: res = await FactService(FactCRUD(), PlaceCRUD()).list_facts(user, place_id, publishable_only=True) schema = get_schema(PlaceCategory(place.category)) out = [] - for f in (res.facts or [])[:60]: + for f in (res.facts or []): spec = schema.get(f.key) if spec and spec.scope == "place" and (f.value or "").strip(): - out.append({"key": f.key, "label": spec.label, "value": f.value}) - return out + # ★ label 은 싣지 않는다 — 아래 '항목 목록' 에 이미 key↔label 이 있다. + # 같은 표를 두 번 보내면 프롬프트만 커지고 모델이 얻는 것은 없다. + out.append({f.key: f.value}) + # ★ 상한을 둔다. 실측(2026-09-22): 필드 43 + fact 수십 개가 실린 프롬프트가 5초 벽을 + # 넘겼다. 무한정 싣지 않는다 — 대화 한 턴에 필요한 맥락은 그렇게 많지 않다. + return out[:30] -async def _choose(place, fields, facts, site_line, message) -> dict: +async def _choose(place, fields, facts, message) -> dict: """LLM 한 번. 고른 도구 이름과 인자만 받는다.""" active = provider.active() async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client: @@ -90,7 +94,6 @@ async def _choose(place, fields, facts, site_line, message) -> dict: tools=registry.describe(), fields=fields, facts=facts, - site={"요약": site_line}, message=message, ), response_schema=prompt.RESPONSE_SCHEMA, @@ -126,10 +129,11 @@ async def chat(user: UserInfo, place_id: str, message: str, confirm: dict | None fields = registry.fields_of(place) facts = await _context_facts(user, place_id, place) - site_line = await registry.REGISTRY["get_site_status"].run(ctx, {}) + # ★ 사이트 상태는 프롬프트에 싣지 않는다. 그 한 줄 때문에 매 턴 사이트 조회 + 슬러그 계산이 + # 돌았고, 정작 모델이 필요할 때는 `get_site_status` 도구를 부르면 된다. try: - choice = await _choose(place, fields, facts, site_line, message) + choice = await _choose(place, fields, facts, message) except LlmError as ex: LOG.w(f"[agent] 도구 선택 실패: {type(ex).__name__}") raise AgentError("AGENT_CALL_FAILED") from ex diff --git a/solution/backend/services/prompts/agent.py b/solution/backend/services/prompts/agent.py index 4d3f0af..a1bd76b 100644 --- a/solution/backend/services/prompts/agent.py +++ b/solution/backend/services/prompts/agent.py @@ -36,7 +36,7 @@ RESPONSE_SCHEMA = { } -def build_prompt(*, place_name: str, tools: list[dict], fields: list[dict], facts: list[dict], site: dict, message: str) -> str: +def build_prompt(*, place_name: str, tools: list[dict], fields: list[dict], facts: list[dict], message: str) -> str: """사장님 발화 → 도구 하나. ★ 모호하면 실행하지 말고 되물으라고 명시한다. 티오더가 "유사한 메뉴가 2개 이상이면 @@ -55,14 +55,11 @@ def build_prompt(*, place_name: str, tools: list[dict], fields: list[dict], fact 쓸 수 있는 도구: {json.dumps(tools, ensure_ascii=False, indent=1)} -가게 정보에 쓸 수 있는 항목(set_fact 의 key 는 반드시 이 중 하나다): -{json.dumps(fields, ensure_ascii=False)} +가게 정보에 쓸 수 있는 항목 — `key: 이름` (set_fact 의 key 는 반드시 이 중 하나다): +{chr(10).join(f"{f['key']}: {f['label']}" for f in fields)} 지금 저장된 값: {json.dumps(facts, ensure_ascii=False)} -사이트 상태: -{json.dumps(site, ensure_ascii=False)} - 사장님 요청: {message}'''