zenaight commited on
Commit
2860609
·
1 Parent(s): 54bbcba

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.

Files changed (2) hide show
  1. ai_chat.py +56 -104
  2. supabase_setup.sql +117 -13
ai_chat.py CHANGED
@@ -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
- "Always base property recommendations solely on listings in our database. "
112
- "You can share listing URLs and other details that are available in the property data."
 
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
- # Note: Listing details are now handled by direct summary above, not included in system_message
 
 
132
 
133
- system_message += "\n\nIMPORTANT: When users ask about specific details for a listing, check if that information is available in the property data. If not available, respond with 'For this listing, I don't have [specific detail] available right now' instead of saying you can't provide URLs or external information."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", "property_search")
 
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)
supabase_setup.sql CHANGED
@@ -26,20 +26,48 @@ CREATE TABLE IF NOT EXISTS messages (
26
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
27
  );
28
 
29
- -- Create personas table for user preferences and context
30
- CREATE TABLE IF NOT EXISTS personas (
31
  wa_id TEXT PRIMARY KEY REFERENCES users(wa_id) ON DELETE CASCADE,
32
  language TEXT,
33
  tone TEXT,
34
- intent TEXT,
35
- budget TEXT,
36
- size_preference_sqm TEXT,
 
 
 
 
 
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 idx_personas_wa_id ON personas(wa_id);
54
- CREATE INDEX IF NOT EXISTS idx_personas_updated_at ON personas(updated_at DESC);
 
 
 
 
 
 
 
 
 
 
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 personas ENABLE ROW LEVEL SECURITY;
 
 
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 personas" ON personas
 
 
 
 
 
 
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 personas
105
- CREATE TRIGGER update_personas_updated_at
106
- BEFORE UPDATE ON personas
 
 
 
 
 
 
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;