feat: Implement Alison AI Classroom IT Support Assistant

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.
This commit is contained in:
google-labs-jules[bot] 2025-09-09 20:55:21 +00:00
parent 8f1fba52b4
commit f5ea337b75
17 changed files with 714 additions and 47 deletions

View file

@ -0,0 +1,63 @@
import os
import logging
from uuid import uuid4
from sqlalchemy.future import select
from app.db import async_session_maker, User, SearchSpace
from .markdown_processor import add_received_markdown_file_document
async def index_alison_docs():
"""
Indexes the documents in the alison_docs directory.
"""
async with async_session_maker() as session:
try:
# 1. Create or get the "alison" user
result = await session.execute(select(User).where(User.email == "alison@surfsense.ai"))
alison_user = result.scalars().first()
if not alison_user:
alison_user = User(
id=uuid4(),
email="alison@surfsense.ai",
hashed_password="dummy_password", # This should be handled more securely in a real application
is_active=True,
is_superuser=False,
is_verified=True,
)
session.add(alison_user)
await session.commit()
await session.refresh(alison_user)
# 2. Create or get the "Alison's Knowledge Base" search space
result = await session.execute(select(SearchSpace).where(SearchSpace.name == "Alison's Knowledge Base"))
alison_search_space = result.scalars().first()
if not alison_search_space:
alison_search_space = SearchSpace(
name="Alison's Knowledge Base",
description="Knowledge base for the Alison IT support assistant.",
user_id=alison_user.id,
)
session.add(alison_search_space)
await session.commit()
await session.refresh(alison_search_space)
# 3. Index the documents
alison_docs_dir = "app/alison_docs"
for filename in os.listdir(alison_docs_dir):
if filename.endswith(".md"):
filepath = os.path.join(alison_docs_dir, filename)
with open(filepath, "r") as f:
content = f.read()
await add_received_markdown_file_document(
session=session,
file_name=filename,
file_in_markdown=content,
search_space_id=alison_search_space.id,
user_id=str(alison_user.id),
)
logging.info("Alison's knowledge base indexed successfully.")
except Exception as e:
logging.error(f"Failed to index Alison's knowledge base: {e}")
await session.rollback()

View file

@ -6,7 +6,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.researcher.configuration import SearchMode
from app.agents.researcher.graph import graph as researcher_graph
from app.agents.researcher.state import State
from app.agents.researcher.state import State as ResearcherState
from app.agents.alison.graph import graph as alison_graph
from app.agents.alison.state import AlisonState
from app.services.streaming_service import StreamingService
@ -20,6 +22,8 @@ async def stream_connector_search_results(
langchain_chat_history: list[Any],
search_mode_str: str,
document_ids_to_add_in_context: list[int],
alison_enabled: bool = False,
user_role: str = "professor",
) -> AsyncGenerator[str, None]:
"""
Stream connector search results to the client
@ -31,60 +35,84 @@ async def stream_connector_search_results(
session: The database session
research_mode: The research mode
selected_connectors: List of selected connectors
alison_enabled: Whether the Alison agent is enabled
user_role: The user's role
Yields:
str: Formatted response strings
"""
streaming_service = StreamingService()
if research_mode == "REPORT_GENERAL":
num_sections = 1
elif research_mode == "REPORT_DEEP":
num_sections = 3
elif research_mode == "REPORT_DEEPER":
num_sections = 6
else:
# Default fallback
num_sections = 1
# Convert UUID to string if needed
user_id_str = str(user_id) if isinstance(user_id, UUID) else user_id
if search_mode_str == "CHUNKS":
search_mode = SearchMode.CHUNKS
elif search_mode_str == "DOCUMENTS":
search_mode = SearchMode.DOCUMENTS
# Simple keyword check to see if the query is IT support-related
it_support_keywords = ["projector", "mic", "microphone", "zoom", "display", "wifi", "internet"]
is_it_support_query = any(keyword in user_query.lower() for keyword in it_support_keywords)
# Sample configuration
config = {
"configurable": {
"user_query": user_query,
"num_sections": num_sections,
"connectors_to_search": selected_connectors,
"user_id": user_id_str,
"search_space_id": search_space_id,
"search_mode": search_mode,
"research_mode": research_mode,
"document_ids_to_add_in_context": document_ids_to_add_in_context,
if alison_enabled and is_it_support_query:
# Use the Alison agent
config = {
"configurable": {
"user_id": user_id_str,
"user_role": user_role,
}
}
}
# Initialize state with database session and streaming service
initial_state = State(
db_session=session,
streaming_service=streaming_service,
chat_history=langchain_chat_history,
)
initial_state = AlisonState(
user_query=user_query,
db_session=session,
streaming_service=streaming_service,
chat_history=langchain_chat_history,
identified_problem=None,
troubleshooting_steps=None,
visual_aids=None,
escalation_required=False,
final_response=None,
)
async for chunk in alison_graph.astream(
initial_state,
config=config,
stream_mode="custom",
):
if isinstance(chunk, dict) and "yield_value" in chunk:
yield chunk["yield_value"]
else:
# Use the Researcher agent
if research_mode == "REPORT_GENERAL":
num_sections = 1
elif research_mode == "REPORT_DEEP":
num_sections = 3
elif research_mode == "REPORT_DEEPER":
num_sections = 6
else:
num_sections = 1
# Run the graph directly
print("\nRunning the complete researcher workflow...")
if search_mode_str == "CHUNKS":
search_mode = SearchMode.CHUNKS
elif search_mode_str == "DOCUMENTS":
search_mode = SearchMode.DOCUMENTS
# Use streaming with config parameter
async for chunk in researcher_graph.astream(
initial_state,
config=config,
stream_mode="custom",
):
if isinstance(chunk, dict) and "yield_value" in chunk:
yield chunk["yield_value"]
config = {
"configurable": {
"user_query": user_query,
"num_sections": num_sections,
"connectors_to_search": selected_connectors,
"user_id": user_id_str,
"search_space_id": search_space_id,
"search_mode": search_mode,
"research_mode": research_mode,
"document_ids_to_add_in_context": document_ids_to_add_in_context,
}
}
initial_state = ResearcherState(
db_session=session,
streaming_service=streaming_service,
chat_history=langchain_chat_history,
)
async for chunk in researcher_graph.astream(
initial_state,
config=config,
stream_mode="custom",
):
if isinstance(chunk, dict) and "yield_value" in chunk:
yield chunk["yield_value"]
yield streaming_service.format_completion()