rdune71 commited on
Commit
0194326
·
1 Parent(s): e2ee43d

Implement developer review recommendations: enhanced session management, improved error handling, and dependency updates

Browse files
Files changed (5) hide show
  1. api/chat.py +20 -15
  2. api/main.py +27 -4
  3. app.py +59 -56
  4. core/session.py +29 -12
  5. requirements.txt +3 -0
api/chat.py CHANGED
@@ -3,8 +3,11 @@ from fastapi import APIRouter, HTTPException
3
  from fastapi.responses import StreamingResponse
4
  from core.llm import LLMClient
5
  from core.memory import save_user_state, load_user_state
 
 
6
 
7
  router = APIRouter()
 
8
 
9
  llm_client = LLMClient(provider="ollama") # Default to Ollama
10
 
@@ -12,28 +15,30 @@ llm_client = LLMClient(provider="ollama") # Default to Ollama
12
  async def chat(user_id: str, message: str):
13
  if not message:
14
  raise HTTPException(status_code=400, detail="Message is required")
15
-
16
- # Load user state from Redis
17
- user_state = load_user_state(user_id)
18
- conversation_history = json.loads(user_state.get("conversation", "[]")) if user_state else []
19
-
20
- # Add user message to history
21
- conversation_history.append({"role": "user", "content": message})
22
-
23
- # Generate AI response
24
  try:
 
 
 
 
 
 
 
 
25
  full_response = ""
26
  response_stream = llm_client.generate(prompt=message, stream=True)
27
-
28
  # Collect streamed response
29
  for chunk in response_stream:
30
  full_response += chunk
31
-
32
- # Save updated conversation
33
  conversation_history.append({"role": "assistant", "content": full_response})
34
- save_user_state(user_id, {"conversation": json.dumps(conversation_history)})
35
-
 
36
  return {"response": full_response}
37
-
38
  except Exception as e:
 
39
  raise HTTPException(status_code=500, detail=f"LLM generation failed: {e}")
 
3
  from fastapi.responses import StreamingResponse
4
  from core.llm import LLMClient
5
  from core.memory import save_user_state, load_user_state
6
+ from core.session import session_manager
7
+ import logging
8
 
9
  router = APIRouter()
10
+ logger = logging.getLogger(__name__)
11
 
12
  llm_client = LLMClient(provider="ollama") # Default to Ollama
13
 
 
15
  async def chat(user_id: str, message: str):
16
  if not message:
17
  raise HTTPException(status_code=400, detail="Message is required")
18
+
 
 
 
 
 
 
 
 
19
  try:
20
+ # Use session manager for better session handling
21
+ session = session_manager.get_session(user_id)
22
+ conversation_history = session.get("conversation", [])
23
+
24
+ # Add user message to history
25
+ conversation_history.append({"role": "user", "content": message})
26
+
27
+ # Generate AI response
28
  full_response = ""
29
  response_stream = llm_client.generate(prompt=message, stream=True)
30
+
31
  # Collect streamed response
32
  for chunk in response_stream:
33
  full_response += chunk
34
+
35
+ # Save updated conversation using session manager
36
  conversation_history.append({"role": "assistant", "content": full_response})
37
+ session_manager.update_session(user_id, {"conversation": conversation_history})
38
+
39
+ logger.info(f"Successfully processed chat for user {user_id}")
40
  return {"response": full_response}
41
+
42
  except Exception as e:
43
+ logger.error(f"LLM generation failed for user {user_id}: {e}")
44
  raise HTTPException(status_code=500, detail=f"LLM generation failed: {e}")
api/main.py CHANGED
@@ -2,8 +2,10 @@ from fastapi import FastAPI
2
  from api.status import router as status_router
3
  from api.chat import router as chat_router
4
  from core.memory import check_redis_health
 
5
 
6
  app = FastAPI()
 
7
 
8
  # Mount routers
9
  app.include_router(status_router, prefix="/api")
@@ -11,13 +13,34 @@ app.include_router(chat_router, prefix="/api")
11
 
12
  @app.get("/")
13
  async def root():
 
