Two related bugs where the server defaulted naive datetimes to UTC instead of the configured user timezone, causing all-day events to land on the previous day and briefings to "disappear" at UTC midnight. - New services/tz.py helpers: get_user_tz, user_today, user_briefing_date (the briefing day flips at 4am local to align with the compilation slot, so the 00:00-04:00 local window still shows yesterday's briefing until the new one is generated). - calendar create/list/update tools now parse naive datetimes in the user's TZ before converting to UTC for storage, and tool descriptions tell the model to pass plain local dates. - briefing_conversations.get_or_create_today_conversation and the reset-today route use user_briefing_date so the in-progress briefing doesn't get replaced at 19:00 NY / UTC midnight. - _run_profile_closeout targets user-local "yesterday" for consistency. Regression tests added for the TZ helpers and the calendar tool. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
3.0 KiB
Python
93 lines
3.0 KiB
Python
"""Create and manage briefing conversations."""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import select
|
|
|
|
from fabledassistant.models import async_session
|
|
from fabledassistant.models.conversation import Conversation, Message
|
|
from fabledassistant.services.tz import user_briefing_date
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def get_or_create_today_conversation(user_id: int, model: str) -> Conversation:
|
|
"""
|
|
Return today's briefing conversation, creating it if it doesn't exist.
|
|
"""
|
|
# "Today" is the user-local briefing day (flips at 4am local), not
|
|
# ``date.today()`` — in a UTC container the latter rolls over at
|
|
# 19:00 NY local and makes the in-progress briefing disappear.
|
|
today = await user_briefing_date(user_id)
|
|
async with async_session() as session:
|
|
result = await session.execute(
|
|
select(Conversation).where(
|
|
Conversation.user_id == user_id,
|
|
Conversation.conversation_type == "briefing",
|
|
Conversation.briefing_date == today,
|
|
)
|
|
)
|
|
conv = result.scalars().first()
|
|
if conv is not None:
|
|
return conv
|
|
|
|
conv = Conversation(
|
|
user_id=user_id,
|
|
title=f"Briefing — {today.isoformat()}",
|
|
model=model,
|
|
conversation_type="briefing",
|
|
briefing_date=today,
|
|
)
|
|
session.add(conv)
|
|
await session.commit()
|
|
await session.refresh(conv)
|
|
return conv
|
|
|
|
|
|
async def post_message(
|
|
conversation_id: int,
|
|
role: str,
|
|
content: str,
|
|
metadata: dict | None = None,
|
|
tool_calls: list | None = None,
|
|
) -> Message:
|
|
"""Append a message to a briefing conversation.
|
|
|
|
``tool_calls`` is accepted on assistant-role messages so the full
|
|
agentic briefing sequence (assistant tool-call turns and tool-role
|
|
results) can be persisted as real conversation rows, keeping the
|
|
receipts in context on chat follow-ups.
|
|
"""
|
|
async with async_session() as session:
|
|
msg = Message(
|
|
conversation_id=conversation_id,
|
|
role=role,
|
|
content=content,
|
|
status="complete",
|
|
msg_metadata=metadata,
|
|
tool_calls=tool_calls,
|
|
)
|
|
session.add(msg)
|
|
# Bump conversation updated_at
|
|
conv = await session.get(Conversation, conversation_id)
|
|
if conv:
|
|
conv.updated_at = datetime.now(timezone.utc)
|
|
await session.commit()
|
|
await session.refresh(msg)
|
|
return msg
|
|
|
|
|
|
async def list_briefing_conversations(user_id: int) -> list[dict]:
|
|
"""Return briefing conversations newest first."""
|
|
async with async_session() as session:
|
|
result = await session.execute(
|
|
select(Conversation).where(
|
|
Conversation.user_id == user_id,
|
|
Conversation.conversation_type == "briefing",
|
|
).order_by(Conversation.briefing_date.desc().nullslast())
|
|
.limit(30)
|
|
)
|
|
convs = list(result.scalars().all())
|
|
return [c.to_dict() for c in convs]
|