zenaight commited on
Commit
6beacc7
·
1 Parent(s): b7a94e7

persona simplified

Browse files
Files changed (2) hide show
  1. ai_chat.py +41 -55
  2. database.py +48 -1
ai_chat.py CHANGED
@@ -19,16 +19,13 @@ def chat_with_session_memory(state):
19
  session_messages = state.get("session_messages", [])
20
 
21
  # Add system message with user context
22
- system_message = "You are a helpful and concise property agent."
23
  if user_info.get("name") and user_info["name"] != "Unknown":
24
  system_message += f" The user's name is {user_info['name']}."
25
 
26
  p = state.get("persona", {})
27
  system_message += (
28
- f" The user prefers {p.get('language','[unspecified]')} and wants a {p.get('tone','neutral')} tone. "
29
- f"They are {p.get('intent','[unspecified]')}ing with a budget up to {p.get('budget','any')} per month, "
30
- f"prefer around {p.get('size_preference_sqm','any')} sqm in {p.get('location_preference','[anywhere]')}, "
31
- f"and must-haves are {', '.join(p.get('must_have',[])) or '[none]'}. "
32
  )
33
 
34
  # Build messages array with history
@@ -72,58 +69,47 @@ class ChatState(TypedDict):
72
  persona: dict
73
 
74
  async def extract_and_update_persona(state):
 
 
 
 
75
  persona = state.get("persona", {})
76
- # 1. Build a list of missing fields:
77
- missing = [f for f in
78
- ("language","tone","intent","budget",
79
- "size_preference_sqm","location_preference","must_have")
80
- if not persona.get(f)
81
- ]
82
- # 2. If any fields are missing:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  if missing:
84
- # Prompt your LLM to extract and normalize all missing fields from
85
- # state["user_message"] in one shot.
86
- # • Receive a dict of field→value pairs.
87
- extraction_prompt = f"""
88
- Extract the following information from this user message: {state["user_message"]}
89
-
90
- Required fields to extract:
91
- - language: The language the user prefers (e.g., "English", "Afrikaans")
92
- - tone: The communication style preference (e.g., "formal", "casual", "friendly")
93
- - intent: What the user wants to do (e.g., "buy", "rent", "sell", "invest")
94
- - budget: Monthly budget in currency (e.g., "2000 ZAR", "R3000")
95
- - size_preference_sqm: Property size preference in square meters (e.g., "80", "120")
96
- - location_preference: Preferred location or area (e.g., "Pretoria", "Johannesburg", "Cape Town")
97
- - must_have: Essential features or requirements (e.g., "parking", "balcony", "2 bedrooms")
98
-
99
- Missing fields: {', '.join(missing)}
100
-
101
- Return only a JSON object with the extracted values for the missing fields. If a field cannot be determined, use null.
102
- """
103
-
104
- try:
105
- response = await llm.ainvoke([{"role": "user", "content": extraction_prompt}])
106
- extracted_data = response.content
107
-
108
- import json
109
- try:
110
- extracted = json.loads(extracted_data)
111
- # • Update the database and in‐memory state:
112
- await update_user_persona(state["wa_id"], extracted)
113
- persona.update(extracted)
114
- state["persona"] = persona
115
- except json.JSONDecodeError:
116
- print(f"Failed to parse LLM response as JSON: {extracted_data}")
117
- except Exception as e:
118
- print(f"Error extracting persona data: {e}")
119
-
120
- # • If there are still missing fields after that:
121
- still_missing = [f for f in missing if f not in persona]
122
- if still_missing:
123
- # Ask just one follow-up for the first missing field
124
- state["response"] = f"What is your {still_missing[0].replace('_',' ')}?"
125
- return state
126
- # 3. No fields missing or all filled:
127
  return {"response": None}
128
 
129
  # --- Build LangGraph ---
 
19
  session_messages = state.get("session_messages", [])
20
 
21
  # Add system message with user context
22
+ system_message = f"Hello {user_info.get('name','there')}! You are a helpful and concise property agent."
23
  if user_info.get("name") and user_info["name"] != "Unknown":
24
  system_message += f" The user's name is {user_info['name']}."
25
 
26
  p = state.get("persona", {})
