zenaight commited on
Commit
42bd84b
·
1 Parent(s): 217b7cf

Update AI chat functionality to improve property recommendation responses

Browse files

- Modified the system message in `chat_with_session_memory` to allow sharing of listing URLs and additional property details, enhancing user interaction.
- Added a `search_status_message` to provide context on search results and fallback responses when specific property details are unavailable.
- Removed debug print statements from various functions to streamline the code and improve readability, ensuring a cleaner implementation.
- Enhanced the property search logic to return relevant status messages, improving communication with users when exact matches are not found.

Files changed (2) hide show
  1. ai_chat.py +34 -62
  2. database.py +1 -27
ai_chat.py CHANGED
@@ -47,7 +47,7 @@ def chat_with_session_memory(state):
47
  system_message = (
48
  f"Hello {user_info.get('name','there')}! You are a helpful and concise property agent. "
49
  "Always base property recommendations solely on listings in our database. "
50
- "Do not suggest or mention any external websites or property portals."
51
  )
52
  if user_info.get("name") and user_info["name"] != "Unknown":
53
  system_message += f" The user's name is {user_info['name']}."
@@ -67,9 +67,12 @@ def chat_with_session_memory(state):
67
  )
68
 
69
  props = state.get("properties", [])
70
- print(f"DEBUG: Properties in state: {len(props)} properties")
 
 
 
 
71
  if props:
72
- print(f"DEBUG: First property: {props[0]}")
73
  system_message += "\nHere are some listings I found:\n"
74
  for p in props[:5]:
75
  title = p.get("title", "[no title]")
@@ -84,12 +87,8 @@ def chat_with_session_memory(state):
84
  f"{price} ({ptype}); features: {features}\n"
85
  )
86
  url = p.get("listing_url")
87
- print(f"DEBUG: Property URL: {url}")
88
  if url:
89
  system_message += f" Link: {url}\n"
90
- print(f"DEBUG: Added URL to system message")
91
- else:
92
- print(f"DEBUG: No URL found for property")
93
 
94
  # After the listings loop, before building messages[]
95
  extras = set()
@@ -108,6 +107,8 @@ def chat_with_session_memory(state):
108
  f"\nI can also share {opts} for any listing—just let me know what you'd like."
109
  )
110
 
 
 
111
  # Build messages array with history
112
  messages = [{"role": "system", "content": system_message}]
113
 
@@ -149,6 +150,7 @@ class ChatState(TypedDict):
149
  persona: dict
150
  intent: dict
151
  properties: list
 
152
 
153
  async def extract_and_update_persona(state):
154
  # a. Define which persona fields to track
@@ -205,11 +207,6 @@ async def extract_and_update_persona(state):
205
  return {"response": None}
206
 
207
  async def extract_and_update_intent(state):
208
- print(f"=== Starting extract_and_update_intent ===")
209
- print(f"Session ID: {state.get('session_id')}")
210
- print(f"User message: {state.get('user_message')}")
211
- print(f"Current intent: {state.get('intent')}")
212
-
213
  intent_fields = ["location_preference", "budget", "size_preference_sqm", "must_have"]
214
  user_message = state["user_message"]
215
  session_id = state["session_id"]
@@ -236,48 +233,32 @@ async def extract_and_update_intent(state):
236
  """
237
 
238
  response = await llm.ainvoke([{"role":"user","content":extraction_prompt}])
239
- print(f"LLM response for intent extraction: {response.content}")
240
 
241
  import json
242
  try:
243
  # Clean up the response content (remove markdown formatting if present)
244
  content = response.content.strip()
245
- print(f"Original content: {repr(content)}")
246
 
247
  if content.startswith('```json'):
248
  content = content[7:] # Remove ```json
249
- print(f"After removing ```json: {repr(content)}")
250
  if content.endswith('```'):
251
  content = content[:-3] # Remove ```
252
- print(f"After removing ```: {repr(content)}")
253
  content = content.strip()
254
- print(f"Final cleaned content: {repr(content)}")
255
 
256
  # Check if cleaned content is JSON
257
  if not content.startswith("{"):
258
- print("Cleaned content is not JSON, treating as explanatory answer")
259
  state["response"] = response.content
260
  return state
261
 
262
  extracted = json.loads(content)
263
- print(f"Successfully parsed intent JSON: {extracted}")
264
  except Exception as e:
265
- print("Failed to parse intent JSON:", response.content)
266
- print("Error:", e)
267
- print(f"Content that failed to parse: {repr(content)}")
268
  extracted = {}
269
 
270
- print(f"Starting field update loop. Extracted: {extracted}")
271
- print(f"Current intent state: {intent}")
272
-
273
  for field in intent_fields:
274
  new_val = extracted.get(field)
275
  old_val = intent.get(field)
276
- print(f"Processing field {field}: new_val={new_val}, old_val={old_val}")
277
 
278
  if new_val is not None and new_val != old_val:
279
- print(f"Updating intent field {field}: {old_val} -> {new_val}")
280
-
281
  # Handle must_have field as array (LLM decides the logic)
282
  if field == "must_have" and new_val:
283
  # Convert to array format for database
@@ -289,16 +270,11 @@ async def extract_and_update_intent(state):
289
  else:
290
  must_have_array = new_val if isinstance(new_val, list) else [str(new_val)]
291
 
292
- print(f"LLM decided must_have: {must_have_array}")
293
  await update_user_intent(session_id, {field: must_have_array})
294
  intent[field] = must_have_array
295
  else:
296
  await update_user_intent(session_id, {field: new_val})
297
  intent[field] = new_val
298
- elif new_val is not None:
299
- print(f"Intent field {field} unchanged: {new_val}")
300
- else:
301
- print(f"Intent field {field} not extracted")
302
 
303
  state["intent"] = intent
304
 
@@ -319,18 +295,13 @@ async def extract_and_search_properties(state):
319
  """
