feat(training): drop info heading and show last training time
This commit is contained in:
1 parent
60f41dd675
commit
2fd41d82b9
4 files changed
+46
-12
No files matched your search
@@ -56,6 +56,7 @@ from user_store import (
|
||||
get_recent_post_history_text,
|
||||
get_tone_of_voice,
|
||||
get_training_summary,
|
||||
get_training_updated_at,
|
||||
save_training_summary,
|
||||
init_db,
|
||||
is_subscribed,
|
||||
@@ -597,6 +598,7 @@ async def _refresh_training_summary(user_id: int, profile: str, notes: list[str]
|
||||
if not summary:
|
||||
return
|
||||
await asyncio.to_thread(save_training_summary, user_id, summary)
|
||||
logger.info("Training summary updated for user %s", user_id)
|
||||
|
||||
|
||||
TRAINING_INFO_TITLES = {
|
||||
@@ -607,7 +609,10 @@ TRAINING_INFO_TITLES = {
|
||||
}
|
||||
|
||||
|
||||
def training_info_rich_html(summary: dict[str, str]) -> str:
|
||||
TRAINING_INFO_TIMEZONE = datetime.timezone(datetime.timedelta(hours=2))
|
||||
|
||||
|
||||
def training_info_rich_html(summary: dict[str, str], updated_at: str = "") -> str:
|
||||
blocks: list[str] = []
|
||||
for field, title in TRAINING_INFO_TITLES.items():
|
||||
value = (summary.get(field) or "").strip()
|
||||
@@ -616,12 +621,26 @@ def training_info_rich_html(summary: dict[str, str]) -> str:
|
||||
blocks.append(f"<h3>{html.escape(title)}</h3><p>{html.escape(value)}</p>")
|
||||
if not blocks:
|
||||
return ""
|
||||
return (
|
||||
"<h2>🎓 Что я о тебе поняла</h2>"
|
||||
+ "".join(blocks)
|
||||
+ "<p>Это краткая выжимка: для генерации я использую полный профиль и все примеры постов.</p>"
|
||||
"<p>Дополнить знания можно в любой момент — просто пришли новые посты или сведения.</p>"
|
||||
)
|
||||
footer = "<p>Это краткая выжимка: для генерации я использую полный профиль и все примеры постов.</p>"
|
||||
stamp = training_time_label(updated_at)
|
||||
if stamp:
|
||||
footer += f"<p>Последнее обучение: {html.escape(stamp)}.</p>"
|
||||
footer += "<p>Дополнить знания можно в любой момент — просто пришли новые посты или сведения.</p>"
|
||||
return "".join(blocks) + footer
|
||||
|
||||
|
||||
def training_time_label(updated_at: str) -> str:
|
||||
"""Локальная дата и время последнего обучения; '' если время неизвестно."""
|
||||
raw = (updated_at or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
try:
|
||||
moment = datetime.datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return ""
|
||||
if moment.tzinfo is None:
|
||||
moment = moment.replace(tzinfo=datetime.timezone.utc)
|
||||
return moment.astimezone(TRAINING_INFO_TIMEZONE).strftime("%d.%m.%Y, %H:%M")
|
||||
|
||||
|
||||
async def training_info_command(message: Message, state: FSMContext) -> None:
|
||||
@@ -644,7 +663,10 @@ async def training_info_command(message: Message, state: FSMContext) -> None:
|
||||
summary = await asyncio.to_thread(get_training_summary, message.from_user.id)
|
||||
finally:
|
||||
await status.finish(None)
|
||||
rich_html = training_info_rich_html(summary)
|
||||
rich_html = training_info_rich_html(
|
||||
summary,
|
||||
await asyncio.to_thread(get_training_updated_at, message.from_user.id),
|
||||
)
|
||||
if not rich_html:
|
||||
await message.answer(
|
||||
"🎓 <b>Пока не знаю о тебе почти ничего</b>\n\n"
|
||||
|
||||
@@ -1184,12 +1184,15 @@ class TrainingSummaryTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
def test_rich_html_contains_only_filled_blocks(self) -> None:
|
||||
rendered = bot.training_info_rich_html(
|
||||
{"about": "Канал про ИИ", "topics": "", "style": "сухо", "audience": ""}
|
||||
{"about": "Канал про ИИ", "topics": "", "style": "сухо", "audience": ""},
|
||||
"2026-10-29 18:11:35",
|
||||
)
|
||||
self.assertIn("<h2>", rendered)
|
||||
self.assertNotIn("<h2>", rendered)
|
||||
self.assertTrue(rendered.startswith("<h3>"))
|
||||
self.assertIn("Канал про ИИ", rendered)
|
||||
self.assertNotIn("Основные темы", rendered)
|
||||
self.assertEqual(bot.training_info_rich_html({}), "")
|
||||
self.assertIn("Последнее обучение: 29.10.2026, 20:11", rendered)
|
||||
self.assertEqual(bot.training_info_rich_html({}, ""), "")
|
||||
|
||||
async def test_summary_is_saved_after_profile_rebuild(self) -> None:
|
||||
with patch.object(bot, "create_tone_profile", new=AsyncMock(return_value="профиль")), \
|
||||
|
||||
@@ -590,6 +590,15 @@ def get_training_summary(user_id: int, db_path: str | None = None) -> dict[str,
|
||||
}
|
||||
|
||||
|
||||
def get_training_updated_at(user_id: int, db_path: str | None = None) -> str:
|
||||
"""Время последнего обучения (UTC, naive) — updated_at строки профиля."""
|
||||
with closing(connect(db_path)) as connection:
|
||||
row = connection.execute(
|
||||
"SELECT updated_at FROM users WHERE telegram_user_id = ?", (user_id,)
|
||||
).fetchone()
|
||||
return str(row["updated_at"] or "") if row else ""
|
||||
|
||||
|
||||
def save_training_summary(
|
||||
user_id: int, summary: dict[str, str], db_path: str | None = None
|
||||
) -> None:
|
||||
|
||||
Reference in new issue
Block a user