Refactor AI chat property handling and intent classification
Browse files- Streamlined the `chat_with_session_memory` function to enhance the system message with available property details and search status, improving user interaction.
- Introduced a new `classify_user_intent` function to determine user intent regarding property searches, ensuring that property searches are only initiated when relevant.
- Updated the database schema in `supabase_setup.sql` to include new tables for user personas and intents, along with properties, enhancing data organization and retrieval.
- Added triggers for automatic timestamp updates on user personas, intents, and properties, ensuring data integrity and accuracy.
- Included sample property data for testing purposes, facilitating easier development and validation of property-related functionalities.
- ai_chat.py +56 -104
- supabase_setup.sql +117 -13
|
@@ -12,104 +12,22 @@ def chat_with_session_memory(state):
|
|
| 12 |
wa_id = state.get("wa_id")
|
| 13 |
wamid = state.get("wamid")
|
| 14 |
|
| 15 |
-
msg_lower = state["user_message"].lower()
|
| 16 |
-
props = state.get("properties", [])
|
| 17 |
-
|
| 18 |
-
#–– Only trigger direct summary for general area searches, not simple greetings ––
|
| 19 |
-
is_general_area_search = (
|
| 20 |
-
("what do you have" in msg_lower or "show me" in msg_lower) and
|
| 21 |
-
any(word in msg_lower for word in ["in ", "area", "jhb", "johannesburg", "cape town", "durban"])
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
# Don't show properties for simple greetings
|
| 25 |
-
is_simple_greeting = any(greeting in msg_lower for greeting in ["hi", "hello", "hey", "good morning", "good afternoon", "good evening"])
|
| 26 |
-
|
| 27 |
-
if props and is_general_area_search and not is_simple_greeting:
|
| 28 |
-
# build a direct summary only for general area searches
|
| 29 |
-
response_text = ""
|
| 30 |
-
status = state.get("search_status_message")
|
| 31 |
-
if status:
|
| 32 |
-
response_text += status + "\n\n"
|
| 33 |
-
response_text += "Here are some listings I found:\n"
|
| 34 |
-
for p in props[:5]:
|
| 35 |
-
response_text += (
|
| 36 |
-
f"- {p.get('title')} in {p.get('location')}, {p.get('city')}: "
|
| 37 |
-
f"{p.get('size_sqm')} sqm, {p.get('price')} ({p.get('price_type')})\n"
|
| 38 |
-
)
|
| 39 |
-
url = p.get("listing_url")
|
| 40 |
-
if url:
|
| 41 |
-
response_text += f" Link: {url}\n"
|
| 42 |
-
response_text += "\nLet me know if you'd like images, features, floorplan PDF, or video tour."
|
| 43 |
-
return {"response": response_text}
|
| 44 |
-
|
| 45 |
-
if props:
|
| 46 |
-
first = props[0]
|
| 47 |
-
# a) Images
|
| 48 |
-
if "image" in msg_lower or "images" in msg_lower:
|
| 49 |
-
imgs = first.get("images") or []
|
| 50 |
-
response = "\n".join(imgs) if imgs else "There aren't any images for that listing."
|
| 51 |
-
return {"response": response}
|
| 52 |
-
# b) Features
|
| 53 |
-
if "feature" in msg_lower:
|
| 54 |
-
feats = first.get("features") or []
|
| 55 |
-
response = "Features:\n" + "\n".join(f"• {f}" for f in feats) if feats else "No feature list available."
|
| 56 |
-
return {"response": response}
|
| 57 |
-
# c) Floorplan
|
| 58 |
-
if "floorplan" in msg_lower:
|
| 59 |
-
fp = first.get("floorplan_pdf")
|
| 60 |
-
response = fp if fp else "There isn't a floorplan PDF for that listing."
|
| 61 |
-
return {"response": response}
|
| 62 |
-
# d) Video
|
| 63 |
-
if "video" in msg_lower or "tour" in msg_lower:
|
| 64 |
-
vid = first.get("video_url")
|
| 65 |
-
response = vid if vid else "There isn't a video tour for that listing."
|
| 66 |
-
return {"response": response}
|
| 67 |
-
|
| 68 |
-
# Direct listing summary (bypass LLM when properties are found)
|
| 69 |
-
props = state.get("properties", [])
|
| 70 |
-
print(f"DEBUG - chat_with_session_memory: {len(props)} properties in state")
|
| 71 |
-
if props:
|
| 72 |
-
print(f"DEBUG - first property: {props[0].get('title')} in {props[0].get('city')}")
|
| 73 |
-
# a) Build a direct summary string
|
| 74 |
-
response_text = ""
|
| 75 |
-
# include any search status
|
| 76 |
-
status = state.get("search_status_message")
|
| 77 |
-
if status:
|
| 78 |
-
response_text += status + "\n\n"
|
| 79 |
-
response_text += "Here are some listings I found:\n"
|
| 80 |
-
for p in props[:5]:
|
| 81 |
-
response_text += (
|
| 82 |
-
f"- {p.get('title')} in {p.get('location')}, {p.get('city')}: "
|
| 83 |
-
f"{p.get('size_sqm')} sqm, {p.get('price')} ({p.get('price_type')})\n"
|
| 84 |
-
)
|
| 85 |
-
url = p.get("listing_url")
|
| 86 |
-
print(f"!!!!!URL: {url}")
|
| 87 |
-
if url:
|
| 88 |
-
response_text += f" Link: {url}\n"
|
| 89 |
-
# b) Suggest extras based on available fields
|
| 90 |
-
extras = []
|
| 91 |
-
if any(p.get("images") for p in props): extras.append("images")
|
| 92 |
-
if any(p.get("features") for p in props): extras.append("feature list")
|
| 93 |
-
if any(p.get("floorplan_pdf") for p in props): extras.append("floorplan PDF")
|
| 94 |
-
if any(p.get("video_url") for p in props): extras.append("video tour")
|
| 95 |
-
if extras:
|
| 96 |
-
response_text += (
|
| 97 |
-
"\nI can also share " + ", ".join(extras) +
|
| 98 |
-
" for any listing—just let me know what you'd like."
|
| 99 |
-
)
|
| 100 |
-
return {"response": response_text}
|
| 101 |
-
|
| 102 |
# Get conversation history from database
|
| 103 |
session_messages = []
|
| 104 |
if session_id:
|
| 105 |
# This will be populated by the async wrapper
|
| 106 |
session_messages = state.get("session_messages", [])
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# Add system message with user context
|
| 109 |
system_message = (
|
| 110 |
f"Hello {user_info.get('name','there')}! You are a helpful and concise property agent. "
|
| 111 |
-
"
|
| 112 |
-
"
|
|
|
|
| 113 |
)
|
| 114 |
if user_info.get("name") and user_info["name"] != "Unknown":
|
| 115 |
system_message += f" The user's name is {user_info['name']}."
|
|
@@ -128,9 +46,31 @@ def chat_with_session_memory(state):
|
|
| 128 |
f"and must-haves: {', '.join(must_have_list) if must_have_list else '[none]'}. "
|
| 129 |
)
|
| 130 |
|
| 131 |
-
#
|
|
|
|
|
|
|
| 132 |
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
# Build messages array with history
|
| 136 |
messages = [{"role": "system", "content": system_message}]
|
|
@@ -174,6 +114,7 @@ class ChatState(TypedDict):
|
|
| 174 |
intent: dict
|
| 175 |
properties: list
|
| 176 |
search_status_message: str
|
|
|
|
| 177 |
|
| 178 |
async def extract_and_update_persona(state):
|
| 179 |
# a. Define which persona fields to track
|
|
@@ -328,10 +269,31 @@ async def extract_and_update_intent(state):
|
|
| 328 |
|
| 329 |
return {"response": None}
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
async def extract_and_search_properties(state):
|
| 332 |
"""
|
| 333 |
Search for properties based on user intent and store results in state.
|
| 334 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
intent = state.get("intent", {})
|
| 336 |
user_message = state.get("user_message", "").lower()
|
| 337 |
print(f"DEBUG - extract_and_search_properties intent: {intent}")
|
|
@@ -345,18 +307,6 @@ async def extract_and_search_properties(state):
|
|
| 345 |
print("DEBUG - No location found, skipping property search")
|
| 346 |
return {"response": None}
|
| 347 |
|
| 348 |
-
# Only search for properties if user is actually asking for them
|
| 349 |
-
property_search_triggers = [
|
| 350 |
-
"what do you have", "show me", "properties", "listings", "warehouse", "office",
|
| 351 |
-
"space", "looking for", "need", "want", "search", "find"
|
| 352 |
-
]
|
| 353 |
-
|
| 354 |
-
is_asking_for_properties = any(trigger in user_message for trigger in property_search_triggers)
|
| 355 |
-
|
| 356 |
-
if not is_asking_for_properties:
|
| 357 |
-
print("DEBUG - User not asking for properties, skipping search")
|
| 358 |
-
return {"response": None}
|
| 359 |
-
|
| 360 |
# Prepare filters for property search
|
| 361 |
filters = {"location_preference": location}
|
| 362 |
|
|
@@ -449,12 +399,14 @@ async def detect_end_chat(state):
|
|
| 449 |
graph = StateGraph(ChatState)
|
| 450 |
graph.add_node("persona_update", RunnableLambda(extract_and_update_persona))
|
| 451 |
graph.add_node("intent_update", RunnableLambda(extract_and_update_intent))
|
|
|
|
| 452 |
graph.add_node("property_search", RunnableLambda(extract_and_search_properties))
|
| 453 |
graph.add_node("exit_check", RunnableLambda(detect_end_chat))
|
| 454 |
graph.add_node("chat", RunnableLambda(chat_with_session_memory))
|
| 455 |
graph.set_entry_point("persona_update")
|
| 456 |
graph.add_edge("persona_update", "intent_update")
|
| 457 |
-
graph.add_edge("intent_update", "
|
|
|
|
| 458 |
graph.add_edge("property_search", "exit_check")
|
| 459 |
graph.add_edge("exit_check", "chat")
|
| 460 |
graph.add_edge("chat", END)
|
|
|
|
| 12 |
wa_id = state.get("wa_id")
|
| 13 |
wamid = state.get("wamid")
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Get conversation history from database
|
| 16 |
session_messages = []
|
| 17 |
if session_id:
|
| 18 |
# This will be populated by the async wrapper
|
| 19 |
session_messages = state.get("session_messages", [])
|
| 20 |
|
| 21 |
+
# Get properties from state
|
| 22 |
+
props = state.get("properties", [])
|
| 23 |
+
search_status = state.get("search_status_message", "")
|
| 24 |
+
|
| 25 |
# Add system message with user context
|
| 26 |
system_message = (
|
| 27 |
f"Hello {user_info.get('name','there')}! You are a helpful and concise property agent. "
|
| 28 |
+
"You may only reference listings passed in state['properties']. "
|
| 29 |
+
"If the user requests more detail, respond with whatever is in that listing dict (URL, images, features, etc.). "
|
| 30 |
+
"Always base property recommendations solely on listings in our database."
|
| 31 |
)
|
| 32 |
if user_info.get("name") and user_info["name"] != "Unknown":
|
| 33 |
system_message += f" The user's name is {user_info['name']}."
|
|
|
|
| 46 |
f"and must-haves: {', '.join(must_have_list) if must_have_list else '[none]'}. "
|
| 47 |
)
|
| 48 |
|
| 49 |
+
# Include search status if present
|
| 50 |
+
if search_status:
|
| 51 |
+
system_message += f"\n\n{search_status}"
|
| 52 |
|
| 53 |
+
# Include property data if available
|
| 54 |
+
if props:
|
| 55 |
+
system_message += "\n\nAvailable listings:\n"
|
| 56 |
+
for p in props[:5]:
|
| 57 |
+
system_message += (
|
| 58 |
+
f"- {p.get('title')} in {p.get('location')}, {p.get('city')}: "
|
| 59 |
+
f"{p.get('size_sqm')} sqm, {p.get('price')} ({p.get('price_type')})\n"
|
| 60 |
+
)
|
| 61 |
+
# Include all available data for the LLM to use
|
| 62 |
+
if p.get("listing_url"):
|
| 63 |
+
system_message += f" URL: {p.get('listing_url')}\n"
|
| 64 |
+
if p.get("images"):
|
| 65 |
+
system_message += f" Images: {', '.join(p.get('images', [])[:3])}\n"
|
| 66 |
+
if p.get("features"):
|
| 67 |
+
system_message += f" Features: {', '.join(p.get('features', [])[:5])}\n"
|
| 68 |
+
if p.get("floorplan_pdf"):
|
| 69 |
+
system_message += f" Floorplan: {p.get('floorplan_pdf')}\n"
|
| 70 |
+
if p.get("video_url"):
|
| 71 |
+
system_message += f" Video: {p.get('video_url')}\n"
|
| 72 |
+
|
| 73 |
+
system_message += "\n\nIMPORTANT: You may only reference listings passed in state['properties']. If the user requests more detail, respond with whatever is in that listing dict (URL, images, features, etc.). If information is not available, respond with 'For this listing, I don't have [specific detail] available right now'."
|
| 74 |
|
| 75 |
# Build messages array with history
|
| 76 |
messages = [{"role": "system", "content": system_message}]
|
|
|
|
| 114 |
intent: dict
|
| 115 |
properties: list
|
| 116 |
search_status_message: str
|
| 117 |
+
classification: str
|
| 118 |
|
| 119 |
async def extract_and_update_persona(state):
|
| 120 |
# a. Define which persona fields to track
|
|
|
|
| 269 |
|
| 270 |
return {"response": None}
|
| 271 |
|
| 272 |
+
async def classify_user_intent(state):
|
| 273 |
+
"""
|
| 274 |
+
Classify the user's message to determine if they want to search for properties.
|
| 275 |
+
"""
|
| 276 |
+
user_message = state["user_message"]
|
| 277 |
+
prompt = f"""
|
| 278 |
+
Classify the user's message into exactly one of:
|
| 279 |
+
- search_listings (user wants to see property listings)
|
| 280 |
+
- request_details (user wants more info on a shown listing)
|
| 281 |
+
- other (anything else)
|
| 282 |
+
Return only the tag.
|
| 283 |
+
Message: {user_message}
|
| 284 |
+
"""
|
| 285 |
+
resp = await llm.ainvoke([{"role":"user","content":prompt}])
|
| 286 |
+
state["classification"] = resp.content.strip()
|
| 287 |
+
return {"response": None}
|
| 288 |
+
|
| 289 |
async def extract_and_search_properties(state):
|
| 290 |
"""
|
| 291 |
Search for properties based on user intent and store results in state.
|
| 292 |
"""
|
| 293 |
+
# Only search when the LLM tagged this as a listings request
|
| 294 |
+
if state.get("classification") != "search_listings":
|
| 295 |
+
return {"response": None}
|
| 296 |
+
|
| 297 |
intent = state.get("intent", {})
|
| 298 |
user_message = state.get("user_message", "").lower()
|
| 299 |
print(f"DEBUG - extract_and_search_properties intent: {intent}")
|
|
|
|
| 307 |
print("DEBUG - No location found, skipping property search")
|
| 308 |
return {"response": None}
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
# Prepare filters for property search
|
| 311 |
filters = {"location_preference": location}
|
| 312 |
|
|
|
|
| 399 |
graph = StateGraph(ChatState)
|
| 400 |
graph.add_node("persona_update", RunnableLambda(extract_and_update_persona))
|
| 401 |
graph.add_node("intent_update", RunnableLambda(extract_and_update_intent))
|
| 402 |
+
graph.add_node("classify_intent", RunnableLambda(classify_user_intent))
|
| 403 |
graph.add_node("property_search", RunnableLambda(extract_and_search_properties))
|
| 404 |
graph.add_node("exit_check", RunnableLambda(detect_end_chat))
|
| 405 |
graph.add_node("chat", RunnableLambda(chat_with_session_memory))
|
| 406 |
graph.set_entry_point("persona_update")
|
| 407 |
graph.add_edge("persona_update", "intent_update")
|
| 408 |
+
graph.add_edge("intent_update", "classify_intent")
|
| 409 |
+
graph.add_edge("classify_intent", "property_search")
|
| 410 |
graph.add_edge("property_search", "exit_check")
|
| 411 |
graph.add_edge("exit_check", "chat")
|
| 412 |
graph.add_edge("chat", END)
|
|
@@ -26,20 +26,48 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
| 26 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 27 |
);
|
| 28 |
|
| 29 |
-
-- Create
|
| 30 |
-
CREATE TABLE IF NOT EXISTS
|
| 31 |
wa_id TEXT PRIMARY KEY REFERENCES users(wa_id) ON DELETE CASCADE,
|
| 32 |
language TEXT,
|
| 33 |
tone TEXT,
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
location_preference TEXT,
|
|
|
|
|
|
|
| 38 |
must_have TEXT[],
|
| 39 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 40 |
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 41 |
);
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
-- Create indexes for better query performance
|
| 44 |
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at DESC);
|
| 45 |
CREATE INDEX IF NOT EXISTS idx_users_updated_at ON users(updated_at DESC);
|
|
@@ -50,14 +78,26 @@ CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
|
|
| 50 |
CREATE INDEX IF NOT EXISTS idx_messages_wa_id ON messages(wa_id);
|
| 51 |
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
|
| 52 |
CREATE INDEX IF NOT EXISTS idx_messages_wamid ON messages(wamid);
|
| 53 |
-
CREATE INDEX IF NOT EXISTS
|
| 54 |
-
CREATE INDEX IF NOT EXISTS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-- Enable Row Level Security (RLS) - optional but recommended
|
| 57 |
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
| 58 |
ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
|
| 59 |
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
|
| 60 |
-
ALTER TABLE
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-- Create policies to allow all operations (you can restrict this based on your needs)
|
| 63 |
CREATE POLICY "Allow all operations on users" ON users
|
|
@@ -69,7 +109,13 @@ CREATE POLICY "Allow all operations on chat_sessions" ON chat_sessions
|
|
| 69 |
CREATE POLICY "Allow all operations on messages" ON messages
|
| 70 |
FOR ALL USING (true);
|
| 71 |
|
| 72 |
-
CREATE POLICY "Allow all operations on
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
FOR ALL USING (true);
|
| 74 |
|
| 75 |
-- Optional: Create a function to automatically update the updated_at timestamp
|
|
@@ -101,8 +147,66 @@ CREATE TRIGGER update_chat_sessions_last_activity
|
|
| 101 |
FOR EACH ROW
|
| 102 |
EXECUTE FUNCTION update_last_activity_column();
|
| 103 |
|
| 104 |
-
-- Create trigger to automatically update updated_at for
|
| 105 |
-
CREATE TRIGGER
|
| 106 |
-
BEFORE UPDATE ON
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
FOR EACH ROW
|
| 108 |
-
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 27 |
);
|
| 28 |
|
| 29 |
+
-- Create user_personas table for user preferences and context
|
| 30 |
+
CREATE TABLE IF NOT EXISTS user_personas (
|
| 31 |
wa_id TEXT PRIMARY KEY REFERENCES users(wa_id) ON DELETE CASCADE,
|
| 32 |
language TEXT,
|
| 33 |
tone TEXT,
|
| 34 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 35 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 36 |
+
);
|
| 37 |
+
|
| 38 |
+
-- Create user_intents table for session-specific property search intent
|
| 39 |
+
CREATE TABLE IF NOT EXISTS user_intents (
|
| 40 |
+
session_id UUID PRIMARY KEY REFERENCES chat_sessions(id) ON DELETE CASCADE,
|
| 41 |
+
wa_id TEXT NOT NULL REFERENCES users(wa_id) ON DELETE CASCADE,
|
| 42 |
location_preference TEXT,
|
| 43 |
+
budget INTEGER,
|
| 44 |
+
size_preference_sqm INTEGER,
|
| 45 |
must_have TEXT[],
|
| 46 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 47 |
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 48 |
);
|
| 49 |
|
| 50 |
+
-- Create properties table for property listings
|
| 51 |
+
CREATE TABLE IF NOT EXISTS properties (
|
| 52 |
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 53 |
+
title TEXT NOT NULL,
|
| 54 |
+
description TEXT,
|
| 55 |
+
location TEXT NOT NULL,
|
| 56 |
+
city TEXT NOT NULL,
|
| 57 |
+
size_sqm INTEGER,
|
| 58 |
+
price INTEGER,
|
| 59 |
+
price_type TEXT DEFAULT 'monthly',
|
| 60 |
+
listing_url TEXT,
|
| 61 |
+
images TEXT[],
|
| 62 |
+
features TEXT[],
|
| 63 |
+
floorplan_pdf TEXT,
|
| 64 |
+
video_url TEXT,
|
| 65 |
+
is_active BOOLEAN DEFAULT TRUE,
|
| 66 |
+
is_featured BOOLEAN DEFAULT FALSE,
|
| 67 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 68 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
-- Create indexes for better query performance
|
| 72 |
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at DESC);
|
| 73 |
CREATE INDEX IF NOT EXISTS idx_users_updated_at ON users(updated_at DESC);
|
|
|
|
| 78 |
CREATE INDEX IF NOT EXISTS idx_messages_wa_id ON messages(wa_id);
|
| 79 |
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
|
| 80 |
CREATE INDEX IF NOT EXISTS idx_messages_wamid ON messages(wamid);
|
| 81 |
+
CREATE INDEX IF NOT EXISTS idx_user_personas_wa_id ON user_personas(wa_id);
|
| 82 |
+
CREATE INDEX IF NOT EXISTS idx_user_personas_updated_at ON user_personas(updated_at DESC);
|
| 83 |
+
CREATE INDEX IF NOT EXISTS idx_user_intents_session_id ON user_intents(session_id);
|
| 84 |
+
CREATE INDEX IF NOT EXISTS idx_user_intents_wa_id ON user_intents(wa_id);
|
| 85 |
+
CREATE INDEX IF NOT EXISTS idx_user_intents_updated_at ON user_intents(updated_at DESC);
|
| 86 |
+
CREATE INDEX IF NOT EXISTS idx_properties_is_active ON properties(is_active);
|
| 87 |
+
CREATE INDEX IF NOT EXISTS idx_properties_city ON properties(city);
|
| 88 |
+
CREATE INDEX IF NOT EXISTS idx_properties_location ON properties(location);
|
| 89 |
+
CREATE INDEX IF NOT EXISTS idx_properties_price ON properties(price);
|
| 90 |
+
CREATE INDEX IF NOT EXISTS idx_properties_size_sqm ON properties(size_sqm);
|
| 91 |
+
CREATE INDEX IF NOT EXISTS idx_properties_is_featured ON properties(is_featured);
|
| 92 |
+
CREATE INDEX IF NOT EXISTS idx_properties_created_at ON properties(created_at DESC);
|
| 93 |
|
| 94 |
-- Enable Row Level Security (RLS) - optional but recommended
|
| 95 |
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
| 96 |
ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
|
| 97 |
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
|
| 98 |
+
ALTER TABLE user_personas ENABLE ROW LEVEL SECURITY;
|
| 99 |
+
ALTER TABLE user_intents ENABLE ROW LEVEL SECURITY;
|
| 100 |
+
ALTER TABLE properties ENABLE ROW LEVEL SECURITY;
|
| 101 |
|
| 102 |
-- Create policies to allow all operations (you can restrict this based on your needs)
|
| 103 |
CREATE POLICY "Allow all operations on users" ON users
|
|
|
|
| 109 |
CREATE POLICY "Allow all operations on messages" ON messages
|
| 110 |
FOR ALL USING (true);
|
| 111 |
|
| 112 |
+
CREATE POLICY "Allow all operations on user_personas" ON user_personas
|
| 113 |
+
FOR ALL USING (true);
|
| 114 |
+
|
| 115 |
+
CREATE POLICY "Allow all operations on user_intents" ON user_intents
|
| 116 |
+
FOR ALL USING (true);
|
| 117 |
+
|
| 118 |
+
CREATE POLICY "Allow all operations on properties" ON properties
|
| 119 |
FOR ALL USING (true);
|
| 120 |
|
| 121 |
-- Optional: Create a function to automatically update the updated_at timestamp
|
|
|
|
| 147 |
FOR EACH ROW
|
| 148 |
EXECUTE FUNCTION update_last_activity_column();
|
| 149 |
|
| 150 |
+
-- Create trigger to automatically update updated_at for user_personas
|
| 151 |
+
CREATE TRIGGER update_user_personas_updated_at
|
| 152 |
+
BEFORE UPDATE ON user_personas
|
| 153 |
+
FOR EACH ROW
|
| 154 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 155 |
+
|
| 156 |
+
-- Create trigger to automatically update updated_at for user_intents
|
| 157 |
+
CREATE TRIGGER update_user_intents_updated_at
|
| 158 |
+
BEFORE UPDATE ON user_intents
|
| 159 |
FOR EACH ROW
|
| 160 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 161 |
+
|
| 162 |
+
-- Create trigger to automatically update updated_at for properties
|
| 163 |
+
CREATE TRIGGER update_properties_updated_at
|
| 164 |
+
BEFORE UPDATE ON properties
|
| 165 |
+
FOR EACH ROW
|
| 166 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 167 |
+
|
| 168 |
+
-- Insert some sample properties for testing
|
| 169 |
+
INSERT INTO properties (title, description, location, city, size_sqm, price, price_type, listing_url, images, features, is_active, is_featured) VALUES
|
| 170 |
+
(
|
| 171 |
+
'2-3 person office space in Edenvale',
|
| 172 |
+
'Modern office space perfect for small teams',
|
| 173 |
+
'Edenvale',
|
| 174 |
+
'Johannesburg',
|
| 175 |
+
40,
|
| 176 |
+
15000,
|
| 177 |
+
'monthly',
|
| 178 |
+
'https://example.com/office-edenvale',
|
| 179 |
+
ARRAY['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
|
| 180 |
+
ARRAY['3 phase power', 'Parking', 'Kitchen', 'Meeting room'],
|
| 181 |
+
TRUE,
|
| 182 |
+
TRUE
|
| 183 |
+
),
|
| 184 |
+
(
|
| 185 |
+
'Warehouse space in Sandton',
|
| 186 |
+
'Large warehouse space with loading dock',
|
| 187 |
+
'Sandton',
|
| 188 |
+
'Johannesburg',
|
| 189 |
+
200,
|
| 190 |
+
25000,
|
| 191 |
+
'monthly',
|
| 192 |
+
'https://example.com/warehouse-sandton',
|
| 193 |
+
ARRAY['https://example.com/warehouse1.jpg'],
|
| 194 |
+
ARRAY['Loading dock', '3 phase power', 'Security', '24/7 access'],
|
| 195 |
+
TRUE,
|
| 196 |
+
FALSE
|
| 197 |
+
),
|
| 198 |
+
(
|
| 199 |
+
'Small office in Cape Town CBD',
|
| 200 |
+
'Cozy office space in the heart of Cape Town',
|
| 201 |
+
'CBD',
|
| 202 |
+
'Cape Town',
|
| 203 |
+
25,
|
| 204 |
+
12000,
|
| 205 |
+
'monthly',
|
| 206 |
+
'https://example.com/office-cape-town',
|
| 207 |
+
ARRAY['https://example.com/cape-town1.jpg'],
|
| 208 |
+
ARRAY['Air conditioning', 'High-speed internet', 'Reception'],
|
| 209 |
+
TRUE,
|
| 210 |
+
FALSE
|
| 211 |
+
)
|
| 212 |
+
ON CONFLICT DO NOTHING;
|