rdune71 commited on
Commit
45df059
·
1 Parent(s): 5b5f50c

Fix critical deployment issues: Redis connection, Ollama URL sanitization, Hugging Face provider, config validation, and enhanced error handling

Browse files
api/status.py CHANGED
@@ -1,12 +1,76 @@
1
- from fastapi import APIRouter
 
 
2
  from services.ollama_monitor import check_ollama_status
 
 
3
 
 
4
  router = APIRouter()
5
 
6
  @router.get("/ollama-status")
7
  async def get_ollama_status():
8
- """
9
- Returns the current status of the Ollama service.
10
- """
11
- status = check_ollama_status()
12
- return status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ import logging
3
+ from core.redis_client import redis_client
4
  from services.ollama_monitor import check_ollama_status
5
+ from utils.config import config
6
+ from utils.config_validator import ConfigValidator
7
 
8
+ logger = logging.getLogger(__name__)
9
  router = APIRouter()
10
 
11
  @router.get("/ollama-status")
12
  async def get_ollama_status():
13
+ """Returns the current status of the Ollama service."""
14
+ try:
15
+ status = check_ollama_status()
16
+ return status
17
+ except Exception as e:
18
+ logger.error(f"Error checking Ollama status: {e}")
19
+ raise HTTPException(status_code=500, detail=str(e))
20
+
21
+ @router.get("/health")
22
+ async def health_check():
23
+ """Comprehensive health check for all services"""
24
+ try:
25
+ # Check configuration
26
+ validator = ConfigValidator()
27
+ config_report = validator.validate_all()
28
+
29
+ # Check Redis
30
+ redis_healthy = redis_client.is_healthy() if redis_client.get_client() else False
31
+
32
+ # Check Ollama
33
+ try:
34
+ ollama_status = check_ollama_status()
35
+ ollama_healthy = ollama_status.get("running", False)
36
+ except Exception as e:
37
+ logger.error(f"Error checking Ollama status: {e}")
38
+ ollama_healthy = False
39
+ ollama_status = {"error": str(e)}
40
+
41
+ # Overall health
42
+ overall_healthy = redis_healthy and ollama_healthy and config_report['valid']
43
+
44
+ return {
45
+ "status": "healthy" if overall_healthy else "degraded",
46
+ "services": {
47
+ "redis": "healthy" if redis_healthy else "unhealthy",
48
+ "ollama": "healthy" if ollama_healthy else "unhealthy",
49
+ "configuration": "valid" if config_report['valid'] else "invalid"
50
+ },
51
+ "details": {
52
+ "redis_healthy": redis_healthy,
53
+ "ollama_status": ollama_status,
54
+ "config_validation": config_report
55
+ }
56
+ }
57
+ except Exception as e:
58
+ logger.error(f"Health check failed: {e}")
59
+ raise HTTPException(status_code=500, detail=str(e))
60
+
61
+ @router.get("/config")
62
+ async def config_status():
63
+ """Return current configuration status"""
64
+ validator = ConfigValidator()
65
+ report = validator.validate_all()
66
+
67
+ return {
68
+ "configuration": {
69
+ "ollama_host": config.ollama_host,
70
+ "hf_api_url": config.hf_api_url,
71
+ "redis_host": config.redis_host,
72
+ "redis_port": config.redis_port,
73
+ "use_fallback": config.use_fallback
74
+ },
75
+ "validation": report
76
+ }
core/memory.py CHANGED
@@ -1,94 +1,23 @@
1
  import json
2
  import time
3
  import logging
 
4
  from utils.config import config
5
 
6
  # Set up logging
7
  logging.basicConfig(level=logging.INFO)
8
  logger = logging.getLogger(__name__)
9
 
