Spaces:
Running
Running
Commit ·
b59b657
1
Parent(s): 458ad02
fyp
Browse files- app/ai/agent/brain.py +5 -12
- app/ai/agent/message_cache.py +22 -107
- app/ai/agent/nodes/__pycache__/casual_chat.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/classify_intent.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/greeting.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/listing_collect.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/listing_publish.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/listing_validate.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/respond.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/search_query.cpython-313.pyc +0 -0
- app/ai/agent/nodes/__pycache__/validate_output.cpython-313.pyc +0 -0
- app/ai/agent/nodes/my_listings.py +4 -1
- app/ai/prompts/__pycache__/system_prompt.cpython-313.pyc +0 -0
- app/ai/services/__pycache__/search_service.cpython-313.pyc +0 -0
- app/ai/tools/__pycache__/greeting_tool.cpython-313.pyc +0 -0
- app/ai/tools/__pycache__/intent_detector_tool.cpython-313.pyc +0 -0
- app/ml/models/__pycache__/ml_listing_extractor.cpython-313.pyc +0 -0
- app/models/__pycache__/user.cpython-313.pyc +0 -0
- app/schemas/__pycache__/auth.cpython-313.pyc +0 -0
- app/schemas/__pycache__/user.cpython-313.pyc +0 -0
- app/services/__pycache__/auth_service.cpython-313.pyc +0 -0
- app/services/__pycache__/otp_service.cpython-313.pyc +0 -0
- pytest.ini +17 -0
- tests/conftest.py +14 -0
- tests/test_language_refactor.py +224 -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,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 |
-
#
|
| 131 |
-
|
| 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
|
| 189 |
-
return "
|
| 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 |
-
|
| 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 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 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 |
-
|
| 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 |
-
"""
|
| 114 |
-
return
|
| 115 |
|
| 116 |
|
| 117 |
def cache_message(
|
|
@@ -121,15 +31,20 @@ def cache_message(
|
|
| 121 |
max_length: str,
|
| 122 |
message: str
|
| 123 |
):
|
| 124 |
-
"""
|
| 125 |
-
|
| 126 |
|
| 127 |
|
| 128 |
def clear_message_cache():
|
| 129 |
-
"""
|
| 130 |
-
|
| 131 |
|
| 132 |
|
| 133 |
def get_cache_stats() -> Dict:
|
| 134 |
-
"""
|
| 135 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"])
|