MedQA / app.py
mgbam's picture
Update app.py
04ae885 verified
raw
history blame
11.1 kB
import streamlit as st
from config.settings import settings
from models import create_db_and_tables, get_session_context, User, ChatMessage, ChatSession
from models.user import UserCreate # For type hinting
from services.auth import create_user_in_db, authenticate_user
from services.logger import app_logger
# Agent will be initialized and used in specific pages like Consult.py
# from agent import get_agent_executor # Import in pages where needed
# --- Page Configuration ---
st.set_page_config(
page_title=settings.APP_TITLE,
page_icon="⚕️", # You can use an emoji or a path to an image
layout="wide",
initial_sidebar_state="expanded"
)
# --- Database Initialization ---
@st.cache_resource # Ensure this runs only once
def init_db():
app_logger.info("Initializing database and tables...")
create_db_and_tables()
app_logger.info("Database initialized.")
init_db()
# --- Session State Initialization ---
if 'authenticated_user' not in st.session_state:
st.session_state.authenticated_user = None # Stores User object upon successful login
if 'current_chat_session_id' not in st.session_state:
st.session_state.current_chat_session_id = None
if 'chat_messages' not in st.session_state: # For the current active chat
st.session_state.chat_messages = []
# --- Authentication Logic ---
def display_login_form():
with st.form("login_form"):
st.subheader("Login")
username_input = st.text_input("Username", key="login_username_input") # Added key for clarity
password_input = st.text_input("Password", type="password", key="login_password_input") # Added key
submit_button = st.form_submit_button("Login")
if submit_button:
# IMPORTANT: The 'user' object returned by authenticate_user needs to have
# its essential attributes (like id, username) already loaded, or if its session
# was closed, those attributes should not be in an "expired" state that requires
# a database refresh. Typically, a query like db.query(User)...first() loads these.
# If authenticate_user involves a commit and session.expire_on_commit=True (default),
# it should call db.refresh(user_object) before closing its session and returning.
user = authenticate_user(username_input, password_input)
if user:
st.session_state.authenticated_user = user
# Accessing user.username and user.id here.
# If 'user' is detached and attributes are expired, this could also fail.
# This implies authenticate_user should return a "usable" object.
st.success(f"Welcome back, {user.username}!")
try:
with get_session_context() as db_session:
# Ensure the user object is attached to the current session if needed,
# or that its ID is accessible even if detached.
# user_id = user.id (if user.id is loaded, this is fine)
# live_user = db_session.merge(user) # if user might be detached but has PK
# Or better if only ID is needed:
live_user = db_session.get(User, user.id) # Re-fetch/attach to current session
if not live_user:
st.error("User session error. Please log in again.")
app_logger.error(f"Failed to re-fetch user with id {user.id} in new session.")
st.session_state.authenticated_user = None # Clear broken state
st.rerun()
return
new_chat_session = ChatSession(user_id=live_user.id, title=f"Session for {live_user.username}")
db_session.add(new_chat_session)
db_session.commit()
db_session.refresh(new_chat_session) # Refresh new_chat_session
st.session_state.current_chat_session_id = new_chat_session.id
st.session_state.chat_messages = [] # Clear previous messages
st.rerun() # Rerun to reflect login state
except Exception as e:
app_logger.error(f"Error creating chat session for user {user.username}: {e}")
st.error(f"Could not start a new session: {e}")
else:
st.error("Invalid username or password.")
def display_signup_form():
with st.form("signup_form"):
st.subheader("Sign Up")
new_username = st.text_input("Choose a Username", key="signup_username_input") # Added key
new_email = st.text_input("Email (Optional)", key="signup_email_input") # Added key
new_password = st.text_input("Choose a Password", type="password", key="signup_password_input") # Added key
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input") # Added key
submit_button = st.form_submit_button("Sign Up")
if submit_button:
if not new_username or not new_password:
st.error("Username and password are required.")
elif new_password != confirm_password:
st.error("Passwords do not match.")
else:
user_data = UserCreate(
username=new_username,
password=new_password,
email=new_email if new_email else None
)
# --- Explanation of the DetachedInstanceError and its handling ---
# The 'create_user_in_db' function creates a User and commits it to the database.
# If the SQLAlchemy session used inside 'create_user_in_db' has 'expire_on_commit=True'
# (which is the default), all attributes of the 'user' object are marked as "expired"
# after the commit.
# If 'create_user_in_db' then closes its session and returns this 'user' object,
# the object becomes "detached".
# When you later try to access an expired attribute (like 'user.username'),
# SQLAlchemy attempts to reload it from the database. Since the object is detached
# (not bound to an active session), this reload fails, raising DetachedInstanceError.
# TO FIX ROBUSTLY (in services/auth.py, inside create_user_in_db):
# After `db_session.commit()`, ensure `db_session.refresh(created_user_object)` is called
# *before* the session is closed and the object is returned. This loads all attributes
# from the database, so they are no longer "expired".
user = create_user_in_db(user_data)
if user:
# IMMEDIATE FIX for this specific line:
# Instead of 'user.username' (which might be on a detached, expired instance),
# use 'new_username', which is the value just submitted by the user and
# used for creation. This avoids the need to access the potentially problematic 'user' object attribute.
st.success(f"Account created for {new_username}. Please log in.")
app_logger.info(f"Account created for {new_username}.")
# If you were to enable direct login here:
# st.session_state.authenticated_user = user
# st.rerun()
# ...then it becomes CRITICAL that 'create_user_in_db' returns a 'user' object
# whose attributes (like .id, .username) are already loaded and not expired,
# as explained in the "TO FIX ROBUSTLY" comment above.
else:
st.error("Username might already be taken or another error occurred during signup.")
app_logger.warning(f"Failed to create user for username: {new_username}")
# --- Main App Logic ---
if not st.session_state.authenticated_user:
st.title(f"Welcome to {settings.APP_TITLE}")
st.markdown("Your AI-powered partner for advanced healthcare insights.")
login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
with login_tab:
display_login_form()
with signup_tab:
display_signup_form()
else:
# If authenticated, Streamlit automatically navigates to pages in the `pages/` directory.
# The content of `app.py` typically acts as the "Home" page if no `1_Home.py` exists,
# or it can be used for global elements like a custom sidebar if not using Streamlit's default page navigation.
# Custom Sidebar for logged-in users
with st.sidebar:
# Ensure authenticated_user object is usable. If it was detached with expired attributes
# upon login, this access could also fail. This reinforces the need for authenticate_user
# to return a "live" or fully-loaded object.
try:
st.markdown(f"### Welcome, {st.session_state.authenticated_user.username}!")
except AttributeError: # Fallback if username is not accessible
st.markdown(f"### Welcome!")
app_logger.error("Could not access username for authenticated_user in sidebar.")
if st.session_state.get("SHOW_LOGO_IN_SIDEBAR", True) and settings.LOGO_PATH and Path(settings.LOGO_PATH).exists(): # Example for conditional logo
try:
st.image(settings.LOGO_PATH, width=100)
except Exception as e:
app_logger.warning(f"Could not load logo from {settings.LOGO_PATH}: {e}")
elif settings.APP_TITLE: # Fallback to title if no logo
st.markdown(f"#### {settings.APP_TITLE}")
st.markdown("---")
if st.button("Logout"):
app_logger.info(f"User {st.session_state.authenticated_user.username} logging out.")
st.session_state.authenticated_user = None
st.session_state.current_chat_session_id = None
st.session_state.chat_messages = []
st.success("You have been logged out.")
st.rerun()
# This content will show if no other page is selected, or if you don't have a 1_Home.py
# If you have 1_Home.py, Streamlit will show that by default after login.
st.sidebar.success("Select a page above to get started.") # Or from the main area below if multi-page app
# Main area content (could be your "Home" page)
st.header(f"Dashboard - {settings.APP_TITLE}")
st.markdown("Navigate using the sidebar to consult with the AI or view your reports.")
st.markdown("---")
st.info("This is the main application area. If you see this, ensure you have a `pages/1_Home.py` or that this `app.py` is your intended landing page after login.")
# Ensure Path is imported if used for logo
from pathlib import Path
app_logger.info(f"Streamlit app '{settings.APP_TITLE}' initialized and running.")