""" Notebook API Router Provides notebook creation, querying, updating, deletion, and record management functions """ import json from typing import AsyncGenerator, Literal from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel from deeptutor.agents.notebook import NotebookSummarizeAgent from deeptutor.services.notebook import notebook_manager router = APIRouter() # === Request/Response Models === class CreateNotebookRequest(BaseModel): """Create notebook request""" name: str description: str = "" color: str = "#3B82F6" icon: str = "book" class UpdateNotebookRequest(BaseModel): """Update notebook request""" name: str | None = None description: str | None = None color: str | None = None icon: str | None = None class AddRecordRequest(BaseModel): """Add record request""" notebook_ids: list[str] record_type: Literal["solve", "question", "research", "co_writer", "chat", "guided_learning"] title: str summary: str = "" user_query: str output: str metadata: dict = {} kb_name: str | None = None class RemoveRecordRequest(BaseModel): """Remove record request""" record_id: str class UpdateRecordRequest(BaseModel): """Update an existing notebook record.""" title: str | None = None summary: str | None = None user_query: str | None = None output: str | None = None metadata: dict | None = None kb_name: str | None = None # === API Endpoints === async def _build_record_summary(request: AddRecordRequest) -> str: if request.summary.strip(): return request.summary.strip() agent = NotebookSummarizeAgent(language=str(request.metadata.get("ui_language", "en"))) return await agent.summarize( title=request.title, record_type=request.record_type, user_query=request.user_query, output=request.output, metadata=request.metadata, ) async def _stream_add_record_with_summary( request: AddRecordRequest, ) -> AsyncGenerator[str, None]: try: agent = NotebookSummarizeAgent(language=str(request.metadata.get("ui_language", "en"))) summary_parts: list[str] = [] if request.summary.strip(): summary_parts.append(request.summary.strip()) yield f"data: {json.dumps({'type': 'summary_chunk', 'content': request.summary.strip()}, ensure_ascii=False)}\n\n" else: async for chunk in agent.stream_summary( title=request.title, record_type=request.record_type, user_query=request.user_query, output=request.output, metadata=request.metadata, ): if not chunk: continue summary_parts.append(chunk) yield f"data: {json.dumps({'type': 'summary_chunk', 'content': chunk}, ensure_ascii=False)}\n\n" summary = "".join(summary_parts).strip() result = notebook_manager.add_record( notebook_ids=request.notebook_ids, record_type=request.record_type, title=request.title, summary=summary, user_query=request.user_query, output=request.output, metadata=request.metadata, kb_name=request.kb_name, ) payload = { "type": "result", "success": True, "summary": summary, "record": result["record"], "added_to_notebooks": result["added_to_notebooks"], } yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" except Exception as exc: payload = {"type": "error", "detail": str(exc)} yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" @router.get("/list") async def list_notebooks(): """ Get all notebook list Returns: Notebook list (includes summary information) """ try: notebooks = notebook_manager.list_notebooks() return {"notebooks": notebooks, "total": len(notebooks)} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/statistics") async def get_statistics(): """ Get notebook statistics Returns: Statistics information """ try: stats = notebook_manager.get_statistics() return stats except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/create") async def create_notebook(request: CreateNotebookRequest): """ Create new notebook Args: request: Create request Returns: Created notebook information """ try: notebook = notebook_manager.create_notebook( name=request.name, description=request.description, color=request.color, icon=request.icon, ) return {"success": True, "notebook": notebook} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/{notebook_id}") async def get_notebook(notebook_id: str): """ Get notebook details Args: notebook_id: Notebook ID Returns: Notebook details (includes all records) """ try: notebook = notebook_manager.get_notebook(notebook_id) if not notebook: raise HTTPException(status_code=404, detail="Notebook not found") return notebook except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/{notebook_id}") async def update_notebook(notebook_id: str, request: UpdateNotebookRequest): """ Update notebook information Args: notebook_id: Notebook ID request: Update request Returns: Updated notebook information """ try: notebook = notebook_manager.update_notebook( notebook_id=notebook_id, name=request.name, description=request.description, color=request.color, icon=request.icon, ) if not notebook: raise HTTPException(status_code=404, detail="Notebook not found") return {"success": True, "notebook": notebook} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.delete("/{notebook_id}") async def delete_notebook(notebook_id: str): """ Delete notebook Args: notebook_id: Notebook ID Returns: Deletion result """ try: success = notebook_manager.delete_notebook(notebook_id) if not success: raise HTTPException(status_code=404, detail="Notebook not found") return {"success": True, "message": "Notebook deleted successfully"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/add_record") async def add_record(request: AddRecordRequest): """ Add record to notebook Args: request: Add record request Returns: Addition result """ try: summary = await _build_record_summary(request) result = notebook_manager.add_record( notebook_ids=request.notebook_ids, record_type=request.record_type, title=request.title, summary=summary, user_query=request.user_query, output=request.output, metadata=request.metadata, kb_name=request.kb_name, ) return { "success": True, "summary": summary, "record": result["record"], "added_to_notebooks": result["added_to_notebooks"], } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/add_record_with_summary") async def add_record_with_summary(request: AddRecordRequest): """Add record to notebook and stream generated summary.""" return StreamingResponse( _stream_add_record_with_summary(request), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, ) @router.delete("/{notebook_id}/records/{record_id}") async def remove_record(notebook_id: str, record_id: str): """ Remove record from notebook Args: notebook_id: Notebook ID record_id: Record ID Returns: Deletion result """ try: success = notebook_manager.remove_record(notebook_id, record_id) if not success: raise HTTPException(status_code=404, detail="Record not found") return {"success": True, "message": "Record removed successfully"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/{notebook_id}/records/{record_id}") async def update_record(notebook_id: str, record_id: str, request: UpdateRecordRequest): """Update an existing notebook record in place.""" try: updated = notebook_manager.update_record( notebook_id=notebook_id, record_id=record_id, title=request.title, summary=request.summary, user_query=request.user_query, output=request.output, metadata=request.metadata, kb_name=request.kb_name, ) if not updated: raise HTTPException(status_code=404, detail="Record not found") return {"success": True, "record": updated} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/health") async def health_check(): """Health check""" return {"status": "healthy", "service": "notebook"}