Update app.py
Browse files
app.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1 |
# /home/user/app/app.py
|
2 |
import streamlit as st
|
3 |
from pathlib import Path
|
4 |
-
from
|
|
|
5 |
|
6 |
from config.settings import settings
|
|
|
7 |
from models import (
|
8 |
create_db_and_tables,
|
9 |
get_session_context, # Your SQLModel session context manager
|
@@ -11,12 +13,12 @@ from models import (
|
|
11 |
ChatMessage,
|
12 |
ChatSession
|
13 |
)
|
14 |
-
from models.user import UserCreate
|
15 |
from services.auth import create_user_in_db, authenticate_user
|
16 |
from services.logger import app_logger
|
17 |
-
from assets.logo import get_logo_path
|
18 |
|
19 |
-
# --- Page Configuration ---
|
20 |
st.set_page_config(
|
21 |
page_title=settings.APP_TITLE,
|
22 |
page_icon="⚕️",
|
@@ -24,22 +26,23 @@ st.set_page_config(
|
|
24 |
initial_sidebar_state="expanded"
|
25 |
)
|
26 |
|
27 |
-
# --- Database Initialization ---
|
28 |
-
@st.cache_resource
|
29 |
def init_db():
|
30 |
app_logger.info("Initializing database and tables...")
|
31 |
create_db_and_tables() # This uses SQLModel.metadata.create_all(engine)
|
32 |
app_logger.info("Database initialized.")
|
|
|
33 |
init_db()
|
34 |
|
35 |
-
# --- Session State Initialization
|
36 |
if 'authenticated_user_id' not in st.session_state:
|
37 |
st.session_state.authenticated_user_id = None
|
38 |
if 'authenticated_username' not in st.session_state:
|
39 |
st.session_state.authenticated_username = None
|
40 |
if 'current_chat_session_id' not in st.session_state:
|
41 |
st.session_state.current_chat_session_id = None
|
42 |
-
if 'chat_messages' not in st.session_state:
|
43 |
st.session_state.chat_messages = []
|
44 |
|
45 |
|
@@ -47,79 +50,100 @@ if 'chat_messages' not in st.session_state:
|
|
47 |
def display_login_form():
|
48 |
with st.form("login_form"):
|
49 |
st.subheader("Login")
|
50 |
-
username_input = st.text_input("Username", key="
|
51 |
-
password_input = st.text_input("Password", type="password", key="
|
52 |
submit_button = st.form_submit_button("Login")
|
53 |
|
54 |
if submit_button:
|
55 |
user_object_from_auth = authenticate_user(username_input, password_input)
|
56 |
|
57 |
-
if user_object_from_auth:
|
58 |
st.success(f"Welcome back, {username_input}!")
|
59 |
-
app_logger.info(f"User {username_input} authenticated successfully.")
|
60 |
|
61 |
try:
|
62 |
with get_session_context() as db_session: # db_session is a SQLModel Session
|
63 |
-
#
|
|
|
64 |
statement = select(User).where(User.username == username_input)
|
65 |
live_user = db_session.exec(statement).first()
|
66 |
-
# --------------------
|
67 |
|
68 |
if not live_user:
|
69 |
-
|
70 |
-
|
|
|
|
|
71 |
st.session_state.authenticated_user_id = None
|
72 |
st.session_state.authenticated_username = None
|
73 |
-
st.rerun()
|
74 |
return
|
75 |
|
|
|
|
|
76 |
st.session_state.authenticated_user_id = live_user.id
|
77 |
st.session_state.authenticated_username = live_user.username
|
78 |
-
app_logger.info(f"Stored user ID {live_user.id} and username '{live_user.username}' in session state.")
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
db_session.add(new_chat_session)
|
82 |
-
|
83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
st.session_state.current_chat_session_id = new_chat_session.id
|
85 |
-
st.session_state.chat_messages = []
|
86 |
-
app_logger.info(f"New chat session (ID: {new_chat_session.id}) created for user {live_user.username}.")
|
87 |
-
|
88 |
-
|
|
|
|
|
|
|
89 |
|
90 |
except Exception as e:
|
91 |
-
app_logger.error(f"Error during post-login session setup for user {username_input}: {e}", exc_info=True)
|
92 |
st.error(f"Could not complete login process: {e}")
|
|
|
93 |
st.session_state.authenticated_user_id = None
|
94 |
st.session_state.authenticated_username = None
|
|
|
|
|
95 |
else:
|
96 |
st.error("Invalid username or password.")
|
97 |
app_logger.warning(f"Failed login attempt for username: {username_input}")
|
98 |
|
99 |
-
# display_signup_form() remains largely the same as it calls create_user_in_db,
|
100 |
-
# which should internally use SQLModel syntax if it interacts with the DB.
|
101 |
-
|
102 |
-
# --- Main App Logic --- (Remains the same, using updated session state keys)
|
103 |
-
# ... (rest of app.py as previously provided, ensuring it uses authenticated_user_id and authenticated_username)
|
104 |
-
# For brevity, I'm omitting the parts of app.py that don't change due to SQLModel query syntax.
|
105 |
-
# The signup form and the main structure checking st.session_state.get("authenticated_user_id")
|
106 |
-
# and the sidebar display are correct from the previous version.
|
107 |
-
# Ensure any other direct DB queries in app.py (if any were added) are also updated.
|
108 |
-
|
109 |
-
# (Make sure the rest of your app.py from the previous good version is here)
|
110 |
def display_signup_form():
|
111 |
with st.form("signup_form"):
|
112 |
st.subheader("Sign Up")
|
113 |
-
new_username = st.text_input("Choose a Username", key="
|
114 |
-
new_email = st.text_input("Email (Optional)", key="
|
115 |
-
new_password = st.text_input("Choose a Password", type="password", key="
|
116 |
-
confirm_password = st.text_input("Confirm Password", type="password", key="
|
117 |
submit_button = st.form_submit_button("Sign Up")
|
118 |
|
119 |
if submit_button:
|
120 |
if not new_username or not new_password:
|
121 |
st.error("Username and password are required.")
|
122 |
-
elif len(new_password) < 6:
|
123 |
st.error("Password must be at least 6 characters long.")
|
124 |
elif new_password != confirm_password:
|
125 |
st.error("Passwords do not match.")
|
@@ -128,45 +152,84 @@ def display_signup_form():
|
|
128 |
username=new_username,
|
129 |
password=new_password,
|
130 |
email=new_email if new_email else None
|
|
|
131 |
)
|
|
|
|
|
132 |
user = create_user_in_db(user_data)
|
133 |
if user:
|
|
|
134 |
st.success(f"Account created for {new_username}. Please log in.")
|
135 |
-
app_logger.info(f"Account created for {new_username}.")
|
136 |
else:
|
137 |
-
|
138 |
-
|
|
|
|
|
139 |
|
140 |
-
|
|
|
141 |
st.title(f"Welcome to {settings.APP_TITLE}")
|
142 |
st.markdown("Your AI-powered partner for advanced healthcare insights.")
|
|
|
143 |
login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
|
144 |
with login_tab:
|
145 |
display_login_form()
|
146 |
with signup_tab:
|
147 |
display_signup_form()
|
148 |
else:
|
|
|
149 |
with st.sidebar:
|
150 |
-
|
|
|
151 |
if logo_path_str_sidebar:
|
152 |
logo_file_sidebar = Path(logo_path_str_sidebar)
|
153 |
if logo_file_sidebar.exists():
|
154 |
try:
|
155 |
-
st.image(str(logo_file_sidebar), width=100)
|
156 |
except Exception as e:
|
157 |
app_logger.warning(f"Could not display logo in sidebar from path '{logo_path_str_sidebar}': {e}")
|
158 |
-
|
|
|
|
|
159 |
st.sidebar.markdown(f"#### {settings.APP_TITLE}")
|
160 |
-
|
|
|
|
|
161 |
st.sidebar.markdown(f"### Welcome, {username_for_display}!")
|
162 |
-
st.sidebar.markdown("---")
|
163 |
-
|
|
|
|
|
|
|
|
|
|
|
164 |
logged_out_username = st.session_state.get("authenticated_username", "UnknownUser")
|
165 |
-
app_logger.info(f"User {logged_out_username} logging out.")
|
|
|
166 |
st.session_state.authenticated_user_id = None
|
167 |
st.session_state.authenticated_username = None
|
168 |
st.session_state.current_chat_session_id = None
|
169 |
st.session_state.chat_messages = []
|
|
|
|
|
|
|
|
|
170 |
st.success("You have been logged out.")
|
171 |
st.rerun()
|
172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# /home/user/app/app.py
|
2 |
import streamlit as st
|
3 |
from pathlib import Path
|
4 |
+
from datetime import datetime # For default chat session title
|
5 |
+
from sqlmodel import select # For SQLModel queries
|
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, # Your SQLModel session context manager
|
|
|
13 |
ChatMessage,
|
14 |
ChatSession
|
15 |
)
|
16 |
+
from models.user import UserCreate # For type hinting
|
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 # For consistent logo handling
|
20 |
|
21 |
+
# --- Page Configuration ---
|
22 |
st.set_page_config(
|
23 |
page_title=settings.APP_TITLE,
|
24 |
page_icon="⚕️",
|
|
|
26 |
initial_sidebar_state="expanded"
|
27 |
)
|
28 |
|
29 |
+
# --- Database Initialization ---
|
30 |
+
@st.cache_resource # Ensure this runs only once
|
31 |
def init_db():
|
32 |
app_logger.info("Initializing database and tables...")
|
33 |
create_db_and_tables() # This uses SQLModel.metadata.create_all(engine)
|
34 |
app_logger.info("Database initialized.")
|
35 |
+
|
36 |
init_db()
|
37 |
|
38 |
+
# --- Session State Initialization (Using primitive types for user auth) ---
|
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: # For the current active chat in Consult page
|
46 |
st.session_state.chat_messages = []
|
47 |
|
48 |
|
|
|
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") # Unique key
|
54 |
+
password_input = st.text_input("Password", type="password", key="login_password_input_main_app") # Unique key
|
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 by authenticate_user.")
|
63 |
|
64 |
try:
|
65 |
with get_session_context() as db_session: # db_session is a SQLModel 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 |
+
# This is a critical inconsistency if authenticate_user said OK.
|
73 |
+
st.error("Authentication inconsistency. User details not found after login. Please contact support or try again.")
|
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() # Force rerun to show login page
|
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 Streamlit session state.")
|
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 ORM object added to SQLModel session.")
|
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 potential ID: {new_chat_session.id}")
|
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. Cannot proceed with session creation.")
|
105 |
+
raise Exception("Failed to obtain ID for new chat session after database flush.")
|
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 ORM object refreshed.")
|
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 = [] # Clear any old messages for the new session
|
115 |
+
app_logger.info(f"New chat session (ID: {new_chat_session.id}) created and assigned for user '{live_user.username}'.")
|
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() # Rerun to reflect login state and navigate to the default page (e.g., Home)
|
121 |
|
122 |
except Exception as e:
|
123 |
+
app_logger.error(f"Error during post-login session setup for user '{username_input}': {e}", exc_info=True)
|
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") # Unique key
|
138 |
+
new_email = st.text_input("Email (Optional)", key="signup_email_input_main_app") # Unique key
|
139 |
+
new_password = st.text_input("Choose a Password", type="password", key="signup_password_input_main_app") # Unique key
|
140 |
+
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input_main_app") # Unique key
|
141 |
submit_button = st.form_submit_button("Sign Up")
|
142 |
|
143 |
if submit_button:
|
144 |
if not new_username or not new_password:
|
145 |
st.error("Username and password are required.")
|
146 |
+
elif len(new_password) < 6: # Example: Minimum password length
|
147 |
st.error("Password must be at least 6 characters long.")
|
148 |
elif new_password != confirm_password:
|
149 |
st.error("Passwords do not match.")
|
|
|
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 |
+
# create_user_in_db returns None if user exists or another error occurred.
|
166 |
+
# The function itself should log the specific reason.
|
167 |
+
st.error("Username or Email might already be taken, or another error occurred during signup. Please check logs or try different credentials.")
|
168 |
+
app_logger.warning(f"Signup failed for username: '{new_username}'. create_user_in_db returned None.")
|
169 |
|
170 |
+
# --- Main App Logic (Checks for authenticated_user_id) ---
|
171 |
+
if not st.session_state.get("authenticated_user_id"): # Check if user_id is set (i.e., user is logged in)
|
172 |
st.title(f"Welcome to {settings.APP_TITLE}")
|
173 |
st.markdown("Your AI-powered partner for advanced healthcare insights.")
|
174 |
+
|
175 |
login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
|
176 |
with login_tab:
|
177 |
display_login_form()
|
178 |
with signup_tab:
|
179 |
display_signup_form()
|
180 |
else:
|
181 |
+
# --- User is Authenticated - Display Sidebar and Page Content ---
|
182 |
with st.sidebar:
|
183 |
+
# Display Logo using get_logo_path for consistency
|
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 |
+
st.image(str(logo_file_sidebar), width=100) # Adjust width as needed
|
190 |
except Exception as e:
|
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 |
+
# Use the stored primitive username
|
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.markdown("---") # Separator
|
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"): # Unique key
|
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 relevant session state variables
|
210 |
st.session_state.authenticated_user_id = None
|
211 |
st.session_state.authenticated_username = None
|
212 |
st.session_state.current_chat_session_id = None
|
213 |
st.session_state.chat_messages = []
|
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 for app.py when logged in.
|
222 |
+
# If you have a `pages/1_Home.py`, Streamlit will render that page's content here by default
|
223 |
+
# when the user navigates to "Home" via the sidebar (or if it's the first page alphabetically).
|
224 |
+
# The content below in app.py might only be visible briefly or if no page is explicitly selected,
|
225 |
+
# or if `app.py` is configured as a page itself.
|
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.")
|