320
  Search for properties based on user intent and store results in state.
321
  """
322
- print(f"=== Starting extract_and_search_properties ===")
323
  intent = state.get("intent", {})
324
- print(f"Intent data: {intent}")
325
 
326
  # Check if we have the minimum required field for property search
327
  location = intent.get("location_preference")
328
 
329
- print(f"Location: {location}")
330
-
331
  if not location:
332
  # Missing location, skip property search
333
- print(f"Missing location - skipping property search")
334
  return {"response": None}
335
 
336
  # Prepare filters for property search
@@ -349,8 +320,6 @@ async def extract_and_search_properties(state):
349
  if must_have and len(must_have) > 0:
350
  filters["must_have"] = must_have
351
 
352
- print(f"Searching with filters: {filters}")
353
-
354
  # Search for properties
355
  properties = await search_properties(filters)
356
  state["properties"] = properties
@@ -364,36 +333,39 @@ async def extract_and_search_properties(state):
364
  filters2 = {k: v for k, v in filters.items() if k != "must_have"}
365
  props2 = await search_properties(filters2)
366
  if props2:
367
- state["properties"] = props2
368
- state["response"] = (
369
- "I don't have an exact match with those features, "
370
- "but here are listings matching your location, budget and size."
371
- )
372
- return state
 
373
 
374
  # Pass 3: drop size
375
- if "size_preference_sqm" in filters2:
376
- filters3 = {k: v for k, v in filters2.items() if k != "size_preference_sqm"}
377
  props3 = await search_properties(filters3)
378
  if props3:
379
- state["properties"] = props3
380
- state["response"] = (
381
- "I don't have that exact size, "
382
- "but here are listings matching your location and budget."
383
- )
384
- return state
 
385
 
386
  # Pass 4: drop budget
387
- if "budget" in filters3:
388
- filters4 = {k: v for k, v in filters3.items() if k != "budget"}
389
  props4 = await search_properties(filters4)
390
  if props4:
391
- state["properties"] = props4
392
- state["response"] = (
393
- f"I don't have listings within your budget in {filters4['location_preference']}, "
394
- "but here are some options in that area."
395
- )
396
- return state
 
397
 
398
  # Final fallback: no listings at all
399
  state["response"] = (
 
47
  system_message = (
48
  f"Hello {user_info.get('name','there')}! You are a helpful and concise property agent. "
49
  "Always base property recommendations solely on listings in our database. "
50
+ "You can share listing URLs and other details that are available in the property data."
51
  )
52
  if user_info.get("name") and user_info["name"] != "Unknown":
53
  system_message += f" The user's name is {user_info['name']}."
 
67
  )
68
 
69
  props = state.get("properties", [])
70
+ search_status = state.get("search_status_message", "")
71
+
72
+ if search_status:
73
+ system_message += f"\n{search_status}\n"
74
+
75
  if props:
 
76
  system_message += "\nHere are some listings I found:\n"
77
  for p in props[:5]:
78
  title = p.get("title", "[no title]")
 
87
  f"{price} ({ptype}); features: {features}\n"
88
  )
89
  url = p.get("listing_url")
 
90
  if url:
91
  system_message += f" Link: {url}\n"
 
 
 
92
 
93
  # After the listings loop, before building messages[]
94
  extras = set()
 
107
  f"\nI can also share {opts} for any listing—just let me know what you'd like."
108
  )
109
 
110
+ 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."
111
+
112
  # Build messages array with history
113
  messages = [{"role": "system", "content": system_message}]
114
 
 
150
  persona: dict
151
  intent: dict
152
  properties: list
153
+ search_status_message: str
154
 
155
  async def extract_and_update_persona(state):
156
  # a. Define which persona fields to track
 
207
  return {"response": None}
208
 
209
  async def extract_and_update_intent(state):
 
 
 
 
 
210
  intent_fields = ["location_preference", "budget", "size_preference_sqm", "must_have"]
211
  user_message = state["user_message"]
212
  session_id = state["session_id"]
 
233
  """
