destinyebuka commited on
Commit
668e4e1
·
1 Parent(s): 3fb52a0
Files changed (2) hide show
  1. app/ai/agent/brain.py +18 -13
  2. app/ai/agent/message_cache.py +135 -0
app/ai/agent/brain.py CHANGED
@@ -101,7 +101,7 @@ async def generate_localized_response(
101
  max_retries: int = 3
102
  ) -> str:
103
  """
104
- Generate a response using LLM in the specified language with retry logic.
105
 
106
  This replaces hardcoded messages with dynamic LLM-generated responses
107
  that respect the user's language preference.
@@ -125,6 +125,13 @@ async def generate_localized_response(
125
  "Super ! 🎉 Votre annonce a été enregistrée avec succès !"
126
  """
127
  import asyncio
 
 
 
 
 
 
 
128
 
129
  language_names = {
130
  "en": "English",
@@ -164,24 +171,22 @@ Response:"""
164
  for attempt in range(max_retries):
165
  try:
166
  response = await brain_llm.ainvoke([HumanMessage(content=prompt)])
167
- return response.content.strip().strip('"')
 
 
 
 
 
168
  except Exception as e:
169
  logger.warning(
170
  f"Failed to generate localized response (attempt {attempt + 1}/{max_retries}): {e}"
171
  )
172
 
173
- # If this was the last attempt, give up
174
  if attempt == max_retries - 1:
175
- logger.error(f"All {max_retries} attempts failed. Using fallback response.")
176
- # Generate a basic fallback in the requested language if possible
177
- fallback_messages = {
178
- "en": "I'm here to help! How can I assist you?",
179
- "fr": "Je suis là pour vous aider ! Comment puis-je vous assister ?",
180
- "es": "¡Estoy aquí para ayudar! ¿Cómo puedo asistirte?",
181
- "pt": "Estou aqui para ajudar! Como posso ajudá-lo?",
182
- "ar": "أنا هنا للمساعدة! كيف يمكنني مساعدتك؟",
183
- }
184
- return fallback_messages.get(language, fallback_messages["en"])
185
 
186
  # Wait before retrying (exponential backoff: 0.5s, 1s, 2s)
187
  await asyncio.sleep(0.5 * (2 ** attempt))
 
101
  max_retries: int = 3
102
  ) -> str:
103
  """
104
+ Generate a response using LLM in the specified language with retry logic and caching.
105
 
106
  This replaces hardcoded messages with dynamic LLM-generated responses
107
  that respect the user's language preference.
 
125
  "Super ! 🎉 Votre annonce a été enregistrée avec succès !"
126
  """
127
  import asyncio
128
+ from app.ai.agent.message_cache import get_cached_message, cache_message
129
+
130
+ # Check cache first (reduces LLM calls for common messages)
131
+ cached = get_cached_message(context, language, tone, max_length)
132
+ if cached:
133
+ logger.debug("Using cached message", language=language, context=context[:30])
134
+ return cached
135
 
136
  language_names = {
137
  "en": "English",
 
171
  for attempt in range(max_retries):
172
  try:
173
  response = await brain_llm.ainvoke([HumanMessage(content=prompt)])
174
+ generated_message = response.content.strip().strip('"')
175
+
176
+ # Cache the generated message
177
+ cache_message(context, language, tone, max_length, generated_message)
178
+
179
+ return generated_message
180
  except Exception as e:
181
  logger.warning(
182
  f"Failed to generate localized response (attempt {attempt + 1}/{max_retries}): {e}"
183
  )
184
 
185
+ # If this was the last attempt, use generic English fallback
186
  if attempt == max_retries - 1:
187
+ logger.error(f"All {max_retries} attempts failed. Using generic English fallback.")
188
+ # Generic English fallback only (as per user's preference)
189
+ return "I'm here to help! How can I assist you?"
 
 
 
 
 
 
 
190
 
191
  # Wait before retrying (exponential backoff: 0.5s, 1s, 2s)
192
  await asyncio.sleep(0.5 * (2 ** attempt))
app/ai/agent/message_cache.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/ai/agent/message_cache.py
2
+ """
3
+ Simple in-memory cache for frequently used LLM-generated messages.
4
+ Reduces latency and costs by caching common responses per language.
5
+ """
6
+
7
+ import hashlib
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional, Dict
10
+ from structlog import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class MessageCache:
16
+ """
17
+ In-memory cache for LLM-generated messages.
18
+
19
+ Cache key format: hash(context + language + tone + max_length)
20
+ TTL: 24 hours (messages are regenerated daily for freshness)
21
+ """
22
+
23
+ def __init__(self, ttl_hours: int = 24):
24
+ self._cache: Dict[str, Dict] = {}
25
+ self._ttl = timedelta(hours=ttl_hours)
26
+ logger.info("MessageCache initialized", ttl_hours=ttl_hours)
27
+
28
+ def _generate_key(self, context: str, language: str, tone: str, max_length: str) -> str:
29
+ """Generate cache key from parameters."""
30
+ combined = f"{context}|{language}|{tone}|{max_length}"
31
+ return hashlib.md5(combined.encode()).hexdigest()
32
+
33
+ def get(
34
+ self,
35
+ context: str,
36
+ language: str,
37
+ tone: str,
38
+ max_length: str
39
+ ) -> Optional[str]:
40
+ """
41
+ Retrieve cached message if available and not expired.
42
+
43
+ Returns:
44
+ Cached message or None if not found/expired
45
+ """
46
+ key = self._generate_key(context, language, tone, max_length)
47
+
48
+ if key not in self._cache:
49
+ return None
50
+
51
+ entry = self._cache[key]
52
+
53
+ # Check if expired
54
+ if datetime.utcnow() > entry["expires_at"]:
55
+ del self._cache[key]
56
+ logger.debug("Cache entry expired", key=key[:8])
57
+ return None
58
+
59
+ logger.debug("Cache hit", key=key[:8], language=language)
60
+ return entry["message"]
61
+
62
+ def set(
63
+ self,
64
+ context: str,
65
+ language: str,
66
+ tone: str,
67
+ max_length: str,
68
+ message: str
69
+ ):
70
+ """Store message in cache with expiration."""
71
+ key = self._generate_key(context, language, tone, max_length)
72
+
73
+ self._cache[key] = {
74
+ "message": message,
75
+ "created_at": datetime.utcnow(),
76
+ "expires_at": datetime.utcnow() + self._ttl,
77
+ "language": language,
78
+ }
79
+
80
+ logger.debug("Cache entry created", key=key[:8], language=language)
81
+
82
+ def clear(self):
83
+ """Clear all cache entries."""
84
+ count = len(self._cache)
85
+ self._cache.clear()
86
+ logger.info("Cache cleared", entries_removed=count)
87
+
88
+ def get_stats(self) -> Dict:
89
+ """Get cache statistics."""
90
+ total = len(self._cache)
91
+ expired = sum(
92
+ 1 for entry in self._cache.values()
93
+ if datetime.utcnow() > entry["expires_at"]
94
+ )
95
+
96
+ return {
97
+ "total_entries": total,
98
+ "expired_entries": expired,
99
+ "active_entries": total - expired,
100
+ }
101
+
102
+
103
+ # Global cache instance
104
+ _message_cache = MessageCache(ttl_hours=24)
105
+
106
+
107
+ def get_cached_message(
108
+ context: str,
109
+ language: str,
110
+ tone: str,
111
+ max_length: str
112
+ ) -> Optional[str]:
113
+ """Get message from cache."""
114
+ return _message_cache.get(context, language, tone, max_length)
115
+
116
+
117
+ def cache_message(
118
+ context: str,
119
+ language: str,
120
+ tone: str,
121
+ max_length: str,
122
+ message: str
123
+ ):
124
+ """Store message in cache."""
125
+ _message_cache.set(context, language, tone, max_length, message)
126
+
127
+
128
+ def clear_message_cache():
129
+ """Clear all cached messages."""
130
+ _message_cache.clear()
131
+
132
+
133
+ def get_cache_stats() -> Dict:
134
+ """Get cache statistics."""
135
+ return _message_cache.get_stats()