zenaight commited on
Commit ·
ab1535a
1
Parent(s): ba64859
propety search
Browse files- .DS_Store +0 -0
- ai_chat.py +59 -4
- database.py +49 -1
.DS_Store
CHANGED
|
Binary files a/.DS_Store and b/.DS_Store differ
|
|
|
ai_chat.py
CHANGED
|
@@ -2,7 +2,7 @@ from langgraph.graph import StateGraph, END
|
|
| 2 |
from langchain_core.runnables import RunnableLambda
|
| 3 |
from typing import TypedDict
|
| 4 |
from config import llm, OPENAI_API_KEY
|
| 5 |
-
from database import get_session_messages, save_message, update_user_persona, update_user_intent
|
| 6 |
|
| 7 |
def chat_with_session_memory(state):
|
| 8 |
"""Chat function with session-based memory"""
|
|
@@ -37,6 +37,22 @@ def chat_with_session_memory(state):
|
|
| 37 |
f"and must-haves: {', '.join(must_have_list) if must_have_list else '[none]'}. "
|
| 38 |
)
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# Build messages array with history
|
| 41 |
messages = [{"role": "system", "content": system_message}]
|
| 42 |
|
|
@@ -77,6 +93,7 @@ class ChatState(TypedDict):
|
|
| 77 |
session_messages: list
|
| 78 |
persona: dict
|
| 79 |
intent: dict
|
|
|
|
| 80 |
|
| 81 |
async def extract_and_update_persona(state):
|
| 82 |
# a. Define which persona fields to track
|
|
@@ -243,18 +260,55 @@ async def extract_and_update_intent(state):
|
|
| 243 |
|
| 244 |
return {"response": None}
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
# --- Build LangGraph ---
|
| 247 |
graph = StateGraph(ChatState)
|
| 248 |
graph.add_node("persona_update", RunnableLambda(extract_and_update_persona))
|
| 249 |
graph.add_node("intent_update", RunnableLambda(extract_and_update_intent))
|
|
|
|
| 250 |
graph.add_node("chat", RunnableLambda(chat_with_session_memory))
|
| 251 |
graph.set_entry_point("persona_update")
|
| 252 |
graph.add_edge("persona_update", "intent_update")
|
| 253 |
-
graph.add_edge("intent_update", "
|
|
|
|
| 254 |
graph.add_edge("chat", END)
|
| 255 |
chat_graph = graph.compile()
|
| 256 |
|
| 257 |
-
async def process_message(user_message: str, user_info: dict = None, session_id: str = None, wa_id: str = None, wamid: str = None, persona: dict = None, intent: dict = None):
|
| 258 |
"""Process a message through the AI chat system with session memory"""
|
| 259 |
if user_info is None:
|
| 260 |
user_info = {}
|
|
@@ -273,7 +327,8 @@ async def process_message(user_message: str, user_info: dict = None, session_id:
|
|
| 273 |
"wamid": wamid,
|
| 274 |
"session_messages": session_messages,
|
| 275 |
"persona": persona or {},
|
| 276 |
-
"intent": intent or {}
|
|
|
|
| 277 |
})
|
| 278 |
|
| 279 |
# Save messages to database
|
|
|
|
| 2 |
from langchain_core.runnables import RunnableLambda
|
| 3 |
from typing import TypedDict
|
| 4 |
from config import llm, OPENAI_API_KEY
|
| 5 |
+
from database import get_session_messages, save_message, update_user_persona, update_user_intent, search_properties
|
| 6 |
|
| 7 |
def chat_with_session_memory(state):
|
| 8 |
"""Chat function with session-based memory"""
|
|
|
|
| 37 |
f"and must-haves: {', '.join(must_have_list) if must_have_list else '[none]'}. "
|
| 38 |
)
|
| 39 |
|
| 40 |
+
props = state.get("properties", [])
|
| 41 |
+
if props:
|
| 42 |
+
system_message += "\nHere are some listings I found:\n"
|
| 43 |
+
for p in props[:5]:
|
| 44 |
+
title = p.get("title", "[no title]")
|
| 45 |
+
loc = p.get("location", "")
|
| 46 |
+
city = p.get("city", "")
|
| 47 |
+
size = p.get("size_sqm", "")
|
| 48 |
+
price = p.get("price", "")
|
| 49 |
+
ptype = p.get("price_type", "")
|
| 50 |
+
features = ", ".join(p.get("features", [])) or "[no features]"
|
| 51 |
+
system_message += (
|
| 52 |
+
f"- {title} in {loc}, {city}: {size} sqm, "
|
| 53 |
+
f"{price} ({ptype}); features: {features}\n"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
# Build messages array with history
|
| 57 |
messages = [{"role": "system", "content": system_message}]
|
| 58 |
|
|
|
|
| 93 |
session_messages: list
|
| 94 |
persona: dict
|
| 95 |
intent: dict
|
| 96 |
+
properties: list
|
| 97 |
|
| 98 |
async def extract_and_update_persona(state):
|
| 99 |
# a. Define which persona fields to track
|
|
|
|
| 260 |
|
| 261 |
return {"response": None}
|
| 262 |
|
| 263 |
+
async def extract_and_search_properties(state):
|
| 264 |
+
"""
|
| 265 |
+
Search for properties based on user intent and store results in state.
|
| 266 |
+
"""
|
| 267 |
+
intent = state.get("intent", {})
|
| 268 |
+
|
| 269 |
+
# Check if we have the minimum required fields for property search
|
| 270 |
+
location = intent.get("location_preference")
|
| 271 |
+
budget = intent.get("budget")
|
| 272 |
+
size = intent.get("size_preference_sqm")
|
| 273 |
+
|
| 274 |
+
if not location or budget is None or size is None:
|
| 275 |
+
# Missing required fields, skip property search
|
| 276 |
+
return {"response": None}
|
| 277 |
+
|
| 278 |
+
# Prepare filters for property search
|
| 279 |
+
filters = {
|
| 280 |
+
"location_preference": location,
|
| 281 |
+
"budget": budget,
|
| 282 |
+
"size_preference_sqm": size,
|
| 283 |
+
"must_have": intent.get("must_have", [])
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
# Search for properties
|
| 287 |
+
properties = await search_properties(filters)
|
| 288 |
+
state["properties"] = properties
|
| 289 |
+
|
| 290 |
+
if not properties:
|
| 291 |
+
# No properties found, provide fallback suggestion
|
| 292 |
+
state["response"] = f"I don't see any listings in {location} that match your criteria. Would you like to look in Johannesburg instead, or adjust your requirements?"
|
| 293 |
+
return state
|
| 294 |
+
|
| 295 |
+
# Properties found, continue to chat
|
| 296 |
+
return {"response": None}
|
| 297 |
+
|
| 298 |
# --- Build LangGraph ---
|
| 299 |
graph = StateGraph(ChatState)
|
| 300 |
graph.add_node("persona_update", RunnableLambda(extract_and_update_persona))
|
| 301 |
graph.add_node("intent_update", RunnableLambda(extract_and_update_intent))
|
| 302 |
+
graph.add_node("property_search", RunnableLambda(extract_and_search_properties))
|
| 303 |
graph.add_node("chat", RunnableLambda(chat_with_session_memory))
|
| 304 |
graph.set_entry_point("persona_update")
|
| 305 |
graph.add_edge("persona_update", "intent_update")
|
| 306 |
+
graph.add_edge("intent_update", "property_search")
|
| 307 |
+
graph.add_edge("property_search", "chat")
|
| 308 |
graph.add_edge("chat", END)
|
| 309 |
chat_graph = graph.compile()
|
| 310 |
|
| 311 |
+
async def process_message(user_message: str, user_info: dict = None, session_id: str = None, wa_id: str = None, wamid: str = None, persona: dict = None, intent: dict = None, properties: list = None):
|
| 312 |
"""Process a message through the AI chat system with session memory"""
|
| 313 |
if user_info is None:
|
| 314 |
user_info = {}
|
|
|
|
| 327 |
"wamid": wamid,
|
| 328 |
"session_messages": session_messages,
|
| 329 |
"persona": persona or {},
|
| 330 |
+
"intent": intent or {},
|
| 331 |
+
"properties": properties or []
|
| 332 |
})
|
| 333 |
|
| 334 |
# Save messages to database
|
database.py
CHANGED
|
@@ -286,4 +286,52 @@ async def update_user_intent(session_id: str, updates: dict):
|
|
| 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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
if not supabase:
|
| 297 |
+
return []
|
| 298 |
+
|
| 299 |
+
try:
|
| 300 |
+
query = supabase.table("properties").select("*").eq("is_active", True)
|
| 301 |
+
|
| 302 |
+
# a. Location: try city first, then location field
|
| 303 |
+
loc = filters.get("location_preference")
|
| 304 |
+
if loc:
|
| 305 |
+
query = query.ilike("city", f"%{loc}%")
|
| 306 |
+
resp = query.execute()
|
| 307 |
+
if not resp.data:
|
| 308 |
+
# fallback to suburb/area match
|
| 309 |
+
query = supabase.table("properties").select("*")\
|
| 310 |
+
.eq("is_active", True)\
|
| 311 |
+
.ilike("location", f"%{loc}%")
|
| 312 |
+
|
| 313 |
+
# b. Budget cap
|
| 314 |
+
budget = filters.get("budget")
|
| 315 |
+
if budget is not None:
|
| 316 |
+
query = query.lte("price", budget)
|
| 317 |
+
|
| 318 |
+
# c. Minimum size
|
| 319 |
+
size = filters.get("size_preference_sqm")
|
| 320 |
+
if size is not None:
|
| 321 |
+
query = query.gte("size_sqm", size)
|
| 322 |
+
|
| 323 |
+
# d. Must-have features
|
| 324 |
+
features = filters.get("must_have") or []
|
| 325 |
+
for feat in features:
|
| 326 |
+
query = query.contains("features", [feat])
|
| 327 |
+
|
| 328 |
+
# e. Order & limit
|
| 329 |
+
query = query.order("is_featured", desc=True).order("price", asc=True).limit(5)
|
| 330 |
+
|
| 331 |
+
# f. Execute and return
|
| 332 |
+
resp = query.execute()
|
| 333 |
+
return resp.data or []
|
| 334 |
+
|
| 335 |
+
except Exception as e:
|
| 336 |
+
print(f"Error searching properties: {e}")
|
| 337 |
+
return []
|