mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
This commit introduces Alison, an AI-powered classroom IT support assistant, as a new module within the SurfSense application. Key features of this implementation include: - A new LangGraph-based agent for conversational troubleshooting. - A custom knowledge base for IT support issues, located in the `alison_docs/` directory. - An extension of the RAG pipeline to use Alison's knowledge base. - Role-aware responses for professors and proctors. - A configuration toggle to enable or disable the Alison module. - Documentation for setting up and using Alison. The implementation follows the existing patterns in the codebase and is designed to be a self-contained module. Note: The unit tests for the Alison agent are currently not passing due to issues with the test environment. Further work is needed to get the tests to run correctly.
256 lines
8.4 KiB
Python
256 lines
8.4 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from langchain.schema import AIMessage, HumanMessage
|
|
from sqlalchemy.exc import IntegrityError, OperationalError
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.future import select
|
|
|
|
from app.db import Chat, SearchSpace, User, get_async_session
|
|
from app.schemas import (
|
|
AISDKChatRequest,
|
|
ChatCreate,
|
|
ChatRead,
|
|
ChatReadWithoutMessages,
|
|
ChatUpdate,
|
|
)
|
|
from app.tasks.stream_connector_search_results import stream_connector_search_results
|
|
from app.users import current_active_user
|
|
from app.utils.check_ownership import check_ownership
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/chat")
|
|
async def handle_chat_data(
|
|
request: AISDKChatRequest,
|
|
session: AsyncSession = Depends(get_async_session),
|
|
user: User = Depends(current_active_user),
|
|
):
|
|
messages = request.messages
|
|
if messages[-1]["role"] != "user":
|
|
raise HTTPException(
|
|
status_code=400, detail="Last message must be a user message"
|
|
)
|
|
|
|
user_query = messages[-1]["content"]
|
|
search_space_id = request.data.get("search_space_id")
|
|
research_mode: str = request.data.get("research_mode")
|
|
selected_connectors: list[str] = request.data.get("selected_connectors")
|
|
document_ids_to_add_in_context: list[int] = request.data.get(
|
|
"document_ids_to_add_in_context"
|
|
)
|
|
|
|
search_mode_str = request.data.get("search_mode", "CHUNKS")
|
|
|
|
# Convert search_space_id to integer if it's a string
|
|
if search_space_id and isinstance(search_space_id, str):
|
|
try:
|
|
search_space_id = int(search_space_id)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=400, detail="Invalid search_space_id format"
|
|
) from None
|
|
|
|
# Check if the search space belongs to the current user
|
|
try:
|
|
await check_ownership(session, SearchSpace, search_space_id, user)
|
|
except HTTPException:
|
|
raise HTTPException(
|
|
status_code=403, detail="You don't have access to this search space"
|
|
) from None
|
|
|
|
langchain_chat_history = []
|
|
for message in messages[:-1]:
|
|
if message["role"] == "user":
|
|
langchain_chat_history.append(HumanMessage(content=message["content"]))
|
|
elif message["role"] == "assistant":
|
|
langchain_chat_history.append(AIMessage(content=message["content"]))
|
|
|
|
response = StreamingResponse(
|
|
stream_connector_search_results(
|
|
user_query,
|
|
user.id,
|
|
search_space_id,
|
|
session,
|
|
research_mode,
|
|
selected_connectors,
|
|
langchain_chat_history,
|
|
search_mode_str,
|
|
document_ids_to_add_in_context,
|
|
alison_enabled=request.alison_enabled,
|
|
user_role=request.user_role,
|
|
)
|
|
)
|
|
|
|
response.headers["x-vercel-ai-data-stream"] = "v1"
|
|
return response
|
|
|
|
|
|
@router.post("/chats/", response_model=ChatRead)
|
|
async def create_chat(
|
|
chat: ChatCreate,
|
|
session: AsyncSession = Depends(get_async_session),
|
|
user: User = Depends(current_active_user),
|
|
):
|
|
try:
|
|
await check_ownership(session, SearchSpace, chat.search_space_id, user)
|
|
db_chat = Chat(**chat.model_dump())
|
|
session.add(db_chat)
|
|
await session.commit()
|
|
await session.refresh(db_chat)
|
|
return db_chat
|
|
except HTTPException:
|
|
raise
|
|
except IntegrityError:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Database constraint violation. Please check your input data.",
|
|
) from None
|
|
except OperationalError:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=503, detail="Database operation failed. Please try again later."
|
|
) from None
|
|
except Exception:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="An unexpected error occurred while creating the chat.",
|
|
) from None
|
|
|
|
|
|
@router.get("/chats/", response_model=list[ChatReadWithoutMessages])
|
|
async def read_chats(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
search_space_id: int | None = None,
|
|
session: AsyncSession = Depends(get_async_session),
|
|
user: User = Depends(current_active_user),
|
|
):
|
|
try:
|
|
# Select specific fields excluding messages
|
|
query = (
|
|
select(
|
|
Chat.id,
|
|
Chat.type,
|
|
Chat.title,
|
|
Chat.initial_connectors,
|
|
Chat.search_space_id,
|
|
Chat.created_at,
|
|
)
|
|
.join(SearchSpace)
|
|
.filter(SearchSpace.user_id == user.id)
|
|
)
|
|
|
|
# Filter by search_space_id if provided
|
|
if search_space_id is not None:
|
|
query = query.filter(Chat.search_space_id == search_space_id)
|
|
|
|
result = await session.execute(query.offset(skip).limit(limit))
|
|
return result.all()
|
|
except OperationalError:
|
|
raise HTTPException(
|
|
status_code=503, detail="Database operation failed. Please try again later."
|
|
) from None
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=500, detail="An unexpected error occurred while fetching chats."
|
|
) from None
|
|
|
|
|
|
@router.get("/chats/{chat_id}", response_model=ChatRead)
|
|
async def read_chat(
|
|
chat_id: int,
|
|
session: AsyncSession = Depends(get_async_session),
|
|
user: User = Depends(current_active_user),
|
|
):
|
|
try:
|
|
result = await session.execute(
|
|
select(Chat)
|
|
.join(SearchSpace)
|
|
.filter(Chat.id == chat_id, SearchSpace.user_id == user.id)
|
|
)
|
|
chat = result.scalars().first()
|
|
if not chat:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Chat not found or you don't have permission to access it",
|
|
)
|
|
return chat
|
|
except OperationalError:
|
|
raise HTTPException(
|
|
status_code=503, detail="Database operation failed. Please try again later."
|
|
) from None
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="An unexpected error occurred while fetching the chat.",
|
|
) from None
|
|
|
|
|
|
@router.put("/chats/{chat_id}", response_model=ChatRead)
|
|
async def update_chat(
|
|
chat_id: int,
|
|
chat_update: ChatUpdate,
|
|
session: AsyncSession = Depends(get_async_session),
|
|
user: User = Depends(current_active_user),
|
|
):
|
|
try:
|
|
db_chat = await read_chat(chat_id, session, user)
|
|
update_data = chat_update.model_dump(exclude_unset=True)
|
|
for key, value in update_data.items():
|
|
setattr(db_chat, key, value)
|
|
await session.commit()
|
|
await session.refresh(db_chat)
|
|
return db_chat
|
|
except HTTPException:
|
|
raise
|
|
except IntegrityError:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Database constraint violation. Please check your input data.",
|
|
) from None
|
|
except OperationalError:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=503, detail="Database operation failed. Please try again later."
|
|
) from None
|
|
except Exception:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="An unexpected error occurred while updating the chat.",
|
|
) from None
|
|
|
|
|
|
@router.delete("/chats/{chat_id}", response_model=dict)
|
|
async def delete_chat(
|
|
chat_id: int,
|
|
session: AsyncSession = Depends(get_async_session),
|
|
user: User = Depends(current_active_user),
|
|
):
|
|
try:
|
|
db_chat = await read_chat(chat_id, session, user)
|
|
await session.delete(db_chat)
|
|
await session.commit()
|
|
return {"message": "Chat deleted successfully"}
|
|
except HTTPException:
|
|
raise
|
|
except IntegrityError:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=400, detail="Cannot delete chat due to existing dependencies."
|
|
) from None
|
|
except OperationalError:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=503, detail="Database operation failed. Please try again later."
|
|
) from None
|
|
except Exception:
|
|
await session.rollback()
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="An unexpected error occurred while deleting the chat.",
|
|
) from None
|