zenaight commited on
Commit
c544d0e
·
1 Parent(s): 1a45c02

Add property search functionality and related database queries

Browse files

- Implemented `handle_property_search` to detect property search intent and return relevant property suggestions.
- Enhanced `process_message` to trigger property search based on user input or persona location preference.
- Added new database functions: `search_properties`, `get_property_by_id`, and `get_available_cities` for property management.
- Improved user experience by providing fallback messages when no properties are found, along with a list of available cities.

Files changed (2) hide show
  1. ai_chat.py +57 -1
  2. database.py +68 -1
ai_chat.py CHANGED
@@ -2,7 +2,7 @@ from langgraph.graph import StateGraph, END
2
  from langchain_core.runnables import RunnableLambda
3
  from typing import TypedDict
4
  from config import llm, OPENAI_API_KEY
5
- from database import get_session_messages, save_message
6
  from persona_manager import (
7
  get_or_create_persona,
8
  should_ask_persona_question,
@@ -12,6 +12,7 @@ from persona_manager import (
12
  get_persona_summary,
13
  extract_persona_from_message
14
  )
 
15
 
16
  def chat_with_session_memory(state):
17
  """Chat function with session-based memory and persona collection"""
@@ -183,6 +184,48 @@ graph.set_entry_point("chat")
183
  graph.add_edge("chat", END)
184
  chat_graph = graph.compile()
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  async def process_message(user_message: str, user_info: dict = None, session_id: str = None, wa_id: str = None, wamid: str = None):
187
  """Process a message through the AI chat system with session memory and persona collection"""
188
  if user_info is None:
@@ -234,6 +277,19 @@ async def process_message(user_message: str, user_info: dict = None, session_id:
234
  conversation_context = " ".join([msg["content"] for msg in session_messages[-5:]]) + " " + user_message
235
  should_ask, field_to_ask = await should_ask_persona_question(updated_persona, conversation_context)
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  # Process with AI
238
  result = await chat_graph.ainvoke({
239
  "user_message": user_message,
 
2
  from langchain_core.runnables import RunnableLambda
3
  from typing import TypedDict
4
  from config import llm, OPENAI_API_KEY
5
+ from database import get_session_messages, save_message, search_properties, get_available_cities
6
  from persona_manager import (
7
  get_or_create_persona,
8
  should_ask_persona_question,
 
12
  get_persona_summary,
13
  extract_persona_from_message
14
  )
15
+ import re
16
 
17
  def chat_with_session_memory(state):
18
  """Chat function with session-based memory and persona collection"""
 
184
  graph.add_edge("chat", END)
185
  chat_graph = graph.compile()
186
 
187
+ async def handle_property_search(user_message, persona):
188
+ """Detects property search intent and returns property suggestions or fallback message."""
189
+ # Extract city from persona or user message
190
+ city = None
191
+ if persona.get("location_preference"):
192
+ city = persona["location_preference"]
193
+ else:
194
+ # Try to extract city from message (very basic, can be improved)
195
+ match = re.search(r"in ([a-zA-Z ]+)", user_message, re.IGNORECASE)
196
+ if match:
197
+ city = match.group(1).strip()
198
+
199
+ # Optionally extract size, price, features from persona
200
+ min_size = persona.get("size_preference_sqm")
201
+ features = persona.get("must_have")
202
+ price_type = None
203
+ if persona.get("intent") in ["lease", "rent"]:
204
+ price_type = "lease"
205
+ elif persona.get("intent") == "buy":
206
+ price_type = "sale"
207
+
208
+ # Search properties
209
+ properties = await search_properties(city=city, min_size=min_size, features=features, price_type=price_type, limit=5)
210
+ if properties:
211
+ messages = []
212
+ for prop in properties:
213
+ msg = f"🏢 *{prop['title']}*\n{prop['description']}\n\n*Location:* {prop['address']} ({prop['city']}, {prop['province']})\n*Size:* {prop['size_sqm']} sqm\n*Price:* R{int(prop['price']):,} per month\n*Type:* {prop['price_type'].capitalize()}\n*Features:* {', '.join(prop['features']) if prop.get('features') else 'N/A'}\n\n[View Listing]({prop['listing_url']})"
214
+ messages.append(msg)
215
+ # Add follow-up question about images
216
+ messages.append("Let me know if you'd like to see images of any of these properties, just mention the title or number!")
217
+ return messages
218
+ else:
219
+ # No results, apologize and list available cities
220
+ available_cities = await get_available_cities()
221
+ if city:
222
+ msg = f"Sorry, we currently have no properties available in {city.title()}."
223
+ else:
224
+ msg = "Sorry, we couldn't find any properties matching your search."
225
+ if available_cities:
226
+ msg += f"\n\nWe do have properties in: {', '.join(available_cities)}."
227
+ return [msg]
228
+
229
  async def process_message(user_message: str, user_info: dict = None, session_id: str = None, wa_id: str = None, wamid: str = None):
230
  """Process a message through the AI chat system with session memory and persona collection"""
231
  if user_info is None:
 
277
  conversation_context = " ".join([msg["content"] for msg in session_messages[-5:]]) + " " + user_message
278
  should_ask, field_to_ask = await should_ask_persona_question(updated_persona, conversation_context)
279
 
280
+ # Check for property search intent
281
+ property_search_phrases = [
282
+ "show me properties", "property in", "warehouse in", "industrial in", "listings in", "toon my eiendomme", "warehouse", "factory", "show me warehouses", "show me listings"
283
+ ]
284
+ is_property_search = any(phrase in user_message.lower() for phrase in property_search_phrases)
285
+ if persona.get("location_preference") or is_property_search:
286
+ property_msgs = await handle_property_search(user_message, updated_persona)
287
+ for msg in property_msgs:
288
+ # Send each property message (simulate WhatsApp message send)
289
+ print(f"Property suggestion: {msg}")
290
+ # If property suggestions were sent, return the first as the AI response (rest will be sent as follow-ups)
291
+ return property_msgs[0] if property_msgs else "Let me know if you'd like to see images or more details!"
292
+
293
  # Process with AI
294
  result = await chat_graph.ainvoke({
295
  "user_message": user_message,
database.py CHANGED
@@ -181,4 +181,71 @@ async def update_user_name(wa_id: str, name: str):
181
  return response.data[0] if response.data else None
182
  except Exception as e:
183
  print(f"Error updating user: {e}")
184
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  return response.data[0] if response.data else None
182
  except Exception as e:
183
  print(f"Error updating user: {e}")
184
+ return None
185
+
186
+ # --- Property Listings ---
187
+ async def search_properties(
188
+ city: str = None,
189
+ min_size: int = None,
190
+ max_size: int = None,
191
+ min_price: float = None,
192
+ max_price: float = None,
193
+ features: list = None,
194
+ price_type: str = None,
195
+ limit: int = 10,
196
+ offset: int = 0
197
+ ) -> list:
198
+ """Search properties with flexible filters. Only returns active listings."""
199
+ if not supabase:
200
+ return []
201
+ try:
202
+ query = supabase.table("properties").select("*").eq("is_active", True)
203
+ if city:
204
+ query = query.ilike("city", f"%{city}%")
205
+ if min_size:
206
+ query = query.gte("size_sqm", min_size)
207
+ if max_size:
208
+ query = query.lte("size_sqm", max_size)
209
+ if min_price:
210
+ query = query.gte("price", min_price)
211
+ if max_price:
212
+ query = query.lte("price", max_price)
213
+ if price_type:
214
+ query = query.eq("price_type", price_type)
215
+ if features:
216
+ for feature in features:
217
+ query = query.contains("features", [feature])
218
+ query = query.range(offset, offset + limit - 1)
219
+ query = query.order("is_featured", desc=True).order("updated_at", desc=True)
220
+ resp = query.execute()
221
+ return resp.data if resp.data else []
222
+ except Exception as e:
223
+ print(f"Error searching properties: {e}")
224
+ return []
225
+
226
+ async def get_property_by_id(property_id: str) -> dict:
227
+ """Get a property by its ID."""
228
+ if not supabase:
229
+ return None
230
+ try:
231
+ resp = supabase.table("properties").select("*").eq("id", property_id).single().execute()
232
+ return resp.data if resp.data else None
233
+ except Exception as e:
234
+ print(f"Error getting property by id: {e}")
235
+ return None
236
+
237
+ async def get_available_cities() -> list:
238
+ """Get a list of all cities with active properties."""
239
+ if not supabase:
240
+ return []
241
+ try:
242
+ resp = supabase.table("properties").select("city").eq("is_active", True).execute()
243
+ cities = set()
244
+ if resp.data:
245
+ for row in resp.data:
246
+ if row.get("city"):
247
+ cities.add(row["city"].strip())
248
+ return sorted(list(cities))
249
+ except Exception as e:
250
+ print(f"Error getting available cities: {e}")
251
+ return []