Kansho/backend/journal_generate.py
2026-08-25 13:57:23 +02:00

111 lines
4.2 KiB
Python

"""Explicit journal draft generation. Never overwrites the current user entry."""
from __future__ import annotations
from context_builder import assemble_text, build_internal_context
from dialogue_store import StoreError, list_conversations_for_day, list_messages
from engine import execute_prompt, load_active_prompt
from journal_policy import require_explicit_generate, source_conversation_ids
from identity_store import list_mappings
from journal_body import clean_title
from journal_shape import shape_journal
from journal_store import current_draft, current_entries, get_day, insert_draft
from writing_profile_store import compile_style_signals, remember_dialogue_style
def _message_ids(profile_id: str, conversation_ids: list[str]) -> list[str]:
ids: list[str] = []
for conversation_id in conversation_ids:
for message in list_messages(profile_id, conversation_id):
ids.append(message["id"])
return ids
def _split_title(content: str) -> tuple[str, str]:
text = (content or "").strip()
if not text:
return "", ""
lines = text.splitlines()
title = clean_title(lines[0])
body = "\n".join(lines[1:]).strip() if len(lines) > 1 else text
if not title or len(title) > 80:
return "", text
return title, body or text
def generate_draft(
profile_id: str,
journal_day_id: str,
conversation_ids: list[str] | None = None,
include_existing: bool = False,
explicit: bool = True,
) -> dict:
require_explicit_generate(explicit)
day = get_day(profile_id, journal_day_id)
day_conversations = list_conversations_for_day(profile_id, journal_day_id)
selected = source_conversation_ids(
conversation_ids,
[item["id"] for item in day_conversations],
)
remember_dialogue_style(profile_id, exclude_conversation_ids=selected)
existing_text = ""
if include_existing:
draft = current_draft(profile_id, journal_day_id)
entries = current_entries(profile_id, journal_day_id)
if draft:
existing_text = draft.get("body") or ""
elif entries:
existing_text = entries[0].get("body") or ""
context = build_internal_context(
profile_id,
space_id=day["space_id"],
journal_day_id=journal_day_id,
purpose="journal_generate",
include_existing=include_existing,
conversation_ids=selected,
existing_text=existing_text,
conversation_id=selected[0] if selected else None,
)
assembled = assemble_text(context)
source_user = [
message.get("body") or ""
for conversation_id in selected
for message in list_messages(profile_id, conversation_id)
if message.get("role") == "user"
]
signals = compile_style_signals(source_user)
profile = assembled.get("writing_profile") or ""
if signals and signals not in profile:
assembled["writing_profile"] = "\n\n".join(part for part in (profile, signals) if part)
prompt = load_active_prompt("mvp.journal_generate")
result = execute_prompt(
prompt,
profile_id,
purpose="journal_generate",
data_class="B",
context=assembled,
)
title, body = _split_title(result.get("content") or "")
person_labels = [
(item.get("local_label") or "").strip()
for item in list_mappings(profile_id)
if (item.get("local_label") or "").strip()
and str(item.get("token") or "").upper().lstrip("[").startswith("PERSON:")
]
title, body = shape_journal(title, body, source_user, person_labels=person_labels)
before_entries = {item["id"]: item.get("current_version_id") for item in current_entries(profile_id, journal_day_id)}
draft = insert_draft(
profile_id,
journal_day_id,
title=title,
body=body,
source_conversation_ids=selected,
source_message_ids=_message_ids(profile_id, selected),
)
after_entries = current_entries(profile_id, journal_day_id)
for item in after_entries:
previous = before_entries.get(item["id"])
if previous is not None and previous != item.get("current_version_id"):
raise StoreError("policy_violation", "Generate darf die Nutzerfassung nicht verändern", 500)
draft["trace"] = result.get("trace")
return draft