gateway: auto-handle unresolved user questions in chat context

This commit is contained in:
Apple
2026-02-20 23:54:52 -08:00
parent 7b5357228f
commit 0a87eadb8d

View File

@@ -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}")