diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index b4e94c732..4b6df350a 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -31,6 +31,7 @@ from .rbac_routes import router as rbac_router from .search_source_connectors_routes import router as search_source_connectors_router from .search_spaces_routes import router as search_spaces_router from .slack_add_connector_route import router as slack_add_connector_router +from .surfsense_docs_routes import router as surfsense_docs_router from .teams_add_connector_route import router as teams_add_connector_router router = APIRouter() @@ -59,3 +60,4 @@ router.include_router(clickup_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks +router.include_router(surfsense_docs_router) # Surfsense documentation for citations diff --git a/surfsense_backend/app/routes/surfsense_docs_routes.py b/surfsense_backend/app/routes/surfsense_docs_routes.py new file mode 100644 index 000000000..a2de65568 --- /dev/null +++ b/surfsense_backend/app/routes/surfsense_docs_routes.py @@ -0,0 +1,89 @@ +""" +Routes for Surfsense documentation. + +These endpoints support the citation system for Surfsense docs, +allowing the frontend to fetch document details when a user clicks +on a [citation:doc-XXX] link. +""" + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from app.db import ( + SurfsenseDocsChunk, + SurfsenseDocsDocument, + User, + get_async_session, +) +from app.schemas.surfsense_docs import ( + SurfsenseDocsChunkRead, + SurfsenseDocsDocumentWithChunksRead, +) +from app.users import current_active_user + +router = APIRouter() + + +@router.get( + "/surfsense-docs/by-chunk/{chunk_id}", + response_model=SurfsenseDocsDocumentWithChunksRead, +) +async def get_surfsense_doc_by_chunk_id( + chunk_id: int, + session: AsyncSession = Depends(get_async_session), + user: User = Depends(current_active_user), +): + """ + Retrieves a Surfsense documentation document based on a chunk ID. + + This endpoint is used by the frontend to resolve [citation:doc-XXX] links. + """ + try: + # Get the chunk + chunk_result = await session.execute( + select(SurfsenseDocsChunk).filter(SurfsenseDocsChunk.id == chunk_id) + ) + chunk = chunk_result.scalars().first() + + if not chunk: + raise HTTPException( + status_code=404, + detail=f"Surfsense docs chunk with id {chunk_id} not found", + ) + + # Get the associated document with all its chunks + document_result = await session.execute( + select(SurfsenseDocsDocument) + .options(selectinload(SurfsenseDocsDocument.chunks)) + .filter(SurfsenseDocsDocument.id == chunk.document_id) + ) + document = document_result.scalars().first() + + if not document: + raise HTTPException( + status_code=404, + detail="Surfsense docs document not found", + ) + + # Sort chunks by ID + sorted_chunks = sorted(document.chunks, key=lambda x: x.id) + + return SurfsenseDocsDocumentWithChunksRead( + id=document.id, + title=document.title, + source=document.source, + content=document.content, + chunks=[ + SurfsenseDocsChunkRead(id=c.id, content=c.content) + for c in sorted_chunks + ], + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to retrieve Surfsense documentation: {e!s}", + ) from e diff --git a/surfsense_backend/app/schemas/surfsense_docs.py b/surfsense_backend/app/schemas/surfsense_docs.py new file mode 100644 index 000000000..7464df342 --- /dev/null +++ b/surfsense_backend/app/schemas/surfsense_docs.py @@ -0,0 +1,27 @@ +""" +Schemas for Surfsense documentation. +""" + +from pydantic import BaseModel, ConfigDict + + +class SurfsenseDocsChunkRead(BaseModel): + """Schema for a Surfsense docs chunk.""" + + id: int + content: str + + model_config = ConfigDict(from_attributes=True) + + +class SurfsenseDocsDocumentWithChunksRead(BaseModel): + """Schema for a Surfsense docs document with its chunks.""" + + id: int + title: str + source: str + content: str + chunks: list[SurfsenseDocsChunkRead] + + model_config = ConfigDict(from_attributes=True) +