"""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