feat: briefing profile note service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
"""Briefing profile note: stores learned user preferences for the briefing assistant."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fabledassistant.services.notes import create_note, list_notes, update_note
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PROFILE_TAG = "briefing-profile"
|
||||||
|
PROFILE_TITLE = "Briefing Profile"
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_profile_note(user_id: int) -> dict | None:
|
||||||
|
"""Find the user's briefing profile note by tag."""
|
||||||
|
notes, _total = await list_notes(user_id, tags=[PROFILE_TAG], limit=1)
|
||||||
|
if not notes:
|
||||||
|
return None
|
||||||
|
note = notes[0]
|
||||||
|
return {
|
||||||
|
"id": note.id,
|
||||||
|
"body": note.body or "",
|
||||||
|
"title": note.title,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_profile_body(user_id: int) -> str:
|
||||||
|
"""Return the body of the briefing profile note, or '' if none exists."""
|
||||||
|
note = await _find_profile_note(user_id)
|
||||||
|
return note["body"] if note else ""
|
||||||
|
|
||||||
|
|
||||||
|
async def get_profile_note_id(user_id: int) -> int | None:
|
||||||
|
note = await _find_profile_note(user_id)
|
||||||
|
return note["id"] if note else None
|
||||||
|
|
||||||
|
|
||||||
|
async def ensure_profile_note(user_id: int) -> int:
|
||||||
|
"""
|
||||||
|
Get or create the briefing profile note.
|
||||||
|
Returns the note id.
|
||||||
|
"""
|
||||||
|
note = await _find_profile_note(user_id)
|
||||||
|
if note:
|
||||||
|
return note["id"]
|
||||||
|
created = await create_note(
|
||||||
|
user_id=user_id,
|
||||||
|
title=PROFILE_TITLE,
|
||||||
|
body=(
|
||||||
|
"# Briefing Profile\n\n"
|
||||||
|
"This note is maintained by the briefing assistant. "
|
||||||
|
"It stores your preferences, patterns, and work schedule.\n\n"
|
||||||
|
"## Work Schedule\n\n"
|
||||||
|
"Office days: (not yet configured)\n\n"
|
||||||
|
"## Locations\n\n"
|
||||||
|
"(configured via Settings → Briefing)\n\n"
|
||||||
|
"## Preferences\n\n"
|
||||||
|
"(the assistant will add observations here over time)\n"
|
||||||
|
),
|
||||||
|
tags=[PROFILE_TAG],
|
||||||
|
)
|
||||||
|
return created.id
|
||||||
|
|
||||||
|
|
||||||
|
async def append_observations(user_id: int, observations: str) -> None:
|
||||||
|
"""
|
||||||
|
Append the assistant's end-of-day observations to the profile note.
|
||||||
|
Creates the note if it doesn't exist.
|
||||||
|
"""
|
||||||
|
if not observations.strip():
|
||||||
|
return
|
||||||
|
note_id = await ensure_profile_note(user_id)
|
||||||
|
note = await _find_profile_note(user_id)
|
||||||
|
if not note:
|
||||||
|
return
|
||||||
|
current_body = note.get("body", "")
|
||||||
|
from datetime import date
|
||||||
|
date_str = date.today().isoformat()
|
||||||
|
new_body = current_body.rstrip() + f"\n\n## Observations — {date_str}\n\n{observations.strip()}\n"
|
||||||
|
await update_note(user_id, note_id, body=new_body)
|
||||||
|
logger.info("Briefing profile updated for user %d", user_id)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_or_create_profile_note_returns_body():
|
||||||
|
"""get_profile_body() should return empty string when no profile note exists."""
|
||||||
|
with patch("fabledassistant.services.briefing_profile.list_notes", new_callable=AsyncMock) as mock_list:
|
||||||
|
mock_list.return_value = ([], 0)
|
||||||
|
from fabledassistant.services.briefing_profile import get_profile_body
|
||||||
|
body = await get_profile_body(user_id=1)
|
||||||
|
assert body == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_task_for_briefing():
|
||||||
|
"""format_task() should return a compact single-line string."""
|
||||||
|
from fabledassistant.services.briefing_pipeline import format_task
|
||||||
|
task = {"title": "Write design doc", "status": "in_progress", "due_date": "2026-03-12", "priority": "high"}
|
||||||
|
result = format_task(task)
|
||||||
|
assert "Write design doc" in result
|
||||||
|
assert "in_progress" in result or "In Progress" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_greeting():
|
||||||
|
"""slot_greeting() should return different strings for different slot names."""
|
||||||
|
from fabledassistant.services.briefing_pipeline import slot_greeting
|
||||||
|
assert "morning" in slot_greeting("compilation").lower() or slot_greeting("compilation")
|
||||||
|
assert slot_greeting("morning") != slot_greeting("midday")
|
||||||
|
assert slot_greeting("midday") != slot_greeting("afternoon")
|
||||||
Reference in New Issue
Block a user