destinyebuka commited on
Commit
aa4ce04
ยท
1 Parent(s): f12463f
app/ai/agent/brain.py CHANGED
@@ -101,7 +101,7 @@ TOOLS_FULL = """
101
 
102
 
103
 
104
- BRAIN_SYSTEM_PROMPT = """You are AIDA, an intelligent AI real estate agent.
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 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,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 intelligent 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).
 
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 real estate agent. How can I help you today?"
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
- search_results=state.search_results if state.search_results else None, # Include search results
113
- my_listings=state.my_listings if state.my_listings else None, # Include user's listings
 
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 real estate agent. How can I help you today?",
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 Aida, your 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,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 Aida, your 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 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 real estate agent. How can I help you today?")
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 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)
 
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
- pubsub = redis_client.pubsub()
76
- await pubsub.subscribe(CHAT_CHANNEL)
 
77
 
78
- try:
79
- async for message in pubsub.listen():
80
- if not self.is_listening:
81
- break
82
-
83
- if message["type"] == "message":
84
- try:
85
- payload = json.loads(message["data"])
86
- # Call the handler with the payload
87
- if self.message_handler:
88
- await self.message_handler(payload)
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
- except Exception as e:
95
- logger.error(f"Redis listener loop crashed: {e}")
96
- # Retry logic could go here
97
- finally:
98
- await pubsub.unsubscribe(CHAT_CHANNEL)
99
- await pubsub.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()