234
 
235
  response = await llm.ainvoke([{"role":"user","content":extraction_prompt}])
 
236
 
237
  import json
238
  try:
239
  # Clean up the response content (remove markdown formatting if present)
240
  content = response.content.strip()
 
241
 
242
  if content.startswith('```json'):
243
  content = content[7:] # Remove ```json
 
244
  if content.endswith('```'):
245
  content = content[:-3] # Remove ```
 
246
  content = content.strip()
 
247
 
248
  # Check if cleaned content is JSON
249
  if not content.startswith("{"):
 
250
  state["response"] = response.content
251
  return state
252
 
253
  extracted = json.loads(content)
 
254
  except Exception as e:
 
 
 
255
  extracted = {}
256
 
 
 
 
257
  for field in intent_fields:
258
  new_val = extracted.get(field)
259
  old_val = intent.get(field)
 
260
 
261
  if new_val is not None and new_val != old_val:
 
 
262
  # Handle must_have field as array (LLM decides the logic)
263
  if field == "must_have" and new_val:
264
  # Convert to array format for database
 
270
  else:
271
  must_have_array = new_val if isinstance(new_val, list) else [str(new_val)]
272
 
 
273
  await update_user_intent(session_id, {field: must_have_array})
274
  intent[field] = must_have_array
275
  else:
276
  await update_user_intent(session_id, {field: new_val})
277
  intent[field] = new_val
 
 
 
 
278
 
279
  state["intent"] = intent
280
 
 
295
  """
296
  Search for properties based on user intent and store results in state.
297
  """
 
298
  intent = state.get("intent", {})
 
299
 
300
  # Check if we have the minimum required field for property search
301
  location = intent.get("location_preference")
302
 
 
 
303
  if not location:
304
  # Missing location, skip property search
 
305
  return {"response": None}
306
 
307
  # Prepare filters for property search
 
320
  if must_have and len(must_have) > 0:
321
  filters["must_have"] = must_have
322
 
 
 
323
  # Search for properties
324
  properties = await search_properties(filters)
325
  state["properties"] = properties
 
333
  filters2 = {k: v for k, v in filters.items() if k != "must_have"}
334
  props2 = await search_properties(filters2)
335
  if props2:
336
+ return {
337
+ "properties": props2,
338
+ "search_status_message": (
339
+ "I don't have an exact match with those features, "
340
+ "but here are listings matching your location, budget and size."
341
+ )
342
+ }
343
 
344
  # Pass 3: drop size
345
+ if "size_preference_sqm" in filters:
346
+ filters3 = {k: v for k, v in filters.items() if k not in ["must_have", "size_preference_sqm"]}
347
  props3 = await search_properties(filters3)
348
  if props3:
349
+ return {
350
+ "properties": props3,
351
+ "search_status_message": (
352
+ "I don't have that exact size, "
353
+ "but here are listings matching your location and budget."
354
+ )
355
+ }
356
 
357
  # Pass 4: drop budget
358
+ if "budget" in filters:
359
+ filters4 = {k: v for k, v in filters.items() if k not in ["must_have", "size_preference_sqm", "budget"]}
360
  props4 = await search_properties(filters4)
361
  if props4:
362
+ return {
363
+ "properties": props4,
364
+ "search_status_message": (
365
+ f"I don't have listings within your budget in {filters4['location_preference']}, "
366
+ "but here are some options in that area."
367
+ )
368
+ }
369
 
370
  # Final fallback: no listings at all
