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.

Files changed (3) hide show
  1. ai_chat.py +67 -4
  2. database.py +28 -38
  3. 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", "chat")
 
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
- Fetches the intent row for this session.
244
- If none exists, inserts a new empty intent record and returns it.
245
  """
246
- if not supabase or session_id == "local_session":
247
- return {"session_id": session_id, "wa_id": wa_id, "created_at": datetime.utcnow().isoformat(), "updated_at": datetime.utcnow().isoformat()}
248
-
249
- try:
250
- resp = supabase\
251
- .table("user_intents")\
252
- .select("*")\
253
- .eq("session_id", session_id)\
254
- .single()\
255
- .execute()
256
- if resp.data:
257
- return resp.data
258
- new_intent = {
259
- "session_id": session_id,
260
- "wa_id": wa_id,
261
- "created_at": datetime.utcnow().isoformat(),
262
- "updated_at": datetime.utcnow().isoformat()
263
- }
264
- insert = supabase.table("user_intents").insert(new_intent).execute()
265
- return insert.data[0] if insert.data else new_intent
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
- Patches only the fields in the intent row that have changed.
273
  """
274
- if not supabase or session_id == "local_session":
275
- return
276
-
277
- try:
278
- updates_with_ts = {**updates, "updated_at": datetime.utcnow().isoformat()}
279
- supabase\
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