66"""
77
88import logging
9+ import os
910
1011import chainlit as cl
12+ import psutil # For memory tracking
1113from langchain_core .runnables .config import RunnableConfig
1214from limits import parse
1315from limits .storage import MemoryStorage
3234# logging.getLogger("openai").setLevel(logging.WARNING)
3335logger = logging .getLogger (__name__ )
3436
37+ # --- Memory Management Constants ---
38+ # Maximum number of message pairs (user+assistant) to keep in memory
39+ MAX_HISTORY_LENGTH = settings .MAX_HISTORY_LENGTH
40+
41+
42+ # Function to trim message history to prevent memory bloat
43+ def trim_message_history (session_id : str ) -> None :
44+ """
45+ Trims the message history for a session when it gets too long.
46+ This helps prevent memory buildup in long conversations.
47+
48+ Args:
49+ session_id: The ID of the current session
50+ """
51+ try :
52+ # Get current message history
53+ history = cl .user_session .get ("message_history" , [])
54+
55+ # If history exceeds max length, trim it
56+ if len (history ) > MAX_HISTORY_LENGTH * 2 : # Each exchange has 2 messages
57+ # Keep only the most recent messages
58+ history = history [- MAX_HISTORY_LENGTH * 2 :]
59+ cl .user_session .set ("message_history" , history )
60+ logger .info (
61+ f"Trimmed message history for session { session_id } "
62+ f"to { len (history )} messages"
63+ )
64+ except Exception as e :
65+ # Log but don't crash if history trimming fails
66+ logger .warning (f"Failed to trim message history: { e } " )
67+
68+
3569# --- Global Initialization ---
3670# Declare placeholders for global objects
3771prompt_manager = None
@@ -88,6 +122,57 @@ def get_session_id():
88122# Remove the slowapi limiter instance for messages
89123# message_limiter = Limiter(key_func=get_session_id) # REMOVED
90124
125+
126+ # --- Helper Functions for Message Processing ---
127+ async def check_initialization () -> bool :
128+ """Check if the application is properly initialized."""
129+ if not INITIALIZATION_SUCCESSFUL :
130+ await cl .ErrorMessage (content = "Application not initialized." ).send ()
131+ return False
132+ return True
133+
134+
135+ async def get_translation_service ():
136+ """Get the translation service from the user session."""
137+ service = cl .user_session .get ("translation_service" )
138+ if not service :
139+ logger .error ("TranslationService not found in user session." )
140+ await cl .ErrorMessage (
141+ content = "Error: Translation service unavailable. "
142+ "Please restart the chat."
143+ ).send ()
144+ return None
145+ return service
146+
147+
148+ async def perform_translation (service , message_content , config ):
149+ """Perform the actual translation using the service."""
150+ if settings .DEBUG :
151+ # When debugging, let the callback handler manage steps
152+ return await service .translate_text (message_content , config = config )
153+ else :
154+ # When not debugging, show a simple progress step
155+ async with cl .Step (name = "Translating..." ):
156+ # Config will have empty callbacks list here
157+ return await service .translate_text (message_content , config = config )
158+
159+
160+ async def log_memory_usage (session_id ):
161+ """Log current memory usage for monitoring."""
162+ try :
163+ # Use psutil to get memory info
164+ process = psutil .Process (os .getpid ())
165+ memory_info = process .memory_info ()
166+ memory_mb = memory_info .rss / 1024 / 1024 # Convert to MB
167+ logger .info (f"Memory usage: { memory_mb :.2f} MB for session { session_id } " )
168+
169+ # If memory usage is high, log a warning
170+ if memory_mb > 400 : # 400MB is getting close to the 512MB limit
171+ logger .warning (f"High memory usage detected: { memory_mb :.2f} MB" )
172+ except Exception as e :
173+ logger .error (f"Failed to log memory usage: { e } " )
174+
175+
91176# --- Chainlit Event Handlers ---
92177
93178
@@ -119,77 +204,63 @@ async def start():
119204# @message_limiter.limit("5/minute") # REMOVED Decorator
120205async def on_message (message : cl .Message ):
121206 """Handle incoming text messages and provide translations."""
122- # --- MANUAL Rate Limit Check (using 'limits' library directly) --- <<< CORRECTED
207+ # --- Rate Limit Check ---
123208 session_id = get_session_id ()
124- # Use the limits strategy's hit() method. It returns False if the limit is exceeded.
125209 if not message_limit_strategy .hit (message_rate_limit , session_id ):
126210 # Limit exceeded
127211 logger .warning (f"Rate limit exceeded for session { session_id } " )
128212 await cl .ErrorMessage (
129213 content = "Rate limit exceeded (5 messages per minute). Please wait a moment."
130214 ).send ()
131- return # Stop processing this message
132- # --- End of Rate Limit Check ---
215+ return
133216
134- # Proceed with message handling only if the rate limit check passed
135- try :
136- # REMOVED await message_limiter.hit("5/minute", get_session_id())
217+ # --- Memory Management ---
218+ trim_message_history (session_id )
137219
138- if not INITIALIZATION_SUCCESSFUL :
139- await cl .ErrorMessage (content = "Application not initialized." ).send ()
140- return
220+ # Track this message in history
221+ history = cl .user_session .get ("message_history" , [])
222+ history .append ({"role" : "user" , "content" : message .content })
223+ cl .user_session .set ("message_history" , history )
141224
142- service = cl .user_session .get ("translation_service" )
225+ try :
226+ # Basic validations
227+ if not await check_initialization ():
228+ return
143229
230+ service = await get_translation_service ()
144231 if not service :
145- logger .error ("TranslationService not found in user session." )
146- await cl .ErrorMessage (
147- content = "Error: Translation service unavailable. "
148- "Please restart the chat."
149- ).send ()
150232 return
151233
152234 if not message .content :
153235 logger .warning ("Received empty message." )
154- return # Ignore empty messages
236+ return
155237
156- # Conditionally add the callback handler for step visibility
238+ # Setup for translation
157239 callbacks = []
158240 if settings .DEBUG :
159241 callbacks .append (cl .LangchainCallbackHandler ())
160- logger .info (
161- "Debug enabled: Adding LangchainCallbackHandler for step visibility."
162- )
242+ logger .info ("Debug enabled: Adding LangchainCallbackHandler." )
163243
164244 config = RunnableConfig (callbacks = callbacks )
165245
166- # Use the service to translate, passing the config (with or without callbacks)
167- if settings .DEBUG :
168- # When debugging, let the callback handler manage steps
169- translation_result = await service .translate_text (
170- message .content , config = config
171- )
172- else :
173- # When not debugging, show a simple progress step
174- async with cl .Step (name = "Translating..." ):
175- # Config will have empty callbacks list here
176- translation_result = await service .translate_text (
177- message .content , config = config
178- )
179- # Optionally set step output
180- # (might be redundant if result is sent immediately after)
181- # step.output = translation_result
182-
183- # Send the final translation result
246+ # Perform translation
247+ translation_result = await perform_translation (service , message .content , config )
248+
249+ # Send result
184250 await cl .Message (content = f"Translation: { translation_result } " ).send ()
185251
252+ # Update history
253+ history = cl .user_session .get ("message_history" , [])
254+ history .append (
255+ {"role" : "assistant" , "content" : f"Translation: { translation_result } " }
256+ )
257+ cl .user_session .set ("message_history" , history )
258+
186259 except TranslationError as e :
187260 logger .error (
188261 f"Translation failed for '{ message .content [:50 ]} ...': { e } " , exc_info = False
189- ) # exc_info=False to avoid redundant stack trace from service layer
190- await cl .ErrorMessage (
191- content = f"Sorry, translation failed: { e } "
192- ).send () # Show specific error if safe
262+ )
263+ await cl .ErrorMessage (content = f"Sorry, translation failed: { e } " ).send ()
193264 except AppError as e :
194265 logger .error (
195266 f"Service error during translation for '{ message .content [:50 ]} ...': { e } " ,
@@ -198,20 +269,30 @@ async def on_message(message: cl.Message):
198269 await cl .ErrorMessage (
199270 content = "Sorry, an application error occurred during translation."
200271 ).send ()
201- except Exception as e : # Catch other potential exceptions from the core logic
202- # This generic catch might now be redundant if specific errors are handled
203- # but kept for safety, ensuring RateLimitExceeded is handled first.
272+ except Exception as e :
204273 logger .error (
205274 f"Unexpected error during translation for '{ message .content [:50 ]} ...': { e } " ,
206275 exc_info = True ,
207276 )
208277 await cl .ErrorMessage (
209- content = (
210- "Sorry, an unexpected error occurred during translation. "
211- "Please try again."
212- )
278+ content = "Sorry, an unexpected error occurred during translation."
213279 ).send ()
280+ finally :
281+ # Log memory usage
282+ await log_memory_usage (session_id )
283+
284+
285+ @cl .on_chat_end
286+ async def on_chat_end ():
287+ """Clean up resources when a chat session ends."""
288+ try :
289+ # Get the session ID for logging
290+ session_id = cl .context .session .id
291+ logger .info (f"Cleaning up resources for ending session { session_id } " )
214292
293+ # Clear user session data to free memory
294+ cl .user_session .clear ()
215295
216- # Removed @cl.on_settings_update as it wasn't used after refactor
217- # Removed @cl.on_chat_end/@cl.on_stop as they were empty
296+ logger .info (f"Successfully cleaned up resources for session { session_id } " )
297+ except Exception as e :
298+ logger .error (f"Error during session cleanup: { e } " , exc_info = True )
0 commit comments