371
  state["response"] = (
database.py CHANGED
@@ -273,87 +273,61 @@ async def update_user_intent(session_id: str, updates: dict):
273
  Patch the intent record with any changed fields.
274
  """
275
  if not supabase or session_id == "local_session":
276
- print(f"Skipping intent update for session {session_id} (no supabase or local session)")
277
  return
278
 
279
  try:
280
  updates_with_ts = { **updates, "updated_at": datetime.utcnow().isoformat() }
281
- print(f"Updating user_intents table for session {session_id} with: {updates_with_ts}")
282
  supabase \
283
  .table("user_intents") \
284
  .update(updates_with_ts) \
285
  .eq("session_id", session_id) \
286
  .execute()
287
- print(f"Successfully updated intent for session {session_id}")
288
  except Exception as e:
289
- print(f"Error updating user intent: {e}")
290
 
291
  # --- Property Search ---
292
  async def search_properties(filters: dict) -> list:
293
  """
294
  Returns up to 5 active properties matching the user's filters.
295
  """
296
- print(f"=== Starting search_properties ===")
297
- print(f"Filters: {filters}")
298
-
299
  if not supabase:
300
- print("No supabase client - returning empty list")
301
  return []
302
 
303
  try:
304
- # First, let's see if there are any properties at all
305
- test_query = supabase.table("properties").select("*").limit(5)
306
- test_resp = test_query.execute()
307
- print(f"Total properties in database: {len(test_resp.data) if test_resp.data else 0}")
308
- if test_resp.data:
309
- print(f"Sample properties: {test_resp.data}")
310
-
311
  query = supabase.table("properties").select("*").eq("is_active", True)
312
- print("Base query created")
313
 
314
  # a. Location: try city first, then location field
315
  loc = filters.get("location_preference")
316
  if loc:
317
- print(f"Searching for location: {loc}")
318
  query = query.ilike("city", f"%{loc}%")
319
- print(f"After city filter: {query}")
320
  resp = query.execute()
321
- print(f"City search result: {resp.data}")
322
  if not resp.data:
323
  # fallback to suburb/area match
324
- print(f"No city matches, trying location field...")
325
  query = supabase.table("properties").select("*")\
326
  .eq("is_active", True)\
327
  .ilike("location", f"%{loc}%")
328
- print(f"After location filter: {query}")
329
 
330
  # b. Budget cap
331
  budget = filters.get("budget")
332
  if budget is not None:
333
- print(f"Adding budget filter: <= {budget}")
334
  query = query.lte("price", budget)
335
 
336
  # c. Minimum size
337
  size = filters.get("size_preference_sqm")
338
  if size is not None:
339
- print(f"Adding size filter: >= {size}")
340
  query = query.gte("size_sqm", size)
341
 
342
  # d. Must-have features
343
  features = filters.get("must_have") or []
344
  for feat in features:
345
- print(f"Adding feature filter: {feat}")
346
  query = query.contains("features", [feat])
347
 
348
  # e. Order & limit
349
  query = query.order("is_featured", desc=True).order("price").limit(5)
350
 
351
  # f. Execute and return
352
- print(f"Executing final query...")
353
  resp = query.execute()
354
- print(f"Query executed. Response: {resp.data}")
355
  return resp.data or []
356
 
357
  except Exception as e:
358
- print(f"Error searching properties: {e}")
359
  return []
 
273
  Patch the intent record with any changed fields.
274
  """
275
  if not supabase or session_id == "local_session":
 
276
  return
277
 
278
  try:
279
  updates_with_ts = { **updates, "updated_at": datetime.utcnow().isoformat() }
 
280
  supabase \
281
  .table("user_intents") \
282
  .update(updates_with_ts) \
283
  .eq("session_id", session_id) \
284
  .execute()
 
285
  except Exception as e:
286
+ pass
287
 
288
  # --- Property Search ---
289
  async def search_properties(filters: dict) -> list:
290
  """
291
  Returns up to 5 active properties matching the user's filters.
292
  """
 
 
 
293
  if not supabase:
 
294
  return []
295
 
296
  try:
 
 
 
 
 
 
 
297
  query = supabase.table("properties").select("*").eq("is_active", True)
 
298
 
299
  # a. Location: try city first, then location field
300
  loc = filters.get("location_preference")
301
  if loc:
 
302
  query = query.ilike("city", f"%{loc}%")
 
303
  resp = query.execute()
 
304
  if not resp.data:
305
  # fallback to suburb/area match
 
306
  query = supabase.table("properties").select("*")\
307
  .eq("is_active", True)\
308
  .ilike("location", f"%{loc}%")
 
309
 
310
  # b. Budget cap
311
  budget = filters.get("budget")
312
  if budget is not None:
 
313
  query = query.lte("price", budget)
314
 
315
  # c. Minimum size
316
  size = filters.get("size_preference_sqm")
317
  if size is not None:
 
318
  query = query.gte("size_sqm", size)
319
 
320
  # d. Must-have features
321
  features = filters.get("must_have") or []
322
  for feat in features:
 
323
  query = query.contains("features", [feat])
324
 
325
  # e. Order & limit
326
  query = query.order("is_featured", desc=True).order("price").limit(5)
327
 
328
  # f. Execute and return
 
329
  resp = query.execute()
 
330
  return resp.data or []
331
 
332
  except Exception as e:
 
333
  return []