2025-10-18 23:46:22 +08:00
|
|
|
from typing import List, Literal, Optional
|
2025-07-17 19:36:11 +08:00
|
|
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
|
from loguru import logger
|
|
|
|
|
|
|
|
|
|
from api.models import NoteCreate, NoteResponse, NoteUpdate
|
|
|
|
|
from open_notebook.domain.notebook import Note
|
|
|
|
|
from open_notebook.exceptions import InvalidInputError
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/notes", response_model=List[NoteResponse])
|
|
|
|
|
async def get_notes(
|
|
|
|
|
notebook_id: Optional[str] = Query(None, description="Filter by notebook ID")
|
|
|
|
|
):
|
|
|
|
|
"""Get all notes with optional notebook filtering."""
|
|
|
|
|
try:
|
|
|
|
|
if notebook_id:
|
|
|
|
|
# Get notes for a specific notebook
|
|
|
|
|
from open_notebook.domain.notebook import Notebook
|
|
|
|
|
notebook = await Notebook.get(notebook_id)
|
|
|
|
|
if not notebook:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
|
notes = await notebook.get_notes()
|
|
|
|
|
else:
|
|
|
|
|
# Get all notes
|
|
|
|
|
notes = await Note.get_all(order_by="updated desc")
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
NoteResponse(
|
2025-10-18 23:46:22 +08:00
|
|
|
id=note.id or "",
|
2025-07-17 19:36:11 +08:00
|
|
|
title=note.title,
|
|
|
|
|
content=note.content,
|
|
|
|
|
note_type=note.note_type,
|
|
|
|
|
created=str(note.created),
|
|
|
|
|
updated=str(note.updated),
|
|
|
|
|
)
|
|
|
|
|
for note in notes
|
|
|
|
|
]
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error fetching notes: {str(e)}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Error fetching notes: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/notes", response_model=NoteResponse)
|
|
|
|
|
async def create_note(note_data: NoteCreate):
|
|
|
|
|
"""Create a new note."""
|
|
|
|
|
try:
|
|
|
|
|
# Auto-generate title if not provided and it's an AI note
|
|
|
|
|
title = note_data.title
|
|
|
|
|
if not title and note_data.note_type == "ai" and note_data.content:
|
|
|
|
|
from open_notebook.graphs.prompt import graph as prompt_graph
|
|
|
|
|
prompt = "Based on the Note below, please provide a Title for this content, with max 15 words"
|
2025-10-18 23:46:22 +08:00
|
|
|
result = await prompt_graph.ainvoke(
|
|
|
|
|
{ # type: ignore[arg-type]
|
|
|
|
|
"input_text": note_data.content,
|
|
|
|
|
"prompt": prompt
|
|
|
|
|
}
|
|
|
|
|
)
|
2025-07-17 19:36:11 +08:00
|
|
|
title = result.get("output", "Untitled Note")
|
|
|
|
|
|
2025-10-18 23:46:22 +08:00
|
|
|
# Validate note_type
|
|
|
|
|
note_type: Optional[Literal["human", "ai"]] = None
|
|
|
|
|
if note_data.note_type in ("human", "ai"):
|
|
|
|
|
note_type = note_data.note_type # type: ignore[assignment]
|
|
|
|
|
elif note_data.note_type is not None:
|
|
|
|
|
raise HTTPException(status_code=400, detail="note_type must be 'human' or 'ai'")
|
|
|
|
|
|
2025-07-17 19:36:11 +08:00
|
|
|
new_note = Note(
|
|
|
|
|
title=title,
|
|
|
|
|
content=note_data.content,
|
2025-10-18 23:46:22 +08:00
|
|
|
note_type=note_type,
|
2025-07-17 19:36:11 +08:00
|
|
|
)
|
|
|
|
|
await new_note.save()
|
|
|
|
|
|
|
|
|
|
# Add to notebook if specified
|
|
|
|
|
if note_data.notebook_id:
|
|
|
|
|
from open_notebook.domain.notebook import Notebook
|
|
|
|
|
notebook = await Notebook.get(note_data.notebook_id)
|
|
|
|
|
if not notebook:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
|
await new_note.add_to_notebook(note_data.notebook_id)
|
|
|
|
|
|
|
|
|
|
return NoteResponse(
|
2025-10-18 23:46:22 +08:00
|
|
|
id=new_note.id or "",
|
2025-07-17 19:36:11 +08:00
|
|
|
title=new_note.title,
|
|
|
|
|
content=new_note.content,
|
|
|
|
|
note_type=new_note.note_type,
|
|
|
|
|
created=str(new_note.created),
|
|
|
|
|
updated=str(new_note.updated),
|
|
|
|
|
)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except InvalidInputError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error creating note: {str(e)}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Error creating note: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/notes/{note_id}", response_model=NoteResponse)
|
|
|
|
|
async def get_note(note_id: str):
|
|
|
|
|
"""Get a specific note by ID."""
|
|
|
|
|
try:
|
|
|
|
|
note = await Note.get(note_id)
|
|
|
|
|
if not note:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Note not found")
|
|
|
|
|
|
|
|
|
|
return NoteResponse(
|
2025-10-18 23:46:22 +08:00
|
|
|
id=note.id or "",
|
2025-07-17 19:36:11 +08:00
|
|
|
title=note.title,
|
|
|
|
|
content=note.content,
|
|
|
|
|
note_type=note.note_type,
|
|
|
|
|
created=str(note.created),
|
|
|
|
|
updated=str(note.updated),
|
|
|
|
|
)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error fetching note {note_id}: {str(e)}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Error fetching note: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/notes/{note_id}", response_model=NoteResponse)
|
|
|
|
|
async def update_note(note_id: str, note_update: NoteUpdate):
|
|
|
|
|
"""Update a note."""
|
|
|
|
|
try:
|
|
|
|
|
note = await Note.get(note_id)
|
|
|
|
|
if not note:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Note not found")
|
|
|
|
|
|
|
|
|
|
# Update only provided fields
|
|
|
|
|
if note_update.title is not None:
|
|
|
|
|
note.title = note_update.title
|
|
|
|
|
if note_update.content is not None:
|
|
|
|
|
note.content = note_update.content
|
|
|
|
|
if note_update.note_type is not None:
|
2025-10-18 23:46:22 +08:00
|
|
|
if note_update.note_type in ("human", "ai"):
|
|
|
|
|
note.note_type = note_update.note_type # type: ignore[assignment]
|
|
|
|
|
else:
|
|
|
|
|
raise HTTPException(status_code=400, detail="note_type must be 'human' or 'ai'")
|
|
|
|
|
|
2025-07-17 19:36:11 +08:00
|
|
|
await note.save()
|
2025-10-18 23:46:22 +08:00
|
|
|
|
2025-07-17 19:36:11 +08:00
|
|
|
return NoteResponse(
|
2025-10-18 23:46:22 +08:00
|
|
|
id=note.id or "",
|
2025-07-17 19:36:11 +08:00
|
|
|
title=note.title,
|
|
|
|
|
content=note.content,
|
|
|
|
|
note_type=note.note_type,
|
|
|
|
|
created=str(note.created),
|
|
|
|
|
updated=str(note.updated),
|
|
|
|
|
)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except InvalidInputError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error updating note {note_id}: {str(e)}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Error updating note: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/notes/{note_id}")
|
|
|
|
|
async def delete_note(note_id: str):
|
|
|
|
|
"""Delete a note."""
|
|
|
|
|
try:
|
|
|
|
|
note = await Note.get(note_id)
|
|
|
|
|
if not note:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Note not found")
|
|
|
|
|
|
|
|
|
|
await note.delete()
|
|
|
|
|
|
|
|
|
|
return {"message": "Note deleted successfully"}
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error deleting note {note_id}: {str(e)}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Error deleting note: {str(e)}")
|