14
  return {"message": "AI Life Coach API is running"}
15
 
16
  @app.get("/health")
17
  async def health_check():
18
- """Health check endpoint"""
19
  redis_healthy = check_redis_health()
20
- return {
21
- "status": "healthy" if redis_healthy else "degraded",
22
- "redis": "healthy" if redis_healthy else "unhealthy"
 
 
 
 
 
 
 
23
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from api.status import router as status_router
3
  from api.chat import router as chat_router
4
  from core.memory import check_redis_health
5
+ import logging
6
 
7
  app = FastAPI()
8
+ logger = logging.getLogger(__name__)
9
 
10
  # Mount routers
11
  app.include_router(status_router, prefix="/api")
 
13
 
14
  @app.get("/")
15
  async def root():
16
+ logger.info("API root endpoint accessed")
17
  return {"message": "AI Life Coach API is running"}
18
 
19
  @app.get("/health")
20
  async def health_check():
21
+ """Comprehensive health check endpoint"""
22
  redis_healthy = check_redis_health()
23
+
24
+ # Additional health checks could be added here
25
+ overall_healthy = redis_healthy
26
+
27
+ health_status = {
28
+ "status": "healthy" if overall_healthy else "degraded",
29
+ "services": {
30
+ "redis": "healthy" if redis_healthy else "unhealthy"
31
+ },
32
+ "timestamp": __import__('time').time()
33
  }
34
+
35
+ if overall_healthy:
36
+ logger.info("Health check passed")
37
+ else:
38
+ logger.warning("Health check degraded")
39
+
40
+ return health_status
41
+
42
+ # Add startup event for initialization logging
43
+ @app.on_event("startup")
44
+ async def startup_event():
45
+ logger.info("AI Life Coach API starting up...")
46
+ logger.info("Redis health: %s", "healthy" if check_redis_health() else "unhealthy")
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # Force redeploy trigger - version 2.2
2
  import streamlit as st
3
  from utils.config import config
4
  import requests
@@ -6,22 +6,29 @@ import json
6
  import os
7
  from core.memory import load_user_state, check_redis_health
8
 
9
- # Set page config
10
  st.set_page_config(page_title="AI Life Coach", page_icon="🧘", layout="centered")
11
 
12
- # Comprehensive session state initialization
13
- session_keys_defaults = {
14
- 'ngrok_url': config.ollama_host,
15
- 'model_status': "checking",
16
- 'available_models': [],
17
- 'selected_model': config.local_model_name,
18
- 'selected_model_index': 0,
19
- 'user_message_input': ""
20
- }
 
 
 
 
 
 
 
 
21
 
22
- for key, default_value in session_keys_defaults.items():
23
- if key not in st.session_state:
24
- st.session_state[key] = default_value
25
 
26
  # Sidebar for user selection
27
  st.sidebar.title("🧘 AI Life Coach")
@@ -32,11 +39,12 @@ st.sidebar.markdown("---")
32
  st.sidebar.subheader("Ollama Connection")
33
  ngrok_input = st.sidebar.text_input(
34
  "Ngrok URL",
35
- value=st.session_state.ngrok_url,
36
  key="ngrok_url_input" # Explicit key
37
  )
38
 
39
- if st.sidebar.button("Update Ngrok URL", key="update_ngrok_button"): # Explicit key
 
40
  st.session_state.ngrok_url = ngrok_input
41
  st.session_state.model_status = "checking"
42
  st.session_state.available_models = []
@@ -54,8 +62,8 @@ NGROK_HEADERS = {
54
  def fetch_available_models(ngrok_url):
55
  try:
56
  response = requests.get(
57
- f"{ngrok_url}/api/tags",
58
- headers=NGROK_HEADERS,
59
  timeout=5
60
  )
61
  if response.status_code == 200:
@@ -65,15 +73,14 @@ def fetch_available_models(ngrok_url):
65
  pass
66
  return []
67
 
68
- # Update available models
69
- if st.session_state.ngrok_url and st.session_state.model_status != "unreachable":
70
- model_names = fetch_available_models(st.session_state.ngrok_url)
71
- if model_names:
72
- st.session_state.available_models = model_names
73
- # If current selected model not in list, select the first one
74
- if st.session_state.selected_model not in model_names:
75
- st.session_state.selected_model = model_names[0]
76
- st.session_state.selected_model_index = 0
77
 
78
  # Model selector dropdown - REPLACED ENTIRE SECTION
79
  st.sidebar.markdown("---")
@@ -108,7 +115,7 @@ if st.session_state.available_models:
108
  else:
109
  st.sidebar.warning("No models available - check Ollama connection")
110
  model_input = st.sidebar.text_input(
111
- "Or enter model name",
112
  value=st.session_state.selected_model,
113
  key="manual_model_input" # Explicit key
114
  )
@@ -132,7 +139,6 @@ def get_ollama_status(ngrok_url):
132
  models = response.json().get("models", [])
133
  model_names = [m.get("name") for m in models]
134
  st.session_state.available_models = model_names
135
-
136
  if models:
137
  selected_model_available = st.session_state.selected_model in model_names
138
  return {
@@ -236,11 +242,9 @@ use_fallback = not ollama_status.get("running", False) or config.use_fallback
236
  # Display Ollama status - Enhanced section with Hugging Face scaling behavior info
237
  if use_fallback:
238
  st.sidebar.warning("🌐 Using Hugging Face fallback (Ollama not available)")
239
-
240
  # Add special note for Hugging Face scaling behavior
241
  if config.hf_api_url and "endpoints.huggingface.cloud" in config.hf_api_url:
242
  st.sidebar.info("ℹ️ HF Endpoint may be initializing (up to 4 min)")
243
-
244
  if "error" in ollama_status:
245
  st.sidebar.caption(f"Error: {ollama_status['error'][:50]}...")
246
  else:
@@ -272,18 +276,17 @@ else:
272
  st.title("🧘 AI Life Coach")
273
  st.markdown("Talk to your personal development assistant.")
274
 
275
- # Show detailed status
276
- with st.expander("🔍 Connection Status"):
277
- st.write("Ollama Status:", ollama_status)
278
- st.write("Model Status:", st.session_state.model_status)
279
- st.write("Selected Model:", st.session_state.selected_model)
280
- st.write("Available Models:", st.session_state.available_models)
281
- st.write("Environment Info:")
282
- st.write("- Is HF Space:", IS_HF_SPACE)
283
- st.write("- Base URL:", BASE_URL or "Not in HF Space")
284
- st.write("- Current Ngrok URL:", st.session_state.ngrok_url)
285
- st.write("- Using Fallback:", use_fallback)
286
- st.write("- Redis Health:", check_redis_health())
287
 
288
  # Function to send message to Ollama
289
  def send_to_ollama(user_input, conversation_history, ngrok_url, model_name):
@@ -299,9 +302,9 @@ def send_to_ollama(user_input, conversation_history, ngrok_url, model_name):
299
  }
300
  }
301
  response = requests.post(
302
- f"{ngrok_url}/api/chat",
303
- json=payload,
304
- headers=NGROK_HEADERS,
305
  timeout=60
306
  )
307
  if response.status_code == 200:
@@ -339,7 +342,7 @@ conversation = get_conversation_history(user)
339
  for msg in conversation:
340
  role = msg["role"].capitalize()
341
  content = msg["content"]
342
- st.markdown(f"**{role}:** {content}")
343
 
344
  # Chat input - REPLACED SECTION
345
  user_input = st.text_input(
@@ -357,7 +360,7 @@ if send_button:
357
  st.warning("Please enter a message.")
358
  else:
359
  # Display user message
360
- st.markdown(f"**You:** {user_input}")
361
 
362
  # Prepare conversation history
363
  conversation_history = [{"role": msg["role"], "content": msg["content"]} for msg in conversation[-5:]]
@@ -370,17 +373,17 @@ if send_button:
370
  backend_used = "Hugging Face"
371
  else:
372
  ai_response = send_to_ollama(
373
- user_input,
374
- conversation_history,
375
- st.session_state.ngrok_url,
376
  st.session_state.selected_model
377
  )
378
  backend_used = "Ollama"
379
-
380
- if ai_response:
381
- st.markdown(f"**AI Coach ({backend_used}):** {ai_response}")
382
- else:
383
- st.error(f"Failed to get response from {backend_used}.")
384
 
385
  # Clear input after sending (this helps prevent duplicate sends)
386
  st.session_state.user_message_input = ""
 
1
+ # Force redeploy trigger - version 2.3
2
  import streamlit as st
3
  from utils.config import config
4
  import requests
 
6
  import os
7
  from core.memory import load_user_state, check_redis_health
8
 
9
+ # Set page config FIRST (before any other Streamlit commands)
10
  st.set_page_config(page_title="AI Life Coach", page_icon="🧘", layout="centered")
11
 
12
+ # ROBUST SESSION STATE INITIALIZATION
13
+ # This must happen before ANY widget creation
14
+ def init_session_state():
15
+ """Initialize all session state variables with proper defaults"""
16
+ defaults = {
17
+ 'ngrok_url': config.ollama_host,
18
+ 'model_status': "checking",
19
+ 'available_models': [],
20
+ 'selected_model': config.local_model_name,
21
+ 'selected_model_index': 0,
22
+ 'user_message_input': "",
23
+ 'user_selector': "Rob" # Add missing default
24
+ }
25
+
26
+ for key, default_value in defaults.items():
27
+ if key not in st.session_state:
28
+ st.session_state[key] = default_value
29
 
30
+ # CALL THIS FIRST
31
+ init_session_state()
 
32
 
33
  # Sidebar for user selection
34
  st.sidebar.title("🧘 AI Life Coach")
 
39
  st.sidebar.subheader("Ollama Connection")
40
  ngrok_input = st.sidebar.text_input(
41
  "Ngrok URL",
42
+ value=st.session_state.ngrok_url,
43
  key="ngrok_url_input" # Explicit key
44
  )
45
 
46
+ if st.sidebar.button("Update Ngrok URL", key="update_ngrok_button"):
47
+ # Explicit key
48
  st.session_state.ngrok_url = ngrok_input
49
  st.session_state.model_status = "checking"
50
  st.session_state.available_models = []
 
62
  def fetch_available_models(ngrok_url):
63
  try:
64
  response = requests.get(
65
+ f"{ngrok_url}/api/tags",
66
+ headers=NGROK_HEADERS,
67
  timeout=5
68
  )
69
  if response.status_code == 200:
 
73
  pass
74
  return []
75
 
76
+ # Update available models if st.session_state.ngrok_url and st.session_state.model_status != "unreachable":
77
+ model_names = fetch_available_models(st.session_state.ngrok_url)
78
+ if model_names:
79
+ st.session_state.available_models = model_names
80
+ # If current selected model not in list, select the first one
81
+ if st.session_state.selected_model not in model_names:
82
+ st.session_state.selected_model = model_names[0]
83
+ st.session_state.selected_model_index = 0
 
84
 
85
  # Model selector dropdown - REPLACED ENTIRE SECTION
86
  st.sidebar.markdown("---")
 
115
  else:
116
  st.sidebar.warning("No models available - check Ollama connection")
117
  model_input = st.sidebar.text_input(
118
+ "Or enter model name",
119
  value=st.session_state.selected_model,
120
  key="manual_model_input" # Explicit key
121
  )
 
139
  models = response.json().get("models", [])
140
  model_names = [m.get("name") for m in models]
141
  st.session_state.available_models = model_names
 
142
  if models:
143
  selected_model_available = st.session_state.selected_model in model_names
144
  return {
 
242
  # Display Ollama status - Enhanced section with Hugging Face scaling behavior info
243
  if use_fallback:
244
  st.sidebar.warning("🌐 Using Hugging Face fallback (Ollama not available)")
 
245
  # Add special note for Hugging Face scaling behavior
246
  if config.hf_api_url and "endpoints.huggingface.cloud" in config.hf_api_url:
247
  st.sidebar.info("ℹ️ HF Endpoint may be initializing (up to 4 min)")
 
248
  if "error" in ollama_status:
249
  st.sidebar.caption(f"Error: {ollama_status['error'][:50]}...")
250
  else:
 
276
  st.title("🧘 AI Life Coach")
277
  st.markdown("Talk to your personal development assistant.")
278
 
279
+ # Show detailed status with st.expander("🔍 Connection Status"):
280
+ st.write("Ollama Status:", ollama_status)
281
+ st.write("Model Status:", st.session_state.model_status)
282
+ st.write("Selected Model:", st.session_state.selected_model)
283
+ st.write("Available Models:", st.session_state.available_models)
284
+ st.write("Environment Info:")
285
+ st.write("- Is HF Space:", IS_HF_SPACE)
286
+ st.write("- Base URL:", BASE_URL or "Not in HF Space")
287
+ st.write("- Current Ngrok URL:", st.session_state.ngrok_url)
288
+ st.write("- Using Fallback:", use_fallback)
289
+ st.write("- Redis Health:", check_redis_health())
 
290
 
291
  # Function to send message to Ollama
292
  def send_to_ollama(user_input, conversation_history, ngrok_url, model_name):
 
302
  }
303
  }
304
  response = requests.post(
305
+ f"{ngrok_url}/api/chat",
306
+ json=payload,
307
+ headers=NGROK_HEADERS,
308
  timeout=60
309
  )
310
  if response.status_code == 200:
 
342
  for msg in conversation:
343
  role = msg["role"].capitalize()
344
  content = msg["content"]
345
+ st.markdown(f"{role}: {content}")
346
 
347
  # Chat input - REPLACED SECTION
348
  user_input = st.text_input(
 
360
  st.warning("Please enter a message.")
361
  else:
362
  # Display user message
363
+ st.markdown(f"You: {user_input}")
364
 
365
  # Prepare conversation history
366
  conversation_history = [{"role": msg["role"], "content": msg["content"]} for msg in conversation[-5:]]
 
373
  backend_used = "Hugging Face"
374
  else:
375
  ai_response = send_to_ollama(
376
+ user_input,
377
+ conversation_history,
378
+ st.session_state.ngrok_url,
379
  st.session_state.selected_model
380
  )
381
  backend_used = "Ollama"
382
+
383
+ if ai_response:
384
+ st.markdown(f"AI Coach ({backend_used}): {ai_response}")
385
+ else:
386
+ st.error(f"Failed to get response from {backend_used}.")
387
 
388
  # Clear input after sending (this helps prevent duplicate sends)
389
  st.session_state.user_message_input = ""
core/session.py CHANGED
@@ -2,6 +2,11 @@ import json
2
  import time
3
  from typing import Dict, Any, Optional
4
  from core.memory import load_user_state, save_user_state
 
 
 
 
 
5
 
6
  class SessionManager:
7
  """Manages user sessions and conversation context"""
@@ -14,7 +19,7 @@ class SessionManager:
14
  session_timeout: Session timeout in seconds (default: 1 hour)
15
  """
16
  self.session_timeout = session_timeout
17
-
18
  def get_session(self, user_id: str) -> Dict[str, Any]:
19
  """
20
  Retrieve user session data
@@ -28,18 +33,20 @@ class SessionManager:
28
  try:
29
  state = load_user_state(user_id)
30
  if not state:
 
31
  return self._create_new_session()
32
 
33
  # Check if session has expired
34
- last_activity = state.get('last_activity', 0)
35
  if time.time() - last_activity > self.session_timeout:
 
36
  return self._create_new_session()
37
-
38
  return state
39
  except Exception as e:
40
- print(f"Error retrieving session for user {user_id}: {e}")
41
  return self._create_new_session()
42
-
43
  def update_session(self, user_id: str, data: Dict[str, Any]) -> bool:
44
  """
45
  Update user session data
@@ -60,11 +67,16 @@ class SessionManager:
60
  session['last_activity'] = time.time()
61
 
62
  # Save updated session
63
- return save_user_state(user_id, session)
 
 
 
 
 
64
  except Exception as e:
65
- print(f"Error updating session for user {user_id}: {e}")
66
  return False
67
-
68
  def clear_session(self, user_id: str) -> bool:
69
  """
70
  Clear user session data
@@ -76,11 +88,14 @@ class SessionManager:
76
  Boolean indicating success
77
  """
78
  try:
79
- return save_user_state(user_id, {})
 
 
 
80
  except Exception as e:
81
- print(f"Error clearing session for user {user_id}: {e}")
82
  return False
83
-
84
  def _create_new_session(self) -> Dict[str, Any]:
85
  """
86
  Create a new session with default values
@@ -88,12 +103,14 @@ class SessionManager:
88
  Returns:
89
  Dictionary containing new session data
90
  """
91
- return {
92
  'conversation': [],
93
  'preferences': {},
94
  'last_activity': time.time(),
95
  'created_at': time.time()
96
  }
 
 
97
 
98
  # Global session manager instance
99
  session_manager = SessionManager()
 
2
  import time
3
  from typing import Dict, Any, Optional
4
  from core.memory import load_user_state, save_user_state
5
+ import logging
6
+
7
+ # Set up logging
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
 
11
  class SessionManager:
12
  """Manages user sessions and conversation context"""
 
19
  session_timeout: Session timeout in seconds (default: 1 hour)
20
  """
21
  self.session_timeout = session_timeout
22
+
23
  def get_session(self, user_id: str) -> Dict[str, Any]:
24
  """
25
  Retrieve user session data
 
33
  try:
34
  state = load_user_state(user_id)
35
  if not state:
36
+ logger.info(f"Creating new session for user {user_id}")
37
  return self._create_new_session()
38
 
39
  # Check if session has expired
40
+ last_activity = float(state.get('last_activity', 0))
41
  if time.time() - last_activity > self.session_timeout:
42
+ logger.info(f"Session expired for user {user_id}, creating new session")
43
  return self._create_new_session()
44
+
45
  return state
46
  except Exception as e:
47
+ logger.error(f"Error retrieving session for user {user_id}: {e}")
48
  return self._create_new_session()
49
+
50
  def update_session(self, user_id: str, data: Dict[str, Any]) -> bool:
51
  """
52
  Update user session data
 
67
  session['last_activity'] = time.time()
68
 
69
  # Save updated session
70
+ result = save_user_state(user_id, session)
71
+ if result:
72
+ logger.debug(f"Successfully updated session for user {user_id}")
73
+ else:
74
+ logger.warning(f"Failed to save session for user {user_id}")
75
+ return result
76
  except Exception as e:
77
+ logger.error(f"Error updating session for user {user_id}: {e}")
78
  return False
79
+
80
  def clear_session(self, user_id: str) -> bool:
81
  """
82
  Clear user session data
 
88
  Boolean indicating success
89
  """
90
  try:
91
+ result = save_user_state(user_id, {})
92
+ if result:
93
+ logger.info(f"Cleared session for user {user_id}")
94
+ return result
95
  except Exception as e:
96
+ logger.error(f"Error clearing session for user {user_id}: {e}")
97
  return False
98
+
99
  def _create_new_session(self) -> Dict[str, Any]:
100
  """
101
  Create a new session with default values
 
103
  Returns:
104
  Dictionary containing new session data
105
  """
106
+ session = {
107
  'conversation': [],
108
  'preferences': {},
109
  'last_activity': time.time(),
110
  'created_at': time.time()
111
  }
112
+ logger.debug("Created new session")
113
+ return session
114
 
115
  # Global session manager instance
116
  session_manager = SessionManager()
requirements.txt CHANGED
@@ -8,3 +8,6 @@ tavily-python>=0.1.0,<1.0.0
8
  requests==2.31.0
9
  docker==6.1.3
10
  pygame==2.5.2
 
 
 
 
8
  requests==2.31.0
9
  docker==6.1.3
10
  pygame==2.5.2
11
+ # Add these for enhanced functionality
12
+ pydantic==1.10.7
13
+ typing-extensions>=4.5.0