634 lines
19 KiB
Python
634 lines
19 KiB
Python
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)
|