27
  system_message += (
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
 
69
  persona: dict
70
 
71
  async def extract_and_update_persona(state):
72
+ # a. Define which persona fields to track
73
+ persona_fields = ["language", "tone"]
74
+ user_message = state["user_message"]
75
+ wa_id = state["wa_id"]
76
  persona = state.get("persona", {})
77
+
78
+ # b. Build a one-shot extraction prompt
79
+ extraction_prompt = f"""
80
+ Extract and normalize the user's language and tone preferences from this message:
81
+ {user_message}
82
+
83
+ Normalize any shorthand or typos before deciding language and tone.
84
+ Return only a JSON object with keys "language" and "tone", and use null for unknown.
85
+ """
86
+
87
+ # c. Call the LLM
88
+ response = await llm.ainvoke([{"role":"user","content":extraction_prompt}])
89
+ import json
90
+ extracted = {}
91
+ try:
92
+ extracted = json.loads(response.content)
93
+ except:
94
+ print("Failed to parse persona JSON:", response.content)
95
+
96
+ # d. Update DB and in-memory state for any changed values
97
+ for field in persona_fields:
98
+ new_val = extracted.get(field)
99
+ old_val = persona.get(field)
100
+ if new_val is not None and new_val != old_val:
101
+ await update_user_persona(wa_id, {field: new_val})
102
+ persona[field] = new_val
103
+
104
+ state["persona"] = persona
105
+
106
+ # e. If any field still unset, ask a gentle follow-up
107
+ missing = [f for f in persona_fields if state["persona"].get(f) is None]
108
  if missing:
109
+ state["response"] = f"Hi there! What is your {missing[0]} preference?"
110
+ return state
111
+
112
+ # f. All persona fields present—proceed to chat
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  return {"response": None}
114
 
115
  # --- Build LangGraph ---
database.py CHANGED
@@ -220,4 +220,51 @@ async def update_user_persona(wa_id: str, updates: dict):
220
  .eq("wa_id", wa_id)\
221
  .execute()
222
  except Exception as e:
223
- print(f"Error updating user persona: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  .eq("wa_id", wa_id)\
221
  .execute()
222
  except Exception as e:
223
+ print(f"Error updating user persona: {e}")
224
+
225
+ # --- Intent Management ---
226
+ async def get_or_create_user_intent(session_id: str, wa_id: str) -> dict:
227
+ """
228
+ Fetches the intent row for this session.
229
+ If none exists, inserts a new empty intent record and returns it.
230
+ """
231
+ if not supabase or session_id == "local_session":
232
+ return {"session_id": session_id, "wa_id": wa_id, "created_at": datetime.utcnow().isoformat(), "updated_at": datetime.utcnow().isoformat()}
233
+
234
+ try:
235
+ resp = supabase\
236
+ .table("user_intents")\
237
+ .select("*")\
238
+ .eq("session_id", session_id)\
239
+ .single()\
240
+ .execute()
241
+ if resp.data:
242
+ return resp.data
243
+ new_intent = {
244
+ "session_id": session_id,
245
+ "wa_id": wa_id,
246
+ "created_at": datetime.utcnow().isoformat(),
247
+ "updated_at": datetime.utcnow().isoformat()
248
+ }
249
+ insert = supabase.table("user_intents").insert(new_intent).execute()
250
+ return insert.data[0] if insert.data else new_intent
251
+ except Exception as e:
252
+ print(f"Error in get_or_create_user_intent: {e}")
253
+ return {"session_id": session_id, "wa_id": wa_id, "created_at": datetime.utcnow().isoformat(), "updated_at": datetime.utcnow().isoformat()}
254
+
255
+ async def update_user_intent(session_id: str, updates: dict):
256
+ """
257
+ Patches only the fields in the intent row that have changed.
258
+ """
259
+ if not supabase or session_id == "local_session":
260
+ return
261
+
262
+ try:
263
+ updates_with_ts = {**updates, "updated_at": datetime.utcnow().isoformat()}
264
+ supabase\
265
+ .table("user_intents")\
266
+ .update(updates_with_ts)\
267
+ .eq("session_id", session_id)\
268
+ .execute()
269
+ except Exception as e:
270
+ print(f"Error updating user intent: {e}")