Update src/streamlit_app.py
Browse files- src/streamlit_app.py +121 -60
src/streamlit_app.py
CHANGED
@@ -10,6 +10,9 @@ import json
|
|
10 |
import logging
|
11 |
from typing import Optional, Dict, Any
|
12 |
import uuid
|
|
|
|
|
|
|
13 |
|
14 |
# Configure logging
|
15 |
logging.basicConfig(level=logging.INFO)
|
@@ -38,23 +41,11 @@ WORD_LIMIT = 180
|
|
38 |
|
39 |
class ConfigManager:
|
40 |
def __init__(self):
|
41 |
-
self.hf_token = os.
|
42 |
-
self.google_creds_json = os.
|
43 |
-
self.google_sheets_id = os.
|
44 |
|
45 |
missing_vars = []
|
46 |
-
if not os.environ.get("HF_TOKEN"):
|
47 |
-
missing_vars.append("HF_TOKEN (using mock)")
|
48 |
-
if not self.google_creds_json:
|
49 |
-
missing_vars.append("GOOGLE_SHEETS_CREDENTIALS (Google integration disabled)")
|
50 |
-
if not os.environ.get("GOOGLE_SHEETS_ID"):
|
51 |
-
missing_vars.append("GOOGLE_SHEETS_ID (using mock)")
|
52 |
-
|
53 |
-
if missing_vars:
|
54 |
-
st.warning(
|
55 |
-
f"⚠ Missing environment variables: {', '.join(missing_vars)}. "
|
56 |
-
"Some features may be disabled or mocked."
|
57 |
-
)
|
58 |
|
59 |
def get_google_client(self):
|
60 |
if not self.google_creds_json:
|
@@ -75,55 +66,52 @@ class ConfigManager:
|
|
75 |
|
76 |
class GoogleSheetsLogger:
|
77 |
"""Handles Google Sheets logging functionality"""
|
78 |
-
|
79 |
def __init__(self, config: ConfigManager):
|
80 |
self.config = config
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
"""Initialize Google Sheets connection with caching"""
|
86 |
try:
|
87 |
-
|
|
|
|
|
|
|
|
|
88 |
scope = [
|
89 |
"https://www.googleapis.com/auth/spreadsheets",
|
90 |
"https://www.googleapis.com/auth/drive"
|
91 |
]
|
92 |
creds = Credentials.from_service_account_info(creds_dict, scopes=scope)
|
93 |
client = gspread.authorize(creds)
|
94 |
-
spreadsheet = client.open_by_key(
|
95 |
sheet = spreadsheet.sheet1
|
96 |
-
|
97 |
# Initialize headers if not present
|
98 |
headers = sheet.row_values(1)
|
99 |
if not headers:
|
100 |
sheet.append_row([
|
101 |
-
"Timestamp", "Session_ID", "Button_Clicked",
|
102 |
"Question", "Subject", "Help_Click_Count", "Retry_Count"
|
103 |
])
|
104 |
-
|
105 |
return sheet
|
|
|
106 |
except Exception as e:
|
107 |
logger.error(f"Error connecting to Google Sheets: {e}")
|
108 |
-
|
109 |
return None
|
110 |
-
|
111 |
-
def log_button_click(self, button_name
|
112 |
-
help_clicks: int, retry_count: int):
|
113 |
-
"""Log button click to Google Sheets"""
|
114 |
try:
|
115 |
-
sheet = self.
|
116 |
if not sheet:
|
117 |
-
logger.
|
118 |
return
|
119 |
-
|
120 |
-
# Generate session ID if not exists
|
121 |
if "session_id" not in st.session_state:
|
122 |
st.session_state.session_id = str(uuid.uuid4())
|
123 |
-
|
124 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
125 |
-
|
126 |
-
# Log the data
|
127 |
sheet.append_row([
|
128 |
timestamp,
|
129 |
st.session_state.session_id,
|
@@ -133,16 +121,17 @@ class GoogleSheetsLogger:
|
|
133 |
help_clicks,
|
134 |
retry_count
|
135 |
])
|
136 |
-
logger.info(f"Successfully logged button click: {button_name} for question: {question[:50]}...")
|
137 |
except Exception as e:
|
138 |
logger.error(f"Error logging to Google Sheets: {e}")
|
139 |
-
|
140 |
|
141 |
class AIAssistant:
|
142 |
"""Handles AI model interactions"""
|
143 |
|
144 |
def __init__(self, config: ConfigManager):
|
145 |
-
self.
|
|
|
|
|
146 |
self.base_prompt = f"""
|
147 |
You are an AI assistant designed to support high school students in the subject of {SUBJECT}.
|
148 |
Your role is to offer friendly, helpful, concise, in-depth guidance, just like a supportive teacher would.
|
@@ -171,32 +160,73 @@ Please follow these guidelines:
|
|
171 |
}
|
172 |
|
173 |
def generate_response(self, button_name: str, question: str, retry_count: int = 0) -> str:
|
174 |
-
"""Generate AI response based on button type and question"""
|
175 |
try:
|
176 |
-
|
|
|
177 |
if retry_count > 0:
|
178 |
-
|
|
|
179 |
|
180 |
-
|
181 |
-
full_prompt = f"{prompt}\n\nQuestion:\n{question}"
|
182 |
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
190 |
|
191 |
-
|
192 |
-
|
193 |
-
return cleaned_response.strip()
|
194 |
-
|
195 |
except Exception as e:
|
196 |
-
logger.
|
197 |
return "Sorry, I encountered an error generating a response. Please try again."
|
198 |
|
199 |
|
|
|
|
|
|
|
200 |
class SessionManager:
|
201 |
"""Manages Streamlit session state"""
|
202 |
|
@@ -666,6 +696,36 @@ class HelpInterface:
|
|
666 |
st.success("🎉 Glad I could help! Feel free to ask for help anytime.")
|
667 |
st.rerun()
|
668 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
669 |
def main():
|
670 |
"""Main application entry point"""
|
671 |
st.markdown("""
|
@@ -778,7 +838,8 @@ def main():
|
|
778 |
</style>
|
779 |
""", unsafe_allow_html=True)
|
780 |
|
781 |
-
if __name__ == "__main__":
|
782 |
-
main()
|
783 |
|
784 |
|
|
|
|
|
|
|
|
10 |
import logging
|
11 |
from typing import Optional, Dict, Any
|
12 |
import uuid
|
13 |
+
from huggingface_hub.utils import HfHubHTTPError
|
14 |
+
import httpx
|
15 |
+
|
16 |
|
17 |
# Configure logging
|
18 |
logging.basicConfig(level=logging.INFO)
|
|
|
41 |
|
42 |
class ConfigManager:
|
43 |
def __init__(self):
|
44 |
+
self.hf_token = os.getenv("HF_TOKEN")
|
45 |
+
self.google_creds_json = os.getenv("GOOGLE_SHEETS_CREDENTIALS")
|
46 |
+
self.google_sheets_id = os.getenv("GOOGLE_SHEETS_ID")
|
47 |
|
48 |
missing_vars = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
def get_google_client(self):
|
51 |
if not self.google_creds_json:
|
|
|
66 |
|
67 |
class GoogleSheetsLogger:
|
68 |
"""Handles Google Sheets logging functionality"""
|
69 |
+
|
70 |
def __init__(self, config: ConfigManager):
|
71 |
self.config = config
|
72 |
+
|
73 |
+
@st.cache_resource(show_spinner=False)
|
74 |
+
def _init_google_sheets(creds_json: str, sheet_id: str):
|
75 |
+
"""Initialize Google Sheets connection with caching based on inputs."""
|
|
|
76 |
try:
|
77 |
+
if not creds_json or not sheet_id:
|
78 |
+
logger.info("Google Sheets disabled: missing creds_json or sheet_id.")
|
79 |
+
return None
|
80 |
+
|
81 |
+
creds_dict = json.loads(creds_json)
|
82 |
scope = [
|
83 |
"https://www.googleapis.com/auth/spreadsheets",
|
84 |
"https://www.googleapis.com/auth/drive"
|
85 |
]
|
86 |
creds = Credentials.from_service_account_info(creds_dict, scopes=scope)
|
87 |
client = gspread.authorize(creds)
|
88 |
+
spreadsheet = client.open_by_key(sheet_id)
|
89 |
sheet = spreadsheet.sheet1
|
90 |
+
|
91 |
# Initialize headers if not present
|
92 |
headers = sheet.row_values(1)
|
93 |
if not headers:
|
94 |
sheet.append_row([
|
95 |
+
"Timestamp", "Session_ID", "Button_Clicked",
|
96 |
"Question", "Subject", "Help_Click_Count", "Retry_Count"
|
97 |
])
|
98 |
+
|
99 |
return sheet
|
100 |
+
|
101 |
except Exception as e:
|
102 |
logger.error(f"Error connecting to Google Sheets: {e}")
|
103 |
+
# Do NOT crash the UI; just return None so logging becomes a no-op
|
104 |
return None
|
105 |
+
|
106 |
+
def log_button_click(self, button_name, question, subject, help_clicks, retry_count):
|
|
|
|
|
107 |
try:
|
108 |
+
sheet = _get_sheet(self.config.google_creds_json, self.config.google_sheets_id)
|
109 |
if not sheet:
|
110 |
+
logger.info("Google Sheets not available; skipping log.")
|
111 |
return
|
|
|
|
|
112 |
if "session_id" not in st.session_state:
|
113 |
st.session_state.session_id = str(uuid.uuid4())
|
|
|
114 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
115 |
sheet.append_row([
|
116 |
timestamp,
|
117 |
st.session_state.session_id,
|
|
|
121 |
help_clicks,
|
122 |
retry_count
|
123 |
])
|
|
|
124 |
except Exception as e:
|
125 |
logger.error(f"Error logging to Google Sheets: {e}")
|
126 |
+
|
127 |
|
128 |
class AIAssistant:
|
129 |
"""Handles AI model interactions"""
|
130 |
|
131 |
def __init__(self, config: ConfigManager):
|
132 |
+
self.model = MODEL
|
133 |
+
self.client = InferenceClient(model=self.model, token=config.hf_token, timeout=60.0)
|
134 |
+
|
135 |
self.base_prompt = f"""
|
136 |
You are an AI assistant designed to support high school students in the subject of {SUBJECT}.
|
137 |
Your role is to offer friendly, helpful, concise, in-depth guidance, just like a supportive teacher would.
|
|
|
160 |
}
|
161 |
|
162 |
def generate_response(self, button_name: str, question: str, retry_count: int = 0) -> str:
|
|
|
163 |
try:
|
164 |
+
# Build prompts
|
165 |
+
system_text = self.base_prompt + self.prompt_templates.get(button_name, "")
|
166 |
if retry_count > 0:
|
167 |
+
system_text += f"\nPlease provide a different explanation. This is attempt {retry_count + 1}."
|
168 |
+
user_text = f"Question:\n{question}"
|
169 |
|
170 |
+
full_prompt = f"{system_text}\n\n{user_text}" # still used for text_generation
|
|
|
171 |
|
172 |
+
try:
|
173 |
+
# Try classic text-generation first
|
174 |
+
text = self.client.text_generation(
|
175 |
+
prompt=full_prompt,
|
176 |
+
max_new_tokens=300,
|
177 |
+
temperature=0.7,
|
178 |
+
repetition_penalty=1.1,
|
179 |
+
model=self.model, # explicit even though client is bound
|
180 |
+
)
|
181 |
+
except (HfHubHTTPError, ValueError) as e:
|
182 |
+
# If the provider/model doesn't support text-generation, fall back to chat
|
183 |
+
msg = str(e)
|
184 |
+
unsupported = (
|
185 |
+
"Task 'text-generation' not supported" in msg
|
186 |
+
or "doesn't support task 'text-generation'" in msg
|
187 |
+
or "Available tasks: ['conversational']" in msg
|
188 |
+
)
|
189 |
+
if unsupported:
|
190 |
+
# OpenAI-style chat interface
|
191 |
+
# Build messages: system + user
|
192 |
+
messages = [
|
193 |
+
{"role": "system", "content": system_text},
|
194 |
+
{"role": "user", "content": user_text},
|
195 |
+
]
|
196 |
+
chat = self.client.chat_completion(
|
197 |
+
messages=messages,
|
198 |
+
max_tokens=350,
|
199 |
+
temperature=0.7,
|
200 |
+
model=self.model,
|
201 |
+
)
|
202 |
+
# Robust extraction
|
203 |
+
text = ""
|
204 |
+
try:
|
205 |
+
# chat.choices[0].message.content (OpenAI-like)
|
206 |
+
choices = getattr(chat, "choices", None) or chat.get("choices", [])
|
207 |
+
if choices:
|
208 |
+
msg0 = choices[0].get("message") or {}
|
209 |
+
text = msg0.get("content") or ""
|
210 |
+
if not text:
|
211 |
+
# Some providers return 'generated_text'
|
212 |
+
text = getattr(chat, "generated_text", None) or chat.get("generated_text", "") or ""
|
213 |
+
except Exception:
|
214 |
+
text = str(chat)
|
215 |
+
else:
|
216 |
+
raise
|
217 |
+
except (httpx.ReadTimeout, httpx.ConnectTimeout):
|
218 |
+
return "The model request timed out. Please try again."
|
219 |
|
220 |
+
cleaned = re.sub(r"<think>.*?</think>", "", text or "", flags=re.DOTALL)
|
221 |
+
return cleaned.strip() or "Sorry, I couldn't generate a response."
|
|
|
|
|
222 |
except Exception as e:
|
223 |
+
logger.exception(f"Error generating AI response: {e}")
|
224 |
return "Sorry, I encountered an error generating a response. Please try again."
|
225 |
|
226 |
|
227 |
+
|
228 |
+
|
229 |
+
|
230 |
class SessionManager:
|
231 |
"""Manages Streamlit session state"""
|
232 |
|
|
|
696 |
st.success("🎉 Glad I could help! Feel free to ask for help anytime.")
|
697 |
st.rerun()
|
698 |
|
699 |
+
@st.cache_resource(show_spinner=False)
|
700 |
+
def _get_sheet(_creds_json: str, _sheet_id: str):
|
701 |
+
try:
|
702 |
+
if not _creds_json or not _sheet_id:
|
703 |
+
logger.info("Sheets disabled: missing creds or id.")
|
704 |
+
return None
|
705 |
+
|
706 |
+
creds_dict = json.loads(_creds_json)
|
707 |
+
scope = [
|
708 |
+
"https://www.googleapis.com/auth/spreadsheets",
|
709 |
+
"https://www.googleapis.com/auth/drive",
|
710 |
+
]
|
711 |
+
creds = Credentials.from_service_account_info(creds_dict, scopes=scope)
|
712 |
+
client = gspread.authorize(creds)
|
713 |
+
spreadsheet = client.open_by_key(_sheet_id)
|
714 |
+
sheet = spreadsheet.sheet1
|
715 |
+
|
716 |
+
headers = sheet.row_values(1)
|
717 |
+
if not headers:
|
718 |
+
sheet.append_row([
|
719 |
+
"Timestamp","Session_ID","Button_Clicked",
|
720 |
+
"Question","Subject","Help_Click_Count","Retry_Count"
|
721 |
+
])
|
722 |
+
return sheet
|
723 |
+
except Exception as e:
|
724 |
+
logger.error(f"Error connecting to Google Sheets: {e}")
|
725 |
+
return None
|
726 |
+
|
727 |
+
|
728 |
+
|
729 |
def main():
|
730 |
"""Main application entry point"""
|
731 |
st.markdown("""
|
|
|
838 |
</style>
|
839 |
""", unsafe_allow_html=True)
|
840 |
|
|
|
|
|
841 |
|
842 |
|
843 |
+
|
844 |
+
if __name__ == "__main__":
|
845 |
+
main()
|