destinyebuka commited on
Commit
b59b657
·
1 Parent(s): 458ad02
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 and caching.
105
 
106
  This replaces hardcoded messages with dynamic LLM-generated responses
107
  that respect the user's language preference.
@@ -125,13 +125,9 @@ async def generate_localized_response(
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",
@@ -173,9 +169,6 @@ Response:"""
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(
@@ -185,8 +178,8 @@ Response:"""
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))
 
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
  "Super ! 🎉 Votre annonce a été enregistrée avec succès !"
126
  """
127
  import asyncio
 
128
 
129
+ # NOTE: Caching disabled per requirements - always generate fresh responses
130
+ # This ensures dynamic, context-aware messages every time
 
 
 
131
 
132
  language_names = {
133
  "en": "English",
 
169
  response = await brain_llm.ainvoke([HumanMessage(content=prompt)])
170
  generated_message = response.content.strip().strip('"')
171
 
 
 
 
172
  return generated_message
173
  except Exception as e:
174
  logger.warning(
 
178
  # If this was the last attempt, use generic English fallback
179
  if attempt == max_retries - 1:
180
  logger.error(f"All {max_retries} attempts failed. Using generic English fallback.")
181
+ # Generic English fallback (no multilingual fallbacks as per spec)
182
+ return "Service temporarily unavailable. Please try again."
183
 
184
  # Wait before retrying (exponential backoff: 0.5s, 1s, 2s)
185
  await asyncio.sleep(0.5 * (2 ** attempt))
app/ai/agent/message_cache.py CHANGED
@@ -1,107 +1,17 @@
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(
@@ -110,8 +20,8 @@ def get_cached_message(
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(
@@ -121,15 +31,20 @@ def cache_message(
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()
 
 
 
 
 
 
1
  # app/ai/agent/message_cache.py
2
  """