10
- # Try to import redis, but handle if not available
11
- try:
12
- import redis
13
- REDIS_AVAILABLE = True
14
- except ImportError:
15
- REDIS_AVAILABLE = False
16
- redis = None
17
-
18
- def get_redis_client():
19
- """Create and return Redis client with retry logic"""
20
- if not REDIS_AVAILABLE:
21
- return None
22
-
23
- # Debug print to verify secrets are loaded (separate lines to avoid formatting issues)
24
- logger.info(f"[DEBUG] Redis Host = {config.redis_host}")
25
- logger.info(f"[DEBUG] Redis Port = {config.redis_port}")
26
-
27
- last_exception = None
28
-
29
- for attempt in range(config.redis_retries + 1):
30
- try:
31
- # Handle empty username/password gracefully
32
- # Try with SSL first (required for many cloud providers)
33
- redis_client = redis.Redis(
34
- host=config.redis_host,
35
- port=config.redis_port,
36
- username=config.redis_username if config.redis_username else None,
37
- password=config.redis_password if config.redis_password else None,
38
- decode_responses=True,
39
- socket_connect_timeout=5,
40
- socket_timeout=5,
41
- ssl=True, # Enable TLS (required for Redis Cloud and many providers)
42
- ssl_cert_reqs=None # Skip cert verification (commonly required)
43
- )
44
- # Test the connection
45
- redis_client.ping()
46
- logger.info("Successfully connected to Redis with SSL")
47
- return redis_client
48
- except Exception as e:
49
- # If SSL fails, try without SSL
50
- try:
51
- redis_client = redis.Redis(
52
- host=config.redis_host,
53
- port=config.redis_port,
54
- username=config.redis_username if config.redis_username else None,
55
- password=config.redis_password if config.redis_password else None,
56
- decode_responses=True,
57
- socket_connect_timeout=5,
58
- socket_timeout=5
59
- )
60
- # Test the connection
61
- redis_client.ping()
62
- logger.info("Successfully connected to Redis without SSL")
63
- return redis_client
64
- except Exception as e2:
65
- last_exception = e2
66
- if attempt < config.redis_retries:
67
- time.sleep(config.redis_retry_delay * (2 ** attempt)) # Exponential backoff
68
- continue
69
-
70
- logger.warning(f"Could not connect to Redis after {config.redis_retries + 1} attempts: {last_exception}")
71
- return None
72
-
73
- # Initialize Redis connection with better error handling
74
- redis_client = None
75
- try:
76
- redis_client = get_redis_client()
77
- if redis_client is None and REDIS_AVAILABLE:
78
- logger.warning("Could not connect to Redis - using in-memory storage")
79
- except Exception as e:
80
- logger.error(f"Redis initialization failed: {e}")
81
-
82
  def save_user_state(user_id: str, state: dict):
83
  """Save user state to Redis with fallback to in-memory storage"""
84
- global redis_client
85
- if not redis_client:
86
  # Fallback: use in-memory storage (will not persist across restarts)
87
  logger.info("Redis not available, using in-memory storage for user state")
88
  return False
89
-
90
  try:
91
- redis_client.hset(f"user:{user_id}", mapping=state)
92
  return True
93
  except Exception as e:
94
  logger.error(f"Error saving user state: {e}")
@@ -96,24 +25,17 @@ def save_user_state(user_id: str, state: dict):
96
 
97
  def load_user_state(user_id: str):
98
  """Load user state from Redis with fallback"""
99
- global redis_client
100
- if not redis_client:
101
  logger.info("Redis not available, returning empty state")
102
  return {}
103
-
104
  try:
105
- return redis_client.hgetall(f"user:{user_id}")
106
  except Exception as e:
107
  logger.error(f"Error loading user state: {e}")
108
  return {}
109
 
110
  def check_redis_health():
111
  """Check if Redis is healthy"""
112
- global redis_client
113
- if not redis_client:
114
- return False
115
- try:
116
- redis_client.ping()
117
- return True
118
- except:
119
- return False
 
1
  import json
2
  import time
3
  import logging
4
+ from core.redis_client import redis_client
5
  from utils.config import config
