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,49 @@
from langgraph.graph import StateGraph, END
from .state import AlisonState
from .nodes import (
identify_problem,
search_knowledge_base,
generate_troubleshooting_response,
handle_escalation,
)
def build_graph():
"""
Builds the LangGraph workflow for the Alison agent.
"""
workflow = StateGraph(AlisonState)
workflow.add_node("identify_problem", identify_problem)
workflow.add_node("search_knowledge_base", search_knowledge_base)
workflow.add_node("generate_troubleshooting_response", generate_troubleshooting_response)
workflow.add_node("handle_escalation", handle_escalation)
workflow.set_entry_point("identify_problem")
workflow.add_edge("identify_problem", "search_knowledge_base")
workflow.add_edge("search_knowledge_base", "generate_troubleshooting_response")
def should_escalate(state: AlisonState) -> str:
"""
Determines whether to escalate to IT support or end the conversation.
"""
if state.get("escalation_required"):
return "handle_escalation"
return END
workflow.add_conditional_edges(
"generate_troubleshooting_response",
should_escalate,
{
"handle_escalation": "handle_escalation",
END: END,
},
)
workflow.add_edge("handle_escalation", END)
graph = workflow.compile()
graph.name = "Alison IT Support Assistant"
return graph
graph = build_graph()

View file

@ -0,0 +1,129 @@
import json
from typing import Any, List
from langchain_core.runnables import RunnableConfig
from langgraph.types import StreamWriter
from .state import AlisonState
from .prompts import (
get_alison_system_prompt,
get_problem_identification_prompt,
get_escalation_prompt,
)
from langchain_core.messages import SystemMessage, HumanMessage
from app.services.llm_service import get_user_fast_llm
async def identify_problem(state: AlisonState, config: RunnableConfig, writer: StreamWriter) -> dict[str, Any]:
"""
Identifies the user's problem based on their query.
"""
user_id = config["configurable"]["user_id"]
user_query = state["user_query"]
llm = await get_user_fast_llm(state["db_session"], user_id)
if not llm:
# Handle case where LLM is not configured
# For now, we'll just return a default problem
return {"identified_problem": "Could not identify problem: LLM not configured."}
prompt = get_problem_identification_prompt().format(user_query=user_query)
messages = [
SystemMessage(content=get_alison_system_prompt()),
HumanMessage(content=prompt),
]
response = await llm.ainvoke(messages)
identified_problem = response.content.strip()
return {"identified_problem": identified_problem}
from app.retriever.alison_knowledge_retriever import AlisonKnowledgeRetriever
async def search_knowledge_base(state: AlisonState, config: RunnableConfig, writer: StreamWriter) -> dict[str, Any]:
"""
Searches the knowledge base for troubleshooting guides related to the identified problem.
"""
identified_problem = state["identified_problem"]
if not identified_problem:
return {"troubleshooting_steps": [], "visual_aids": []}
retriever = AlisonKnowledgeRetriever(state["db_session"])
documents = await retriever.hybrid_search(identified_problem, top_k=3)
if not documents:
return {"troubleshooting_steps": [], "visual_aids": []}
# For now, we'll just return the content of the first document.
# We can improve this later to synthesize an answer from multiple documents.
first_document_content = documents[0]["content"]
return {"troubleshooting_steps": [first_document_content], "visual_aids": []}
async def generate_troubleshooting_response(state: AlisonState, config: RunnableConfig, writer: StreamWriter) -> dict[str, Any]:
"""
Generates a response with troubleshooting steps and visual aids.
"""
troubleshooting_steps = state.get("troubleshooting_steps")
if not troubleshooting_steps:
return {"escalation_required": True}
user_role = config["configurable"].get("user_role", "professor")
content = troubleshooting_steps[0] # Assuming only one document is returned for now
# Simple markdown parsing
sections = {}
current_section = None
for line in content.split('\\n'):
if line.startswith("## "):
current_section = line[3:].strip()
sections[current_section] = []
elif current_section:
sections[current_section].append(line)
response_parts = []
if "Issue" in sections:
response_parts.append(f"I understand you're having an issue with: **{''.join(sections['Issue'])}**")
response_parts.append("Here are some steps you can try:")
if "Troubleshooting Steps" in sections:
response_parts.extend(sections["Troubleshooting Steps"])
if "Role-Specific Advice" in sections:
advice_text = '\\n'.join(sections['Role-Specific Advice'])
if f"- **{user_role.capitalize()}:**" in advice_text:
role_advice = [line for line in advice_text.split('\\n') if line.startswith(f"- **{user_role.capitalize()}:**")]
if role_advice:
response_parts.append("\\n**Advice for you as a {user_role}:**")
response_parts.append(role_advice[0].replace(f"- **{user_role.capitalize()}:**", "").strip())
if "Visual Aid" in sections:
for line in sections["Visual Aid"]:
if "[Image:" in line:
response_parts.append(f"You can also refer to this visual guide: {line}")
final_response = "\\n".join(response_parts)
return {"final_response": final_response}
async def handle_escalation(state: AlisonState, config: RunnableConfig, writer: StreamWriter) -> dict[str, Any]:
"""
Generates a response for escalating the issue to IT support.
"""
user_id = config["configurable"]["user_id"]
identified_problem = state["identified_problem"]
llm = await get_user_fast_llm(state["db_session"], user_id)
if not llm:
return {"final_response": "I am unable to resolve this issue. Please contact IT support."}
prompt = get_escalation_prompt().format(identified_problem=identified_problem)
messages = [
SystemMessage(content=get_alison_system_prompt()),
HumanMessage(content=prompt),
]
response = await llm.ainvoke(messages)
escalation_message = response.content.strip()
return {"final_response": escalation_message}