3
+ DEPRECATED: Message caching has been disabled.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ As of 2026-02-16, all LLM responses are generated fresh to ensure:
6
+ 1. Dynamic, context-aware messages
7
+ 2. No stale cached responses
8
+ 3. Accurate language-specific generation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ This file is kept as a placeholder to prevent import errors.
11
+ All cache functions are no-ops that do nothing.
12
+ """
13
 
14
+ from typing import Optional, Dict
 
15
 
16
 
17
  def get_cached_message(
 
20
  tone: str,
21
  max_length: str
22
  ) -> Optional[str]:
23
+ """DEPRECATED: Always returns None (cache disabled)."""
24
+ return None
25
 
26
 
27
  def cache_message(
 
31
  max_length: str,
32
  message: str
33
  ):
34
+ """DEPRECATED: No-op (cache disabled)."""
35
+ pass
36
 
37
 
38
  def clear_message_cache():
39
+ """DEPRECATED: No-op (cache disabled)."""
40
+ pass
41
 
42
 
43
  def get_cache_stats() -> Dict:
44
+ """DEPRECATED: Returns empty stats (cache disabled)."""
45
+ return {
46
+ "total_entries": 0,
47
+ "expired_entries": 0,
48
+ "active_entries": 0,
49
+ "status": "DISABLED"
50
+ }
app/ai/agent/nodes/__pycache__/casual_chat.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/casual_chat.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/casual_chat.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/classify_intent.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/classify_intent.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/classify_intent.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/greeting.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/greeting.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/greeting.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/listing_collect.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/listing_collect.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/listing_collect.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/listing_publish.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/listing_publish.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/listing_publish.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/listing_validate.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/listing_validate.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/listing_validate.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/respond.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/respond.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/respond.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/search_query.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/search_query.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/search_query.cpython-313.pyc differ
 
app/ai/agent/nodes/__pycache__/validate_output.cpython-313.pyc CHANGED
Binary files a/app/ai/agent/nodes/__pycache__/validate_output.cpython-313.pyc and b/app/ai/agent/nodes/__pycache__/validate_output.cpython-313.pyc differ
 
app/ai/agent/nodes/my_listings.py CHANGED
@@ -59,7 +59,10 @@ async def my_listings_handler(state: AgentState) -> AgentState:
59
 
60
  # Store in state
61
  state.my_listings = listings
62
-
 
 
 
63
  # Generate personalized message using LLM-based function from brain.py
64
  from app.ai.agent.brain import _generate_my_listings_message
65
 
 
59
 
60
  # Store in state
61
  state.my_listings = listings
62
+
63
+ # CRITICAL: Store listings in temp_data so frontend can render the cards
64
+ state.temp_data["listings"] = listings
65
+
66
  # Generate personalized message using LLM-based function from brain.py
67
  from app.ai.agent.brain import _generate_my_listings_message
68
 
app/ai/prompts/__pycache__/system_prompt.cpython-313.pyc CHANGED
Binary files a/app/ai/prompts/__pycache__/system_prompt.cpython-313.pyc and b/app/ai/prompts/__pycache__/system_prompt.cpython-313.pyc differ
 
app/ai/services/__pycache__/search_service.cpython-313.pyc CHANGED
Binary files a/app/ai/services/__pycache__/search_service.cpython-313.pyc and b/app/ai/services/__pycache__/search_service.cpython-313.pyc differ
 
app/ai/tools/__pycache__/greeting_tool.cpython-313.pyc CHANGED
Binary files a/app/ai/tools/__pycache__/greeting_tool.cpython-313.pyc and b/app/ai/tools/__pycache__/greeting_tool.cpython-313.pyc differ
 
app/ai/tools/__pycache__/intent_detector_tool.cpython-313.pyc CHANGED
Binary files a/app/ai/tools/__pycache__/intent_detector_tool.cpython-313.pyc and b/app/ai/tools/__pycache__/intent_detector_tool.cpython-313.pyc differ
 
app/ml/models/__pycache__/ml_listing_extractor.cpython-313.pyc CHANGED
Binary files a/app/ml/models/__pycache__/ml_listing_extractor.cpython-313.pyc and b/app/ml/models/__pycache__/ml_listing_extractor.cpython-313.pyc differ
 
app/models/__pycache__/user.cpython-313.pyc CHANGED
Binary files a/app/models/__pycache__/user.cpython-313.pyc and b/app/models/__pycache__/user.cpython-313.pyc differ
 
app/schemas/__pycache__/auth.cpython-313.pyc CHANGED
Binary files a/app/schemas/__pycache__/auth.cpython-313.pyc and b/app/schemas/__pycache__/auth.cpython-313.pyc differ
 
app/schemas/__pycache__/user.cpython-313.pyc CHANGED
Binary files a/app/schemas/__pycache__/user.cpython-313.pyc and b/app/schemas/__pycache__/user.cpython-313.pyc differ
 
app/services/__pycache__/auth_service.cpython-313.pyc CHANGED
Binary files a/app/services/__pycache__/auth_service.cpython-313.pyc and b/app/services/__pycache__/auth_service.cpython-313.pyc differ
 
app/services/__pycache__/otp_service.cpython-313.pyc CHANGED
Binary files a/app/services/__pycache__/otp_service.cpython-313.pyc and b/app/services/__pycache__/otp_service.cpython-313.pyc differ
 
pytest.ini ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [pytest]
2
+ # Pytest configuration for AIDA tests
3
+
4
+ # Add AIDA directory to Python path
5
+ pythonpath = .
6
+
7
+ # Test discovery patterns
8
+ testpaths = tests
9
+ python_files = test_*.py
10
+ python_classes = Test*
11
+ python_functions = test_*
12
+
13
+ # Enable asyncio mode for async tests
14
+ asyncio_mode = auto
15
+
16
+ # Verbose output by default
17
+ addopts = -v
tests/conftest.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/conftest.py
2
+ """
3
+ Pytest configuration file.
4
+
5
+ Sets up Python path so that tests can import from the app module.
6
+ """
7
+
8
+ import sys
9
+ import os
10
+
11
+ # Add the AIDA directory to Python path so 'app' module can be found
12
+ aida_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
+ if aida_dir not in sys.path:
14
+ sys.path.insert(0, aida_dir)
tests/test_language_refactor.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/test_language_refactor.py
2
+ """
3
+ Verification tests for the language logic refactoring.
4
+
5
+ Tests:
6
+ 1. _generate_my_listings_message returns a string
7
+ 2. Language detection works for "Bonjour" (mocking the LLM response)
8
+ 3. Caching is indeed removed (no import errors)
9
+
10
+ Run with: pytest tests/test_language_refactor.py -v
11
+ """
12
+
13
+ import pytest
14
+ import asyncio
15
+ from unittest.mock import AsyncMock, patch, MagicMock
16
+
17
+
18
+ class TestCachingRemoved:
19
+ """Test that caching has been properly disabled."""
20
+
21
+ def test_cache_functions_are_noops(self):
22
+ """Verify cache functions exist but do nothing."""
23
+ from app.ai.agent.message_cache import (
24
+ get_cached_message,
25
+ cache_message,
26
+ clear_message_cache,
27
+ get_cache_stats
28
+ )
29
+
30
+ # get_cached_message should always return None
31
+ result = get_cached_message("test context", "en", "friendly", "short")
32
+ assert result is None, "get_cached_message should always return None (caching disabled)"
33
+
34
+ # cache_message should not raise
35
+ cache_message("test context", "en", "friendly", "short", "test message")
36
+
37
+ # clear_message_cache should not raise
38
+ clear_message_cache()
39
+
40
+ # get_cache_stats should return disabled status
41
+ stats = get_cache_stats()
42
+ assert stats.get("status") == "DISABLED" or stats.get("active_entries") == 0
43
+
44
+ def test_generate_localized_response_no_cache_import_issues(self):
45
+ """Verify generate_localized_response can be imported without errors."""
46
+ try:
47
+ from app.ai.agent.brain import generate_localized_response
48
+ assert callable(generate_localized_response)
49
+ except ImportError as e:
50
+ pytest.fail(f"Import error: {e}")
51
+
52
+
53
+ class TestGenerateMyListingsMessage:
54
+ """Test the _generate_my_listings_message function."""
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_returns_string_for_empty_listings(self):
58
+ """Test that empty listings returns a helpful message."""
59
+ from app.ai.agent.brain import _generate_my_listings_message
60
+
61
+ # Mock the LLM call
62
+ with patch('app.ai.agent.brain.brain_llm') as mock_llm:
63
+ mock_response = MagicMock()
64
+ mock_response.content = "You don't have any listings yet! 🏠 Would you like me to help you create your first one?"
65
+ mock_llm.ainvoke = AsyncMock(return_value=mock_response)
66
+
67
+ result = await _generate_my_listings_message(
68
+ listings=[],
69
+ language="en",
70
+ user_name="John"
71
+ )
72
+
73
+ assert isinstance(result, str), "_generate_my_listings_message should return a string"
74
+ assert len(result) > 0, "Result should not be empty"
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_returns_string_for_listings_with_data(self):
78
+ """Test that listings with data returns a personalized message."""
79
+ from app.ai.agent.brain import _generate_my_listings_message
80
+
81
+ # Mock the LLM call
82
+ with patch('app.ai.agent.brain.brain_llm') as mock_llm:
83
+ mock_response = MagicMock()
84
+ mock_response.content = "Hey John! 🏠 Here are your 2 listings (1 for rent, 1 for sale). 💡 Tip: Long-press any listing to edit or delete it!"
85
+ mock_llm.ainvoke = AsyncMock(return_value=mock_response)
86
+
87
+ test_listings = [
88
+ {"_id": "123", "title": "Apartment", "listing_type": "rent"},
89
+ {"_id": "456", "title": "House", "listing_type": "sale"},
90
+ ]
91
+
92
+ result = await _generate_my_listings_message(
93
+ listings=test_listings,
94
+ language="en",
95
+ user_name="John Doe"
96
+ )
97
+
98
+ assert isinstance(result, str), "_generate_my_listings_message should return a string"
99
+ assert len(result) > 0, "Result should not be empty"
100
+
101
+
102
+ class TestLanguageDetection:
103
+ """Test language detection in classify_intent."""
104
+
105
+ @pytest.mark.asyncio
106
+ async def test_french_detection_bonjour(self):
107
+ """Test that 'Bonjour' is detected as French."""
108
+ from app.ai.agent.state import AgentState
109
+
110
+ # Mock the LLM response for classification
111
+ with patch('app.ai.agent.nodes.classify_intent.llm') as mock_llm:
112
+ mock_response = MagicMock()
113
+ mock_response.content = '''
114
+ {
115
+ "type": "greeting",
116
+ "confidence": 0.95,
117
+ "reasoning": "User greeted in French",
118
+ "language": "fr",
119
+ "requires_auth": false,
120
+ "next_action": "greet"
121
+ }
122
+ '''
123
+ mock_llm.ainvoke = AsyncMock(return_value=mock_response)
124
+
125
+ from app.ai.agent.nodes.classify_intent import classify_intent
126
+
127
+ # Create minimal state with required fields
128
+ state = AgentState(
129
+ user_id="test_user",
130
+ session_id="test_session",
131
+ user_role="renter" # Required field
132
+ )
133
+ state.last_user_message = "Bonjour"
134
+
135
+ result_state = await classify_intent(state)
136
+
137
+ # Verify language was detected
138
+ assert result_state.language_detected == "fr", \
139
+ f"Expected language_detected='fr', got '{result_state.language_detected}'"
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_spanish_detection_hola(self):
143
+ """Test that 'Hola' is detected as Spanish."""
144
+ from app.ai.agent.state import AgentState
145
+
146
+ # Mock the LLM response for classification
147
+ with patch('app.ai.agent.nodes.classify_intent.llm') as mock_llm:
148
+ mock_response = MagicMock()
149
+ mock_response.content = '''
150
+ {
151
+ "type": "greeting",
152
+ "confidence": 0.95,
153
+ "reasoning": "User greeted in Spanish",
154
+ "language": "es",
155
+ "requires_auth": false,
156
+ "next_action": "greet"
157
+ }
158
+ '''
159
+ mock_llm.ainvoke = AsyncMock(return_value=mock_response)
160
+
161
+ from app.ai.agent.nodes.classify_intent import classify_intent
162
+
163
+ # Create minimal state with required fields
164
+ state = AgentState(
165
+ user_id="test_user",
166
+ session_id="test_session",
167
+ user_role="renter" # Required field
168
+ )
169
+ state.last_user_message = "Hola, busco un apartamento"
170
+
171
+ result_state = await classify_intent(state)
172
+
173
+ # Verify language was detected
174
+ assert result_state.language_detected == "es", \
175
+ f"Expected language_detected='es', got '{result_state.language_detected}'"
176
+
177
+
178
+ class TestGenerateLocalizedResponse:
179
+ """Test the generate_localized_response function."""
180
+
181
+ @pytest.mark.asyncio
182
+ async def test_returns_string_on_success(self):
183
+ """Test that a successful LLM call returns a string."""
184
+ from app.ai.agent.brain import generate_localized_response
185
+
186
+ # Mock the LLM call
187
+ with patch('app.ai.agent.brain.brain_llm') as mock_llm:
188
+ mock_response = MagicMock()
189
+ mock_response.content = "Bienvenue! Comment puis-je vous aider?"
190
+ mock_llm.ainvoke = AsyncMock(return_value=mock_response)
191
+
192
+ result = await generate_localized_response(
193
+ context="Greet user warmly",
194
+ language="fr",
195
+ tone="friendly",
196
+ max_length="short"
197
+ )
198
+
199
+ assert isinstance(result, str)
200
+ assert len(result) > 0
201
+
202
+ @pytest.mark.asyncio
203
+ async def test_fallback_on_failure(self):
204
+ """Test that LLM failure returns generic English fallback."""
205
+ from app.ai.agent.brain import generate_localized_response
206
+
207
+ # Mock the LLM call to fail
208
+ with patch('app.ai.agent.brain.brain_llm') as mock_llm:
209
+ mock_llm.ainvoke = AsyncMock(side_effect=Exception("LLM Error"))
210
+
211
+ result = await generate_localized_response(
212
+ context="Greet user warmly",
213
+ language="fr",
214
+ tone="friendly",
215
+ max_length="short",
216
+ max_retries=1 # Reduce retries for faster test
217
+ )
218
+
219
+ # Should return the fallback message
220
+ assert result == "Service temporarily unavailable. Please try again."
221
+
222
+
223
+ if __name__ == "__main__":
224
+ pytest.main([__file__, "-v"])