6
 
7
  # Set up logging
8
  logging.basicConfig(level=logging.INFO)
9
  logger = logging.getLogger(__name__)
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def save_user_state(user_id: str, state: dict):
12
  """Save user state to Redis with fallback to in-memory storage"""
13
+ client = redis_client.get_client()
14
+ if not client:
15
  # Fallback: use in-memory storage (will not persist across restarts)
16
  logger.info("Redis not available, using in-memory storage for user state")
17
  return False
18
+
19
  try:
20
+ client.hset(f"user:{user_id}", mapping=state)
21
  return True
22
  except Exception as e:
23
  logger.error(f"Error saving user state: {e}")
 
25
 
26
  def load_user_state(user_id: str):
27
  """Load user state from Redis with fallback"""
28
+ client = redis_client.get_client()
29
+ if not client:
30
  logger.info("Redis not available, returning empty state")
31
  return {}
32
+
33
  try:
34
+ return client.hgetall(f"user:{user_id}")
35
  except Exception as e:
36
  logger.error(f"Error loading user state: {e}")
37
  return {}
38
 
39
  def check_redis_health():
40
  """Check if Redis is healthy"""
41
+ return redis_client.is_healthy()
 
 
 
 
 
 
 
core/providers/base.py CHANGED
@@ -14,6 +14,8 @@ class LLMProvider(ABC):
14
  self.timeout = timeout
15
  self.max_retries = max_retries
16
  self.is_available = True
 
 
17
 
18
  @abstractmethod
19
  def generate(self, prompt: str, conversation_history: List[Dict]) -> Optional[str]:
@@ -31,16 +33,30 @@ class LLMProvider(ABC):
31
  pass
32
 
33
  def _retry_with_backoff(self, func, *args, **kwargs):
34
- """Retry logic with exponential backoff"""
35
  last_exception = None
36
 
37
  for attempt in range(self.max_retries):
38
  try:
