from __future__ import annotations from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel, Field from auth import require_auth from dialogue_store import StoreError, delete_conversation, get_conversation, list_messages from dialogue_turn import run_turn, visible_for_role from privacy_gateway import GatewayRequest, inspect from engine import EngineError from journal_generate import generate_draft from journal_policy import PolicyError from journal_store import ( create_day_conversation, create_user_space, day_payload, get_day, get_entry, get_or_create_day, get_user_space, list_days, list_user_spaces, list_versions, rename_user_space, restore_entry, save_day_scratch, save_entry, soft_delete_entry, ) from media_store import delete_media, get_media, list_media, save_media from writing_profile_store import ( accept_suggestion as accept_writing_suggestion, export_document as export_writing_document, get_profile, import_corpus, list_corpus, rebuild_brief, reject_suggestion as reject_writing_suggestion, remember_dialogue_style, remember_journal_entry, restore_document as restore_writing_document, set_governance as set_writing_governance, update_facet, update_trait, ) from profile_review import ( ingest_import, ingest_journal, import_result as import_review_result, open_paste_review, review_status, run_api_review, accept_proposal, reject_proposal, ) from interaction_profile_store import ( accept_suggestion as accept_interaction_suggestion, export_document as export_interaction_document, get_profile as get_interaction_profile, reject_suggestion as reject_interaction_suggestion, restore_document as restore_interaction_document, set_governance as set_interaction_governance, upsert_preference, ) from profile_documents import export_bundle, restore_bundle router = APIRouter(prefix="/api/journal", tags=["journal"]) def _http(exc: StoreError | PolicyError | EngineError): raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc class SpaceWrite(BaseModel): title: str = Field(min_length=1) class DayWrite(BaseModel): calendar_date: str = Field(min_length=10, max_length=10) class ConversationWrite(BaseModel): title: str = "" class TurnWrite(BaseModel): body: str = Field(min_length=1) id: str | None = None class GenerateWrite(BaseModel): conversation_ids: list[str] | None = None include_existing: bool = False class EntryWrite(BaseModel): journal_day_id: str title: str = "" body: str = "" entry_id: str | None = None origin: str = "user_edit" source_conversation_ids: list[str] = Field(default_factory=list) source_message_ids: list[str] = Field(default_factory=list) class RestoreWrite(BaseModel): version_id: str class ScratchItem(BaseModel): id: str | None = None text: str = "" done: bool = False class ScratchWrite(BaseModel): items: list[ScratchItem] = Field(default_factory=list) class ImportWrite(BaseModel): body: str = Field(min_length=1) occurred_at: str | None = None context_hint: str | None = None class CorpusWrite(BaseModel): items: list[dict] class TraitWrite(BaseModel): statement: str | None = None locked: bool | None = None facet_key: str | None = None label: str | None = None class GovernanceWrite(BaseModel): governance: str class FacetWrite(BaseModel): value: str | None = None locked: bool | None = None class PreferenceWrite(BaseModel): scope: str = "global" pref_key: str value: str locked: bool | None = None origin: str = "manual" class ReviewImportWrite(BaseModel): result: dict | str review_id: str | None = None class ReviewAcceptWrite(BaseModel): review_id: str | None = None indexes: list[int] | None = None baseline: bool = False @router.get("/spaces") def spaces(session: dict = Depends(require_auth)): return list_user_spaces(session["profile_id"]) @router.post("/spaces") def create_space(body: SpaceWrite, session: dict = Depends(require_auth)): try: return create_user_space(session["profile_id"], body.title) except StoreError as exc: _http(exc) @router.get("/spaces/{space_id}") def space_detail(space_id: str, session: dict = Depends(require_auth)): try: space = get_user_space(session["profile_id"], space_id) return {"space": space, "days": list_days(session["profile_id"], space_id)} except StoreError as exc: _http(exc) @router.patch("/spaces/{space_id}") def space_rename(space_id: str, body: SpaceWrite, session: dict = Depends(require_auth)): try: return rename_user_space(session["profile_id"], space_id, body.title) except StoreError as exc: _http(exc) @router.post("/spaces/{space_id}/days") def open_day(space_id: str, body: DayWrite, session: dict = Depends(require_auth)): try: day = get_or_create_day(session["profile_id"], space_id, body.calendar_date) return day_payload(session["profile_id"], day["id"]) except StoreError as exc: _http(exc) @router.get("/days/{journal_day_id}") def read_day(journal_day_id: str, session: dict = Depends(require_auth)): try: return day_payload(session["profile_id"], journal_day_id) except StoreError as exc: _http(exc) @router.patch("/days/{journal_day_id}/scratch") def write_scratch(journal_day_id: str, body: ScratchWrite, session: dict = Depends(require_auth)): try: return { "scratch": save_day_scratch( session["profile_id"], journal_day_id, [{"id": item.id, "text": item.text, "done": item.done} for item in body.items], ) } except StoreError as exc: _http(exc) @router.post("/days/{journal_day_id}/conversations") def start_conversation(journal_day_id: str, body: ConversationWrite, session: dict = Depends(require_auth)): try: return create_day_conversation(session["profile_id"], journal_day_id, body.title) except StoreError as exc: _http(exc) @router.delete("/conversations/{conversation_id}") def remove_conversation(conversation_id: str, session: dict = Depends(require_auth)): try: result = delete_conversation(session["profile_id"], conversation_id) remember_dialogue_style(session["profile_id"]) return result except StoreError as exc: _http(exc) @router.get("/conversations/{conversation_id}") def read_conversation(conversation_id: str, session: dict = Depends(require_auth)): try: conv = get_conversation(session["profile_id"], conversation_id) return {"conversation": conv, "messages": list_messages(session["profile_id"], conversation_id)} except StoreError as exc: _http(exc) @router.get("/egress-status") def egress_status(session: dict = Depends(require_auth)): result = inspect(GatewayRequest(prompt_id=None, purpose="dialogue_turn", data_class="B")) return {"ready": result.allowed, "reason": result.reason, "provider": result.provider} @router.post("/conversations/{conversation_id}/turn") def conversation_turn(conversation_id: str, body: TurnWrite, session: dict = Depends(require_auth)): try: return visible_for_role( run_turn(session["profile_id"], conversation_id, body.body, message_id=body.id), session.get("role"), ) except (StoreError, EngineError) as exc: _http(exc) @router.post("/days/{journal_day_id}/generate") def generate(journal_day_id: str, body: GenerateWrite, session: dict = Depends(require_auth)): try: return visible_for_role( generate_draft( session["profile_id"], journal_day_id, conversation_ids=body.conversation_ids, include_existing=body.include_existing, explicit=True, ), session.get("role"), ) except (StoreError, PolicyError, EngineError) as exc: _http(exc) @router.post("/entries") def write_entry(body: EntryWrite, session: dict = Depends(require_auth)): try: get_day(session["profile_id"], body.journal_day_id) entry = save_entry( session["profile_id"], body.journal_day_id, body.title, body.body, origin=body.origin, entry_id=body.entry_id, source_conversation_ids=body.source_conversation_ids, source_message_ids=body.source_message_ids, ) remember_journal_entry( session["profile_id"], entry["id"], entry.get("body") or "", origin=body.origin, ) ingest_journal( session["profile_id"], entry["id"], entry.get("body") or "", origin=body.origin, ) return entry except (StoreError, PolicyError) as exc: _http(exc) @router.get("/entries/{entry_id}") def read_entry(entry_id: str, session: dict = Depends(require_auth)): try: entry = get_entry(session["profile_id"], entry_id) entry["media"] = list_media(session["profile_id"], entry_id) entry["versions"] = list_versions(session["profile_id"], entry_id) return entry except StoreError as exc: _http(exc) @router.get("/entries/{entry_id}/versions") def entry_versions(entry_id: str, session: dict = Depends(require_auth)): try: return list_versions(session["profile_id"], entry_id) except StoreError as exc: _http(exc) @router.post("/entries/{entry_id}/restore") def entry_restore(entry_id: str, body: RestoreWrite, session: dict = Depends(require_auth)): try: entry = restore_entry(session["profile_id"], entry_id, body.version_id) remember_journal_entry( session["profile_id"], entry["id"], entry.get("body") or "", origin=entry.get("origin"), ) ingest_journal( session["profile_id"], entry["id"], entry.get("body") or "", origin=entry.get("origin"), ) return entry except StoreError as exc: _http(exc) @router.delete("/entries/{entry_id}") def entry_delete(entry_id: str, session: dict = Depends(require_auth)): try: return soft_delete_entry(session["profile_id"], entry_id) except StoreError as exc: _http(exc) @router.post("/entries/{entry_id}/media") async def entry_media(entry_id: str, file: UploadFile = File(...), session: dict = Depends(require_auth)): try: data = await file.read() return save_media( session["profile_id"], entry_id, file.filename or "media", file.content_type or "", data, ) except StoreError as exc: _http(exc) @router.get("/media/{media_id}") def read_media(media_id: str, session: dict = Depends(require_auth)): try: row, path = get_media(session["profile_id"], media_id) return FileResponse(path, media_type=row["content_type"], filename=row["filename"]) except StoreError as exc: _http(exc) @router.delete("/media/{media_id}") def remove_media(media_id: str, session: dict = Depends(require_auth)): try: return delete_media(session["profile_id"], media_id) except StoreError as exc: _http(exc) @router.get("/writing-profile") def writing_profile(session: dict = Depends(require_auth)): return get_profile(session["profile_id"]) @router.patch("/writing-profile") def writing_patch(body: GovernanceWrite, session: dict = Depends(require_auth)): try: return set_writing_governance(session["profile_id"], body.governance) except StoreError as exc: _http(exc) @router.patch("/writing-profile/facets/{facet_key}") def writing_facet(facet_key: str, body: FacetWrite, session: dict = Depends(require_auth)): try: return update_facet( session["profile_id"], facet_key, value=body.value, locked=body.locked, ) except StoreError as exc: _http(exc) @router.patch("/writing-profile/traits/{slug}") def writing_trait(slug: str, body: TraitWrite, session: dict = Depends(require_auth)): try: return update_trait( session["profile_id"], slug, statement=body.statement, locked=body.locked, facet_key=body.facet_key, label=body.label, ) except StoreError as exc: _http(exc) @router.post("/writing-profile/suggestions/{suggestion_id}/accept") def writing_accept(suggestion_id: str, session: dict = Depends(require_auth)): try: return accept_writing_suggestion(session["profile_id"], suggestion_id) except StoreError as exc: _http(exc) @router.post("/writing-profile/suggestions/{suggestion_id}/reject") def writing_reject(suggestion_id: str, session: dict = Depends(require_auth)): try: return reject_writing_suggestion(session["profile_id"], suggestion_id) except StoreError as exc: _http(exc) @router.post("/writing-profile/import") def writing_import(body: ImportWrite, session: dict = Depends(require_auth)): try: return ingest_import( session["profile_id"], body.body, occurred_at=body.occurred_at, context_hint=body.context_hint, ) except StoreError as exc: _http(exc) @router.post("/writing-profile/corpus") def writing_corpus(body: CorpusWrite, session: dict = Depends(require_auth)): try: return import_corpus(session["profile_id"], body.items) except StoreError as exc: _http(exc) @router.get("/writing-profile/corpus") def writing_corpus_list(session: dict = Depends(require_auth)): return {"items": list_corpus(session["profile_id"])} @router.post("/writing-profile/rebuild") def writing_rebuild(session: dict = Depends(require_auth)): return rebuild_brief(session["profile_id"]) @router.get("/writing-profile/review") def writing_review_status(session: dict = Depends(require_auth)): return review_status(session["profile_id"]) @router.post("/writing-profile/review/paste") def writing_review_paste(session: dict = Depends(require_auth)): try: return open_paste_review(session["profile_id"]) except StoreError as exc: _http(exc) @router.post("/writing-profile/initial-build/paste") def writing_initial_build_paste(session: dict = Depends(require_auth)): try: return open_paste_review(session["profile_id"], mode="initial_build") except StoreError as exc: _http(exc) @router.post("/writing-profile/review/api") def writing_review_api(session: dict = Depends(require_auth)): try: return run_api_review(session["profile_id"]) except (StoreError, EngineError) as exc: _http(exc) @router.post("/writing-profile/initial-build/api") def writing_initial_build_api(session: dict = Depends(require_auth)): try: return run_api_review(session["profile_id"], mode="initial_build") except (StoreError, EngineError) as exc: _http(exc) @router.post("/writing-profile/review/import") def writing_review_import(body: ReviewImportWrite, session: dict = Depends(require_auth)): try: return import_review_result(session["profile_id"], body.result, review_id=body.review_id) except StoreError as exc: _http(exc) @router.post("/writing-profile/review/accept") def writing_review_accept(body: ReviewAcceptWrite, session: dict = Depends(require_auth)): try: return accept_proposal( session["profile_id"], body.review_id, indexes=body.indexes, baseline=body.baseline, ) except StoreError as exc: _http(exc) @router.post("/writing-profile/review/reject") def writing_review_reject(body: ReviewAcceptWrite, session: dict = Depends(require_auth)): try: return reject_proposal(session["profile_id"], body.review_id, indexes=body.indexes) except StoreError as exc: _http(exc) @router.get("/writing-profile/export") def writing_export(session: dict = Depends(require_auth)): return export_writing_document(session["profile_id"]) @router.post("/writing-profile/restore") def writing_restore(body: dict, session: dict = Depends(require_auth)): try: return restore_writing_document(session["profile_id"], body) except StoreError as exc: _http(exc) @router.get("/interaction-profile") def interaction_profile(session: dict = Depends(require_auth)): return get_interaction_profile(session["profile_id"]) @router.patch("/interaction-profile") def interaction_patch(body: GovernanceWrite, session: dict = Depends(require_auth)): try: return set_interaction_governance(session["profile_id"], body.governance) except StoreError as exc: _http(exc) @router.patch("/interaction-profile/preferences") def interaction_pref(body: PreferenceWrite, session: dict = Depends(require_auth)): try: return upsert_preference( session["profile_id"], scope=body.scope, pref_key=body.pref_key, value=body.value, origin=body.origin if body.origin in {"explicit", "manual"} else "manual", locked=body.locked, ) except StoreError as exc: _http(exc) @router.post("/interaction-profile/suggestions/{suggestion_id}/accept") def interaction_accept(suggestion_id: str, session: dict = Depends(require_auth)): try: return accept_interaction_suggestion(session["profile_id"], suggestion_id) except StoreError as exc: _http(exc) @router.post("/interaction-profile/suggestions/{suggestion_id}/reject") def interaction_reject(suggestion_id: str, session: dict = Depends(require_auth)): try: return reject_interaction_suggestion(session["profile_id"], suggestion_id) except StoreError as exc: _http(exc) @router.get("/interaction-profile/export") def interaction_export(session: dict = Depends(require_auth)): return export_interaction_document(session["profile_id"]) @router.post("/interaction-profile/restore") def interaction_restore(body: dict, session: dict = Depends(require_auth)): try: return restore_interaction_document(session["profile_id"], body) except StoreError as exc: _http(exc) @router.get("/profiles/export") def profiles_export(session: dict = Depends(require_auth)): return export_bundle(session["profile_id"]) @router.post("/profiles/restore") def profiles_restore(body: dict, session: dict = Depends(require_auth)): try: return restore_bundle(session["profile_id"], body) except StoreError as exc: _http(exc)