SilverElixir commited on
Commit
85833be
·
1 Parent(s): 6e8e909

Resolve declined nicknames both ways, cover fixes with tests

Browse files
Files changed (2) hide show
  1. lumen_chat_state.py +10 -3
  2. tests/test_bot_state.py +50 -8
lumen_chat_state.py CHANGED
@@ -484,10 +484,14 @@ def _restore_user_names(raw: Any) -> dict[str, dict[str, str]]:
484
 
485
 
486
  def _resolve_nick(state: dict[str, Any], nick: str) -> str | None:
487
- """Прозвище в показное имя: ищет по username и first_name без регистра."""
 
 
 
 
488
  try:
489
  needle = (nick or "").strip().lstrip("@").lower()
490
- if not needle:
491
  return None
492
  bucket = state.get("user_names") or {}
493
  if not isinstance(bucket, dict):
@@ -497,7 +501,10 @@ def _resolve_nick(state: dict[str, Any], nick: str) -> str | None:
497
  continue
498
  for field in ("username", "first_name"):
499
  val = entry.get(field)
500
- if isinstance(val, str) and needle in val.lower():
 
 
 
501
  return _sender_display(entry.get("username"), entry.get("first_name"))
502
  except Exception:
503
  pass
 
484
 
485
 
486
  def _resolve_nick(state: dict[str, Any], nick: str) -> str | None:
487
+ """Прозвище в показное имя: ищет по username и first_name без регистра.
488
+
489
+ Смотрит в обе стороны: «блин» находит «Блинчик», а «блинчика» тоже
490
+ находит «Блинчик» (склонения в живой речи). Короткие совпадения
491
+ отбрасываем, иначе двухбуквенные огрызки шумят на каждом сообщении."""
492
  try:
493
  needle = (nick or "").strip().lstrip("@").lower()
494
+ if len(needle) < 3:
495
  return None
496
  bucket = state.get("user_names") or {}
497
  if not isinstance(bucket, dict):
 
501
  continue
502
  for field in ("username", "first_name"):
503
  val = entry.get(field)
504
+ if not isinstance(val, str):
505
+ continue
506
+ low = val.lower()
507
+ if needle in low or (len(low) >= 4 and low in needle):
508
  return _sender_display(entry.get("username"), entry.get("first_name"))
509
  except Exception:
510
  pass
tests/test_bot_state.py CHANGED
@@ -3043,14 +3043,17 @@ def test_combined_memory_caps_background_at_100():
3043
  from collections import deque
3044
  state = {"history": [{"role": "user", "content": f"вопрос {i}"} for i in range(60)],
3045
  "ctx": deque(), "recent_media_ids": {}, "user_names": {}}
3046
- for i in range(60):
3047
- message = SimpleNamespace(chat=SimpleNamespace(id=9001),
3048
- from_user=SimpleNamespace(id=7, username="m1st", first_name="M"),
3049
- text=f"фон {i}")
3050
- bot._record_passive_group_context(message, state, f"фон {i}")
3051
- assert len(state["history"]) == 60
3052
- assert len(state["history"]) + len(state["ctx"]) <= bot.MAX_TOTAL_MEMORY_MESSAGES
3053
- assert state["ctx"][0].endswith("фон 20")
 
 
 
3054
 
3055
 
3056
  def test_sender_directory_resolves_nicknames():
@@ -3079,3 +3082,42 @@ def test_reply_block_quotes_replied_message():
3079
  _bot.BOT_USERNAME = prev_username
3080
  assert "прошлый ответ бота" in block
3081
  assert _bot._reply_target_block(SimpleNamespace(reply_to_message=None), state) == ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3043
  from collections import deque
3044
  state = {"history": [{"role": "user", "content": f"вопрос {i}"} for i in range(60)],
3045
  "ctx": deque(), "recent_media_ids": {}, "user_names": {}}
3046
+ try:
3047
+ for i in range(60):
3048
+ message = SimpleNamespace(chat=SimpleNamespace(id=9001),
3049
+ from_user=SimpleNamespace(id=7, username="m1st", first_name="M"),
3050
+ text=f"фон {i}")
3051
+ bot._record_passive_group_context(message, state, f"фон {i}")
3052
+ assert len(state["history"]) == 60
3053
+ assert len(state["history"]) + len(state["ctx"]) <= bot.MAX_TOTAL_MEMORY_MESSAGES
3054
+ assert state["ctx"][0].endswith("фон 20")
3055
+ finally:
3056
+ bot._dirty_chat_ids.discard(9001)
3057
 
3058
 
3059
  def test_sender_directory_resolves_nicknames():
 
3082
  _bot.BOT_USERNAME = prev_username
3083
  assert "прошлый ответ бота" in block
3084
  assert _bot._reply_target_block(SimpleNamespace(reply_to_message=None), state) == ""
3085
+
3086
+
3087
+ def test_sender_display_shows_both_names():
3088
+ # Ник с first_name в скобках: иначе "блинчик" терялся при наличии username.
3089
+ from lumen_chat_state import _sender_display
3090
+ assert _sender_display("The_6JluH4ik", "Блинчик") == "@The_6JluH4ik (Блинчик)"
3091
+ assert _sender_display("m1st", "m1st") == "@m1st"
3092
+ assert _sender_display(None, "Блинчик") == "Блинчик"
3093
+ assert _sender_display(None, None) == "User"
3094
+
3095
+
3096
+ def test_tagged_prompt_hints_nickname():
3097
+ # Прозвище из вопроса связывается с автором: модель не гадает кто это.
3098
+ state = {"history": [], "user_names": {}}
3099
+ bot._note_sender(state, 11, "The_6JluH4ik", "Блинчик")
3100
+ message = SimpleNamespace(chat=SimpleNamespace(id=9004),
3101
+ from_user=SimpleNamespace(id=7, username="m1st", first_name="M"),
3102
+ text="проверь слова блинчика", reply_to_message=None)
3103
+ prompt = bot._tagged_prompt(state, message, "проверь слова блинчика")
3104
+ assert "[@m1st (M)]" in prompt
3105
+ assert "Подсказка" in prompt and "Блинчик" in prompt
3106
+
3107
+
3108
+ def test_ctx_survives_serialize_restore():
3109
+ # Фон переживает рестарт: иначе единые 100 сообщений сбрасывались деплоем.
3110
+ from collections import deque
3111
+ cid = 9005
3112
+ state = {"history": [{"role": "user", "content": "вопрос"}],
3113
+ "ctx": deque(["@m1st (M): фоновая реплика"]),
3114
+ "recent_media_ids": {}, "user_names": {"7": {"username": "m1st", "first_name": "M"}},
3115
+ "last_activity": 1.0, "lang": "ru"}
3116
+ try:
3117
+ snapshot = bot._serialize_chat_state(state)
3118
+ assert snapshot["ctx"] == ["@m1st (M): фоновая реплика"]
3119
+ bot._restore_single_chat(cid, snapshot)
3120
+ assert list(bot.chat_state[cid]["ctx"]) == ["@m1st (M): фоновая реплика"]
3121
+ assert bot.chat_state[cid]["user_names"] == {"7": {"username": "m1st", "first_name": "M"}}
3122
+ finally:
3123
+ bot.chat_state.pop(cid, None)