feat(journal): conversational LLM-generated daily prep (replaces card)
The structured prep card was data-rich but voiceless. Replaced with an LLM-generated conversational opener — same shape the briefing's compilation slot had — that renders as a normal assistant chat bubble at the top of the day's conversation. Backend (services/journal_prep.py): - Renamed generate_daily_prep -> gather_daily_sections (still pure data fetching, no LLM); kept the old name as a backwards-compat alias. - New _generate_prep_prose: hands the gathered sections to generate_completion with a warm-conversational system prompt; returns prose. Falls back to a plain greeting if the LLM call fails or no model is configured. - ensure_daily_prep_message now persists the prep as role='assistant' with the prose as content. Structured sections stay on msg_metadata for provenance. Auto-upgrades legacy system-role preps in place on next call. Frontend: - Drop the <article class="daily-prep"> structured block from JournalView. The prep is now just the first chat bubble — picks up the existing Illuminated Transcript styling automatically. - Drop dayMessages / prepMessage / prepSections / asArray helpers — no longer needed. - ChatMessage hideMessage filter: comment refined to clarify it only catches LEGACY system-role prep rows. Current preps are assistant-role and render normally. Net effect: open /journal -> first thing you see is a warm assistant bubble that talks about your day -> input bar below to reply. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,10 @@ function formatMs(ms: number | null | undefined): string {
|
|||||||
|
|
||||||
const metadata = computed(() => (props.message.metadata ?? {}) as Record<string, unknown>);
|
const metadata = computed(() => (props.message.metadata ?? {}) as Record<string, unknown>);
|
||||||
|
|
||||||
// Hide daily-prep system messages — they're rendered separately as a structured
|
// Hide LEGACY system-role daily_prep messages from earlier versions. The
|
||||||
// card in JournalView. Without this filter they render as a flat unstyled block
|
// current journal prep is a normal assistant message (renders with proper
|
||||||
// inside the chat (no .role-system bubble styling exists).
|
// bubble styling). Old system-role rows lurking in existing conversations
|
||||||
|
// would render as a flat unstyled block — filter them.
|
||||||
const hideMessage = computed(() =>
|
const hideMessage = computed(() =>
|
||||||
props.message.role === "system" && metadata.value.kind === "daily_prep"
|
props.message.role === "system" && metadata.value.kind === "daily_prep"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
triggerJournalPrep,
|
triggerJournalPrep,
|
||||||
listEvents,
|
listEvents,
|
||||||
type EventEntry,
|
type EventEntry,
|
||||||
type JournalMessage,
|
|
||||||
} from '@/api/client'
|
} from '@/api/client'
|
||||||
|
|
||||||
interface WeatherDay {
|
interface WeatherDay {
|
||||||
@@ -53,21 +52,8 @@ const days = ref<string[]>([])
|
|||||||
const todayDate = ref<string | null>(null)
|
const todayDate = ref<string | null>(null)
|
||||||
const selectedDay = ref<string | null>(null)
|
const selectedDay = ref<string | null>(null)
|
||||||
const dayConvId = ref<number | null>(null)
|
const dayConvId = ref<number | null>(null)
|
||||||
const dayMessages = ref<JournalMessage[]>([])
|
|
||||||
const isToday = computed(() => selectedDay.value !== null && selectedDay.value === todayDate.value)
|
const isToday = computed(() => selectedDay.value !== null && selectedDay.value === todayDate.value)
|
||||||
|
|
||||||
// ── Daily prep card ──────────────────────────────────────────────────────────
|
|
||||||
const prepMessage = computed<JournalMessage | null>(() => {
|
|
||||||
return dayMessages.value.find(
|
|
||||||
(m) => m.role === 'system' && (m.metadata as { kind?: string } | null)?.kind === 'daily_prep',
|
|
||||||
) ?? null
|
|
||||||
})
|
|
||||||
const prepSections = computed<Record<string, unknown>>(() => {
|
|
||||||
const meta = prepMessage.value?.metadata as { sections?: Record<string, unknown> } | null
|
|
||||||
return meta?.sections ?? {}
|
|
||||||
})
|
|
||||||
function asArray(v: unknown): unknown[] { return Array.isArray(v) ? v : [] }
|
|
||||||
|
|
||||||
// ── Weather panel ────────────────────────────────────────────────────────────
|
// ── Weather panel ────────────────────────────────────────────────────────────
|
||||||
const weatherData = ref<WeatherData[]>([])
|
const weatherData = ref<WeatherData[]>([])
|
||||||
const selectedWeatherIdx = ref(0)
|
const selectedWeatherIdx = ref(0)
|
||||||
@@ -153,7 +139,6 @@ async function loadEvents() {
|
|||||||
// ── Day load + switch ────────────────────────────────────────────────────────
|
// ── Day load + switch ────────────────────────────────────────────────────────
|
||||||
async function loadDay(iso: string) {
|
async function loadDay(iso: string) {
|
||||||
const payload = iso === todayDate.value ? await getJournalToday() : await getJournalDay(iso)
|
const payload = iso === todayDate.value ? await getJournalToday() : await getJournalDay(iso)
|
||||||
dayMessages.value = payload.messages
|
|
||||||
if (payload.conversation) {
|
if (payload.conversation) {
|
||||||
dayConvId.value = payload.conversation.id
|
dayConvId.value = payload.conversation.id
|
||||||
await chatStore.fetchConversation(payload.conversation.id)
|
await chatStore.fetchConversation(payload.conversation.id)
|
||||||
@@ -167,7 +152,6 @@ async function loadAll() {
|
|||||||
const today = await getJournalToday()
|
const today = await getJournalToday()
|
||||||
todayDate.value = today.day_date
|
todayDate.value = today.day_date
|
||||||
selectedDay.value = today.day_date
|
selectedDay.value = today.day_date
|
||||||
dayMessages.value = today.messages
|
|
||||||
if (today.conversation) {
|
if (today.conversation) {
|
||||||
dayConvId.value = today.conversation.id
|
dayConvId.value = today.conversation.id
|
||||||
await chatStore.fetchConversation(today.conversation.id)
|
await chatStore.fetchConversation(today.conversation.id)
|
||||||
@@ -259,55 +243,8 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Center: prep card + chat -->
|
<!-- Center: chat (prep is the first assistant message inside) -->
|
||||||
<div class="journal-center">
|
<div class="journal-center">
|
||||||
<article v-if="prepMessage" class="daily-prep">
|
|
||||||
<header><h2>Today</h2></header>
|
|
||||||
|
|
||||||
<section v-if="asArray(prepSections.tasks).length" class="prep-section">
|
|
||||||
<h3>Tasks</h3>
|
|
||||||
<ul>
|
|
||||||
<li v-for="t in (asArray(prepSections.tasks) as { id: number; title: string; due_date: string | null }[])" :key="t.id">
|
|
||||||
{{ t.title }}<span v-if="t.due_date"> · due {{ t.due_date }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="asArray(prepSections.events).length" class="prep-section">
|
|
||||||
<h3>Calendar</h3>
|
|
||||||
<ul>
|
|
||||||
<li v-for="(e, i) in (asArray(prepSections.events) as { id: number; title: string; start_dt: string }[])" :key="i">
|
|
||||||
{{ e.title }} — {{ e.start_dt }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="asArray(prepSections.projects).length" class="prep-section">
|
|
||||||
<h3>Active projects</h3>
|
|
||||||
<ul>
|
|
||||||
<li v-for="p in (asArray(prepSections.projects) as { id: number; title: string }[])" :key="p.id">{{ p.title }}</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="asArray(prepSections.recent_moments).length" class="prep-section">
|
|
||||||
<h3>Recent moments</h3>
|
|
||||||
<ul>
|
|
||||||
<li v-for="m in (asArray(prepSections.recent_moments) as { id: number; content: string; day_date: string }[])" :key="m.id">
|
|
||||||
<em>{{ m.day_date }}</em> — {{ m.content }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="asArray(prepSections.open_threads).length" class="prep-section">
|
|
||||||
<h3>Open threads</h3>
|
|
||||||
<ul>
|
|
||||||
<li v-for="m in (asArray(prepSections.open_threads) as { id: number; content: string; day_date: string }[])" :key="m.id">
|
|
||||||
<em>{{ m.day_date }}</em> — {{ m.content }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<ChatPanel
|
<ChatPanel
|
||||||
variant="full"
|
variant="full"
|
||||||
briefingMode
|
briefingMode
|
||||||
@@ -445,31 +382,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.journal-chat-panel { flex: 1; min-height: 0; }
|
.journal-chat-panel { flex: 1; min-height: 0; }
|
||||||
|
|
||||||
.daily-prep {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
margin: 1rem 1rem 0.5rem;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.daily-prep > header h2 {
|
|
||||||
font-family: 'Fraunces', Georgia, serif;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
.prep-section { margin-top: 0.75rem; }
|
|
||||||
.prep-section h3 {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
margin: 0 0 0.25rem 0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.prep-section ul { margin: 0; padding-left: 1.1rem; font-size: 0.88rem; color: var(--color-text); }
|
|
||||||
.prep-section li { margin-bottom: 0.15rem; }
|
|
||||||
|
|
||||||
/* ── Right column ─────────────────────────────────────────────────────────── */
|
/* ── Right column ─────────────────────────────────────────────────────────── */
|
||||||
.journal-right {
|
.journal-right {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
"""Daily prep generator for the Journal.
|
"""Daily prep generator for the Journal.
|
||||||
|
|
||||||
Runs once per day per user (scheduled, or lazy on first journal-open of a
|
Runs once per day per user (scheduled, or lazy on first journal-open of a
|
||||||
new day). Gathers raw data into a structured prep block that becomes the
|
new day). Two phases:
|
||||||
first message of the day's journal Conversation.
|
|
||||||
|
|
||||||
The output shape lives on Message.msg_metadata as:
|
1. Gather structured data (tasks/events/weather/projects/recent moments/
|
||||||
{ kind: 'daily_prep', sections: { tasks, events, weather,
|
open threads) — deterministic, no LLM call.
|
||||||
projects, recent_moments, open_threads } }
|
2. Hand the structured data to the LLM and ask it for a warm conversational
|
||||||
|
opener — flowing prose, not a card. The result is persisted as the first
|
||||||
|
*assistant* message in today's journal Conversation, so it renders with
|
||||||
|
the standard Illuminated Transcript bubble styling alongside the rest of
|
||||||
|
the conversation.
|
||||||
|
|
||||||
Deliberately deterministic — no LLM call here. The journal LLM weaves
|
The structured data is preserved on ``Message.msg_metadata.sections`` for
|
||||||
narrative into the conversation later, when the user actually starts
|
provenance and future tooling, but is NOT visually surfaced as a card.
|
||||||
talking. Generating prose at scheduled-prep time would burn model time
|
|
||||||
on something the user may never read.
|
Message shape:
|
||||||
|
role: 'assistant'
|
||||||
|
content: <prose opener>
|
||||||
|
msg_metadata: { kind: 'daily_prep', sections: { ...raw data... } }
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -20,23 +26,29 @@ import logging
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from fabledassistant.config import Config
|
||||||
from fabledassistant.models import Conversation, Message, async_session
|
from fabledassistant.models import Conversation, Message, async_session
|
||||||
from fabledassistant.services.events import list_events
|
from fabledassistant.services.events import list_events
|
||||||
from fabledassistant.services.journal_search import search_journal
|
from fabledassistant.services.journal_search import search_journal
|
||||||
from fabledassistant.services.notes import list_notes
|
from fabledassistant.services.notes import list_notes
|
||||||
from fabledassistant.services.projects import list_projects
|
from fabledassistant.services.projects import list_projects
|
||||||
|
from fabledassistant.services.settings import get_setting
|
||||||
from fabledassistant.services.weather import get_cached_weather_rows
|
from fabledassistant.services.weather import get_cached_weather_rows
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def generate_daily_prep(
|
async def gather_daily_sections(
|
||||||
*,
|
*,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
day_date: datetime.date,
|
day_date: datetime.date,
|
||||||
user_timezone: str,
|
user_timezone: str,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Gather all daily-prep sections and return them as a dict."""
|
"""Gather all daily-prep sections and return them as a dict.
|
||||||
|
|
||||||
|
Pure data fetching — no LLM call. Each section degrades to an empty
|
||||||
|
list/dict on failure so the caller always gets a complete shape.
|
||||||
|
"""
|
||||||
sections: dict = {}
|
sections: dict = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -66,7 +78,6 @@ async def generate_daily_prep(
|
|||||||
try:
|
try:
|
||||||
day_start = datetime.datetime.combine(day_date, datetime.time.min)
|
day_start = datetime.datetime.combine(day_date, datetime.time.min)
|
||||||
day_end = datetime.datetime.combine(day_date, datetime.time.max)
|
day_end = datetime.datetime.combine(day_date, datetime.time.max)
|
||||||
# list_events already returns dicts.
|
|
||||||
sections["events"] = await list_events(
|
sections["events"] = await list_events(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
date_from=day_start,
|
date_from=day_start,
|
||||||
@@ -137,6 +148,156 @@ async def _open_threads(*, user_id: int, day_date: datetime.date) -> list[dict]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _render_sections_for_prompt(sections: dict) -> str:
|
||||||
|
"""Render the gathered sections as a structured plain-text block for the LLM."""
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
tasks = sections.get("tasks") or []
|
||||||
|
if tasks:
|
||||||
|
lines.append("TASKS (todo or in-progress):")
|
||||||
|
for t in tasks[:12]:
|
||||||
|
line = f" - {t.get('title', '?')}"
|
||||||
|
if t.get("due_date"):
|
||||||
|
line += f" (due {t['due_date']})"
|
||||||
|
if t.get("priority") and t["priority"] not in (None, "none"):
|
||||||
|
line += f" [{t['priority']} priority]"
|
||||||
|
if t.get("status") == "in_progress":
|
||||||
|
line += " [in progress]"
|
||||||
|
lines.append(line)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
events = sections.get("events") or []
|
||||||
|
if events:
|
||||||
|
lines.append("CALENDAR EVENTS TODAY:")
|
||||||
|
for e in events[:8]:
|
||||||
|
title = e.get("title", "Untitled")
|
||||||
|
when = e.get("start_dt", "?")
|
||||||
|
location = e.get("location") or ""
|
||||||
|
line = f" - {title} at {when}"
|
||||||
|
if location:
|
||||||
|
line += f" ({location})"
|
||||||
|
lines.append(line)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
weather = sections.get("weather") or []
|
||||||
|
if weather:
|
||||||
|
lines.append("WEATHER:")
|
||||||
|
for w in weather:
|
||||||
|
label = w.get("location_label") or w.get("location_key") or "Location"
|
||||||
|
forecast_json = w.get("forecast_json") or {}
|
||||||
|
daily = forecast_json.get("daily") or {}
|
||||||
|
today_max = (daily.get("temperature_2m_max") or [None])[0]
|
||||||
|
today_min = (daily.get("temperature_2m_min") or [None])[0]
|
||||||
|
precip = (daily.get("precipitation_probability_max") or [None])[0]
|
||||||
|
bits = [label]
|
||||||
|
if today_max is not None and today_min is not None:
|
||||||
|
bits.append(f"high {today_max}° / low {today_min}°")
|
||||||
|
if precip is not None:
|
||||||
|
bits.append(f"{precip}% chance of precipitation")
|
||||||
|
lines.append(" - " + ", ".join(bits))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
projects = sections.get("projects") or []
|
||||||
|
if projects:
|
||||||
|
lines.append("ACTIVE PROJECTS:")
|
||||||
|
for p in projects[:5]:
|
||||||
|
line = f" - {p.get('title', '?')}"
|
||||||
|
if p.get("auto_summary"):
|
||||||
|
summary = p["auto_summary"][:160]
|
||||||
|
line += f" — {summary}"
|
||||||
|
lines.append(line)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
recent_moments = sections.get("recent_moments") or []
|
||||||
|
if recent_moments:
|
||||||
|
lines.append("RECENT JOURNAL MOMENTS (last few days):")
|
||||||
|
for m in recent_moments[:8]:
|
||||||
|
day = m.get("day_date", "?")
|
||||||
|
content = (m.get("content") or "").strip()
|
||||||
|
lines.append(f" - [{day}] {content}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
open_threads = sections.get("open_threads") or []
|
||||||
|
if open_threads:
|
||||||
|
lines.append("OPEN THREADS (mentioned recently but not resolved):")
|
||||||
|
for m in open_threads[:5]:
|
||||||
|
day = m.get("day_date", "?")
|
||||||
|
content = (m.get("content") or "").strip()
|
||||||
|
lines.append(f" - [{day}] {content}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
return "(No data for today — quiet morning.)"
|
||||||
|
return "\n".join(lines).rstrip()
|
||||||
|
|
||||||
|
|
||||||
|
_PREP_SYSTEM_PROMPT = (
|
||||||
|
"You are opening the user's daily journal with a warm, conversational greeting. "
|
||||||
|
"Weave the data they're handing you into a flowing prose welcome — like a "
|
||||||
|
"thoughtful friend recapping what's on the user's plate today and gently inviting "
|
||||||
|
"them to journal about it.\n\n"
|
||||||
|
"Rules:\n"
|
||||||
|
"- Write flowing sentences. NO markdown, NO bullet points, NO headers.\n"
|
||||||
|
"- Reference items that genuinely matter; skip categories with no real data.\n"
|
||||||
|
"- If RECENT JOURNAL MOMENTS are present, briefly acknowledge what they were doing, thinking, or feeling.\n"
|
||||||
|
"- If OPEN THREADS are present, gently surface one or two as questions worth revisiting.\n"
|
||||||
|
"- Aim for 5 to 9 sentences. Warm but brief.\n"
|
||||||
|
"- Close with one short open invitation to journal — examples: \"What's on your mind?\", "
|
||||||
|
"\"How are you starting the day?\", \"Anything you want to set down before this kicks off?\"\n"
|
||||||
|
"- Don't fabricate details. If a category is empty, just skip it; don't lie about what's there.\n"
|
||||||
|
"- Don't list things mechanically (\"You have 3 tasks today...\"). Talk like a person."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_prep_text(day_date: datetime.date) -> str:
|
||||||
|
"""If the LLM call fails, return a minimal greeting so the user still sees something."""
|
||||||
|
weekday = day_date.strftime("%A")
|
||||||
|
return f"Good morning. {weekday}, {day_date.isoformat()}. What's on your mind?"
|
||||||
|
|
||||||
|
|
||||||
|
async def _generate_prep_prose(
|
||||||
|
*,
|
||||||
|
sections: dict,
|
||||||
|
day_date: datetime.date,
|
||||||
|
user_id: int,
|
||||||
|
) -> str:
|
||||||
|
"""Ask the LLM for a conversational journal opener built from the sections."""
|
||||||
|
from fabledassistant.services.llm import generate_completion
|
||||||
|
|
||||||
|
model = (await get_setting(user_id, "default_model", "")) or Config.OLLAMA_MODEL
|
||||||
|
if not model:
|
||||||
|
logger.warning("No LLM model configured for daily prep — using fallback text")
|
||||||
|
return _fallback_prep_text(day_date)
|
||||||
|
|
||||||
|
rendered = _render_sections_for_prompt(sections)
|
||||||
|
user_trigger = (
|
||||||
|
f"Today is {day_date.strftime('%A, %B %-d, %Y')} ({day_date.isoformat()}).\n\n"
|
||||||
|
f"Here is what I gathered for you:\n\n{rendered}\n\n"
|
||||||
|
f"Write the opener for today's journal."
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": _PREP_SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": user_trigger},
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
prose = await generate_completion(
|
||||||
|
messages=messages,
|
||||||
|
model=model,
|
||||||
|
max_tokens=600,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Daily prep prose generation failed for day %s", day_date)
|
||||||
|
return _fallback_prep_text(day_date)
|
||||||
|
|
||||||
|
prose = (prose or "").strip()
|
||||||
|
if not prose:
|
||||||
|
logger.warning("LLM returned empty prep prose for day %s — using fallback", day_date)
|
||||||
|
return _fallback_prep_text(day_date)
|
||||||
|
return prose
|
||||||
|
|
||||||
|
|
||||||
async def ensure_daily_prep_message(
|
async def ensure_daily_prep_message(
|
||||||
*,
|
*,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
@@ -146,8 +307,13 @@ async def ensure_daily_prep_message(
|
|||||||
) -> Message:
|
) -> Message:
|
||||||
"""Get or create today's journal Conversation, then ensure the prep message exists.
|
"""Get or create today's journal Conversation, then ensure the prep message exists.
|
||||||
|
|
||||||
With force=True, regenerate even if a prep message already exists today
|
The prep message is an *assistant* role message containing the prose opener,
|
||||||
(used by the manual /api/journal/trigger-prep endpoint).
|
with the structured sections preserved on ``msg_metadata``. If a legacy
|
||||||
|
system-role prep exists from an earlier version, it gets upgraded in place
|
||||||
|
on the next call.
|
||||||
|
|
||||||
|
With ``force=True`` the prose is regenerated even when a prep already exists.
|
||||||
|
Used by the manual /api/journal/trigger-prep endpoint.
|
||||||
"""
|
"""
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
@@ -168,33 +334,50 @@ async def ensure_daily_prep_message(
|
|||||||
session.add(conv)
|
session.add(conv)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
prep_stmt = select(Message).where(
|
# Find any existing prep (system or assistant role from any version).
|
||||||
Message.conversation_id == conv.id,
|
prep_stmt = select(Message).where(Message.conversation_id == conv.id)
|
||||||
Message.role == "system",
|
|
||||||
)
|
|
||||||
existing_prep = None
|
existing_prep = None
|
||||||
for msg in (await session.execute(prep_stmt)).scalars():
|
for msg in (await session.execute(prep_stmt)).scalars():
|
||||||
if msg.msg_metadata and msg.msg_metadata.get("kind") == "daily_prep":
|
if msg.msg_metadata and msg.msg_metadata.get("kind") == "daily_prep":
|
||||||
existing_prep = msg
|
existing_prep = msg
|
||||||
break
|
break
|
||||||
|
|
||||||
if existing_prep and not force:
|
# If we already have an assistant-role prep with prose content and the
|
||||||
|
# caller didn't ask to force regeneration, we're done.
|
||||||
|
if (
|
||||||
|
existing_prep
|
||||||
|
and not force
|
||||||
|
and existing_prep.role == "assistant"
|
||||||
|
and (existing_prep.content or "").strip()
|
||||||
|
):
|
||||||
return existing_prep
|
return existing_prep
|
||||||
|
|
||||||
sections = await generate_daily_prep(
|
sections = await gather_daily_sections(
|
||||||
user_id=user_id, day_date=day_date, user_timezone=user_timezone
|
user_id=user_id, day_date=day_date, user_timezone=user_timezone
|
||||||
)
|
)
|
||||||
|
prose = await _generate_prep_prose(
|
||||||
|
sections=sections, day_date=day_date, user_id=user_id
|
||||||
|
)
|
||||||
|
new_metadata = {"kind": "daily_prep", "sections": sections}
|
||||||
|
|
||||||
if existing_prep:
|
if existing_prep:
|
||||||
existing_prep.msg_metadata = {"kind": "daily_prep", "sections": sections}
|
# Upgrade in place: bump role, replace content + metadata.
|
||||||
|
existing_prep.role = "assistant"
|
||||||
|
existing_prep.content = prose
|
||||||
|
existing_prep.msg_metadata = new_metadata
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return existing_prep
|
return existing_prep
|
||||||
|
|
||||||
prep_msg = Message(
|
prep_msg = Message(
|
||||||
conversation_id=conv.id,
|
conversation_id=conv.id,
|
||||||
role="system",
|
role="assistant",
|
||||||
content="",
|
content=prose,
|
||||||
msg_metadata={"kind": "daily_prep", "sections": sections},
|
msg_metadata=new_metadata,
|
||||||
)
|
)
|
||||||
session.add(prep_msg)
|
session.add(prep_msg)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return prep_msg
|
return prep_msg
|
||||||
|
|
||||||
|
|
||||||
|
# Backwards-compat alias — older imports may use the old name.
|
||||||
|
generate_daily_prep = gather_daily_sections
|
||||||
|
|||||||
Reference in New Issue
Block a user