39
- return func(*args, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
40
  except Exception as e:
41
  last_exception = e
 
 
 
42
  if attempt < self.max_retries - 1: # Don't sleep on last attempt
43
- sleep_time = (2 ** attempt) * 0.5 # Exponential backoff starting at 0.5s
44
  logger.warning(f"Attempt {attempt + 1} failed: {str(e)}. Retrying in {sleep_time}s...")
45
  time.sleep(sleep_time)
46
  else:
@@ -48,14 +64,29 @@ class LLMProvider(ABC):
48
 
49
  raise last_exception
50
 
51
- def _is_rate_limited(self, error: Exception) -> bool:
52
- """Check if the error is related to rate limiting"""
53
  error_str = str(error).lower()
54
- rate_limit_indicators = [
55
- "rate limit",
56
- "too many requests",
57
- "quota exceeded",
58
- "429",
59
- "limit exceeded"
60
- ]
61
- return any(indicator in error_str for indicator in rate_limit_indicators)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  self.timeout = timeout
15
  self.max_retries = max_retries
16
  self.is_available = True
17
+ self.failure_count = 0
18
+ self.last_failure_time = None
19
 
20
  @abstractmethod
21
  def generate(self, prompt: str, conversation_history: List[Dict]) -> Optional[str]:
 
33
  pass
34
 
35
  def _retry_with_backoff(self, func, *args, **kwargs):
36
+ """Retry logic with exponential backoff and circuit breaker"""
37
  last_exception = None
38
 
39
  for attempt in range(self.max_retries):
40
  try:
41
+ # Simple circuit breaker - fail fast if too many recent failures
42
+ if self.failure_count > 5 and self.last_failure_time:
43
+ time_since_failure = time.time() - self.last_failure_time
44
+ if time_since_failure < 60: # Wait 1 minute after 5 failures
45
+ raise Exception("Circuit breaker tripped - too many recent failures")
46
+
47
+ result = func(*args, **kwargs)
48
+ # Reset failure count on success
49
+ self.failure_count = 0
50
+ self.last_failure_time = None
51
+ return result
52
+
53
  except Exception as e:
54
  last_exception = e
55
+ self.failure_count += 1
56
+ self.last_failure_time = time.time()
57
+
58
  if attempt < self.max_retries - 1: # Don't sleep on last attempt
59
+ sleep_time = min((2 ** attempt) * 1.0, 10.0) # Cap at 10 seconds
60
  logger.warning(f"Attempt {attempt + 1} failed: {str(e)}. Retrying in {sleep_time}s...")
61
  time.sleep(sleep_time)
62
  else:
 
64
 
65
  raise last_exception
66
 
67
+ def _classify_error(self, error: Exception) -> str:
68
+ """Classify error type for better handling"""
69
  error_str = str(error).lower()
70
+
71
+ # Network errors
72
+ if any(term in error_str for term in ['connection', 'timeout', 'resolve', 'unreachable']):
73
+ return 'network'
74
+
75
+ # Authentication errors
76
+ if any(term in error_str for term in ['auth', 'unauthorized', 'invalid token', '401', '403']):
77
+ return 'authentication'
78
+
79
+ # Rate limiting
80
+ if any(term in error_str for term in ['rate limit', 'too many requests', 'quota exceeded', '429']):
81
+ return 'rate_limit'
82
+
83
+ # Server errors
84
+ if any(term in error_str for term in ['500', '502', '503', 'server error']):
85
+ return 'server'
86
+
87
+ return 'other'
88
+
89
+ def _is_recoverable_error(self, error: Exception) -> bool:
90
+ """Determine if error is likely recoverable"""
91
+ error_type = self._classify_error(error)
92
+ return error_type in ['network', 'rate_limit', 'server']
core/providers/huggingface.py CHANGED
@@ -25,6 +25,7 @@ class HuggingFaceProvider(LLMProvider):
25
  if not config.hf_token:
26
  raise ValueError("HF_TOKEN not set - required for Hugging Face provider")
27
 
 
28
  self.client = OpenAI(
29
  base_url=config.hf_api_url,
30
  api_key=config.hf_token
@@ -49,7 +50,6 @@ class HuggingFaceProvider(LLMProvider):
49
  def validate_model(self) -> bool:
50
  """Validate if the model is available"""
51
  # For Hugging Face endpoints, we'll assume the model is valid if we can connect
52
- # In production, you might want to ping the endpoint specifically
53
  try:
54
  # Simple connectivity check
55
  self.client.models.list()
 
25
  if not config.hf_token:
26
  raise ValueError("HF_TOKEN not set - required for Hugging Face provider")
27
 
28
+ # Fixed OpenAI client instantiation without deprecated parameters
29
  self.client = OpenAI(
30
  base_url=config.hf_api_url,
31
  api_key=config.hf_token
 
50
  def validate_model(self) -> bool:
51
  """Validate if the model is available"""
52
  # For Hugging Face endpoints, we'll assume the model is valid if we can connect
 
53
  try:
54
  # Simple connectivity check
55
  self.client.models.list()
core/providers/ollama.py CHANGED
@@ -1,5 +1,6 @@
1
  import requests
2
  import logging
 
3
  from typing import List, Dict, Optional, Union
4
  from core.providers.base import LLMProvider
5
  from utils.config import config
@@ -11,13 +12,30 @@ class OllamaProvider(LLMProvider):
11
 
12
  def __init__(self, model_name: str, timeout: int = 30, max_retries: int = 3):
13
  super().__init__(model_name, timeout, max_retries)
14
- self.host = config.ollama_host or "http://localhost:11434"
15
  # Headers to skip ngrok browser warning
16
  self.headers = {
17
  "ngrok-skip-browser-warning": "true",
18
  "User-Agent": "AI-Life-Coach-Ollama"
19
  }
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  def generate(self, prompt: str, conversation_history: List[Dict]) -> Optional[str]:
22
  """Generate a response synchronously"""
23
  try:
 
1
  import requests
2
  import logging
3
+ import re
4
  from typing import List, Dict, Optional, Union
5
  from core.providers.base import LLMProvider
6
  from utils.config import config
 
12
 
13
  def __init__(self, model_name: str, timeout: int = 30, max_retries: int = 3):
14
  super().__init__(model_name, timeout, max_retries)
15
+ self.host = self._sanitize_host(config.ollama_host or "http://localhost:11434")
16
  # Headers to skip ngrok browser warning
17
  self.headers = {
18
  "ngrok-skip-browser-warning": "true",
19
  "User-Agent": "AI-Life-Coach-Ollama"
20
  }
21
 
22
+ def _sanitize_host(self, host: str) -> str:
23
+ """Sanitize host URL by removing whitespace and control characters"""
24
+ if not host:
25
+ return "http://localhost:11434"
26
+
27
+ # Remove leading/trailing whitespace and control characters
28
+ host = host.strip()
29
+
30
+ # Remove any newlines or control characters
31
+ host = re.sub(r'[\r\n\t\0]+', '', host)
32
+
33
+ # Ensure URL has a scheme
34
+ if not host.startswith(('http://', 'https://')):
35
+ host = 'http://' + host
36
+
37
+ return host
38
+
39
  def generate(self, prompt: str, conversation_history: List[Dict]) -> Optional[str]:
40
  """Generate a response synchronously"""
41
  try:
core/redis_client.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import redis
2
+ import logging
3
+ import time
4
+ import re
5
+ from typing import Optional
6
+ from utils.config import config
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class RedisClient:
11
+ """Enhanced Redis client with proper connection handling"""
12
+
13
+ _instance = None
14
+ _redis_client = None
15
+
16
+ def __new__(cls):
17
+ if cls._instance is None:
18
+ cls._instance = super(RedisClient, cls).__new__(cls)
19
+ return cls._instance
20
+
21
+ def __init__(self):
22
+ if not hasattr(self, '_initialized'):
23
+ self._initialized = True
24
+ self._connect()
25
+
26
+ def _connect(self):
27
+ """Establish Redis connection with proper error handling"""
28
+ if not config.redis_host or config.redis_host == "localhost":
29
+ logger.info("Redis not configured, skipping connection")
30
+ return None
31
+
32
+ try:
33
+ # Parse host and port if port is included in host
34
+ host, port = self._parse_host_port(config.redis_host, config.redis_port)
35
+
36
+ logger.info(f"Connecting to Redis at {host}:{port}")
37
+
38
+ # Try SSL connection first (required for Redis Cloud)
39
+ self._redis_client = redis.Redis(
40
+ host=host,
41
+ port=port,
42
+ username=config.redis_username or None,
43
+ password=config.redis_password or None,
44
+ decode_responses=True,
45
+ socket_connect_timeout=5,
46
+ socket_timeout=5,
47
+ ssl=True,
48
+ ssl_cert_reqs=None, # Skip cert verification for Redis Cloud
49
+ retry_on_timeout=True,
50
+ health_check_interval=30
51
+ )
52
+
53
+ # Test connection
54
+ self._redis_client.ping()
55
+ logger.info("Successfully connected to Redis with SSL")
56
+
57
+ except Exception as e:
58
+ logger.warning(f"SSL connection failed: {e}, trying without SSL")
59
+ try:
60
+ # Fallback to non-SSL connection
61
+ host, port = self._parse_host_port(config.redis_host, config.redis_port)
62
+
63
+ self._redis_client = redis.Redis(
64
+ host=host,
65
+ port=port,
66
+ username=config.redis_username or None,
67
+ password=config.redis_password or None,
68
+ decode_responses=True,
69
+ socket_connect_timeout=5,
70
+ socket_timeout=5,
71
+ retry_on_timeout=True,
72
+ health_check_interval=30
73
+ )
74
+
75
+ # Test connection
76
+ self._redis_client.ping()
77
+ logger.info("Successfully connected to Redis without SSL")
78
+
79
+ except Exception as e2:
80
+ logger.error(f"Could not connect to Redis: {e2}")
81
+ self._redis_client = None
82
+
83
+ def _parse_host_port(self, host_string: str, default_port: int) -> tuple:
84
+ """Parse host and port from host string"""
85
+ # Remove any whitespace or control characters
86
+ host_string = host_string.strip()
87
+
88
+ # Check if port is included in host (e.g., "host:port")
89
+ if ':' in host_string and not host_string.startswith('['): # IPv6 addresses start with [
90
+ parts = host_string.rsplit(':', 1) # Split only on the last colon
91
+ try:
92
+ host = parts[0]
93
+ port = int(parts[1])
94
+ return host, port
95
+ except ValueError:
96
+ # Port is not a valid integer
97
+ return host_string, default_port
98
+
99
+ return host_string, default_port
100
+
101
+ def get_client(self) -> Optional[redis.Redis]:
102
+ """Get Redis client instance"""
103
+ return self._redis_client
104
+
105
+ def is_healthy(self) -> bool:
106
+ """Check if Redis connection is healthy"""
107
+ if not self._redis_client:
108
+ return False
109
+ try:
110
+ self._redis_client.ping()
111
+ return True
112
+ except:
113
+ return False
114
+
115
+ def reconnect(self):
116
+ """Reconnect to Redis"""
117
+ self._connect()
118
+
119
+ # Global Redis client instance
120
+ redis_client = RedisClient()
core/session.py CHANGED
@@ -12,8 +12,7 @@ class SessionManager:
12
  """Manages user sessions and conversation context"""
13
 
14
  def __init__(self, session_timeout: int = 3600):
15
- """
16
- Initialize session manager
17
 
18
  Args:
19
  session_timeout: Session timeout in seconds (default: 1 hour)
@@ -21,8 +20,7 @@ class SessionManager:
21
  self.session_timeout = session_timeout
22
 
23
  def get_session(self, user_id: str) -> Dict[str, Any]:
24
- """
25
- Retrieve user session data
26
 
27
  Args:
28
  user_id: Unique identifier for the user
@@ -35,21 +33,20 @@ class SessionManager:
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
53
 
54
  Args:
55
  user_id: Unique identifier for the user
@@ -72,14 +69,14 @@ class SessionManager:
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
83
 
84
  Args:
85
  user_id: Unique identifier for the user
@@ -97,8 +94,7 @@ class SessionManager:
97
  return False
98
 
99
  def _create_new_session(self) -> Dict[str, Any]:
100
- """
101
- Create a new session with default values
102
 
103
  Returns:
104
  Dictionary containing new session data
 
12
  """Manages user sessions and conversation context"""
13
 
14
  def __init__(self, session_timeout: int = 3600):
15
+ """Initialize session manager
 
16
 
17
  Args:
18
  session_timeout: Session timeout in seconds (default: 1 hour)
 
20
  self.session_timeout = session_timeout
21
 
22
  def get_session(self, user_id: str) -> Dict[str, Any]:
23
+ """Retrieve user session data
 
24
 
25
  Args:
26
  user_id: Unique identifier for the user
 
33
  if not state:
34
  logger.info(f"Creating new session for user {user_id}")
35
  return self._create_new_session()
36
+
37
  # Check if session has expired
38
  last_activity = float(state.get('last_activity', 0))
39
  if time.time() - last_activity > self.session_timeout:
40
  logger.info(f"Session expired for user {user_id}, creating new session")
41
  return self._create_new_session()
42
+
43
  return state
44
  except Exception as e:
45
  logger.error(f"Error retrieving session for user {user_id}: {e}")
46
  return self._create_new_session()
47
 
48
  def update_session(self, user_id: str, data: Dict[str, Any]) -> bool:
49
+ """Update user session data
 
50
 
51
  Args:
52
  user_id: Unique identifier for the user
 
69
  logger.debug(f"Successfully updated session for user {user_id}")
70
  else:
71
  logger.warning(f"Failed to save session for user {user_id}")
72
+
73
  return result
74
  except Exception as e:
75
  logger.error(f"Error updating session for user {user_id}: {e}")
76
  return False
77
 
78
  def clear_session(self, user_id: str) -> bool:
79
+ """Clear user session data
 
80
 
81
  Args:
82
  user_id: Unique identifier for the user
 
94
  return False
95
 
96
  def _create_new_session(self) -> Dict[str, Any]:
97
+ """Create a new session with default values
 
98
 
99
  Returns:
100
  Dictionary containing new session data
utils/config.py CHANGED
@@ -1,4 +1,6 @@
1
  import os
 
 
2
  from dotenv import load_dotenv
3
 
4
  class Config:
@@ -13,13 +15,13 @@ class Config:
13
  self.openai_api_key = os.getenv("OPENAI_API_KEY")
14
 
15
  # API endpoints
16
- self.hf_api_url = os.getenv("HF_API_ENDPOINT_URL", "https://api-inference.huggingface.co/v1/")
17
 
18
  # Fallback settings
19
  self.use_fallback = os.getenv("USE_FALLBACK", "true").lower() == "true"
20
 
21
  # Redis configuration (optional for HF)
22
- self.redis_host = os.getenv("REDIS_HOST", "localhost")
23
  self.redis_port = int(os.getenv("REDIS_PORT", "6379"))
24
  self.redis_username = os.getenv("REDIS_USERNAME", "")
25
  self.redis_password = os.getenv("REDIS_PASSWORD", "")
@@ -28,10 +30,41 @@ class Config:
28
 
29
  # Local model configuration
30
  self.local_model_name = os.getenv("LOCAL_MODEL_NAME", "mistral:latest")
31
- self.ollama_host = os.getenv("OLLAMA_HOST", "")
32
 
33
  # OpenWeather API
34
  self.openweather_api_key = os.getenv("OPENWEATHER_API_KEY")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  # Global config instance
37
  config = Config()
 
1
  import os
2
+ import re
3
+ import urllib.parse
4
  from dotenv import load_dotenv
5
 
6
  class Config:
 
15
  self.openai_api_key = os.getenv("OPENAI_API_KEY")
16
 
17
  # API endpoints
18
+ self.hf_api_url = self._sanitize_url(os.getenv("HF_API_ENDPOINT_URL", "https://api-inference.huggingface.co/v1/"))
19
 
20
  # Fallback settings
21
  self.use_fallback = os.getenv("USE_FALLBACK", "true").lower() == "true"
22
 
23
  # Redis configuration (optional for HF)
24
+ self.redis_host = self._sanitize_host(os.getenv("REDIS_HOST", ""))
25
  self.redis_port = int(os.getenv("REDIS_PORT", "6379"))
26
  self.redis_username = os.getenv("REDIS_USERNAME", "")
27
  self.redis_password = os.getenv("REDIS_PASSWORD", "")
 
30
 
31
  # Local model configuration
32
  self.local_model_name = os.getenv("LOCAL_MODEL_NAME", "mistral:latest")
33
+ self.ollama_host = self._sanitize_url(os.getenv("OLLAMA_HOST", ""))
34
 
35
  # OpenWeather API
36
  self.openweather_api_key = os.getenv("OPENWEATHER_API_KEY")
37
+
38
+ def _sanitize_url(self, url: str) -> str:
39
+ """Sanitize URL by removing whitespace and control characters"""
40
+ if not url:
41
+ return ""
42
+
43
+ # Remove leading/trailing whitespace and control characters
44
+ url = url.strip()
45
+
46
+ # Remove any trailing newlines or control characters
47
+ url = re.sub(r'[\r\n\t\0]+', '', url)
48
+
49
+ # Validate URL format
50
+ if url and not re.match(r'^https?://', url):
51
+ # If no scheme, add http:// by default
52
+ url = 'http://' + url
53
+
54
+ return url
55
+
56
+ def _sanitize_host(self, host: str) -> str:
57
+ """Sanitize host by removing whitespace and control characters"""
58
+ if not host:
59
+ return ""
60
+
61
+ # Remove leading/trailing whitespace and control characters
62
+ host = host.strip()
63
+
64
+ # Remove any newlines or control characters
65
+ host = re.sub(r'[\r\n\t\0]+', '', host)
66
+
67
+ return host
68
 
69
  # Global config instance
70
  config = Config()
utils/config_validator.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import re
3
+ import urllib.parse
4
+ from utils.config import config
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class ConfigValidator:
9
+ """Utility for validating configuration values"""
10
+
11
+ @staticmethod
12
+ def validate_all() -> dict:
13
+ """Validate all configuration values and return report"""
14
+ report = {
15
+ 'valid': True,
16
+ 'errors': [],
17
+ 'warnings': []
18
+ }
19
+
20
+ # Validate Ollama host
21
+ if config.ollama_host:
22
+ if not ConfigValidator._is_valid_url(config.ollama_host):
23
+ report['errors'].append(f"Invalid OLLAMA_HOST format: {config.ollama_host}")
24
+ report['valid'] = False
25
+ else:
26
+ report['warnings'].append("OLLAMA_HOST not set, local Ollama won't be available")
27
+
28
+ # Validate Redis configuration
29
+ if config.redis_host:
30
+ if not ConfigValidator._is_valid_host(config.redis_host):
31
+ report['warnings'].append(f"Redis host may be invalid: {config.redis_host}")
32
+ else:
33
+ report['warnings'].append("Redis not configured, using in-memory storage")
34
+
35
+ # Validate Hugging Face token
36
+ if config.hf_token:
37
+ if len(config.hf_token) < 10: # Basic sanity check
38
+ report['warnings'].append("HF_TOKEN seems too short to be valid")
39
+ else:
40
+ report['warnings'].append("HF_TOKEN not set, Hugging Face models won't be available")
41
+
42
+ # Validate OpenAI API key
43
+ if config.openai_api_key:
44
+ if not re.match(r'^sk-[a-zA-Z0-9]{32,}', config.openai_api_key):
45
+ report['warnings'].append("OPENAI_API_KEY format looks invalid")
46
+ else:
47
+ report['warnings'].append("OPENAI_API_KEY not set, OpenAI models won't be available")
48
+
49
+ return report
50
+
51
+ @staticmethod
52
+ def _is_valid_url(url: str) -> bool:
53
+ """Check if URL is valid"""
54
+ try:
55
+ result = urllib.parse.urlparse(url)
56
+ return all([result.scheme, result.netloc])
57
+ except:
58
+ return False
59
+
60
+ @staticmethod
61
+ def _is_valid_host(host: str) -> bool:
62
+ """Check if host is valid"""
63
+ if not host:
64
+ return False
65
+
66
+ # Basic regex for hostname validation
67
+ pattern = r'^[a-zA-Z0-9.-]+$'
68
+ return bool(re.match(pattern, host))
69
+
70
+ def validate_configuration():
71
+ """Validate configuration and log results"""
72
+ validator = ConfigValidator()
73
+ report = validator.validate_all()
74
+
75
+ if report['errors']:
76
+ logger.error("Configuration validation errors:")
77
+ for error in report['errors']:
78
+ logger.error(f" - {error}")
79
+
80
+ if report['warnings']:
81
+ logger.warning("Configuration warnings:")
82
+ for warning in report['warnings']:
83
+ logger.warning(f" - {warning}")
84
+
85
+ return report['valid']
86
+
87
+ # Run validation on import
88
+ validate_configuration()