From 0a87eadb8df3826c5abaa2a3a1f49f2385944ab7 Mon Sep 17 00:00:00 2001 From: Apple Date: Fri, 20 Feb 2026 23:54:52 -0800 Subject: [PATCH] gateway: auto-handle unresolved user questions in chat context --- gateway-bot/http_api.py | 73 ++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/gateway-bot/http_api.py b/gateway-bot/http_api.py index acb99d1c..e644470f 100644 --- a/gateway-bot/http_api.py +++ b/gateway-bot/http_api.py @@ -171,23 +171,59 @@ def _extract_unanswered_user_messages( if not isinstance(events, list) or not current_user_id: return [] - pending: List[str] = [] + def _normalize_tokens(raw: str) -> set: + toks = re.findall(r"[a-zA-Zа-яА-ЯіїєґІЇЄҐ0-9]{3,}", (raw or "").lower()) + stop = { + "що", "як", "коли", "де", "хто", "чому", "який", "яка", "яке", "скільки", "чи", + "what", "how", "when", "where", "who", "why", "which", + "and", "for", "the", "this", "that", "with", "from", + } + return {t for t in toks if t not in stop} + + def _looks_like_ack_or_generic(raw: str) -> bool: + t = (raw or "").strip().lower() + if not t: + return True + markers = [ + "привіт", "вітаю", "чим можу допомогти", "ок", "добре", "дякую", "готово", + "hello", "hi", "how can i help", "thanks", "okay", "done", + ] + return any(m in t for m in markers) and len(t) < 180 + + def _assistant_resolves_question(question_text: str, assistant_text: str) -> bool: + if _looks_like_ack_or_generic(assistant_text): + return False + q_tokens = _normalize_tokens(question_text) + a_tokens = _normalize_tokens(assistant_text) + if not q_tokens or not a_tokens: + return False + overlap = len(q_tokens.intersection(a_tokens)) + # Require at least partial semantic overlap, otherwise do not auto-close. + return overlap >= 2 or (overlap >= 1 and len(q_tokens) <= 3) + + pending: List[Dict[str, str]] = [] for ev in events: role = str(ev.get("role") or ev.get("type") or "").lower() text = str(ev.get("body_text") or "").strip() if not text: continue - if role == "user" and str(ev.get("user_id") or "") == current_user_id: - pending.append(text) + if role == "user" and str(ev.get("user_id") or "") == current_user_id and _is_question_like(text): + pending.append({"text": text}) continue if role in ("assistant", "agent") and pending: - # Assume latest agent reply resolved the oldest pending user question. - pending.pop(0) + # Resolve only matching question; do not auto-close all pending items. + resolved_idx = None + for idx, item in enumerate(pending): + if _assistant_resolves_question(item["text"], text): + resolved_idx = idx + break + if resolved_idx is not None: + pending.pop(resolved_idx) # Keep the latest unresolved items only. if len(pending) > max_items: pending = pending[-max_items:] - return pending + return [p["text"] for p in pending] def _is_question_like(text: str) -> bool: @@ -2831,14 +2867,20 @@ async def handle_telegram_webhook( current_user_id=f"tg:{user_id}", max_items=3, ) + unresolved_non_current: List[str] = [] unresolved_block = "" if unresolved_questions: # Do not duplicate current prompt if it matches one pending message. - filtered = [q for q in unresolved_questions if q.strip() != (text or "").strip()] - if filtered: - unresolved_block = "[Невідповідані питання цього користувача]\n" + "\n".join( - f"- {q}" for q in filtered - ) + "\n\n" + unresolved_non_current = [q for q in unresolved_questions if q.strip() != (text or "").strip()] + if unresolved_non_current: + unresolved_block = ( + "[КРИТИЧНО: є невідповідані питання цього користувача. " + "Спочатку коротко відповідай на них, потім на поточне повідомлення. " + "Не змінюй тему і не ігноруй pending-питання.]\n" + "[Невідповідані питання цього користувача]\n" + + "\n".join(f"- {q}" for q in unresolved_non_current) + + "\n\n" + ) if local_history: # Add conversation history to message for better context understanding @@ -2897,6 +2939,8 @@ async def handle_telegram_webhook( "preferred_response_language": preferred_lang, "preferred_response_language_label": preferred_lang_label, "response_style_preference": response_style_pref, + "has_unresolved_questions": bool(unresolved_non_current), + "unresolved_questions_count": len(unresolved_non_current), }, "context": { "agent_name": agent_config.name, @@ -2937,6 +2981,13 @@ async def handle_telegram_webhook( + f"\n\n(Мова відповіді: {preferred_lang_label}.)" + "\n(Не потрібно щоразу представлятися по імені або писати шаблонне: 'чим можу допомогти'.)" ) + + if unresolved_non_current: + router_request["message"] = ( + router_request["message"] + + "\n\n(Пріоритет відповіді: 1) закрий невідповідані питання користувача; " + "2) дай відповідь на поточне повідомлення. Якщо питання пов'язані, дай одну узгоджену відповідь.)" + ) # Send to Router logger.info(f"Sending to Router: agent={agent_config.agent_id}, dao={dao_id}, user=tg:{user_id}")