create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from shiny import App, ui, reactive, render
|
| 2 |
+
import uuid
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from chatbot import ask_question, clear_conversation
|
| 8 |
+
from shinyswatch import theme
|
| 9 |
+
|
| 10 |
+
# Define UI
|
| 11 |
+
app_ui = ui.page_fluid(
|
| 12 |
+
ui.panel_title("American University Academic Advisor Chatbot"),
|
| 13 |
+
|
| 14 |
+
# Use Bootstrap row/column layout
|
| 15 |
+
ui.row(
|
| 16 |
+
# Settings panel (left side)
|
| 17 |
+
ui.column(3,
|
| 18 |
+
ui.div(
|
| 19 |
+
{"class": "card mb-3"},
|
| 20 |
+
ui.div(
|
| 21 |
+
{"class": "card-header"},
|
| 22 |
+
ui.h4("Settings", class_="mb-0")
|
| 23 |
+
),
|
| 24 |
+
ui.div(
|
| 25 |
+
{"class": "card-body"},
|
| 26 |
+
ui.tags.div(
|
| 27 |
+
{"class": "mb-3"},
|
| 28 |
+
ui.tags.label("Session ID:", class_="form-label"),
|
| 29 |
+
ui.tags.div(str(uuid.uuid4()), class_="form-text")
|
| 30 |
+
),
|
| 31 |
+
ui.input_action_button("clear_chat", "Clear Chat History",
|
| 32 |
+
class_="btn btn-warning btn-block mb-3 w-100"),
|
| 33 |
+
# Export button
|
| 34 |
+
ui.download_button("download_chat", "Export Chat History",
|
| 35 |
+
class_="btn btn-primary btn-block mb-3 w-100"),
|
| 36 |
+
ui.hr(),
|
| 37 |
+
ui.h5("Model Parameters"),
|
| 38 |
+
ui.input_slider("n_results", "Documents to retrieve",
|
| 39 |
+
min=0, max=16, value=8),
|
| 40 |
+
ui.input_slider("temperature", "Temperature",
|
| 41 |
+
min=0.1, max=1.0, value=0.7, step=0.1),
|
| 42 |
+
ui.hr(),
|
| 43 |
+
ui.h5("About/Warning"),
|
| 44 |
+
ui.p("This AI advisor is an on-going student research project using a RAG architecture with Python, a Chroma database and the Mistral 7B LLM. It provides answers to questions about American University's academic offerings related to the Math/Stat Department. While it draws from authoritative sources, it is known to produce some answers that are incomplete, inaccurate, or irrelevant. All responses should be checked with the references and one's human advisor.")
|
| 45 |
+
)
|
| 46 |
+
)
|
| 47 |
+
),
|
| 48 |
+
|
| 49 |
+
# Chat area (right side)
|
| 50 |
+
ui.column(9,
|
| 51 |
+
ui.div(
|
| 52 |
+
{"class": "card h-100"},
|
| 53 |
+
ui.div(
|
| 54 |
+
{"class": "card-body"},
|
| 55 |
+
# Chat UI
|
| 56 |
+
ui.chat_ui("academic_chat", width="100%", height="75vh")
|
| 57 |
+
)
|
| 58 |
+
)
|
| 59 |
+
)
|
| 60 |
+
),
|
| 61 |
+
# Named parameters after all positional arguments
|
| 62 |
+
title="American University Academic Advisor",
|
| 63 |
+
theme=theme.flatly # flatly is good for color-blind users (high contrast, distinguishable colors)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
# Define server
|
| 67 |
+
def server(input, output, session):
|
| 68 |
+
# Initialize the Shiny Chat UI
|
| 69 |
+
chat = ui.Chat(
|
| 70 |
+
id="academic_chat",
|
| 71 |
+
messages=[{"role": "assistant", "content": "Hello! I'm your American University Academic Advisor. How can I help you today?"}]
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# Handle clear chat button
|
| 75 |
+
@reactive.Effect
|
| 76 |
+
@reactive.event(input.clear_chat)
|
| 77 |
+
async def _():
|
| 78 |
+
# Clear Shiny chat UI
|
| 79 |
+
await chat.clear()
|
| 80 |
+
await chat.append_message({"role": "assistant",
|
| 81 |
+
"content": "Chat history cleared. How can I help you today?"})
|
| 82 |
+
# Clear internal chatbot conversation
|
| 83 |
+
clear_conversation()
|
| 84 |
+
|
| 85 |
+
# Define download handler for chat history
|
| 86 |
+
@output
|
| 87 |
+
@render.download
|
| 88 |
+
def download_chat():
|
| 89 |
+
# Get current chat messages
|
| 90 |
+
messages = chat.messages()
|
| 91 |
+
|
| 92 |
+
# Create filename with timestamp
|
| 93 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 94 |
+
filename = f"au_advisor_chat_{timestamp}.md"
|
| 95 |
+
|
| 96 |
+
# Format as Markdown
|
| 97 |
+
md_lines = []
|
| 98 |
+
md_lines.append("# American University Academic Advisor Chat")
|
| 99 |
+
md_lines.append(f"*Exported on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n")
|
| 100 |
+
|
| 101 |
+
for msg in messages:
|
| 102 |
+
role = msg.get("role", "unknown")
|
| 103 |
+
content = msg.get("content", "")
|
| 104 |
+
|
| 105 |
+
if role == "user":
|
| 106 |
+
md_lines.append(f"## User\n\n{content}\n")
|
| 107 |
+
elif role == "assistant":
|
| 108 |
+
md_lines.append(f"## Assistant\n\n{content}\n")
|
| 109 |
+
|
| 110 |
+
content = "\n".join(md_lines)
|
| 111 |
+
|
| 112 |
+
# Return as bytes
|
| 113 |
+
return filename, "text/markdown", content.encode("utf-8")
|
| 114 |
+
|
| 115 |
+
# Define callback for user input
|
| 116 |
+
@chat.on_user_submit
|
| 117 |
+
async def handle_user_input(user_input: str):
|
| 118 |
+
# Process the query with the chatbot
|
| 119 |
+
result = ask_question(
|
| 120 |
+
query=user_input,
|
| 121 |
+
n_results=input.n_results(),
|
| 122 |
+
temperature=input.temperature()
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Format the response with better source descriptions
|
| 126 |
+
response = result["response"].strip()
|
| 127 |
+
if result.get("contexts") and result.get("metadata"):
|
| 128 |
+
sources = []
|
| 129 |
+
for i, (context, meta) in enumerate(zip(result.get("contexts", []), result.get("metadata", []))):
|
| 130 |
+
title = meta.get("title", "") if meta else ""
|
| 131 |
+
url = meta.get("url", "")
|
| 132 |
+
|
| 133 |
+
if title in ["Table Row", "Paragraph"] or not title:
|
| 134 |
+
content_preview = context.strip()[:50] + "..." if len(context) > 50 else context.strip()
|
| 135 |
+
description = content_preview
|
| 136 |
+
else:
|
| 137 |
+
description = title
|
| 138 |
+
|
| 139 |
+
if url:
|
| 140 |
+
sources.append(f"[{i+1}] <a href='{url}' target='_blank'>{description}</a>")
|
| 141 |
+
else:
|
| 142 |
+
sources.append(f"[{i+1}] {description}")
|
| 143 |
+
|
| 144 |
+
if sources and "Sources:" not in response:
|
| 145 |
+
response += "<br><br><strong>Sources:</strong><br>" + "<br>".join(sources)
|
| 146 |
+
|
| 147 |
+
# Add the response to the Shiny chat
|
| 148 |
+
await chat.append_message({"role": "assistant", "content": response})
|
| 149 |
+
|
| 150 |
+
# Create and run the app
|
| 151 |
+
app = App(app_ui, server)
|
| 152 |
+
|
| 153 |
+
if __name__ == "__main__":
|
| 154 |
+
# For Shiny apps, make sure to use the correct host/port
|
| 155 |
+
import shiny
|
| 156 |
+
shiny.run_app(
|
| 157 |
+
host="0.0.0.0", # Important to bind to 0.0.0.0, not localhost
|
| 158 |
+
port=7860, # Must match the exposed port
|
| 159 |
+
launch_browser=False
|
| 160 |
+
)
|