Update app.py
Browse files
app.py
CHANGED
@@ -1,22 +1,21 @@
|
|
1 |
# /home/user/app/app.py
|
2 |
import streamlit as st
|
3 |
from pathlib import Path
|
4 |
-
from datetime import datetime
|
5 |
-
from sqlmodel import select
|
6 |
|
7 |
from config.settings import settings
|
8 |
-
# Ensure models are imported correctly based on your __init__.py in models/
|
9 |
from models import (
|
10 |
create_db_and_tables,
|
11 |
-
get_session_context,
|
12 |
User,
|
13 |
ChatMessage,
|
14 |
ChatSession
|
15 |
)
|
16 |
-
from models.user import UserCreate
|
17 |
from services.auth import create_user_in_db, authenticate_user
|
18 |
from services.logger import app_logger
|
19 |
-
from assets.logo import get_logo_path
|
20 |
|
21 |
# --- Page Configuration ---
|
22 |
st.set_page_config(
|
@@ -27,209 +26,166 @@ st.set_page_config(
|
|
27 |
)
|
28 |
|
29 |
# --- Database Initialization ---
|
30 |
-
@st.cache_resource
|
31 |
def init_db():
|
32 |
app_logger.info("Initializing database and tables...")
|
33 |
-
create_db_and_tables()
|
34 |
app_logger.info("Database initialized.")
|
35 |
-
|
36 |
init_db()
|
37 |
|
38 |
-
# --- Session State Initialization
|
39 |
if 'authenticated_user_id' not in st.session_state:
|
40 |
st.session_state.authenticated_user_id = None
|
41 |
if 'authenticated_username' not in st.session_state:
|
42 |
st.session_state.authenticated_username = None
|
43 |
if 'current_chat_session_id' not in st.session_state:
|
44 |
st.session_state.current_chat_session_id = None
|
45 |
-
if 'chat_messages' not in st.session_state:
|
46 |
st.session_state.chat_messages = []
|
|
|
|
|
47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
|
49 |
# --- Authentication Logic ---
|
50 |
def display_login_form():
|
|
|
|
|
|
|
51 |
with st.form("login_form"):
|
52 |
st.subheader("Login")
|
53 |
-
username_input = st.text_input("Username", key="login_username_input_main_app")
|
54 |
-
password_input = st.text_input("Password", type="password", key="login_password_input_main_app")
|
55 |
submit_button = st.form_submit_button("Login")
|
56 |
|
57 |
if submit_button:
|
58 |
user_object_from_auth = authenticate_user(username_input, password_input)
|
59 |
-
|
60 |
-
if user_object_from_auth: # Indicates successful authentication
|
61 |
st.success(f"Welcome back, {username_input}!")
|
62 |
-
app_logger.info(f"User '{username_input}' authenticated successfully
|
63 |
-
|
64 |
try:
|
65 |
-
with get_session_context() as db_session:
|
66 |
-
# Re-fetch the user to ensure we have a live object for this session
|
67 |
-
# and to get definitive ID/username if authenticate_user returned a detached object.
|
68 |
statement = select(User).where(User.username == username_input)
|
69 |
live_user = db_session.exec(statement).first()
|
70 |
-
|
71 |
if not live_user:
|
72 |
-
|
73 |
-
|
74 |
-
app_logger.error(f"CRITICAL: User '{username_input}' authenticated but then NOT FOUND in DB by username query.")
|
75 |
-
# Clear potentially corrupted auth state
|
76 |
st.session_state.authenticated_user_id = None
|
77 |
st.session_state.authenticated_username = None
|
78 |
-
st.rerun()
|
79 |
return
|
80 |
-
|
81 |
-
app_logger.info(f"Live user object for '{live_user.username}' (ID: {live_user.id}) obtained in session.")
|
82 |
-
# Store primitive data in st.session_state
|
83 |
st.session_state.authenticated_user_id = live_user.id
|
84 |
st.session_state.authenticated_username = live_user.username
|
85 |
-
app_logger.info(f"Stored user ID {live_user.id} and username '{live_user.username}' in
|
86 |
-
|
87 |
-
# Create a new chat session for the user upon login
|
88 |
-
app_logger.debug(f"Attempting to create new chat session for user_id: {live_user.id}")
|
89 |
new_chat_session = ChatSession(
|
90 |
user_id=live_user.id,
|
91 |
title=f"Session for {live_user.username} - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
92 |
-
# start_time is default_factory=datetime.utcnow in the model
|
93 |
)
|
94 |
db_session.add(new_chat_session)
|
95 |
-
app_logger.debug("New ChatSession
|
96 |
-
|
97 |
-
# Flush the session to persist new_chat_session and get its ID
|
98 |
-
app_logger.debug("Flushing SQLModel session to assign ID to new_chat_session...")
|
99 |
db_session.flush()
|
100 |
-
app_logger.debug(f"Session flushed. new_chat_session
|
101 |
-
|
102 |
-
# Check if ID was assigned; if not, something is wrong with DB/model setup for auto-increment PK.
|
103 |
if new_chat_session.id is None:
|
104 |
-
app_logger.error("CRITICAL: new_chat_session.id is None after flush.
|
105 |
-
raise Exception("Failed to obtain ID for new chat session after
|
106 |
-
|
107 |
-
# Refresh the object to load any server-side defaults or confirm state
|
108 |
-
app_logger.debug(f"Refreshing new_chat_session (ID: {new_chat_session.id}) from database...")
|
109 |
db_session.refresh(new_chat_session)
|
110 |
-
app_logger.debug("new_chat_session
|
111 |
-
|
112 |
-
# Store the new chat session ID for use in the Consult page
|
113 |
st.session_state.current_chat_session_id = new_chat_session.id
|
114 |
-
st.session_state.chat_messages = []
|
115 |
-
app_logger.info(f"New chat session (ID: {new_chat_session.id}) created
|
116 |
-
# The commit will be handled by the get_session_context manager upon successful exit of this 'with' block.
|
117 |
-
|
118 |
-
# If all operations within the 'with' block succeed, the context manager will commit.
|
119 |
app_logger.info(f"Post-login setup complete for '{username_input}'. Rerunning app.")
|
120 |
-
st.rerun()
|
121 |
-
|
122 |
except Exception as e:
|
123 |
-
app_logger.error(f"Error during post-login session setup for
|
124 |
st.error(f"Could not complete login process: {e}")
|
125 |
-
# Clear potentially partial auth state to force re-login or prevent inconsistent state
|
126 |
st.session_state.authenticated_user_id = None
|
127 |
st.session_state.authenticated_username = None
|
128 |
st.session_state.current_chat_session_id = None
|
129 |
-
# No rerun here, let user see the error and try again or contact support.
|
130 |
else:
|
131 |
st.error("Invalid username or password.")
|
132 |
app_logger.warning(f"Failed login attempt for username: {username_input}")
|
133 |
|
134 |
def display_signup_form():
|
|
|
|
|
135 |
with st.form("signup_form"):
|
136 |
st.subheader("Sign Up")
|
137 |
-
new_username = st.text_input("Choose a Username", key="signup_username_input_main_app")
|
138 |
-
new_email = st.text_input("Email (Optional)", key="signup_email_input_main_app")
|
139 |
-
new_password = st.text_input("Choose a Password", type="password", key="signup_password_input_main_app")
|
140 |
-
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input_main_app")
|
141 |
submit_button = st.form_submit_button("Sign Up")
|
142 |
-
|
143 |
if submit_button:
|
144 |
-
if not new_username or not new_password:
|
145 |
-
|
146 |
-
elif
|
147 |
-
st.error("Password must be at least 6 characters long.")
|
148 |
-
elif new_password != confirm_password:
|
149 |
-
st.error("Passwords do not match.")
|
150 |
else:
|
151 |
-
user_data = UserCreate(
|
152 |
-
username=new_username,
|
153 |
-
password=new_password,
|
154 |
-
email=new_email if new_email else None
|
155 |
-
# full_name can be added if part of UserCreate and signup form
|
156 |
-
)
|
157 |
-
# create_user_in_db should handle its own session and return a User object or None.
|
158 |
-
# It's responsible for hashing password and committing the new user.
|
159 |
user = create_user_in_db(user_data)
|
160 |
if user:
|
161 |
-
# Use 'new_username' (from form input) for the message
|
162 |
st.success(f"Account created for {new_username}. Please log in.")
|
163 |
app_logger.info(f"Account created for '{new_username}'.")
|
164 |
else:
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
|
|
|
|
180 |
else:
|
181 |
-
# --- User is Authenticated
|
182 |
with st.sidebar:
|
183 |
-
|
184 |
-
logo_path_str_sidebar = get_logo_path() # Assumes assets/logo.py is set up
|
185 |
if logo_path_str_sidebar:
|
186 |
logo_file_sidebar = Path(logo_path_str_sidebar)
|
187 |
if logo_file_sidebar.exists():
|
188 |
-
try:
|
189 |
-
|
190 |
-
|
191 |
-
app_logger.warning(f"Could not display logo in sidebar from path '{logo_path_str_sidebar}': {e}")
|
192 |
-
# else: # get_logo_path should log if file doesn't exist
|
193 |
-
# app_logger.warning(f"Sidebar logo path from get_logo_path() does not exist: {logo_path_str_sidebar}")
|
194 |
-
elif settings.APP_TITLE: # Fallback to title if no logo path or logo load fails
|
195 |
-
st.sidebar.markdown(f"#### {settings.APP_TITLE}")
|
196 |
|
197 |
-
|
198 |
-
username_for_display = st.session_state.get("authenticated_username", "User") # Fallback to "User"
|
199 |
st.sidebar.markdown(f"### Welcome, {username_for_display}!")
|
200 |
-
st.sidebar.
|
201 |
-
|
202 |
-
# Navigation is handled by Streamlit's multi-page app feature (pages/ directory)
|
203 |
-
# This app.py acts as the entry point.
|
204 |
-
# Streamlit automatically lists pages from the `pages/` directory here in the sidebar.
|
205 |
|
206 |
-
if st.sidebar.button("Logout", key="sidebar_logout_button_main_app"):
|
207 |
logged_out_username = st.session_state.get("authenticated_username", "UnknownUser")
|
208 |
app_logger.info(f"User '{logged_out_username}' logging out.")
|
209 |
-
# Clear all
|
210 |
-
st.session_state.
|
211 |
-
|
212 |
-
|
213 |
-
st.session_state.
|
214 |
-
# Potentially clear other page-specific states if needed
|
215 |
-
# for key in list(st.session_state.keys()):
|
216 |
-
# if key not in ['rerun_requested', ...]: # preserve essential Streamlit keys
|
217 |
-
# del st.session_state[key]
|
218 |
st.success("You have been logged out.")
|
219 |
st.rerun()
|
220 |
|
221 |
-
# Main content area
|
222 |
-
#
|
223 |
-
#
|
224 |
-
|
225 |
-
|
|
|
|
|
226 |
|
227 |
-
# For a typical MPA setup, you might not need much content directly in app.py's main area
|
228 |
-
# once the user is logged in, as page files will take over.
|
229 |
-
# A simple instruction or pointer could be useful.
|
230 |
-
st.sidebar.success("Select a page from the navigation.")
|
231 |
-
# Optionally, add a default landing message in the main area if no page is loaded yet,
|
232 |
-
# though Streamlit usually picks the first page in `pages/`.
|
233 |
-
# st.info("Main application area. Select a page from the sidebar to begin.")
|
234 |
|
235 |
app_logger.info(f"Streamlit app '{settings.APP_TITLE}' (app.py) processed.")
|
|
|
1 |
# /home/user/app/app.py
|
2 |
import streamlit as st
|
3 |
from pathlib import Path
|
4 |
+
from datetime import datetime
|
5 |
+
from sqlmodel import select
|
6 |
|
7 |
from config.settings import settings
|
|
|
8 |
from models import (
|
9 |
create_db_and_tables,
|
10 |
+
get_session_context,
|
11 |
User,
|
12 |
ChatMessage,
|
13 |
ChatSession
|
14 |
)
|
15 |
+
from models.user import UserCreate
|
16 |
from services.auth import create_user_in_db, authenticate_user
|
17 |
from services.logger import app_logger
|
18 |
+
from assets.logo import get_logo_path
|
19 |
|
20 |
# --- Page Configuration ---
|
21 |
st.set_page_config(
|
|
|
26 |
)
|
27 |
|
28 |
# --- Database Initialization ---
|
29 |
+
@st.cache_resource
|
30 |
def init_db():
|
31 |
app_logger.info("Initializing database and tables...")
|
32 |
+
create_db_and_tables()
|
33 |
app_logger.info("Database initialized.")
|
|
|
34 |
init_db()
|
35 |
|
36 |
+
# --- Session State Initialization ---
|
37 |
if 'authenticated_user_id' not in st.session_state:
|
38 |
st.session_state.authenticated_user_id = None
|
39 |
if 'authenticated_username' not in st.session_state:
|
40 |
st.session_state.authenticated_username = None
|
41 |
if 'current_chat_session_id' not in st.session_state:
|
42 |
st.session_state.current_chat_session_id = None
|
43 |
+
if 'chat_messages' not in st.session_state:
|
44 |
st.session_state.chat_messages = []
|
45 |
+
if 'user_understands_disclaimer' not in st.session_state:
|
46 |
+
st.session_state.user_understands_disclaimer = False
|
47 |
|
48 |
+
# --- Disclaimer Function ---
|
49 |
+
def display_disclaimer():
|
50 |
+
st.warning(f"**Important Disclaimer:** {settings.MAIN_DISCLAIMER_LONG}")
|
51 |
+
st.info(settings.SIMULATION_DISCLAIMER)
|
52 |
+
if st.button("I Understand and Agree to Proceed", key="disclaimer_agree_btn"):
|
53 |
+
st.session_state.user_understands_disclaimer = True
|
54 |
+
st.rerun()
|
55 |
|
56 |
# --- Authentication Logic ---
|
57 |
def display_login_form():
|
58 |
+
# (This function remains the same as the last full app.py provided, which had the fix for
|
59 |
+
# `Instance '<ChatSession ...>' is not persistent within this Session`.
|
60 |
+
# Ensure that version is used here for robustness.)
|
61 |
with st.form("login_form"):
|
62 |
st.subheader("Login")
|
63 |
+
username_input = st.text_input("Username", key="login_username_input_main_app")
|
64 |
+
password_input = st.text_input("Password", type="password", key="login_password_input_main_app")
|
65 |
submit_button = st.form_submit_button("Login")
|
66 |
|
67 |
if submit_button:
|
68 |
user_object_from_auth = authenticate_user(username_input, password_input)
|
69 |
+
if user_object_from_auth:
|
|
|
70 |
st.success(f"Welcome back, {username_input}!")
|
71 |
+
app_logger.info(f"User '{username_input}' authenticated successfully.")
|
|
|
72 |
try:
|
73 |
+
with get_session_context() as db_session:
|
|
|
|
|
74 |
statement = select(User).where(User.username == username_input)
|
75 |
live_user = db_session.exec(statement).first()
|
|
|
76 |
if not live_user:
|
77 |
+
st.error("Auth inconsistency. User not found post-login.")
|
78 |
+
app_logger.error(f"CRITICAL: User '{username_input}' auth OK but NOT FOUND in DB.")
|
|
|
|
|
79 |
st.session_state.authenticated_user_id = None
|
80 |
st.session_state.authenticated_username = None
|
81 |
+
st.rerun()
|
82 |
return
|
|
|
|
|
|
|
83 |
st.session_state.authenticated_user_id = live_user.id
|
84 |
st.session_state.authenticated_username = live_user.username
|
85 |
+
app_logger.info(f"Stored user ID {live_user.id} and username '{live_user.username}' in session state.")
|
|
|
|
|
|
|
86 |
new_chat_session = ChatSession(
|
87 |
user_id=live_user.id,
|
88 |
title=f"Session for {live_user.username} - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
|
|
89 |
)
|
90 |
db_session.add(new_chat_session)
|
91 |
+
app_logger.debug("New ChatSession added to SQLModel session.")
|
|
|
|
|
|
|
92 |
db_session.flush()
|
93 |
+
app_logger.debug(f"Session flushed. new_chat_session ID: {new_chat_session.id}")
|
|
|
|
|
94 |
if new_chat_session.id is None:
|
95 |
+
app_logger.error("CRITICAL: new_chat_session.id is None after flush.")
|
96 |
+
raise Exception("Failed to obtain ID for new chat session after flush.")
|
|
|
|
|
|
|
97 |
db_session.refresh(new_chat_session)
|
98 |
+
app_logger.debug("new_chat_session refreshed.")
|
|
|
|
|
99 |
st.session_state.current_chat_session_id = new_chat_session.id
|
100 |
+
st.session_state.chat_messages = []
|
101 |
+
app_logger.info(f"New chat session (ID: {new_chat_session.id}) created for user '{live_user.username}'.")
|
|
|
|
|
|
|
102 |
app_logger.info(f"Post-login setup complete for '{username_input}'. Rerunning app.")
|
103 |
+
st.rerun()
|
|
|
104 |
except Exception as e:
|
105 |
+
app_logger.error(f"Error during post-login session setup for '{username_input}': {e}", exc_info=True)
|
106 |
st.error(f"Could not complete login process: {e}")
|
|
|
107 |
st.session_state.authenticated_user_id = None
|
108 |
st.session_state.authenticated_username = None
|
109 |
st.session_state.current_chat_session_id = None
|
|
|
110 |
else:
|
111 |
st.error("Invalid username or password.")
|
112 |
app_logger.warning(f"Failed login attempt for username: {username_input}")
|
113 |
|
114 |
def display_signup_form():
|
115 |
+
# (This function remains largely the same as the last full app.py provided.
|
116 |
+
# Ensure it correctly calls your `create_user_in_db` which handles hashing and DB commit.)
|
117 |
with st.form("signup_form"):
|
118 |
st.subheader("Sign Up")
|
119 |
+
new_username = st.text_input("Choose a Username", key="signup_username_input_main_app")
|
120 |
+
new_email = st.text_input("Email (Optional)", key="signup_email_input_main_app")
|
121 |
+
new_password = st.text_input("Choose a Password", type="password", key="signup_password_input_main_app")
|
122 |
+
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input_main_app")
|
123 |
submit_button = st.form_submit_button("Sign Up")
|
|
|
124 |
if submit_button:
|
125 |
+
if not new_username or not new_password: st.error("Username and password are required.")
|
126 |
+
elif len(new_password) < 6: st.error("Password must be at least 6 characters long.")
|
127 |
+
elif new_password != confirm_password: st.error("Passwords do not match.")
|
|
|
|
|
|
|
128 |
else:
|
129 |
+
user_data = UserCreate(username=new_username, password=new_password, email=new_email if new_email else None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
user = create_user_in_db(user_data)
|
131 |
if user:
|
|
|
132 |
st.success(f"Account created for {new_username}. Please log in.")
|
133 |
app_logger.info(f"Account created for '{new_username}'.")
|
134 |
else:
|
135 |
+
st.error("Username/Email might be taken, or another error occurred. Check logs or try different credentials.")
|
136 |
+
app_logger.warning(f"Signup failed for '{new_username}'. create_user_in_db returned None.")
|
137 |
+
|
138 |
+
# --- Main App Logic ---
|
139 |
+
if not st.session_state.get("authenticated_user_id"): # Not logged in
|
140 |
+
if not st.session_state.user_understands_disclaimer:
|
141 |
+
st.title(f"Welcome to {settings.APP_TITLE}")
|
142 |
+
st.markdown("Your AI-powered partner for advanced healthcare insights.")
|
143 |
+
st.markdown("---")
|
144 |
+
display_disclaimer()
|
145 |
+
st.stop() # Stop further execution until disclaimer is agreed to
|
146 |
+
else:
|
147 |
+
# Disclaimer agreed, show login/signup
|
148 |
+
st.title(f"Welcome to {settings.APP_TITLE}")
|
149 |
+
st.caption(settings.MAIN_DISCLAIMER_SHORT) # Short disclaimer always visible on login page
|
150 |
+
login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
|
151 |
+
with login_tab:
|
152 |
+
display_login_form()
|
153 |
+
with signup_tab:
|
154 |
+
display_signup_form()
|
155 |
else:
|
156 |
+
# --- User is Authenticated ---
|
157 |
with st.sidebar:
|
158 |
+
logo_path_str_sidebar = get_logo_path()
|
|
|
159 |
if logo_path_str_sidebar:
|
160 |
logo_file_sidebar = Path(logo_path_str_sidebar)
|
161 |
if logo_file_sidebar.exists():
|
162 |
+
try: st.image(str(logo_file_sidebar), width=100)
|
163 |
+
except Exception as e: app_logger.warning(f"Could not display sidebar logo: {e}")
|
164 |
+
elif settings.APP_TITLE: st.sidebar.markdown(f"#### {settings.APP_TITLE}")
|
|
|
|
|
|
|
|
|
|
|
165 |
|
166 |
+
username_for_display = st.session_state.get("authenticated_username", "User")
|
|
|
167 |
st.sidebar.markdown(f"### Welcome, {username_for_display}!")
|
168 |
+
st.sidebar.info(settings.MAIN_DISCLAIMER_SHORT) # Constant reminder
|
169 |
+
st.sidebar.markdown("---")
|
|
|
|
|
|
|
170 |
|
171 |
+
if st.sidebar.button("Logout", key="sidebar_logout_button_main_app"):
|
172 |
logged_out_username = st.session_state.get("authenticated_username", "UnknownUser")
|
173 |
app_logger.info(f"User '{logged_out_username}' logging out.")
|
174 |
+
# Clear all session state, or selectively
|
175 |
+
for key in list(st.session_state.keys()): # Clear most session state keys on logout
|
176 |
+
if key not in ['query_params']: # Keep essential Streamlit keys if needed
|
177 |
+
del st.session_state[key]
|
178 |
+
st.session_state.user_understands_disclaimer = False # Require disclaimer agreement again
|
|
|
|
|
|
|
|
|
179 |
st.success("You have been logged out.")
|
180 |
st.rerun()
|
181 |
|
182 |
+
# Main content area: Streamlit MPA handles this by showing the selected page
|
183 |
+
# from the `pages/` directory.
|
184 |
+
# If on app.py itself, give a pointer.
|
185 |
+
current_page = st.session_state.get('page_script_hash', None)
|
186 |
+
if not current_page or current_page == st.elements.utils.calc_page_script_hash('app.py'):
|
187 |
+
st.sidebar.success("Select a page from the navigation.")
|
188 |
+
st.info("Please select a page from the sidebar (e.g., Home, Consult, Reports) to begin.")
|
189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
190 |
|
191 |
app_logger.info(f"Streamlit app '{settings.APP_TITLE}' (app.py) processed.")
|