File size: 11,055 Bytes
6145bc0 864b488 0b77233 864b488 0b77233 864b488 0b77233 64f8a92 0b77233 0a2437f 0b77233 0a2437f 0b77233 04ae885 0b77233 64f8a92 0b77233 04ae885 0b77233 04ae885 0b77233 04ae885 0b77233 64f8a92 0b77233 04ae885 0b77233 64f8a92 0b77233 04ae885 0b77233 04ae885 0b77233 04ae885 0b77233 04ae885 0a2437f 0b77233 04ae885 0b77233 0a2437f 04ae885 0b77233 04ae885 0b77233 04ae885 0b77233 04ae885 0b77233 04ae885 0b77233 04ae885 0b77233 64f8a92 04ae885 |
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 |
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.") |