-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
244 lines (197 loc) · 8.59 KB
/
Copy pathapp.py
File metadata and controls
244 lines (197 loc) · 8.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env python
"""agent-alz-assistant - Agentic AI assistant for Alzheimer's disease research with literature retrieval and knowledge synthesis"""
import asyncio
import bcrypt
import json
import logging
import os
import sys
import shutil
import uuid
from datetime import datetime
from logging.handlers import RotatingFileHandler
from pathlib import Path
from nicegui import ui, app as nicegui_app
from dotenv import load_dotenv
from agent_alz_assistant.agent import ClaudeAgent
# Load environment variables
load_dotenv()
# Set up query logging (thread-safe)
LOGS_DIR = Path("logs")
LOGS_DIR.mkdir(exist_ok=True)
query_logger = logging.getLogger("query_log")
query_logger.setLevel(logging.INFO)
query_handler = RotatingFileHandler(
LOGS_DIR / "queries.jsonl",
maxBytes=10*1024*1024, # 10MB per file
backupCount=5, # Keep 5 backup files
)
query_handler.setFormatter(logging.Formatter("%(message)s"))
query_logger.addHandler(query_handler)
query_logger.propagate = False # Don't propagate to root logger
# Clean up stale NiceGUI storage to prevent session issues
# DISABLED BY DEFAULT - cleaning storage logs users out
# Set CLEAN_STORAGE=true in .env if you need to clear sessions
NICEGUI_STORAGE = Path(".nicegui")
if NICEGUI_STORAGE.exists() and os.getenv("CLEAN_STORAGE", "false").lower() == "true":
print(f"[INFO] Cleaning stale NiceGUI storage at {NICEGUI_STORAGE}")
try:
shutil.rmtree(NICEGUI_STORAGE)
print("[INFO] Storage cleaned successfully")
except Exception as e:
print(f"[WARNING] Could not clean storage: {e}")
# Clean up old plot files on restart
PLOTS_DIR = Path("static/plots")
if PLOTS_DIR.exists() and os.getenv("CLEAN_PLOTS", "true").lower() == "true":
print(f"[INFO] Cleaning old plots at {PLOTS_DIR}")
try:
for plot_file in PLOTS_DIR.glob("*.png"):
plot_file.unlink()
print("[INFO] Plots cleaned successfully")
except Exception as e:
print(f"[WARNING] Could not clean plots: {e}")
# Authentication settings
DISABLE_AUTH = os.getenv("DISABLE_AUTH", "false").lower() == "true"
PASSWORD_HASH = os.getenv("APP_PASSWORD_HASH", "").encode()
if DISABLE_AUTH:
print("[WARNING] Authentication is DISABLED! Anyone can access this app.")
print("[WARNING] Set DISABLE_AUTH=false in .env to re-enable authentication.")
# Get storage secret from environment (required for security)
STORAGE_SECRET = os.getenv("STORAGE_SECRET")
if not STORAGE_SECRET:
raise ValueError("STORAGE_SECRET must be set in .env file - see .env.example")
# Get port from environment (required)
PORT = os.getenv("PORT")
if not PORT:
raise ValueError("PORT must be set in .env file - see .env.example")
PORT = int(PORT)
# Initialize agent
agent = ClaudeAgent()
class ChatMessage:
"""Container for a chat message"""
def __init__(self, role: str, content: str):
self.role = role
self.content = content
# Store conversation history
conversation_history = []
def check_password(password: str) -> bool:
"""Check if password matches the hash"""
if not PASSWORD_HASH:
return True # No password set, allow access
try:
return bcrypt.checkpw(password.encode(), PASSWORD_HASH)
except Exception as e:
print(f"[ERROR] Password check failed: {e}")
return False
@ui.page("/login")
async def login():
"""Login page"""
def try_login():
if check_password(password_input.value):
nicegui_app.storage.user["authenticated"] = True
ui.navigate.to("/")
else:
ui.notify("Invalid password", color="negative")
password_input.value = ""
with ui.column().classes("absolute-center items-center"):
ui.markdown("# agent-alz-assistant")
ui.markdown("_Alzheimer's Disease Research Assistant_")
password_input = ui.input("Password", password=True, password_toggle_button=True).classes("w-64").on("keydown.enter", try_login)
ui.button("Login", on_click=try_login).classes("w-64")
@ui.page("/")
async def index():
"""Main chat interface"""
# Check authentication (skip if disabled)
if not DISABLE_AUTH and not nicegui_app.storage.user.get("authenticated", False):
ui.navigate.to("/login")
return
# Generate a unique session ID for this page load
# Each page load gets a fresh session (no persistence across reloads)
session_id = str(uuid.uuid4())
# Header
with ui.column().classes("w-full max-w-4xl mx-auto p-4"):
ui.markdown("# agent-alz-assistant")
ui.markdown("_Agentic AI assistant for Alzheimer's disease research with literature retrieval and knowledge synthesis_")
sample_questions = [
"What is APOE4 and how does it relate to Alzheimer's?",
"What are the most accurate blood biomarkers for early AD detection?",
"Explain the role of tau protein in Alzheimer's disease",
"Create a plot summarizing diagnostic accuracy of tests for Alzheimer's disease",
]
# Chat container with bottom padding to prevent overlap with footer
chat_container = ui.column().classes("w-full max-w-4xl mx-auto gap-4 p-4").style("min-height: 0; padding-bottom: 10px")
# Input area (fixed at bottom)
with ui.footer().classes("bg-white"):
with ui.column().classes("w-full max-w-4xl mx-auto p-2"):
user_input = ui.textarea(
placeholder="Ask me anything...",
on_change=lambda: None,
).classes("w-full").props("outlined autofocus rows=2")
# Sample questions label and buttons
ui.label("Sample questions:").classes("text-sm text-gray-600 mt-2")
with ui.row().classes("w-full gap-2 flex-wrap"):
for question in sample_questions:
ui.button(
question,
on_click=lambda q=question: user_input.set_value(q)
).props("dense flat color=primary").classes("text-xs normal-case")
async def send_message():
"""Send user message and get agent response"""
query = user_input.value
if not query.strip():
return
# Clear input
user_input.value = ""
# Add user message to UI
with chat_container:
with ui.row().classes("w-full justify-end"):
ui.chat_message(
text=query, name="You", sent=True
).classes("bg-blue-100")
# Add user message to history
conversation_history.append(ChatMessage("user", query))
# Log query (thread-safe)
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"session_id": session_id,
"user_query": query,
"query_length": len(query),
"is_sample_question": query in sample_questions,
"type": "user_query"
}
query_logger.info(json.dumps(log_entry))
# Show thinking indicator
with chat_container:
thinking = ui.chat_message(
text="Thinking...", name="Assistant", sent=False
).classes("bg-gray-100")
# Get response from agent
try:
response = await agent.chat(query, session_id, conversation_history)
# Remove thinking indicator
chat_container.remove(thinking)
# Add assistant response with markdown rendering
with chat_container:
with ui.chat_message(name="Assistant", sent=False).classes("bg-green-100"):
ui.markdown(response)
# Add to history
conversation_history.append(ChatMessage("assistant", response))
except Exception as e:
chat_container.remove(thinking)
with chat_container:
ui.chat_message(
text=f"Error: {str(e)}", name="System", sent=False
).classes("bg-red-100")
ui.button("Send", on_click=send_message).classes("w-full").props(
"color=primary"
)
if __name__ in {"__main__", "__mp_main__"}:
# Serve static files for plots
nicegui_app.add_static_files('/static', 'static')
ui.run(
title="agent-alz-assistant",
port=PORT,
reload=False,
show=True,
storage_secret=STORAGE_SECRET,
)