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.
- ai_chat.py +57 -1
- 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 []
|