diff --git a/src/fabledassistant/routes/briefing.py b/src/fabledassistant/routes/briefing.py index 9ddb1c5..eb6dad5 100644 --- a/src/fabledassistant/routes/briefing.py +++ b/src/fabledassistant/routes/briefing.py @@ -9,9 +9,15 @@ from sqlalchemy import select from fabledassistant.auth import login_required from fabledassistant.models import async_session +from fabledassistant.models.conversation import Conversation, Message from fabledassistant.models.rss_feed import RssFeed from fabledassistant.services import rss as rss_svc from fabledassistant.services import weather as weather_svc +from fabledassistant.services.briefing_conversations import ( + get_or_create_today_conversation, + list_briefing_conversations, + post_message, +) from fabledassistant.services.settings import get_setting, set_settings_batch logger = logging.getLogger(__name__) @@ -164,3 +170,65 @@ async def refresh_weather(): logger.warning("Failed to refresh weather for %s", key, exc_info=True) return jsonify({"refreshed": refreshed}) + + +# ── Briefing Conversations ───────────────────────────────────────────────────── + +@briefing_bp.route("/conversations", methods=["GET"]) +@_REQUIRE +async def get_conversations(): + convs = await list_briefing_conversations(g.user.id) + return jsonify({"conversations": convs}) + + +@briefing_bp.route("/conversations/today", methods=["GET"]) +@_REQUIRE +async def get_today_conversation(): + model = await get_setting(g.user.id, "default_model", "") + conv = await get_or_create_today_conversation(g.user.id, model) + # Load messages + async with async_session() as session: + result = await session.execute( + select(Message) + .where(Message.conversation_id == conv.id) + .order_by(Message.created_at) + ) + messages = [m.to_dict() for m in result.scalars().all()] + data = conv.to_dict() + data["messages"] = messages + return jsonify(data) + + +@briefing_bp.route("/conversations//messages", methods=["GET"]) +@_REQUIRE +async def get_conversation_messages(conv_id: int): + # Verify ownership + async with async_session() as session: + conv = await session.get(Conversation, conv_id) + if not conv or conv.user_id != g.user.id or conv.conversation_type != "briefing": + return jsonify({"error": "Not found"}), 404 + result = await session.execute( + select(Message) + .where(Message.conversation_id == conv_id) + .order_by(Message.created_at) + ) + messages = [m.to_dict() for m in result.scalars().all()] + return jsonify({"messages": messages}) + + +@briefing_bp.route("/trigger", methods=["POST"]) +@_REQUIRE +async def manual_trigger(): + """Dev/admin endpoint to manually trigger a briefing compilation.""" + data = await request.get_json() or {} + slot = data.get("slot", "compilation") + if slot not in ("compilation", "morning", "midday", "afternoon"): + return jsonify({"error": "invalid slot"}), 400 + + from fabledassistant.services.briefing_pipeline import run_compilation + + model = await get_setting(g.user.id, "default_model", "") + conv = await get_or_create_today_conversation(g.user.id, model) + text = await run_compilation(g.user.id, slot, model) + msg = await post_message(conv.id, "assistant", text) + return jsonify({"conversation_id": conv.id, "message_id": msg.id, "slot": slot}) diff --git a/src/fabledassistant/services/briefing_conversations.py b/src/fabledassistant/services/briefing_conversations.py new file mode 100644 index 0000000..b521af7 --- /dev/null +++ b/src/fabledassistant/services/briefing_conversations.py @@ -0,0 +1,74 @@ +"""Create and manage briefing conversations.""" + +import logging +from datetime import date, datetime, timezone + +from sqlalchemy import select + +from fabledassistant.models import async_session +from fabledassistant.models.conversation import Conversation, Message + +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 = date.today() + 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) -> Message: + """Append a message to a briefing conversation.""" + async with async_session() as session: + msg = Message( + conversation_id=conversation_id, + role=role, + content=content, + status="complete", + ) + 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]