Spaces:
Running
Running
Commit ยท
aa4ce04
1
Parent(s): f12463f
fyp
Browse files- app/ai/agent/brain.py +10 -2
- app/ai/agent/dm_brain.py +1 -1
- app/ai/agent/nodes/greeting.py +1 -1
- app/ai/agent/nodes/respond.py +13 -3
- app/ai/services/agent_executor.py +12 -1
- app/ai/tools/greeting_tool.py +4 -4
- app/ai/tools/intent_detector_tool.py +1 -1
- app/services/redis_pubsub.py +41 -24
app/ai/agent/brain.py
CHANGED
|
@@ -101,7 +101,7 @@ TOOLS_FULL = """
|
|
| 101 |
|
| 102 |
|
| 103 |
|
| 104 |
-
BRAIN_SYSTEM_PROMPT = """You are AIDA, an
|
| 105 |
|
| 106 |
You help users:
|
| 107 |
1. List their properties for rent or sale
|
|
@@ -326,7 +326,7 @@ CRITICAL:
|
|
| 326 |
- Always write a friendly, natural response in the "response" field.
|
| 327 |
|
| 328 |
EXAMPLES:
|
| 329 |
-
- User: "Hello" โ {{"thinking": "User greeting", "response": "Hey there! ๐ I'm AIDA, your
|
| 330 |
- User: "Thanks" โ {{"thinking": "User thanking me", "response": "You're welcome! ๐ Is there anything else I can help you with?", "tool": null, "is_final": true, "show_data": false}}
|
| 331 |
- User: "Show my listings" โ {{"thinking": "User wants listings", "response": "Here are your listings!", "tool": "get_my_listings", "is_final": true, "show_data": true}}"""
|
| 332 |
|
|
@@ -761,6 +761,10 @@ async def execute_tool(tool_name: str, params: Dict[str, Any], state: AgentState
|
|
| 761 |
from app.ai.services.search_extractor import extract_search_params
|
| 762 |
from app.ai.services.search_service import search_listings_hybrid, search_mongodb
|
| 763 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
# Step 1: Extract params from the full user message (LLM is smart)
|
| 765 |
search_params = await extract_search_params(state.last_user_message)
|
| 766 |
|
|
@@ -855,6 +859,10 @@ async def execute_tool(tool_name: str, params: Dict[str, Any], state: AgentState
|
|
| 855 |
from bson import ObjectId
|
| 856 |
from app.services.listing_service import enrich_listings_batch
|
| 857 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
db = await get_db()
|
| 859 |
listings_cursor = db.listings.find(
|
| 860 |
{"user_id": state.user_id}
|
|
|
|
| 101 |
|
| 102 |
|
| 103 |
|
| 104 |
+
BRAIN_SYSTEM_PROMPT = """You are AIDA, an AI Real Estate Agent.
|
| 105 |
|
| 106 |
You help users:
|
| 107 |
1. List their properties for rent or sale
|
|
|
|
| 326 |
- Always write a friendly, natural response in the "response" field.
|
| 327 |
|
| 328 |
EXAMPLES:
|
| 329 |
+
- User: "Hello" โ {{"thinking": "User greeting", "response": "Hey there! ๐ I'm AIDA, your AI Real Estate Agent. How can I help you today?", "tool": null, "is_final": true, "show_data": false}}
|
| 330 |
- User: "Thanks" โ {{"thinking": "User thanking me", "response": "You're welcome! ๐ Is there anything else I can help you with?", "tool": null, "is_final": true, "show_data": false}}
|
| 331 |
- User: "Show my listings" โ {{"thinking": "User wants listings", "response": "Here are your listings!", "tool": "get_my_listings", "is_final": true, "show_data": true}}"""
|
| 332 |
|
|
|
|
| 761 |
from app.ai.services.search_extractor import extract_search_params
|
| 762 |
from app.ai.services.search_service import search_listings_hybrid, search_mongodb
|
| 763 |
|
| 764 |
+
# SMART UI: Clear old my_listings when doing new search
|
| 765 |
+
state.my_listings = None
|
| 766 |
+
state.temp_data.pop("my_listings", None)
|
| 767 |
+
|
| 768 |
# Step 1: Extract params from the full user message (LLM is smart)
|
| 769 |
search_params = await extract_search_params(state.last_user_message)
|
| 770 |
|
|
|
|
| 859 |
from bson import ObjectId
|
| 860 |
from app.services.listing_service import enrich_listings_batch
|
| 861 |
|
| 862 |
+
# SMART UI: Clear old search_results when getting my listings
|
| 863 |
+
state.search_results = None
|
| 864 |
+
state.temp_data.pop("search_results", None)
|
| 865 |
+
|
| 866 |
db = await get_db()
|
| 867 |
listings_cursor = db.listings.find(
|
| 868 |
{"user_id": state.user_id}
|
app/ai/agent/dm_brain.py
CHANGED
|
@@ -42,7 +42,7 @@ class DmDecision(BaseModel):
|
|
| 42 |
response: Optional[str] = Field(default=None, description="Response to user if no tool needed")
|
| 43 |
is_final: bool = Field(default=False, description="True if this is the final response")
|
| 44 |
|
| 45 |
-
DM_SYSTEM_PROMPT = """You are AIDA, an
|
| 46 |
|
| 47 |
In this Direct Message (DM) conversation, you primarily help users with:
|
| 48 |
1. Managing property alerts (creating, listing, or deleting notifications based on search criteria).
|
|
|
|
| 42 |
response: Optional[str] = Field(default=None, description="Response to user if no tool needed")
|
| 43 |
is_final: bool = Field(default=False, description="True if this is the final response")
|
| 44 |
|
| 45 |
+
DM_SYSTEM_PROMPT = """You are AIDA, an AI Real Estate Agent.
|
| 46 |
|
| 47 |
In this Direct Message (DM) conversation, you primarily help users with:
|
| 48 |
1. Managing property alerts (creating, listing, or deleting notifications based on search criteria).
|
app/ai/agent/nodes/greeting.py
CHANGED
|
@@ -128,7 +128,7 @@ async def greeting_handler(state: AgentState) -> AgentState:
|
|
| 128 |
if not is_valid:
|
| 129 |
logger.warning("Response validation failed", error=error)
|
| 130 |
# Use simple fallback (but this shouldn't happen with good LLM)
|
| 131 |
-
cleaned_text = "Hello! ๐ I'm AIDA, your
|
| 132 |
else:
|
| 133 |
# Sanitize response
|
| 134 |
cleaned_text = ResponseValidator.sanitize_response(cleaned_text)
|
|
|
|
| 128 |
if not is_valid:
|
| 129 |
logger.warning("Response validation failed", error=error)
|
| 130 |
# Use simple fallback (but this shouldn't happen with good LLM)
|
| 131 |
+
cleaned_text = "Hello! ๐ I'm AIDA, your AI Real Estate Agent. How can I help you today?"
|
| 132 |
else:
|
| 133 |
# Sanitize response
|
| 134 |
cleaned_text = ResponseValidator.sanitize_response(cleaned_text)
|
app/ai/agent/nodes/respond.py
CHANGED
|
@@ -97,6 +97,15 @@ async def respond_to_user(state: AgentState) -> AgentState:
|
|
| 97 |
# Check if we need to signal card update
|
| 98 |
replace_last_message = state.temp_data.get("replace_last_message", False)
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
response = AgentResponse(
|
| 101 |
success=state.last_error is None,
|
| 102 |
text=response_text,
|
|
@@ -109,8 +118,9 @@ async def respond_to_user(state: AgentState) -> AgentState:
|
|
| 109 |
},
|
| 110 |
draft=None, # Don't expose raw draft - only use draft_ui for display
|
| 111 |
draft_ui=draft_ui,
|
| 112 |
-
|
| 113 |
-
|
|
|
|
| 114 |
tool_result=tool_result,
|
| 115 |
error=state.last_error,
|
| 116 |
metadata={
|
|
@@ -253,7 +263,7 @@ def _get_fallback_response(state: AgentState) -> str:
|
|
| 253 |
"""
|
| 254 |
|
| 255 |
fallback_responses = {
|
| 256 |
-
FlowState.GREETING: "Hello! ๐ I'm AIDA, your
|
| 257 |
FlowState.AUTHENTICATE: "Setting up your session...",
|
| 258 |
FlowState.CLASSIFY_INTENT: "I'm analyzing your request... One moment.",
|
| 259 |
FlowState.LISTING_COLLECT: "Tell me more about the property you want to list. What city is it in?",
|
|
|
|
| 97 |
# Check if we need to signal card update
|
| 98 |
replace_last_message = state.temp_data.get("replace_last_message", False)
|
| 99 |
|
| 100 |
+
# ============================================================
|
| 101 |
+
# SMART UI DATA: Only include data relevant to current action
|
| 102 |
+
# This prevents old listings/results from showing when switching actions
|
| 103 |
+
# ============================================================
|
| 104 |
+
|
| 105 |
+
# Determine which UI data to include based on action
|
| 106 |
+
include_search_results = action in ["search_properties", "search", "search_query", "search_results"]
|
| 107 |
+
include_my_listings = action in ["get_my_listings", "my_listings", "view_listings"]
|
| 108 |
+
|
| 109 |
response = AgentResponse(
|
| 110 |
success=state.last_error is None,
|
| 111 |
text=response_text,
|
|
|
|
| 118 |
},
|
| 119 |
draft=None, # Don't expose raw draft - only use draft_ui for display
|
| 120 |
draft_ui=draft_ui,
|
| 121 |
+
# SMART: Only include data relevant to current action
|
| 122 |
+
search_results=state.search_results if include_search_results and state.search_results else None,
|
| 123 |
+
my_listings=state.my_listings if include_my_listings and state.my_listings else None,
|
| 124 |
tool_result=tool_result,
|
| 125 |
error=state.last_error,
|
| 126 |
metadata={
|
|
|
|
| 263 |
"""
|
| 264 |
|
| 265 |
fallback_responses = {
|
| 266 |
+
FlowState.GREETING: "Hello! ๐ I'm AIDA, your AI Real Estate Agent. How can I help you today?",
|
| 267 |
FlowState.AUTHENTICATE: "Setting up your session...",
|
| 268 |
FlowState.CLASSIFY_INTENT: "I'm analyzing your request... One moment.",
|
| 269 |
FlowState.LISTING_COLLECT: "Tell me more about the property you want to list. What city is it in?",
|
app/ai/services/agent_executor.py
CHANGED
|
@@ -105,12 +105,23 @@ class CacheService:
|
|
| 105 |
"""Cache a successful response"""
|
| 106 |
if not key or not redis_client or not response.success:
|
| 107 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
await redis_client.setex(
|
| 111 |
key,
|
| 112 |
CacheService.CACHE_TTL,
|
| 113 |
-
json.dumps(response.dict())
|
| 114 |
)
|
| 115 |
logger.info(f"๐พ Response cached: {key}")
|
| 116 |
except Exception as e:
|
|
|
|
| 105 |
"""Cache a successful response"""
|
| 106 |
if not key or not redis_client or not response.success:
|
| 107 |
return
|
| 108 |
+
|
| 109 |
+
# Skip caching responses with complex data (listings have datetime fields)
|
| 110 |
+
if response.search_results or response.my_listings:
|
| 111 |
+
logger.info(f"โญ๏ธ Skipping cache for response with listing data (has datetime)")
|
| 112 |
+
return
|
| 113 |
|
| 114 |
try:
|
| 115 |
+
# Use custom encoder for datetime objects
|
| 116 |
+
def json_serializer(obj):
|
| 117 |
+
if hasattr(obj, 'isoformat'):
|
| 118 |
+
return obj.isoformat()
|
| 119 |
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
| 120 |
+
|
| 121 |
await redis_client.setex(
|
| 122 |
key,
|
| 123 |
CacheService.CACHE_TTL,
|
| 124 |
+
json.dumps(response.dict(), default=json_serializer)
|
| 125 |
)
|
| 126 |
logger.info(f"๐พ Response cached: {key}")
|
| 127 |
except Exception as e:
|
app/ai/tools/greeting_tool.py
CHANGED
|
@@ -160,7 +160,7 @@ Important:
|
|
| 160 |
Keep it conversational (2-3 sentences max). Be genuine and warm.
|
| 161 |
|
| 162 |
Examples:
|
| 163 |
-
- "Hello!" โ "Hello! ๐ I'm
|
| 164 |
- "Bonjour!" โ "Bonjour! ๐ Je suis Aida, votre assistant immobilier. Comment puis-je vous aider?"
|
| 165 |
- "Hola!" โ "ยกHola! ๐ Soy Aida, tu asistente de bienes raรญces. ยฟCรณmo puedo ayudarte hoy?"
|
| 166 |
|
|
@@ -190,11 +190,11 @@ Now generate YOUR natural response to their greeting:"""
|
|
| 190 |
|
| 191 |
# Fallback greeting (language-aware)
|
| 192 |
fallback_replies = {
|
| 193 |
-
"landlord": "Hello! ๐ I'm
|
| 194 |
-
"renter": "Hi there! ๐ I'm
|
| 195 |
}
|
| 196 |
|
| 197 |
-
fallback = fallback_replies.get(user_role, "Hello! ๐ I'm
|
| 198 |
|
| 199 |
return {
|
| 200 |
"success": False,
|
|
|
|
| 160 |
Keep it conversational (2-3 sentences max). Be genuine and warm.
|
| 161 |
|
| 162 |
Examples:
|
| 163 |
+
- "Hello!" โ "Hello! ๐ I'm AIDA, your AI Real Estate Agent. How can I help you today?"
|
| 164 |
- "Bonjour!" โ "Bonjour! ๐ Je suis Aida, votre assistant immobilier. Comment puis-je vous aider?"
|
| 165 |
- "Hola!" โ "ยกHola! ๐ Soy Aida, tu asistente de bienes raรญces. ยฟCรณmo puedo ayudarte hoy?"
|
| 166 |
|
|
|
|
| 190 |
|
| 191 |
# Fallback greeting (language-aware)
|
| 192 |
fallback_replies = {
|
| 193 |
+
"landlord": "Hello! ๐ I'm AIDA, your AI Real Estate Agent. Great to see you! How can I help you list or manage properties today?",
|
| 194 |
+
"renter": "Hi there! ๐ I'm AIDA, your AI Real Estate Agent. Nice to meet you! How can I help you find the perfect place?"
|
| 195 |
}
|
| 196 |
|
| 197 |
+
fallback = fallback_replies.get(user_role, "Hello! ๐ I'm AIDA, your AI Real Estate Agent. How can I help you today?")
|
| 198 |
|
| 199 |
return {
|
| 200 |
"success": False,
|
app/ai/tools/intent_detector_tool.py
CHANGED
|
@@ -90,7 +90,7 @@ WHO YOU ARE:
|
|
| 90 |
- Created by: Lojiz team
|
| 91 |
- You are SPECIALIZED for real estate, NOT a general-purpose AI
|
| 92 |
- NEVER claim to be "DeepSeek", "GPT", or any other AI
|
| 93 |
-
- If asked who you are: "I'm
|
| 94 |
|
| 95 |
YOUR CAPABILITIES (LLM-POWERED):
|
| 96 |
1. list_property - Help users LIST properties (create/post new listings)
|
|
|
|
| 90 |
- Created by: Lojiz team
|
| 91 |
- You are SPECIALIZED for real estate, NOT a general-purpose AI
|
| 92 |
- NEVER claim to be "DeepSeek", "GPT", or any other AI
|
| 93 |
+
- If asked who you are: "I'm AIDA, Lojiz's AI Real Estate Agent!"
|
| 94 |
|
| 95 |
YOUR CAPABILITIES (LLM-POWERED):
|
| 96 |
1. list_property - Help users LIST properties (create/post new listings)
|
app/services/redis_pubsub.py
CHANGED
|
@@ -71,32 +71,49 @@ class RedisPubSubService:
|
|
| 71 |
logger.info(f"โ
Redis Pub/Sub listener started on channel: {CHAT_CHANNEL}")
|
| 72 |
|
| 73 |
async def _listener_loop(self):
|
| 74 |
-
"""Background loop to listen for Redis messages"""
|
| 75 |
-
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
except json.JSONDecodeError:
|
| 90 |
-
logger.warning("Received invalid JSON in Redis message")
|
| 91 |
-
except Exception as e:
|
| 92 |
-
logger.error(f"Error processing Redis message: {e}")
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
# Singleton instance
|
| 102 |
redis_pubsub = RedisPubSubService()
|
|
|
|
| 71 |
logger.info(f"โ
Redis Pub/Sub listener started on channel: {CHAT_CHANNEL}")
|
| 72 |
|
| 73 |
async def _listener_loop(self):
|
| 74 |
+
"""Background loop to listen for Redis messages with auto-reconnect"""
|
| 75 |
+
retry_count = 0
|
| 76 |
+
max_retries = 10
|
| 77 |
+
base_delay = 1 # seconds
|
| 78 |
|
| 79 |
+
while self.is_listening and retry_count < max_retries:
|
| 80 |
+
pubsub = None
|
| 81 |
+
try:
|
| 82 |
+
pubsub = redis_client.pubsub()
|
| 83 |
+
await pubsub.subscribe(CHAT_CHANNEL)
|
| 84 |
+
logger.info(f"๐ก Redis Pub/Sub connected to {CHAT_CHANNEL}")
|
| 85 |
+
retry_count = 0 # Reset on successful connection
|
| 86 |
+
|
| 87 |
+
async for message in pubsub.listen():
|
| 88 |
+
if not self.is_listening:
|
| 89 |
+
break
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
if message["type"] == "message":
|
| 92 |
+
try:
|
| 93 |
+
payload = json.loads(message["data"])
|
| 94 |
+
if self.message_handler:
|
| 95 |
+
await self.message_handler(payload)
|
| 96 |
+
except json.JSONDecodeError:
|
| 97 |
+
logger.warning("Received invalid JSON in Redis message")
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.error(f"Error processing Redis message: {e}")
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
retry_count += 1
|
| 103 |
+
delay = min(base_delay * (2 ** retry_count), 60) # Max 60s delay
|
| 104 |
+
logger.error(f"Redis listener crashed: {e}. Reconnecting in {delay}s (attempt {retry_count}/{max_retries})")
|
| 105 |
+
await asyncio.sleep(delay)
|
| 106 |
+
|
| 107 |
+
finally:
|
| 108 |
+
if pubsub:
|
| 109 |
+
try:
|
| 110 |
+
await pubsub.unsubscribe(CHAT_CHANNEL)
|
| 111 |
+
await pubsub.close()
|
| 112 |
+
except:
|
| 113 |
+
pass
|
| 114 |
+
|
| 115 |
+
if retry_count >= max_retries:
|
| 116 |
+
logger.error(f"โ Redis Pub/Sub gave up after {max_retries} retries")
|
| 117 |
|
| 118 |
# Singleton instance
|
| 119 |
redis_pubsub = RedisPubSubService()
|