1
0
Fork 0
forked from zovos/bot_tg

ai update fix history message

This commit is contained in:
q 2026-04-12 10:49:10 +03:00
parent a33443936c
commit 4a02af047a

View file

@ -162,6 +162,37 @@ def _set_state_from_conn(conn: sqlite3.Connection, chat_id: int, key: str, value
) )
def _get_talk_state_from_conn(
conn: sqlite3.Connection,
chat_id: int,
user_id: int,
key: str,
default: str = "",
) -> str:
row = conn.execute(
"SELECT value FROM talk_state WHERE chat_id = ? AND user_id = ? AND key = ?",
(chat_id, user_id, key),
).fetchone()
return row["value"] if row else default
def _set_talk_state_from_conn(
conn: sqlite3.Connection,
chat_id: int,
user_id: int,
key: str,
value: str,
) -> None:
conn.execute(
"""
INSERT INTO talk_state (chat_id, user_id, key, value)
VALUES (?, ?, ?, ?)
ON CONFLICT(chat_id, user_id, key) DO UPDATE SET value = excluded.value
""",
(chat_id, user_id, key, value),
)
def _init_db() -> None: def _init_db() -> None:
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True) Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
with _connect_db() as conn: with _connect_db() as conn:
@ -190,12 +221,42 @@ def _init_db() -> None:
) )
""" """
) )
conn.execute(
"""
CREATE TABLE IF NOT EXISTS talk_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role TEXT NOT NULL,
name TEXT NOT NULL,
text TEXT NOT NULL,
created_at REAL NOT NULL DEFAULT 0
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS talk_state (
chat_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (chat_id, user_id, key)
)
"""
)
conn.execute( conn.execute(
"CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_id ON chat_history(chat_id, id)" "CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_id ON chat_history(chat_id, id)"
) )
conn.execute( conn.execute(
"CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_role_id ON chat_history(chat_id, role, id)" "CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_role_id ON chat_history(chat_id, role, id)"
) )
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_talk_history_chat_user_id
ON talk_history(chat_id, user_id, id)
"""
)
conn.commit() conn.commit()
@ -235,11 +296,19 @@ def _photo_memory_text(caption: str) -> str:
return "[Фото] Без подписи." return "[Фото] Без подписи."
def push_message(chat_id: int, role: str, name: str, text: str) -> None: def push_message(
chat_id: int,
role: str,
name: str,
text: str,
*,
user_id: int | None = None,
) -> None:
cleaned_text = _clean_text(text) cleaned_text = _clean_text(text)
if not cleaned_text: if not cleaned_text:
return return
with _connect_db() as conn: with _connect_db() as conn:
if user_id is None:
conn.execute( conn.execute(
""" """
INSERT INTO chat_history (chat_id, role, name, text, created_at) INSERT INTO chat_history (chat_id, role, name, text, created_at)
@ -247,16 +316,27 @@ def push_message(chat_id: int, role: str, name: str, text: str) -> None:
""", """,
(chat_id, role, name, cleaned_text, time.time()), (chat_id, role, name, cleaned_text, time.time()),
) )
else:
conn.execute(
"""
INSERT INTO talk_history (chat_id, user_id, role, name, text, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(chat_id, user_id, role, name, cleaned_text, time.time()),
)
conn.commit() conn.commit()
def _get_summary(chat_id: int) -> str: def _get_summary(chat_id: int, user_id: int | None = None) -> str:
with _connect_db() as conn: with _connect_db() as conn:
if user_id is None:
return _get_state_from_conn(conn, chat_id, "summary", "") return _get_state_from_conn(conn, chat_id, "summary", "")
return _get_talk_state_from_conn(conn, chat_id, user_id, "summary", "")
def _get_history_rows(chat_id: int) -> list[sqlite3.Row]: def _get_history_rows(chat_id: int, user_id: int | None = None) -> list[sqlite3.Row]:
with _connect_db() as conn: with _connect_db() as conn:
if user_id is None:
return conn.execute( return conn.execute(
""" """
SELECT id, role, name, text, created_at SELECT id, role, name, text, created_at
@ -266,6 +346,15 @@ def _get_history_rows(chat_id: int) -> list[sqlite3.Row]:
""", """,
(chat_id,), (chat_id,),
).fetchall() ).fetchall()
return conn.execute(
"""
SELECT id, role, name, text, created_at
FROM talk_history
WHERE chat_id = ? AND user_id = ?
ORDER BY id ASC
""",
(chat_id, user_id),
).fetchall()
def _latest_reply_stats(chat_id: int) -> tuple[float, int]: def _latest_reply_stats(chat_id: int) -> tuple[float, int]:
@ -293,13 +382,25 @@ def _latest_reply_stats(chat_id: int) -> tuple[float, int]:
return float(last_assistant["created_at"] or 0.0), int(user_messages_since_reply) return float(last_assistant["created_at"] or 0.0), int(user_messages_since_reply)
def _store_summary_and_prune(chat_id: int, summary: str, last_row_id: int) -> None: def _store_summary_and_prune(
chat_id: int,
summary: str,
last_row_id: int,
user_id: int | None = None,
) -> None:
with _connect_db() as conn: with _connect_db() as conn:
if user_id is None:
_set_state_from_conn(conn, chat_id, "summary", summary) _set_state_from_conn(conn, chat_id, "summary", summary)
conn.execute( conn.execute(
"DELETE FROM chat_history WHERE chat_id = ? AND id <= ?", "DELETE FROM chat_history WHERE chat_id = ? AND id <= ?",
(chat_id, last_row_id), (chat_id, last_row_id),
) )
else:
_set_talk_state_from_conn(conn, chat_id, user_id, "summary", summary)
conn.execute(
"DELETE FROM talk_history WHERE chat_id = ? AND user_id = ? AND id <= ?",
(chat_id, user_id, last_row_id),
)
conn.commit() conn.commit()
@ -317,6 +418,7 @@ def _build_messages(
system_prompt: str, system_prompt: str,
current_text: str | None = None, current_text: str | None = None,
current_name: str | None = None, current_name: str | None = None,
user_id: int | None = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
current_payload = None current_payload = None
current_budget = 0 current_budget = 0
@ -327,14 +429,14 @@ def _build_messages(
} }
current_budget = len(current_payload["content"]) current_budget = len(current_payload["content"])
summary = _get_summary(chat_id).strip() summary = _get_summary(chat_id, user_id=user_id).strip()
summary_block = "" summary_block = ""
if summary: if summary:
summary_block = f"Краткая память чата:\n{summary[:SUMMARY_CHAR_BUDGET]}" summary_block = f"Краткая память чата:\n{summary[:SUMMARY_CHAR_BUDGET]}"
used_chars = len(system_prompt) + len(summary_block) + current_budget used_chars = len(system_prompt) + len(summary_block) + current_budget
recent_messages: list[dict[str, str]] = [] recent_messages: list[dict[str, str]] = []
for row in reversed(_get_history_rows(chat_id)[-RECENT_MESSAGES_LIMIT:]): for row in reversed(_get_history_rows(chat_id, user_id=user_id)[-RECENT_MESSAGES_LIMIT:]):
llm_message = _format_row_for_llm(row) llm_message = _format_row_for_llm(row)
if used_chars + len(llm_message["content"]) > PROMPT_CHAR_BUDGET: if used_chars + len(llm_message["content"]) > PROMPT_CHAR_BUDGET:
break break
@ -424,11 +526,11 @@ async def _call_llm(
return cleaned_content return cleaned_content
async def _maybe_refresh_summary(chat_id: int) -> None: async def _maybe_refresh_summary(chat_id: int, user_id: int | None = None) -> None:
async with _summary_lock: async with _summary_lock:
# Сворачиваем старую часть истории в summary, чтобы не пихать весь лог в модель. # Сворачиваем старую часть истории в summary, чтобы не пихать весь лог в модель.
for _ in range(6): for _ in range(6):
rows = await asyncio.to_thread(_get_history_rows, chat_id) rows = await asyncio.to_thread(_get_history_rows, chat_id, user_id)
if len(rows) <= SUMMARY_TRIGGER_MESSAGES: if len(rows) <= SUMMARY_TRIGGER_MESSAGES:
return return
@ -438,7 +540,7 @@ async def _maybe_refresh_summary(chat_id: int) -> None:
batch_size = min(available_to_summarize, SUMMARY_BATCH_MESSAGES) batch_size = min(available_to_summarize, SUMMARY_BATCH_MESSAGES)
rows_to_summarize = rows[:batch_size] rows_to_summarize = rows[:batch_size]
current_summary = await asyncio.to_thread(_get_summary, chat_id) current_summary = await asyncio.to_thread(_get_summary, chat_id, user_id)
transcript = "\n".join( transcript = "\n".join(
f"{row['name'] if row['role'] == 'user' else BOT_MEMORY_NAME}: " f"{row['name'] if row['role'] == 'user' else BOT_MEMORY_NAME}: "
f"{_clip_text(row['text'], SUMMARY_LINE_CHAR_LIMIT)}" f"{_clip_text(row['text'], SUMMARY_LINE_CHAR_LIMIT)}"
@ -470,10 +572,10 @@ async def _maybe_refresh_summary(chat_id: int) -> None:
cleaned_summary = summary.strip() cleaned_summary = summary.strip()
if not cleaned_summary: if not cleaned_summary:
logger.warning("Summary refresh produced empty text for chat_id=%s", chat_id) logger.warning("Summary refresh produced empty text for chat_id=%s user_id=%s", chat_id, user_id)
return return
last_row_id = int(rows_to_summarize[-1]["id"]) last_row_id = int(rows_to_summarize[-1]["id"])
await asyncio.to_thread(_store_summary_and_prune, chat_id, cleaned_summary, last_row_id) await asyncio.to_thread(_store_summary_and_prune, chat_id, cleaned_summary, last_row_id, user_id)
async def _generate_response( async def _generate_response(
@ -483,11 +585,12 @@ async def _generate_response(
current_text: str | None = None, current_text: str | None = None,
current_name: str | None = None, current_name: str | None = None,
current_content: Any = None, current_content: Any = None,
user_id: int | None = None,
max_tokens: int, max_tokens: int,
temperature: float, temperature: float,
top_p: float, top_p: float,
) -> str: ) -> str:
await _maybe_refresh_summary(chat_id) await _maybe_refresh_summary(chat_id, user_id=user_id)
if current_content is None: if current_content is None:
messages = await asyncio.to_thread( messages = await asyncio.to_thread(
_build_messages, _build_messages,
@ -495,9 +598,10 @@ async def _generate_response(
system_prompt, system_prompt,
current_text, current_text,
current_name, current_name,
user_id,
) )
else: else:
messages = await asyncio.to_thread(_build_messages, chat_id, system_prompt, None, None) messages = await asyncio.to_thread(_build_messages, chat_id, system_prompt, None, None, user_id)
current_payload = {"role": "user", "content": current_content} current_payload = {"role": "user", "content": current_content}
current_budget = _estimate_content_length(current_content) current_budget = _estimate_content_length(current_content)
used_chars = sum(_estimate_content_length(message.get("content", "")) for message in messages) used_chars = sum(_estimate_content_length(message.get("content", "")) for message in messages)
@ -771,13 +875,15 @@ async def handle_talk(message: Message) -> None:
return return
chat_id = message.chat.id chat_id = message.chat.id
user_id = message.from_user.id
user_name = message.from_user.first_name or "кент" user_name = message.from_user.first_name or "кент"
user_text = _clean_text(parts[1]) user_text = _clean_text(parts[1])
if not user_text: if not user_text:
await message.reply(USER_FALLBACK_TEXT) await message.reply(USER_FALLBACK_TEXT)
return return
await asyncio.to_thread(push_message, chat_id, "user", user_name, user_text) # /talk хранит отдельную память по конкретному пользователю, чтобы не мешать чужие истории.
await asyncio.to_thread(push_message, chat_id, "user", user_name, user_text, user_id=user_id)
async with _reply_lock: async with _reply_lock:
try: try:
@ -785,6 +891,7 @@ async def handle_talk(message: Message) -> None:
response = await _generate_response( response = await _generate_response(
chat_id, chat_id,
system_prompt=SYSTEM_PROMPT, system_prompt=SYSTEM_PROMPT,
user_id=user_id,
max_tokens=220, max_tokens=220,
temperature=0.8, temperature=0.8,
top_p=0.9, top_p=0.9,
@ -797,13 +904,21 @@ async def handle_talk(message: Message) -> None:
normalized_response = _normalize_reply(response) normalized_response = _normalize_reply(response)
if not normalized_response: if not normalized_response:
logger.warning( logger.warning(
"Talk command produced empty/skip response. chat_id=%s user=%s raw=%s", "Talk command produced empty/skip response. chat_id=%s user_id=%s user=%s raw=%s",
chat_id, chat_id,
user_id,
user_name, user_name,
_clip_text(response, 200), _clip_text(response, 200),
) )
normalized_response = EMPTY_RESPONSE_TEXT normalized_response = EMPTY_RESPONSE_TEXT
await asyncio.to_thread(push_message, chat_id, "assistant", BOT_MEMORY_NAME, normalized_response) await asyncio.to_thread(
push_message,
chat_id,
"assistant",
BOT_MEMORY_NAME,
normalized_response,
user_id=user_id,
)
await message.reply(normalized_response, parse_mode=None) await message.reply(normalized_response, parse_mode=None)