zenaight commited on
Commit ·
a822a4f
1
Parent(s): 997047c
test
Browse files- .DS_Store +0 -0
- ai_chat.py +101 -405
.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
ai_chat.py
CHANGED
|
@@ -16,7 +16,28 @@ from config import supabase
|
|
| 16 |
from datetime import datetime
|
| 17 |
import re
|
| 18 |
|
| 19 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
"""Chat function with session-based memory and persona collection"""
|
| 21 |
user_message = state["user_message"]
|
| 22 |
user_info = state.get("user_info", {})
|
|
@@ -24,8 +45,6 @@ def chat_with_session_memory(state):
|
|
| 24 |
wa_id = state.get("wa_id")
|
| 25 |
wamid = state.get("wamid")
|
| 26 |
persona = state.get("persona", {})
|
| 27 |
-
is_persona_question = state.get("is_persona_question", False)
|
| 28 |
-
current_field = state.get("current_field")
|
| 29 |
|
| 30 |
# Get conversation history from database
|
| 31 |
session_messages = []
|
|
@@ -33,133 +52,86 @@ def chat_with_session_memory(state):
|
|
| 33 |
# This will be populated by the async wrapper
|
| 34 |
session_messages = state.get("session_messages", [])
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
if is_valid:
|
| 42 |
-
# Update persona in database (handled by async wrapper)
|
| 43 |
-
# Check if there are more persona questions to ask
|
| 44 |
-
updated_persona = persona.copy()
|
| 45 |
-
updated_persona[current_field] = parsed_value
|
| 46 |
-
missing_fields = [f for f in PERSONA_FIELDS.keys() if not updated_persona.get(f)]
|
| 47 |
-
|
| 48 |
-
if missing_fields:
|
| 49 |
-
# Ask next persona question
|
| 50 |
-
next_field = missing_fields[0]
|
| 51 |
-
next_question = PERSONA_FIELDS[next_field]["prompt"]
|
| 52 |
-
return {
|
| 53 |
-
"response": f"{response_message}\n\n{next_question}",
|
| 54 |
-
"user_message": user_message,
|
| 55 |
-
"ai_response": f"{response_message}\n\n{next_question}",
|
| 56 |
-
"session_id": session_id,
|
| 57 |
-
"wa_id": wa_id,
|
| 58 |
-
"wamid": wamid,
|
| 59 |
-
"update_persona": True,
|
| 60 |
-
"persona_field": current_field,
|
| 61 |
-
"persona_value": parsed_value,
|
| 62 |
-
"ask_persona_question": True,
|
| 63 |
-
"current_field": next_field
|
| 64 |
-
}
|
| 65 |
-
else:
|
| 66 |
-
# Persona complete - continue with natural conversation
|
| 67 |
-
return {
|
| 68 |
-
"response": f"{response_message}\n\nGreat! Now I have a good understanding of what you're looking for. How can I help you find the right industrial property?",
|
| 69 |
-
"user_message": user_message,
|
| 70 |
-
"ai_response": f"{response_message}\n\nGreat! Now I have a good understanding of what you're looking for. How can I help you find the right industrial property?",
|
| 71 |
-
"session_id": session_id,
|
| 72 |
-
"wa_id": wa_id,
|
| 73 |
-
"wamid": wamid,
|
| 74 |
-
"update_persona": True,
|
| 75 |
-
"persona_field": current_field,
|
| 76 |
-
"persona_value": parsed_value
|
| 77 |
-
}
|
| 78 |
-
else:
|
| 79 |
-
# Invalid response, ask for clarification or re-ask
|
| 80 |
-
return {
|
| 81 |
-
"response": response_message,
|
| 82 |
-
"user_message": user_message,
|
| 83 |
-
"ai_response": response_message,
|
| 84 |
-
"session_id": session_id,
|
| 85 |
-
"wa_id": wa_id,
|
| 86 |
-
"wamid": wamid,
|
| 87 |
-
"ask_persona_question": True,
|
| 88 |
-
"current_field": current_field
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
# Regular conversation - check if we should ask persona questions
|
| 92 |
-
should_ask, field_to_ask = state.get("persona_check", (False, None))
|
| 93 |
-
|
| 94 |
-
if should_ask and field_to_ask:
|
| 95 |
-
# Ask persona question
|
| 96 |
-
question = PERSONA_FIELDS[field_to_ask]["prompt"]
|
| 97 |
-
return {
|
| 98 |
-
"response": question,
|
| 99 |
-
"user_message": user_message,
|
| 100 |
-
"ai_response": question,
|
| 101 |
-
"session_id": session_id,
|
| 102 |
-
"wa_id": wa_id,
|
| 103 |
-
"wamid": wamid,
|
| 104 |
-
"ask_persona_question": True,
|
| 105 |
-
"current_field": field_to_ask
|
| 106 |
-
}
|
| 107 |
|
| 108 |
-
# Add system message with
|
| 109 |
-
system_message = """You are a friendly, professional industrial property agent assistant. Be conversational and human-like in your responses.
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
Key guidelines:
|
| 112 |
-
- ALWAYS respond naturally to greetings (hi, hello, etc.) with a warm greeting back FIRST
|
| 113 |
-
- Use the user's name if available to make it personal
|
| 114 |
- Be helpful and informative about industrial properties
|
| 115 |
-
- Don't immediately jump to asking questions - have a natural conversation first
|
| 116 |
-
- If someone asks for clarification about property terms, explain clearly and helpfully
|
| 117 |
-
- Only ask about their property needs when they show interest in searching or viewing properties
|
| 118 |
- Keep responses conversational and not robotic
|
| 119 |
-
- If user
|
| 120 |
-
-
|
| 121 |
-
-
|
| 122 |
-
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
# Add persona context if available
|
| 130 |
-
persona_summary = get_persona_summary(persona)
|
| 131 |
-
if persona_summary != "New user - profile incomplete":
|
| 132 |
-
system_message += f"\nUser profile: {persona_summary}. Use this information to provide relevant property advice."
|
| 133 |
-
|
| 134 |
-
# Build messages array with history
|
| 135 |
-
messages = [{"role": "system", "content": system_message}]
|
| 136 |
-
|
| 137 |
-
# Add conversation history (last 30 messages)
|
| 138 |
-
for msg in session_messages[-30:]:
|
| 139 |
-
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 140 |
-
|
| 141 |
-
# Add current user message
|
| 142 |
-
messages.append({"role": "user", "content": user_message})
|
| 143 |
|
|
|
|
|
|
|
|
|
|
| 144 |
try:
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
response = llm.invoke(messages)
|
| 149 |
-
ai_response = response.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
# Save messages to database (this will be handled by the async wrapper)
|
| 152 |
return {
|
| 153 |
"response": ai_response,
|
| 154 |
"user_message": user_message,
|
| 155 |
"ai_response": ai_response,
|
| 156 |
"session_id": session_id,
|
| 157 |
"wa_id": wa_id,
|
| 158 |
-
"wamid": wamid
|
|
|
|
|
|
|
| 159 |
}
|
|
|
|
| 160 |
except Exception as e:
|
| 161 |
-
print(f"Error in
|
| 162 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
class ChatState(TypedDict):
|
| 165 |
user_message: str
|
|
@@ -170,18 +142,12 @@ class ChatState(TypedDict):
|
|
| 170 |
wamid: str
|
| 171 |
session_messages: list
|
| 172 |
persona: dict
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
parsed_response: tuple
|
| 176 |
-
persona_check: tuple
|
| 177 |
-
update_persona: bool
|
| 178 |
-
persona_field: str
|
| 179 |
-
persona_value: any
|
| 180 |
-
ask_persona_question: bool
|
| 181 |
|
| 182 |
# --- Build LangGraph ---
|
| 183 |
graph = StateGraph(ChatState)
|
| 184 |
-
graph.add_node("chat",
|
| 185 |
graph.set_entry_point("chat")
|
| 186 |
graph.add_edge("chat", END)
|
| 187 |
chat_graph = graph.compile()
|
|
@@ -371,305 +337,35 @@ async def process_message(user_message: str, user_info: dict = None, session_id:
|
|
| 371 |
if user_message.lower() in ["hi", "hello", "hey", "good morning", "good afternoon", "good evening", "morning", "afternoon", "evening"]:
|
| 372 |
return f"Hi {user_info.get('name', 'there')}! 👋\n\nI'm your property agent assistant. I can help you find industrial properties, warehouses, offices, and commercial spaces. What are you looking for today?"
|
| 373 |
|
| 374 |
-
# Detect if user is expressing interest in a specific property
|
| 375 |
-
property_1_phrases = ["property 1", "first", "1", "one", "office", "19 sqm", "2300", "edenvale"]
|
| 376 |
-
property_2_phrases = ["property 2", "second", "2", "two", "warehouse", "304 sqm", "22000", "rutland"]
|
| 377 |
-
user_response_lower = user_message.lower()
|
| 378 |
-
if any(phrase in user_response_lower for phrase in property_1_phrases):
|
| 379 |
-
return "Great choice! The 19 sqm office space in Eastleigh Exchange is perfect for 2-3 people. It's available for R2,300/month with all-inclusive amenities. Would you like me to tell you more about the features, show you photos, or help you schedule a viewing?"
|
| 380 |
-
if any(phrase in user_response_lower for phrase in property_2_phrases):
|
| 381 |
-
return "Excellent! The 304 sqm warehouse at Rutland Works is ideal for manufacturing. It features high roller shutter access, dual-level layout, and office space. Available for R22,000/month. Would you like more details about the features, photos, or to schedule a viewing?"
|
| 382 |
-
|
| 383 |
# Get user persona
|
| 384 |
persona = await get_or_create_persona(wa_id) if wa_id else {}
|
| 385 |
|
| 386 |
-
#
|
| 387 |
-
|
| 388 |
-
if session_id:
|
| 389 |
-
session_messages = await get_session_messages(session_id, limit=30)
|
| 390 |
-
|
| 391 |
-
# Check if we should ask persona questions
|
| 392 |
-
conversation_context = " ".join([msg["content"] for msg in session_messages[-5:]]) + " " + user_message
|
| 393 |
-
should_ask, field_to_ask = await should_ask_persona_question(persona, conversation_context)
|
| 394 |
-
|
| 395 |
-
# Check if this is a response to a persona question
|
| 396 |
-
is_persona_question = False
|
| 397 |
-
current_field = None
|
| 398 |
-
parsed_response = (False, None, "")
|
| 399 |
-
|
| 400 |
-
# Check if the last AI message was a persona question
|
| 401 |
-
if session_messages and session_messages[-1]["role"] == "assistant":
|
| 402 |
-
last_ai_message = session_messages[-1]["content"]
|
| 403 |
-
for field, field_info in PERSONA_FIELDS.items():
|
| 404 |
-
if field_info["prompt"] in last_ai_message:
|
| 405 |
-
is_persona_question = True
|
| 406 |
-
current_field = field
|
| 407 |
-
# Parse user response
|
| 408 |
-
parsed_response = await parse_user_response(user_message, field)
|
| 409 |
-
break
|
| 410 |
-
|
| 411 |
-
# Proactive persona extraction from any message
|
| 412 |
-
extracted_persona = {}
|
| 413 |
-
if wa_id:
|
| 414 |
-
# Try to extract persona fields from the current message (ALWAYS)
|
| 415 |
-
extracted_persona = await extract_persona_from_message(user_message, persona)
|
| 416 |
-
print(f"Extracted persona from message: {extracted_persona}")
|
| 417 |
-
|
| 418 |
-
# Update persona with extracted information for persona_check calculation
|
| 419 |
updated_persona = persona.copy()
|
| 420 |
for field, value in extracted_persona.items():
|
| 421 |
if value is not None:
|
| 422 |
updated_persona[field] = value
|
| 423 |
|
| 424 |
-
#
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
# Check for property search intent - be more flexible
|
| 430 |
-
property_search_phrases = [
|
| 431 |
-
"show me properties", "property in", "warehouse in", "industrial in", "listings in", "toon my eiendomme", "warehouse", "factory", "show me warehouses", "show me listings", "show me", "show", "do you have", "have any", "properties like", "any properties", "any listings", "suggestions", "options", "results", "found", "search", "looking for", "where are", "what about", "what do you have"
|
| 432 |
-
]
|
| 433 |
-
is_property_search = any(phrase in user_message.lower() for phrase in property_search_phrases)
|
| 434 |
-
|
| 435 |
-
# Only trigger property search if user explicitly asks for properties, NOT for greetings
|
| 436 |
-
greeting_phrases = ["hi", "hello", "hey", "good morning", "good afternoon", "good evening", "morning", "afternoon", "evening"]
|
| 437 |
-
is_greeting = any(phrase in user_message.lower() for phrase in greeting_phrases)
|
| 438 |
-
|
| 439 |
-
print(f"Debug - is_property_search: {is_property_search}, is_greeting: {is_greeting}")
|
| 440 |
-
print(f"Debug - user_message: '{user_message}'")
|
| 441 |
-
|
| 442 |
-
# Check if user is asking for promised property suggestions (only if last message was about searching)
|
| 443 |
-
if session_messages and session_messages[-1]["role"] == "assistant" and updated_persona.get("location_preference"):
|
| 444 |
-
last_ai_message = session_messages[-1]["content"]
|
| 445 |
-
if any(phrase in last_ai_message.lower() for phrase in ["gather some options", "keep you posted", "suitable listings", "looking for properties", "search for properties"]):
|
| 446 |
-
# User is asking for the property suggestions that were promised
|
| 447 |
-
print("User is asking for promised property suggestions")
|
| 448 |
-
property_msgs = await handle_property_search(user_message, updated_persona)
|
| 449 |
-
for msg in property_msgs:
|
| 450 |
-
print(f"Property suggestion: {msg}")
|
| 451 |
-
if property_msgs:
|
| 452 |
-
# Add some visual separators to make it more readable
|
| 453 |
-
formatted_messages = []
|
| 454 |
-
for i, msg in enumerate(property_msgs):
|
| 455 |
-
if i == 0: # Intro message
|
| 456 |
-
formatted_messages.append(msg)
|
| 457 |
-
elif i == len(property_msgs) - 1: # Last message (follow-up)
|
| 458 |
-
formatted_messages.append(f"\n{msg}")
|
| 459 |
-
else: # Property descriptions
|
| 460 |
-
formatted_messages.append(f"\n🏢 **Property {i}**\n{msg}")
|
| 461 |
-
|
| 462 |
-
return "\n\n".join(formatted_messages)
|
| 463 |
-
return "Let me know if you'd like to see images or more details!"
|
| 464 |
-
|
| 465 |
-
# Check if user is expressing interest in a specific property
|
| 466 |
-
if session_messages and session_messages[-1]["role"] == "assistant":
|
| 467 |
-
last_ai_message = session_messages[-1]["content"]
|
| 468 |
-
if "Which of these properties catches your eye" in last_ai_message:
|
| 469 |
-
# User is responding to property suggestions
|
| 470 |
-
user_response_lower = user_message.lower()
|
| 471 |
-
|
| 472 |
-
# Check for property selection
|
| 473 |
-
if any(phrase in user_response_lower for phrase in ["property 1", "first", "1", "one", "office", "19 sqm", "2300", "edenvale"]):
|
| 474 |
-
return "Great choice! The 19 sqm office space in Eastleigh Exchange is perfect for 2-3 people. It's available for R2,300/month with all-inclusive amenities. Would you like me to tell you more about the features, show you photos, or help you schedule a viewing?"
|
| 475 |
-
|
| 476 |
-
elif any(phrase in user_response_lower for phrase in ["property 2", "second", "2", "two", "warehouse", "304 sqm", "22000", "rutland"]):
|
| 477 |
-
return "Excellent! The 304 sqm warehouse at Rutland Works is ideal for manufacturing. It features high roller shutter access, dual-level layout, and office space. Available for R22,000/month. Would you like more details about the features, photos, or to schedule a viewing?"
|
| 478 |
-
|
| 479 |
-
elif any(phrase in user_response_lower for phrase in ["both", "all", "show me", "more", "details", "photos", "pictures"]):
|
| 480 |
-
return "I'd be happy to show you more details! Which property would you like to know more about first - the office space (Property 1) or the warehouse (Property 2)?"
|
| 481 |
-
|
| 482 |
-
else:
|
| 483 |
-
return "I'm not sure which property you're interested in. Could you please specify 'Property 1' (the office space) or 'Property 2' (the warehouse)?"
|
| 484 |
-
|
| 485 |
-
# Check if user is asking about property suggestions/results in general
|
| 486 |
-
if any(phrase in user_message.lower() for phrase in ["where are", "suggestions", "options", "results", "found", "what about", "what do you have", "show me what", "any properties"]):
|
| 487 |
-
print("User is asking about property suggestions/results")
|
| 488 |
-
property_msgs = await handle_property_search(user_message, updated_persona)
|
| 489 |
-
for msg in property_msgs:
|
| 490 |
-
print(f"Property suggestion: {msg}")
|
| 491 |
-
if property_msgs:
|
| 492 |
-
# Add some visual separators to make it more readable
|
| 493 |
-
formatted_messages = []
|
| 494 |
-
for i, msg in enumerate(property_msgs):
|
| 495 |
-
if i == 0: # Intro message
|
| 496 |
-
formatted_messages.append(msg)
|
| 497 |
-
elif i == len(property_msgs) - 1: # Last message (follow-up)
|
| 498 |
-
formatted_messages.append(f"\n{msg}")
|
| 499 |
-
else: # Property descriptions
|
| 500 |
-
formatted_messages.append(f"\n🏢 **Property {i}**\n{msg}")
|
| 501 |
-
|
| 502 |
-
return "\n\n".join(formatted_messages)
|
| 503 |
-
return "Let me know if you'd like to see images or more details!"
|
| 504 |
-
|
| 505 |
-
# Handle confusion or clarification requests
|
| 506 |
-
if user_message.lower() in ["?", "huh", "what", "confused", "not sure", "what do you mean"]:
|
| 507 |
-
if session_messages and session_messages[-1]["role"] == "assistant":
|
| 508 |
-
last_ai_message = session_messages[-1]["content"]
|
| 509 |
-
if "Which of these properties catches your eye" in last_ai_message:
|
| 510 |
-
return "I just showed you a property in Edenvale! It's a 304 sqm warehouse with office space, parking, and 24/7 security. Would you like me to tell you more about it, or would you prefer to see other options?"
|
| 511 |
-
elif "property" in last_ai_message.lower():
|
| 512 |
-
return "I'm here to help you find industrial properties! I can search for warehouses, factories, or commercial spaces. What type of property are you looking for?"
|
| 513 |
-
return "I'm here to help you find industrial properties! What can I assist you with today?"
|
| 514 |
-
|
| 515 |
-
if is_property_search and not is_greeting:
|
| 516 |
-
print(f"Property search detected: {user_message}")
|
| 517 |
-
property_msgs = await handle_property_search(user_message, updated_persona)
|
| 518 |
-
for msg in property_msgs:
|
| 519 |
-
# Send each property message (simulate WhatsApp message send)
|
| 520 |
-
print(f"Property suggestion: {msg}")
|
| 521 |
-
# Return all messages as a single response (they will be sent sequentially)
|
| 522 |
-
if property_msgs:
|
| 523 |
-
# Add some visual separators to make it more readable
|
| 524 |
-
formatted_messages = []
|
| 525 |
-
for i, msg in enumerate(property_msgs):
|
| 526 |
-
if i == 0: # Intro message
|
| 527 |
-
formatted_messages.append(msg)
|
| 528 |
-
elif i == len(property_msgs) - 1: # Last message (follow-up)
|
| 529 |
-
formatted_messages.append(f"\n{msg}")
|
| 530 |
-
else: # Property descriptions
|
| 531 |
-
formatted_messages.append(f"\n🏢 **Property {i}**\n{msg}")
|
| 532 |
-
|
| 533 |
-
return "\n\n".join(formatted_messages)
|
| 534 |
-
return "Let me know if you'd like to see images or more details!"
|
| 535 |
-
|
| 536 |
-
# Handle greetings with persona context
|
| 537 |
-
if is_greeting and updated_persona.get("location_preference"):
|
| 538 |
-
# User has a persona, ask if they're still looking
|
| 539 |
-
persona_summary = get_persona_summary(updated_persona)
|
| 540 |
-
greeting_msg = f"Hi {user_info.get('name', 'there')}! 👋\n\nI see you were previously looking for properties in {updated_persona['location_preference'].title()}"
|
| 541 |
-
|
| 542 |
-
if updated_persona.get("size_preference_sqm"):
|
| 543 |
-
greeting_msg += f" around {updated_persona['size_preference_sqm']} sqm"
|
| 544 |
-
|
| 545 |
-
if updated_persona.get("budget"):
|
| 546 |
-
greeting_msg += f" with a budget of R{int(updated_persona['budget']):,} per month"
|
| 547 |
-
|
| 548 |
-
greeting_msg += ".\n\nAre you still looking for properties with these requirements, or would you like to start fresh?"
|
| 549 |
-
|
| 550 |
-
return greeting_msg
|
| 551 |
-
|
| 552 |
-
# Handle simple greetings without persona
|
| 553 |
-
if is_greeting:
|
| 554 |
-
return f"Hi {user_info.get('name', 'there')}! 👋\n\nI'm your property agent assistant. I can help you find industrial properties, warehouses, offices, and commercial spaces. What are you looking for today?"
|
| 555 |
-
|
| 556 |
-
# Handle responses to the greeting question
|
| 557 |
-
if session_messages and session_messages[-1]["role"] == "assistant":
|
| 558 |
-
last_ai_message = session_messages[-1]["content"]
|
| 559 |
-
if "Are you still looking for properties with these requirements" in last_ai_message:
|
| 560 |
-
user_response_lower = user_message.lower()
|
| 561 |
-
|
| 562 |
-
# Check for affirmative responses
|
| 563 |
-
affirmative_words = ["yes", "yeah", "yep", "sure", "ok", "okay", "correct", "right", "that's right", "exactly", "still looking", "still searching"]
|
| 564 |
-
negative_words = ["no", "nope", "not", "start fresh", "new search", "different", "change", "reset"]
|
| 565 |
-
|
| 566 |
-
if any(word in user_response_lower for word in affirmative_words):
|
| 567 |
-
# User wants to continue with current persona, search for properties
|
| 568 |
-
property_msgs = await handle_property_search(user_message, updated_persona)
|
| 569 |
-
for msg in property_msgs:
|
| 570 |
-
print(f"Property suggestion: {msg}")
|
| 571 |
-
if property_msgs:
|
| 572 |
-
# Add some visual separators to make it more readable
|
| 573 |
-
formatted_messages = []
|
| 574 |
-
for i, msg in enumerate(property_msgs):
|
| 575 |
-
if i == 0: # Intro message
|
| 576 |
-
formatted_messages.append(msg)
|
| 577 |
-
elif i == len(property_msgs) - 1: # Last message (follow-up)
|
| 578 |
-
formatted_messages.append(f"\n{msg}")
|
| 579 |
-
else: # Property descriptions
|
| 580 |
-
formatted_messages.append(f"\n🏢 **Property {i}**\n{msg}")
|
| 581 |
-
|
| 582 |
-
return "\n\n".join(formatted_messages)
|
| 583 |
-
return "Let me know if you'd like to see images or more details!"
|
| 584 |
-
|
| 585 |
-
elif any(word in user_response_lower for word in negative_words):
|
| 586 |
-
# User wants to start fresh, reset persona
|
| 587 |
-
if wa_id:
|
| 588 |
-
# Reset persona fields
|
| 589 |
-
reset_data = {
|
| 590 |
-
"location_preference": None,
|
| 591 |
-
"size_preference_sqm": None,
|
| 592 |
-
"budget": None,
|
| 593 |
-
"must_have": None,
|
| 594 |
-
"intent": None,
|
| 595 |
-
"updated_at": datetime.utcnow().isoformat()
|
| 596 |
-
}
|
| 597 |
-
try:
|
| 598 |
-
supabase.table("user_personas").update(reset_data).eq("wa_id", wa_id).execute()
|
| 599 |
-
print(f"Reset persona for user {wa_id}")
|
| 600 |
-
except Exception as e:
|
| 601 |
-
print(f"Error resetting persona: {e}")
|
| 602 |
-
|
| 603 |
-
return f"Perfect! Let's start fresh. Hi {user_info.get('name', 'there')}! 👋\n\nHow can I help you find the perfect industrial property today?"
|
| 604 |
-
|
| 605 |
-
# Handle "new requirements" or "start fresh" requests
|
| 606 |
-
new_requirements_phrases = ["new requirements", "new search", "start fresh", "different requirements", "change requirements", "reset", "clear", "new criteria"]
|
| 607 |
-
if any(phrase in user_message.lower() for phrase in new_requirements_phrases):
|
| 608 |
-
if wa_id:
|
| 609 |
-
# Reset persona fields
|
| 610 |
-
reset_data = {
|
| 611 |
-
"location_preference": None,
|
| 612 |
-
"size_preference_sqm": None,
|
| 613 |
-
"budget": None,
|
| 614 |
-
"must_have": None,
|
| 615 |
-
"intent": None,
|
| 616 |
-
"updated_at": datetime.utcnow().isoformat()
|
| 617 |
-
}
|
| 618 |
-
try:
|
| 619 |
-
supabase.table("user_personas").update(reset_data).eq("wa_id", wa_id).execute()
|
| 620 |
-
print(f"Reset persona for user {wa_id} due to new requirements")
|
| 621 |
-
except Exception as e:
|
| 622 |
-
print(f"Error resetting persona: {e}")
|
| 623 |
-
|
| 624 |
-
return f"Perfect! I've cleared your previous requirements. Hi {user_info.get('name', 'there')}! 👋\n\nWhat are your new requirements for an industrial property?"
|
| 625 |
-
|
| 626 |
-
# Handle property detail requests
|
| 627 |
-
if session_messages and session_messages[-1]["role"] == "assistant":
|
| 628 |
-
last_ai_message = session_messages[-1]["content"]
|
| 629 |
-
|
| 630 |
-
# Check if the last message was asking about property details
|
| 631 |
-
if "Which of these properties catches your eye" in last_ai_message or "I can tell you more about any of them" in last_ai_message:
|
| 632 |
-
# User is responding to property suggestions, handle their interest
|
| 633 |
-
return await handle_property_interest(user_message, updated_persona)
|
| 634 |
-
|
| 635 |
-
# Check if we're asking what specific info they want
|
| 636 |
-
elif "What would you like to know about it" in last_ai_message or "What specific information" in last_ai_message:
|
| 637 |
-
# User is telling us what they want to know
|
| 638 |
-
return await handle_property_info_request(user_message, session_messages, updated_persona)
|
| 639 |
|
| 640 |
-
#
|
| 641 |
-
|
| 642 |
"user_message": user_message,
|
| 643 |
"user_info": user_info,
|
| 644 |
"session_id": session_id,
|
| 645 |
"wa_id": wa_id,
|
| 646 |
"wamid": wamid,
|
| 647 |
"session_messages": session_messages,
|
| 648 |
-
"persona": updated_persona,
|
| 649 |
-
"
|
| 650 |
-
"
|
| 651 |
-
|
| 652 |
-
"persona_check": (should_ask, field_to_ask)
|
| 653 |
-
})
|
| 654 |
-
|
| 655 |
-
print("LangGraph result:", result) # Debugging
|
| 656 |
|
| 657 |
-
#
|
| 658 |
-
|
| 659 |
-
if value is not None:
|
| 660 |
-
print(f"Saving extracted persona field {field} with value {value} for user {wa_id}")
|
| 661 |
-
await update_persona_field(wa_id, field, value)
|
| 662 |
-
|
| 663 |
-
# Save persona field if present in result
|
| 664 |
-
if result.get("persona_field") and result.get("persona_value") is not None and wa_id:
|
| 665 |
-
print(f"Saving persona field {result['persona_field']} with value {result['persona_value']} for user {wa_id}")
|
| 666 |
-
await update_persona_field(wa_id, result["persona_field"], result["persona_value"])
|
| 667 |
-
elif result.get("update_persona") and wa_id:
|
| 668 |
-
await update_persona_field(wa_id, result["persona_field"], result["persona_value"])
|
| 669 |
-
|
| 670 |
-
# Save messages to database
|
| 671 |
-
if session_id and wa_id and wamid:
|
| 672 |
-
await save_message(session_id, wa_id, wamid, "user", user_message)
|
| 673 |
-
await save_message(session_id, wa_id, f"{wamid}_ai", "assistant", result["response"])
|
| 674 |
|
| 675 |
return result["response"]
|
|
|
|
| 16 |
from datetime import datetime
|
| 17 |
import re
|
| 18 |
|
| 19 |
+
async def search_properties_for_ai(city=None, min_size=None, features=None, price_type=None, limit=5):
|
| 20 |
+
"""Search properties for AI responses"""
|
| 21 |
+
try:
|
| 22 |
+
properties = await search_properties(city=city, min_size=min_size, features=features, price_type=price_type, limit=limit)
|
| 23 |
+
return properties
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(f"Error searching properties: {e}")
|
| 26 |
+
return []
|
| 27 |
+
|
| 28 |
+
async def get_property_details_for_ai(property_id):
|
| 29 |
+
"""Get detailed property information for AI responses"""
|
| 30 |
+
try:
|
| 31 |
+
properties = await search_properties(limit=100) # Get all properties
|
| 32 |
+
for prop in properties:
|
| 33 |
+
if prop['id'] == property_id:
|
| 34 |
+
return prop
|
| 35 |
+
return None
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"Error getting property details: {e}")
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
async def chat_with_session_memory(state):
|
| 41 |
"""Chat function with session-based memory and persona collection"""
|
| 42 |
user_message = state["user_message"]
|
| 43 |
user_info = state.get("user_info", {})
|
|
|
|
| 45 |
wa_id = state.get("wa_id")
|
| 46 |
wamid = state.get("wamid")
|
| 47 |
persona = state.get("persona", {})
|
|
|
|
|
|
|
| 48 |
|
| 49 |
# Get conversation history from database
|
| 50 |
session_messages = []
|
|
|
|
| 52 |
# This will be populated by the async wrapper
|
| 53 |
session_messages = state.get("session_messages", [])
|
| 54 |
|
| 55 |
+
# Build context for the AI
|
| 56 |
+
context = f"User: {user_info.get('name', 'there')}\n"
|
| 57 |
+
if persona:
|
| 58 |
+
context += f"Persona: {persona}\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
# Add system message with context
|
| 61 |
+
system_message = f"""You are a friendly, professional industrial property agent assistant. Be conversational and human-like in your responses.
|
| 62 |
+
|
| 63 |
+
Context:
|
| 64 |
+
{context}
|
| 65 |
|
| 66 |
Key guidelines:
|
|
|
|
|
|
|
| 67 |
- Be helpful and informative about industrial properties
|
|
|
|
|
|
|
|
|
|
| 68 |
- Keep responses conversational and not robotic
|
| 69 |
+
- If user asks for property search, help them find properties
|
| 70 |
+
- Always respond naturally and conversationally
|
| 71 |
+
- If user asks for specific property information (URL, photos, features, viewing), you can search the database for that information
|
| 72 |
+
- Use the conversation context to understand which property they're referring to
|
| 73 |
+
|
| 74 |
+
Available actions you can take:
|
| 75 |
+
- Search for properties by city, size, features, or price
|
| 76 |
+
- Get detailed information about specific properties
|
| 77 |
+
- Provide property URLs, photos, features, and viewing arrangements
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
Respond naturally to the user's message: {user_message}"""
|
| 80 |
+
|
| 81 |
+
# Use LLM to generate response
|
| 82 |
try:
|
| 83 |
+
messages = [{"role": "system", "content": system_message}]
|
| 84 |
+
|
| 85 |
+
# Add recent conversation history for context
|
| 86 |
+
for msg in session_messages[-5:]: # Last 5 messages for context
|
| 87 |
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 88 |
+
|
| 89 |
+
messages.append({"role": "user", "content": user_message})
|
| 90 |
|
| 91 |
response = llm.invoke(messages)
|
| 92 |
+
ai_response = response.content.strip()
|
| 93 |
+
|
| 94 |
+
# If the AI response indicates it needs property information, search the database
|
| 95 |
+
if any(keyword in user_message.lower() for keyword in ["property", "properties", "warehouse", "office", "space", "listing", "url", "photos", "features"]):
|
| 96 |
+
# Extract potential search parameters from user message
|
| 97 |
+
city = None
|
| 98 |
+
if persona.get("location_preference"):
|
| 99 |
+
city = persona["location_preference"]
|
| 100 |
+
|
| 101 |
+
# Search for properties
|
| 102 |
+
properties = await search_properties_for_ai(city=city, limit=5)
|
| 103 |
+
|
| 104 |
+
if properties:
|
| 105 |
+
# Add property information to the response
|
| 106 |
+
property_info = "\n\nAvailable properties:\n"
|
| 107 |
+
for i, prop in enumerate(properties[:3], 1):
|
| 108 |
+
property_info += f"{i}. {prop['title']} - {prop['location']}, {prop['city']} - {prop['size_sqm']} sqm - R{prop['price']:,.0f}/month\n"
|
| 109 |
+
property_info += f" URL: {prop.get('listing_url', 'N/A')}\n"
|
| 110 |
+
property_info += f" Features: {', '.join(prop.get('features', [])[:5])}\n\n"
|
| 111 |
+
|
| 112 |
+
ai_response += property_info
|
| 113 |
|
|
|
|
| 114 |
return {
|
| 115 |
"response": ai_response,
|
| 116 |
"user_message": user_message,
|
| 117 |
"ai_response": ai_response,
|
| 118 |
"session_id": session_id,
|
| 119 |
"wa_id": wa_id,
|
| 120 |
+
"wamid": wamid,
|
| 121 |
+
"current_property": None,
|
| 122 |
+
"property_details": {}
|
| 123 |
}
|
| 124 |
+
|
| 125 |
except Exception as e:
|
| 126 |
+
print(f"Error in LangGraph: {e}")
|
| 127 |
+
return {
|
| 128 |
+
"response": "I'm having trouble processing your request right now. Could you please try again?",
|
| 129 |
+
"user_message": user_message,
|
| 130 |
+
"ai_response": "I'm having trouble processing your request right now. Could you please try again?",
|
| 131 |
+
"session_id": session_id,
|
| 132 |
+
"wa_id": wa_id,
|
| 133 |
+
"wamid": wamid
|
| 134 |
+
}
|
| 135 |
|
| 136 |
class ChatState(TypedDict):
|
| 137 |
user_message: str
|
|
|
|
| 142 |
wamid: str
|
| 143 |
session_messages: list
|
| 144 |
persona: dict
|
| 145 |
+
current_property: str
|
| 146 |
+
property_details: dict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
# --- Build LangGraph ---
|
| 149 |
graph = StateGraph(ChatState)
|
| 150 |
+
graph.add_node("chat", chat_with_session_memory)
|
| 151 |
graph.set_entry_point("chat")
|
| 152 |
graph.add_edge("chat", END)
|
| 153 |
chat_graph = graph.compile()
|
|
|
|
| 337 |
if user_message.lower() in ["hi", "hello", "hey", "good morning", "good afternoon", "good evening", "morning", "afternoon", "evening"]:
|
| 338 |
return f"Hi {user_info.get('name', 'there')}! 👋\n\nI'm your property agent assistant. I can help you find industrial properties, warehouses, offices, and commercial spaces. What are you looking for today?"
|
| 339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
# Get user persona
|
| 341 |
persona = await get_or_create_persona(wa_id) if wa_id else {}
|
| 342 |
|
| 343 |
+
# Extract persona information from message
|
| 344 |
+
extracted_persona = await extract_persona_from_message(user_message, persona)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
updated_persona = persona.copy()
|
| 346 |
for field, value in extracted_persona.items():
|
| 347 |
if value is not None:
|
| 348 |
updated_persona[field] = value
|
| 349 |
|
| 350 |
+
# Get session messages
|
| 351 |
+
session_messages = []
|
| 352 |
+
if session_id:
|
| 353 |
+
session_messages = await get_session_messages(session_id, limit=30)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
+
# Prepare state for LangGraph - let the AI handle everything
|
| 356 |
+
state = {
|
| 357 |
"user_message": user_message,
|
| 358 |
"user_info": user_info,
|
| 359 |
"session_id": session_id,
|
| 360 |
"wa_id": wa_id,
|
| 361 |
"wamid": wamid,
|
| 362 |
"session_messages": session_messages,
|
| 363 |
+
"persona": updated_persona,
|
| 364 |
+
"current_property": None,
|
| 365 |
+
"property_details": {}
|
| 366 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
+
# Run LangGraph - the AI will handle property detection and responses naturally
|
| 369 |
+
result = await chat_graph.ainvoke(state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
return result["response"]
|