View file

@ -0,0 +1,43 @@
def get_alison_system_prompt():
"""
Returns the system prompt for the Alison agent.
"""
return """\
You are Alison, a friendly and helpful AI Classroom IT Support Assistant. Your goal is to help users troubleshoot common classroom technology issues.
You have access to a knowledge base of troubleshooting guides. When a user describes a problem, your primary goal is to identify the issue and provide clear, step-by-step instructions based on the information in the knowledge base.
- If the user's query is ambiguous, ask clarifying questions to better understand the problem.
- Always be polite and empathetic.
- If a troubleshooting guide provides role-specific advice (e.g., for a "professor" or "proctor"), tailor your response to the user's role.
- If a guide includes a link to a visual aid, make sure to include it in your response.
- If the troubleshooting steps do not resolve the issue, or if the guide suggests escalating to IT support, your final step should be to recommend contacting IT support and provide instructions on how to do so.
"""
def get_problem_identification_prompt():
"""
Returns the prompt for identifying the user's problem from the query.
"""
return """\
Given the user's query, identify the specific technical problem they are facing.
Your response should be a short, concise phrase that summarizes the problem.
User Query: {user_query}
Identified Problem:
"""
def get_escalation_prompt():
"""
Returns the prompt for generating an escalation message.
"""
return """\
The troubleshooting steps have not resolved the issue. Generate a friendly message that informs the user that the problem requires assistance from IT support.
The message should include:
- A summary of the problem.
- A recommendation to contact IT support.
- Instructions on how to contact IT support (e.g., "You can contact IT support by visiting the help desk at [Location] or by calling [Phone Number].").
Problem: {identified_problem}
"""

View file

@ -0,0 +1,16 @@
from typing import TypedDict, List, Any
class AlisonState(TypedDict):
"""
Represents the state of the Alison agent's workflow.
"""
user_query: str
identified_problem: str | None
troubleshooting_steps: List[str] | None
visual_aids: List[str] | None
escalation_required: bool
user_role: str # "professor" or "proctor"
final_response: str | None
db_session: Any
chat_history: Any
streaming_service: Any