zenaight commited on
Commit ·
53c8a1f
1
Parent(s): 483b968
Enhance intent management in AI chat functionality
Browse files- Introduced intent extraction and updating logic in the `ai_chat.py` module to capture user property search preferences, including location, budget, size, and must-have features.
- Updated the `process_message` function to include intent data, ensuring a more personalized chat experience.
- Modified the database functions to support intent management, allowing for the creation and updating of user intent records.
- Improved the interaction flow by prompting users for missing intent information, enhancing engagement and response relevance.
- ai_chat.py +67 -4
- database.py +28 -38
- main.py +6 -2
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
|
| 6 |
|
| 7 |
def chat_with_session_memory(state):
|
| 8 |
"""Chat function with session-based memory"""
|
|
@@ -28,6 +28,14 @@ def chat_with_session_memory(state):
|
|
| 28 |
f" The user prefers {p.get('language','[unspecified]')} and wants a {p.get('tone','neutral')} tone."
|
| 29 |
)
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# Build messages array with history
|
| 32 |
messages = [{"role": "system", "content": system_message}]
|
| 33 |
|
|
@@ -67,6 +75,7 @@ class ChatState(TypedDict):
|
|
| 67 |
wamid: str
|
| 68 |
session_messages: list
|
| 69 |
persona: dict
|
|
|
|
| 70 |
|
| 71 |
async def extract_and_update_persona(state):
|
| 72 |
# a. Define which persona fields to track
|
|
@@ -122,16 +131,69 @@ async def extract_and_update_persona(state):
|
|
| 122 |
# f. All persona fields present—proceed to chat
|
| 123 |
return {"response": None}
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
# --- Build LangGraph ---
|
| 126 |
graph = StateGraph(ChatState)
|
| 127 |
graph.add_node("persona_update", RunnableLambda(extract_and_update_persona))
|
|
|
|
| 128 |
graph.add_node("chat", RunnableLambda(chat_with_session_memory))
|
| 129 |
graph.set_entry_point("persona_update")
|
| 130 |
-
graph.add_edge("persona_update", "
|
|
|
|
| 131 |
graph.add_edge("chat", END)
|
| 132 |
chat_graph = graph.compile()
|
| 133 |
|
| 134 |
-
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):
|
| 135 |
"""Process a message through the AI chat system with session memory"""
|
| 136 |
if user_info is None:
|
| 137 |
user_info = {}
|
|
@@ -149,7 +211,8 @@ async def process_message(user_message: str, user_info: dict = None, session_id:
|
|
| 149 |
"wa_id": wa_id,
|
| 150 |
"wamid": wamid,
|
| 151 |
"session_messages": session_messages,
|
| 152 |
-
"persona": persona or {}
|
|
|
|
| 153 |
})
|
| 154 |
|
| 155 |
# 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
|
| 6 |
|
| 7 |
def chat_with_session_memory(state):
|
| 8 |
"""Chat function with session-based memory"""
|
|
|
|
| 28 |
f" The user prefers {p.get('language','[unspecified]')} and wants a {p.get('tone','neutral')} tone."
|
| 29 |
)
|
| 30 |
|
| 31 |
+
intent_data = state.get("intent", {})
|
| 32 |
+
system_message += (
|
| 33 |
+
f" They're looking for a property in {intent_data.get('location_preference','[any area]')}, "
|
| 34 |
+
f"with a budget up to {intent_data.get('budget','[any amount]')} per month, "
|
| 35 |
+
f"around {intent_data.get('size_preference_sqm','[size]')} sqm, "
|
| 36 |
+
f"and must-haves: {', '.join(intent_data.get('must_have',[])) or '[none]'}. "
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
# Build messages array with history
|
| 40 |
messages = [{"role": "system", "content": system_message}]
|
| 41 |
|
|
|
|
| 75 |
wamid: str
|
| 76 |
session_messages: list
|
| 77 |
persona: dict
|
| 78 |
+
intent: dict
|
| 79 |
|
| 80 |
async def extract_and_update_persona(state):
|
| 81 |
# a. Define which persona fields to track
|
|
|
|
| 131 |
# f. All persona fields present—proceed to chat
|
| 132 |
return {"response": None}
|
| 133 |
|
| 134 |
+
async def extract_and_update_intent(state):
|
| 135 |
+
intent_fields = ["location_preference", "budget", "size_preference_sqm", "must_have"]
|
| 136 |
+
user_message = state["user_message"]
|
| 137 |
+
session_id = state["session_id"]
|
| 138 |
+
intent = state.get("intent", {})
|
| 139 |
+
|
| 140 |
+
extraction_prompt = f"""
|
| 141 |
+
Extract and normalize the user's current property search intent from this message:
|
| 142 |
+
{user_message}
|
| 143 |
+
|
| 144 |
+
Normalize abbreviations (e.g., 'sqm' → 'square metres') and correct any spelling mistakes.
|
| 145 |
+
If the user is asking a definition or clarification (e.g. 'What does square metre mean?'), answer that question fully and do not update the intent. After answering, wait for the next user message to extract values.
|
| 146 |
+
Return only a JSON object with keys {intent_fields}, using null for unknown.
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
response = await llm.ainvoke([{"role":"user","content":extraction_prompt}])
|
| 150 |
+
|
| 151 |
+
# Check if response is an explanatory answer rather than JSON
|
| 152 |
+
if not response.content.strip().startswith("{"):
|
| 153 |
+
state["response"] = response.content
|
| 154 |
+
return state
|
| 155 |
+
|
| 156 |
+
import json
|
| 157 |
+
try:
|
| 158 |
+
extracted = json.loads(response.content)
|
| 159 |
+
except:
|
| 160 |
+
print("Failed to parse intent JSON:", response.content)
|
| 161 |
+
extracted = {}
|
| 162 |
+
|
| 163 |
+
for field in intent_fields:
|
| 164 |
+
new_val = extracted.get(field)
|
| 165 |
+
old_val = intent.get(field)
|
| 166 |
+
if new_val is not None and new_val != old_val:
|
| 167 |
+
await update_user_intent(session_id, {field: new_val})
|
| 168 |
+
intent[field] = new_val
|
| 169 |
+
|
| 170 |
+
state["intent"] = intent
|
| 171 |
+
|
| 172 |
+
missing = [f for f in intent_fields if state["intent"].get(f) is None]
|
| 173 |
+
if missing:
|
| 174 |
+
questions = {
|
| 175 |
+
"location_preference": "Hi there! Which area or suburb are you interested in?",
|
| 176 |
+
"budget": "Hi there! What is your monthly budget?",
|
| 177 |
+
"size_preference_sqm": "Hi there! How many square metres do you need?",
|
| 178 |
+
"must_have": "Hi there! What features are must-haves for you?"
|
| 179 |
+
}
|
| 180 |
+
state["response"] = questions.get(missing[0], f"Hi there! Could you tell me your {missing[0]}?")
|
| 181 |
+
return state
|
| 182 |
+
|
| 183 |
+
return {"response": None}
|
| 184 |
+
|
| 185 |
# --- Build LangGraph ---
|
| 186 |
graph = StateGraph(ChatState)
|
| 187 |
graph.add_node("persona_update", RunnableLambda(extract_and_update_persona))
|
| 188 |
+
graph.add_node("intent_update", RunnableLambda(extract_and_update_intent))
|
| 189 |
graph.add_node("chat", RunnableLambda(chat_with_session_memory))
|
| 190 |
graph.set_entry_point("persona_update")
|
| 191 |
+
graph.add_edge("persona_update", "intent_update")
|
| 192 |
+
graph.add_edge("intent_update", "chat")
|
| 193 |
graph.add_edge("chat", END)
|
| 194 |
chat_graph = graph.compile()
|
| 195 |
|
| 196 |
+
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):
|
| 197 |
"""Process a message through the AI chat system with session memory"""
|
| 198 |
if user_info is None:
|
| 199 |
user_info = {}
|
|
|
|
| 211 |
"wa_id": wa_id,
|
| 212 |
"wamid": wamid,
|
| 213 |
"session_messages": session_messages,
|
| 214 |
+
"persona": persona or {},
|
| 215 |
+
"intent": intent or {}
|
| 216 |
})
|
| 217 |
|
| 218 |
# Save messages to database
|
database.py
CHANGED
|
@@ -240,46 +240,36 @@ async def update_user_persona(wa_id: str, updates: dict):
|
|
| 240 |
# --- Intent Management ---
|
| 241 |
async def get_or_create_user_intent(session_id: str, wa_id: str) -> dict:
|
| 242 |
"""
|
| 243 |
-
|
| 244 |
-
If none exists, inserts a new empty intent record and returns it.
|
| 245 |
"""
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
except Exception as e:
|
| 267 |
-
print(f"Error in get_or_create_user_intent: {e}")
|
| 268 |
-
return {"session_id": session_id, "wa_id": wa_id, "created_at": datetime.utcnow().isoformat(), "updated_at": datetime.utcnow().isoformat()}
|
| 269 |
|
| 270 |
async def update_user_intent(session_id: str, updates: dict):
|
| 271 |
"""
|
| 272 |
-
|
| 273 |
"""
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
.table("user_intents")\
|
| 281 |
-
.update(updates_with_ts)\
|
| 282 |
-
.eq("session_id", session_id)\
|
| 283 |
-
.execute()
|
| 284 |
-
except Exception as e:
|
| 285 |
-
print(f"Error updating user intent: {e}")
|
|
|
|
| 240 |
# --- Intent Management ---
|
| 241 |
async def get_or_create_user_intent(session_id: str, wa_id: str) -> dict:
|
| 242 |
"""
|
| 243 |
+
Fetch or create the intent record for this session.
|
|
|
|
| 244 |
"""
|
| 245 |
+
resp = supabase \
|
| 246 |
+
.table("user_intents") \
|
| 247 |
+
.select("*") \
|
| 248 |
+
.eq("session_id", session_id) \
|
| 249 |
+
.single() \
|
| 250 |
+
.execute()
|
| 251 |
+
if resp.data:
|
| 252 |
+
return resp.data
|
| 253 |
+
new_intent = {
|
| 254 |
+
"session_id": session_id,
|
| 255 |
+
"wa_id": wa_id,
|
| 256 |
+
"location_preference": None,
|
| 257 |
+
"budget": None,
|
| 258 |
+
"size_preference_sqm": None,
|
| 259 |
+
"must_have": None,
|
| 260 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 261 |
+
"updated_at": datetime.utcnow().isoformat()
|
| 262 |
+
}
|
| 263 |
+
insert = supabase.table("user_intents").insert(new_intent).execute()
|
| 264 |
+
return insert.data[0] if insert.data else new_intent
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
async def update_user_intent(session_id: str, updates: dict):
|
| 267 |
"""
|
| 268 |
+
Patch the intent record with any changed fields.
|
| 269 |
"""
|
| 270 |
+
updates_with_ts = { **updates, "updated_at": datetime.utcnow().isoformat() }
|
| 271 |
+
supabase \
|
| 272 |
+
.table("user_intents") \
|
| 273 |
+
.update(updates_with_ts) \
|
| 274 |
+
.eq("session_id", session_id) \
|
| 275 |
+
.execute()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
CHANGED
|
@@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse
|
|
| 3 |
|
| 4 |
# Import our modular components
|
| 5 |
from config import supabase
|
| 6 |
-
from database import get_or_create_user, update_user_activity, get_or_create_active_session, get_user_persona
|
| 7 |
from whatsapp import send_whatsapp_message
|
| 8 |
from ai_chat import process_message
|
| 9 |
from api_routes import router
|
|
@@ -49,6 +49,9 @@ async def receive_message(req: Request):
|
|
| 49 |
# Get user persona
|
| 50 |
persona = await get_user_persona(wa_id)
|
| 51 |
|
|
|
|
|
|
|
|
|
|
| 52 |
# Process with AI including session memory
|
| 53 |
ai_response = await process_message(
|
| 54 |
user_message=user_message,
|
|
@@ -56,7 +59,8 @@ async def receive_message(req: Request):
|
|
| 56 |
session_id=session["id"],
|
| 57 |
wa_id=wa_id,
|
| 58 |
wamid=wamid,
|
| 59 |
-
persona=persona
|
|
|
|
| 60 |
)
|
| 61 |
|
| 62 |
# Send response back to WhatsApp
|
|
|
|
| 3 |
|
| 4 |
# Import our modular components
|
| 5 |
from config import supabase
|
| 6 |
+
from database import get_or_create_user, update_user_activity, get_or_create_active_session, get_user_persona, get_or_create_user_intent
|
| 7 |
from whatsapp import send_whatsapp_message
|
| 8 |
from ai_chat import process_message
|
| 9 |
from api_routes import router
|
|
|
|
| 49 |
# Get user persona
|
| 50 |
persona = await get_user_persona(wa_id)
|
| 51 |
|
| 52 |
+
# Get or create user intent
|
| 53 |
+
intent = await get_or_create_user_intent(session["id"], wa_id)
|
| 54 |
+
|
| 55 |
# Process with AI including session memory
|
| 56 |
ai_response = await process_message(
|
| 57 |
user_message=user_message,
|
|
|
|
| 59 |
session_id=session["id"],
|
| 60 |
wa_id=wa_id,
|
| 61 |
wamid=wamid,
|
| 62 |
+
persona=persona,
|
| 63 |
+
intent=intent
|
| 64 |
)
|
| 65 |
|
| 66 |
# Send response back to WhatsApp
|