zenaight commited on
Commit
bcd0eb8
·
1 Parent(s): a758b0c

user personas

Browse files
Files changed (4) hide show
  1. ai_chat.py +112 -7
  2. api_routes.py +34 -1
  3. persona_manager.py +284 -0
  4. supabase_setup.sql +27 -1
ai_chat.py CHANGED
@@ -3,14 +3,25 @@ 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
 
 
 
 
 
 
 
 
6
 
7
  def chat_with_session_memory(state):
8
- """Chat function with session-based memory"""
9
  user_message = state["user_message"]
10
  user_info = state.get("user_info", {})
11
  session_id = state.get("session_id")
12
  wa_id = state.get("wa_id")
13
  wamid = state.get("wamid")
 
 
 
14
 
15
  # Get conversation history from database
16
  session_messages = []
@@ -18,15 +29,68 @@ def chat_with_session_memory(state):
18
  # This will be populated by the async wrapper
19
  session_messages = state.get("session_messages", [])
20
 
21
- # Add system message with user context
22
- system_message = "You are a helpful and concise assistant."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  # Build messages array with history
27
  messages = [{"role": "system", "content": system_message}]
28
 
29
- # Add conversation history (last 10 messages)
30
  for msg in session_messages[-30:]:
31
  messages.append({"role": msg["role"], "content": msg["content"]})
32
 
@@ -61,6 +125,15 @@ class ChatState(TypedDict):
61
  wa_id: str
62
  wamid: str
63
  session_messages: list
 
 
 
 
 
 
 
 
 
64
 
65
  # --- Build LangGraph ---
66
  graph = StateGraph(ChatState)
@@ -70,14 +143,37 @@ graph.add_edge("chat", END)
70
  chat_graph = graph.compile()
71
 
72
  async def process_message(user_message: str, user_info: dict = None, session_id: str = None, wa_id: str = None, wamid: str = None):
73
- """Process a message through the AI chat system with session memory"""
74
  if user_info is None:
75
  user_info = {}
76
 
 
 
 
77
  # Get session messages for context
78
  session_messages = []
79
  if session_id:
80
- session_messages = await get_session_messages(session_id, limit=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  # Process with AI
83
  result = await chat_graph.ainvoke({
@@ -86,9 +182,18 @@ async def process_message(user_message: str, user_info: dict = None, session_id:
86
  "session_id": session_id,
87
  "wa_id": wa_id,
88
  "wamid": wamid,
89
- "session_messages": session_messages
 
 
 
 
 
90
  })
91
 
 
 
 
 
92
  # Save messages to database
93
  if session_id and wa_id and wamid:
94
  await save_message(session_id, wa_id, wamid, "user", user_message)
 
3
  from typing import TypedDict
4
  from config import llm, OPENAI_API_KEY
5
  from database import get_session_messages, save_message
6
+ from persona_manager import (
7
+ get_or_create_persona,
8
+ should_ask_persona_question,
9
+ parse_user_response,
10
+ update_persona_field,
11
+ PERSONA_FIELDS,
12
+ get_persona_summary
13
+ )
14
 
15
  def chat_with_session_memory(state):
16
+ """Chat function with session-based memory and persona collection"""
17
  user_message = state["user_message"]
18
  user_info = state.get("user_info", {})
19
  session_id = state.get("session_id")
20
  wa_id = state.get("wa_id")
21
  wamid = state.get("wamid")
22
+ persona = state.get("persona", {})
23
+ is_persona_question = state.get("is_persona_question", False)
24
+ current_field = state.get("current_field")
25
 
26
  # Get conversation history from database
27
  session_messages = []
 
29
  # This will be populated by the async wrapper
30
  session_messages = state.get("session_messages", [])
31
 
32
+ # Check if we should handle persona collection
33
+ if is_persona_question and current_field:
34
+ # Handle persona question response
35
+ is_valid, parsed_value, response_message = state.get("parsed_response", (False, None, ""))
36
+
37
+ if is_valid:
38
+ # Update persona in database (handled by async wrapper)
39
+ return {
40
+ "response": response_message,
41
+ "user_message": user_message,
42
+ "ai_response": response_message,
43
+ "session_id": session_id,
44
+ "wa_id": wa_id,
45
+ "wamid": wamid,
46
+ "update_persona": True,
47
+ "persona_field": current_field,
48
+ "persona_value": parsed_value
49
+ }
50
+ else:
51
+ # Invalid response, ask for clarification or re-ask
52
+ return {
53
+ "response": response_message,
54
+ "user_message": user_message,
55
+ "ai_response": response_message,
56
+ "session_id": session_id,
57
+ "wa_id": wa_id,
58
+ "wamid": wamid,
59
+ "ask_persona_question": True,
60
+ "current_field": current_field
61
+ }
62
+
63
+ # Regular conversation - check if we should ask persona questions
64
+ should_ask, field_to_ask = state.get("persona_check", (False, None))
65
+
66
+ if should_ask and field_to_ask:
67
+ # Ask persona question
68
+ question = PERSONA_FIELDS[field_to_ask]["prompt"]
69
+ return {
70
+ "response": question,
71
+ "user_message": user_message,
72
+ "ai_response": question,
73
+ "session_id": session_id,
74
+ "wa_id": wa_id,
75
+ "wamid": wamid,
76
+ "ask_persona_question": True,
77
+ "current_field": field_to_ask
78
+ }
79
+
80
+ # Add system message with user context and persona
81
+ system_message = "You are a helpful industrial property agent assistant."
82
  if user_info.get("name") and user_info["name"] != "Unknown":
83
  system_message += f" The user's name is {user_info['name']}."
84
 
85
+ # Add persona context if available
86
+ persona_summary = get_persona_summary(persona)
87
+ if persona_summary != "New user - profile incomplete":
88
+ system_message += f" User profile: {persona_summary}. Use this information to provide relevant property advice."
89
+
90
  # Build messages array with history
91
  messages = [{"role": "system", "content": system_message}]
92
 
93
+ # Add conversation history (last 30 messages)
94
  for msg in session_messages[-30:]:
95
  messages.append({"role": msg["role"], "content": msg["content"]})
96
 
 
125
  wa_id: str
126
  wamid: str
127
  session_messages: list
128
+ persona: dict
129
+ is_persona_question: bool
130
+ current_field: str
131
+ parsed_response: tuple
132
+ persona_check: tuple
133
+ update_persona: bool
134
+ persona_field: str
135
+ persona_value: any
136
+ ask_persona_question: bool
137
 
138
  # --- Build LangGraph ---
139
  graph = StateGraph(ChatState)
 
143
  chat_graph = graph.compile()
144
 
145
  async def process_message(user_message: str, user_info: dict = None, session_id: str = None, wa_id: str = None, wamid: str = None):
146
+ """Process a message through the AI chat system with session memory and persona collection"""
147
  if user_info is None:
148
  user_info = {}
149
 
150
+ # Get user persona
151
+ persona = await get_or_create_persona(wa_id) if wa_id else {}
152
+
153
  # Get session messages for context
154
  session_messages = []
155
  if session_id:
156
+ session_messages = await get_session_messages(session_id, limit=30)
157
+
158
+ # Check if we should ask persona questions
159
+ conversation_context = " ".join([msg["content"] for msg in session_messages[-5:]]) + " " + user_message
160
+ should_ask, field_to_ask = await should_ask_persona_question(persona, conversation_context)
161
+
162
+ # Check if this is a response to a persona question
163
+ is_persona_question = False
164
+ current_field = None
165
+ parsed_response = (False, None, "")
166
+
167
+ # Check if the last AI message was a persona question
168
+ if session_messages and session_messages[-1]["role"] == "assistant":
169
+ last_ai_message = session_messages[-1]["content"]
170
+ for field, field_info in PERSONA_FIELDS.items():
171
+ if field_info["prompt"] in last_ai_message:
172
+ is_persona_question = True
173
+ current_field = field
174
+ # Parse user response
175
+ parsed_response = await parse_user_response(user_message, field)
176
+ break
177
 
178
  # Process with AI
179
  result = await chat_graph.ainvoke({
 
182
  "session_id": session_id,
183
  "wa_id": wa_id,
184
  "wamid": wamid,
185
+ "session_messages": session_messages,
186
+ "persona": persona,
187
+ "is_persona_question": is_persona_question,
188
+ "current_field": current_field,
189
+ "parsed_response": parsed_response,
190
+ "persona_check": (should_ask, field_to_ask)
191
  })
192
 
193
+ # Handle persona updates
194
+ if result.get("update_persona") and wa_id:
195
+ await update_persona_field(wa_id, result["persona_field"], result["persona_value"])
196
+
197
  # Save messages to database
198
  if session_id and wa_id and wamid:
199
  await save_message(session_id, wa_id, wamid, "user", user_message)
api_routes.py CHANGED
@@ -7,6 +7,7 @@ from datetime import datetime
7
  from config import VERIFY_TOKEN, supabase, OPENAI_API_KEY, WHATSAPP_API_TOKEN
8
  from database import get_user, list_users, update_user_name
9
  from ai_chat import process_message
 
10
 
11
  router = APIRouter()
12
 
@@ -84,4 +85,36 @@ async def update_user_endpoint(wa_id: str, name: str):
84
  if user:
85
  return user
86
  else:
87
- raise HTTPException(status_code=404, detail="User not found")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  from config import VERIFY_TOKEN, supabase, OPENAI_API_KEY, WHATSAPP_API_TOKEN
8
  from database import get_user, list_users, update_user_name
9
  from ai_chat import process_message
10
+ from persona_manager import get_user_persona, get_persona_summary, update_persona_field
11
 
12
  router = APIRouter()
13
 
 
85
  if user:
86
  return user
87
  else:
88
+ raise HTTPException(status_code=404, detail="User not found")
89
+
90
+ # --- Persona Management Endpoints ---
91
+ @router.get("/personas/{wa_id}")
92
+ async def get_persona_endpoint(wa_id: str):
93
+ """Get user persona by WhatsApp ID"""
94
+ if not supabase:
95
+ raise HTTPException(status_code=503, detail="Database not configured")
96
+
97
+ persona = await get_user_persona(wa_id)
98
+ if persona:
99
+ return {
100
+ **persona,
101
+ "summary": get_persona_summary(persona)
102
+ }
103
+ else:
104
+ raise HTTPException(status_code=404, detail="Persona not found")
105
+
106
+ @router.put("/personas/{wa_id}")
107
+ async def update_persona_endpoint(wa_id: str, field: str, value: str):
108
+ """Update a specific persona field"""
109
+ if not supabase:
110
+ raise HTTPException(status_code=503, detail="Database not configured")
111
+
112
+ success = await update_persona_field(wa_id, field, value)
113
+ if success:
114
+ persona = await get_user_persona(wa_id)
115
+ return {
116
+ **persona,
117
+ "summary": get_persona_summary(persona)
118
+ }
119
+ else:
120
+ raise HTTPException(status_code=404, detail="Persona not found")
persona_manager.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Dict, List, Optional, Tuple
3
+ from config import supabase, llm, OPENAI_API_KEY
4
+
5
+ # Persona field definitions and their collection prompts
6
+ PERSONA_FIELDS = {
7
+ "intent": {
8
+ "prompt": "Are you looking to buy a place, or maybe lease for now?",
9
+ "clarification": "Great question. Buying gives you long-term control, but leasing gives flexibility. Happy to guide you either way.",
10
+ "skip_response": "No problem at all. We can always revisit that later."
11
+ },
12
+ "location_preference": {
13
+ "prompt": "Any specific areas you'd prefer to be based in?",
14
+ "clarification": "I can help you find properties in any area. Just let me know what locations work best for you.",
15
+ "skip_response": "No problem at all. We can always revisit that later."
16
+ },
17
+ "size_preference_sqm": {
18
+ "prompt": "Do you have a rough size in mind? For example, 1500 or 3000 sqm?",
19
+ "clarification": "Size can vary a lot - from small workshops to large warehouses. What kind of space do you need?",
20
+ "skip_response": "No problem at all. We can always revisit that later."
21
+ },
22
+ "budget": {
23
+ "prompt": "Is there a budget you'd like me to work within?",
24
+ "clarification": "Budget helps me find the right properties for you. We can discuss options at any price point.",
25
+ "skip_response": "No problem at all. We can always revisit that later."
26
+ },
27
+ "must_have": {
28
+ "prompt": "Anything important that's a must-have? Like truck access or yard space?",
29
+ "clarification": "Must-haves are features you really need, like loading docks, high ceilings, or specific zoning.",
30
+ "skip_response": "No problem at all. We can always revisit that later."
31
+ }
32
+ }
33
+
34
+ async def get_user_persona(wa_id: str) -> Optional[Dict]:
35
+ """Get user persona from database"""
36
+ if not supabase:
37
+ return None
38
+
39
+ try:
40
+ response = supabase.table("user_personas").select("*").eq("wa_id", wa_id).execute()
41
+ return response.data[0] if response.data else None
42
+ except Exception as e:
43
+ print(f"Error getting user persona: {e}")
44
+ return None
45
+
46
+ async def create_user_persona(wa_id: str) -> Dict:
47
+ """Create a new user persona"""
48
+ if not supabase:
49
+ return {"wa_id": wa_id, "language": "English", "tone": "neutral"}
50
+
51
+ try:
52
+ new_persona = {
53
+ "wa_id": wa_id,
54
+ "language": "English",
55
+ "tone": "neutral",
56
+ "created_at": datetime.utcnow().isoformat(),
57
+ "updated_at": datetime.utcnow().isoformat()
58
+ }
59
+ response = supabase.table("user_personas").insert(new_persona).execute()
60
+ return response.data[0] if response.data else new_persona
61
+ except Exception as e:
62
+ print(f"Error creating user persona: {e}")
63
+ return {"wa_id": wa_id, "language": "English", "tone": "neutral"}
64
+
65
+ async def update_persona_field(wa_id: str, field: str, value) -> bool:
66
+ """Update a specific field in user persona"""
67
+ if not supabase:
68
+ return False
69
+
70
+ try:
71
+ update_data = {
72
+ field: value,
73
+ "updated_at": datetime.utcnow().isoformat()
74
+ }
75
+ supabase.table("user_personas").update(update_data).eq("wa_id", wa_id).execute()
76
+ return True
77
+ except Exception as e:
78
+ print(f"Error updating persona field {field}: {e}")
79
+ return False
80
+
81
+ async def get_or_create_persona(wa_id: str) -> Dict:
82
+ """Get existing persona or create new one"""
83
+ persona = await get_user_persona(wa_id)
84
+ if persona:
85
+ return persona
86
+ else:
87
+ return await create_user_persona(wa_id)
88
+
89
+ def get_missing_fields(persona: Dict) -> List[str]:
90
+ """Get list of missing persona fields"""
91
+ missing = []
92
+ for field in PERSONA_FIELDS.keys():
93
+ if not persona.get(field):
94
+ missing.append(field)
95
+ return missing
96
+
97
+ def get_next_field_to_ask(persona: Dict) -> Optional[str]:
98
+ """Get the next field to ask about"""
99
+ missing = get_missing_fields(persona)
100
+ return missing[0] if missing else None
101
+
102
+ async def parse_user_response(user_message: str, field: str) -> Tuple[bool, any, str]:
103
+ """
104
+ Parse user response for a specific field
105
+ Returns: (is_valid, parsed_value, response_message)
106
+ """
107
+ if not OPENAI_API_KEY:
108
+ return False, None, "Sorry, I can't process that right now."
109
+
110
+ try:
111
+ # Create a structured prompt for parsing
112
+ system_prompt = f"""You are a property agent assistant. Parse the user's response for the field '{field}'.
113
+
114
+ Field: {field}
115
+ Field description: {PERSONA_FIELDS[field]['prompt']}
116
+
117
+ Parse the user's response and return ONLY a JSON object with these fields:
118
+ - "is_valid": boolean (true if user provided a valid answer)
119
+ - "value": the parsed value (null if invalid/not provided)
120
+ - "response": string message to send back to user
121
+ - "is_clarification": boolean (true if user asked for clarification)
122
+ - "is_skip": boolean (true if user wants to skip this question)
123
+
124
+ Rules:
125
+ - For intent: accept "buy", "lease", "rent", "purchase", "own"
126
+ - For budget: extract numeric values (e.g., "around 500k" -> 500000)
127
+ - For size: extract square meters (e.g., "1500 sqm" -> 1500)
128
+ - For location: extract location names
129
+ - For must_have: extract features as array (e.g., ["truck access", "yard space"])
130
+ - If user asks for clarification, set is_clarification=true
131
+ - If user says "not sure", "skip", "later", set is_skip=true
132
+ """
133
+
134
+ messages = [
135
+ {"role": "system", "content": system_prompt},
136
+ {"role": "user", "content": user_message}
137
+ ]
138
+
139
+ response = llm.invoke(messages)
140
+
141
+ # Try to parse the JSON response
142
+ import json
143
+ try:
144
+ result = json.loads(response.content)
145
+ return (
146
+ result.get("is_valid", False),
147
+ result.get("value"),
148
+ result.get("response", "I understand. Let me know if you need anything else.")
149
+ )
150
+ except json.JSONDecodeError:
151
+ # Fallback parsing
152
+ return await fallback_parse_response(user_message, field)
153
+
154
+ except Exception as e:
155
+ print(f"Error parsing user response: {e}")
156
+ return False, None, "I didn't quite catch that. Could you rephrase?"
157
+
158
+ async def fallback_parse_response(user_message: str, field: str) -> Tuple[bool, any, str]:
159
+ """Fallback parsing when LLM parsing fails"""
160
+ user_message_lower = user_message.lower()
161
+
162
+ # Check for clarification requests
163
+ clarification_words = ["what", "how", "explain", "difference", "mean", "clarify"]
164
+ if any(word in user_message_lower for word in clarification_words):
165
+ return False, None, PERSONA_FIELDS[field]["clarification"]
166
+
167
+ # Check for skip requests
168
+ skip_words = ["not sure", "skip", "later", "don't know", "maybe later"]
169
+ if any(word in user_message_lower for word in skip_words):
170
+ return True, None, PERSONA_FIELDS[field]["skip_response"]
171
+
172
+ # Basic field-specific parsing
173
+ if field == "intent":
174
+ if any(word in user_message_lower for word in ["buy", "purchase", "own"]):
175
+ return True, "buy", "Got it, you're looking to buy. That's great!"
176
+ elif any(word in user_message_lower for word in ["lease", "rent"]):
177
+ return True, "lease", "Perfect, leasing gives you flexibility."
178
+
179
+ elif field == "budget":
180
+ import re
181
+ numbers = re.findall(r'\d+', user_message)
182
+ if numbers:
183
+ # Assume the largest number is the budget
184
+ budget = max(int(n) for n in numbers)
185
+ if "k" in user_message_lower or "thousand" in user_message_lower:
186
+ budget *= 1000
187
+ elif "m" in user_message_lower or "million" in user_message_lower:
188
+ budget *= 1000000
189
+ return True, budget, f"Thanks! I'll look for properties around ${budget:,}."
190
+
191
+ elif field == "size_preference_sqm":
192
+ import re
193
+ numbers = re.findall(r'\d+', user_message)
194
+ if numbers:
195
+ size = max(int(n) for n in numbers)
196
+ return True, size, f"Perfect! {size} sqm should give you good options."
197
+
198
+ elif field == "location_preference":
199
+ # Extract location names (basic approach)
200
+ locations = ["downtown", "suburb", "industrial", "warehouse district"]
201
+ found_location = None
202
+ for loc in locations:
203
+ if loc in user_message_lower:
204
+ found_location = loc
205
+ break
206
+
207
+ if found_location:
208
+ return True, found_location, f"Great! {found_location.title()} has good options."
209
+ else:
210
+ # Assume the whole message is a location
211
+ return True, user_message.strip(), f"Got it! I'll look in {user_message.strip()}."
212
+
213
+ elif field == "must_have":
214
+ features = []
215
+ feature_keywords = {
216
+ "truck": ["truck", "loading", "dock"],
217
+ "yard": ["yard", "space", "outdoor"],
218
+ "office": ["office", "admin"],
219
+ "parking": ["parking", "car"],
220
+ "high_ceiling": ["ceiling", "height", "tall"]
221
+ }
222
+
223
+ for feature, keywords in feature_keywords.items():
224
+ if any(keyword in user_message_lower for keyword in keywords):
225
+ features.append(feature)
226
+
227
+ if features:
228
+ return True, features, f"Perfect! I'll make sure to find places with {', '.join(features)}."
229
+
230
+ return False, None, "I didn't quite understand. Could you try again?"
231
+
232
+ async def should_ask_persona_question(persona: Dict, conversation_context: str = "") -> Tuple[bool, Optional[str]]:
233
+ """
234
+ Determine if we should ask a persona question
235
+ Returns: (should_ask, field_to_ask)
236
+ """
237
+ # Check if persona is complete
238
+ missing_fields = get_missing_fields(persona)
239
+
240
+ if not missing_fields:
241
+ return False, None
242
+
243
+ # Check if user is asking about properties (indicates they want to search)
244
+ search_indicators = [
245
+ "property", "warehouse", "industrial", "space", "building",
246
+ "available", "looking for", "need", "find", "search"
247
+ ]
248
+
249
+ conversation_lower = conversation_context.lower()
250
+ is_searching = any(indicator in conversation_lower for indicator in search_indicators)
251
+
252
+ # If user is actively searching, ask persona questions
253
+ if is_searching:
254
+ return True, missing_fields[0]
255
+
256
+ # If this is a new user (no fields filled), ask the first question
257
+ if len(missing_fields) == len(PERSONA_FIELDS):
258
+ return True, missing_fields[0]
259
+
260
+ return False, None
261
+
262
+ def get_persona_summary(persona: Dict) -> str:
263
+ """Get a human-readable summary of the user's persona"""
264
+ summary_parts = []
265
+
266
+ if persona.get("intent"):
267
+ summary_parts.append(f"Looking to {persona['intent']}")
268
+
269
+ if persona.get("location_preference"):
270
+ summary_parts.append(f"in {persona['location_preference']}")
271
+
272
+ if persona.get("size_preference_sqm"):
273
+ summary_parts.append(f"around {persona['size_preference_sqm']} sqm")
274
+
275
+ if persona.get("budget"):
276
+ summary_parts.append(f"budget ~${persona['budget']:,}")
277
+
278
+ if persona.get("must_have"):
279
+ summary_parts.append(f"must have: {', '.join(persona['must_have'])}")
280
+
281
+ if summary_parts:
282
+ return " | ".join(summary_parts)
283
+ else:
284
+ return "New user - profile incomplete"
supabase_setup.sql CHANGED
@@ -26,6 +26,20 @@ CREATE TABLE IF NOT EXISTS messages (
26
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
27
  );
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  -- Create indexes for better query performance
30
  CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at DESC);
31
  CREATE INDEX IF NOT EXISTS idx_users_updated_at ON users(updated_at DESC);
@@ -36,11 +50,14 @@ CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
36
  CREATE INDEX IF NOT EXISTS idx_messages_wa_id ON messages(wa_id);
37
  CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
38
  CREATE INDEX IF NOT EXISTS idx_messages_wamid ON messages(wamid);
 
 
39
 
40
  -- Enable Row Level Security (RLS) - optional but recommended
41
  ALTER TABLE users ENABLE ROW LEVEL SECURITY;
42
  ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
43
  ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
 
44
 
45
  -- Create policies to allow all operations (you can restrict this based on your needs)
46
  CREATE POLICY "Allow all operations on users" ON users
@@ -52,6 +69,9 @@ CREATE POLICY "Allow all operations on chat_sessions" ON chat_sessions
52
  CREATE POLICY "Allow all operations on messages" ON messages
53
  FOR ALL USING (true);
54
 
 
 
 
55
  -- Optional: Create a function to automatically update the updated_at timestamp
56
  CREATE OR REPLACE FUNCTION update_updated_at_column()
57
  RETURNS TRIGGER AS $$
@@ -79,4 +99,10 @@ $$ language 'plpgsql';
79
  CREATE TRIGGER update_chat_sessions_last_activity
80
  BEFORE UPDATE ON chat_sessions
81
  FOR EACH ROW
82
- EXECUTE FUNCTION update_last_activity_column();
 
 
 
 
 
 
 
26
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
27
  );
28
 
29
+ -- Create user_personas table
30
+ CREATE TABLE IF NOT EXISTS user_personas (
31
+ wa_id TEXT PRIMARY KEY REFERENCES users(wa_id) ON DELETE CASCADE,
32
+ language TEXT DEFAULT 'English',
33
+ tone TEXT DEFAULT 'neutral',
34
+ intent TEXT,
35
+ budget NUMERIC,
36
+ size_preference_sqm INTEGER,
37
+ location_preference TEXT,
38
+ must_have TEXT[],
39
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
40
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
41
+ );
42
+
43
  -- Create indexes for better query performance
44
  CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at DESC);
45
  CREATE INDEX IF NOT EXISTS idx_users_updated_at ON users(updated_at DESC);
 
50
  CREATE INDEX IF NOT EXISTS idx_messages_wa_id ON messages(wa_id);
51
  CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
52
  CREATE INDEX IF NOT EXISTS idx_messages_wamid ON messages(wamid);
53
+ CREATE INDEX IF NOT EXISTS idx_user_personas_intent ON user_personas(intent);
54
+ CREATE INDEX IF NOT EXISTS idx_user_personas_location ON user_personas(location_preference);
55
 
56
  -- Enable Row Level Security (RLS) - optional but recommended
57
  ALTER TABLE users ENABLE ROW LEVEL SECURITY;
58
  ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
59
  ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
60
+ ALTER TABLE user_personas ENABLE ROW LEVEL SECURITY;
61
 
62
  -- Create policies to allow all operations (you can restrict this based on your needs)
63
  CREATE POLICY "Allow all operations on users" ON users
 
69
  CREATE POLICY "Allow all operations on messages" ON messages
70
  FOR ALL USING (true);
71
 
72
+ CREATE POLICY "Allow all operations on user_personas" ON user_personas
73
+ FOR ALL USING (true);
74
+
75
  -- Optional: Create a function to automatically update the updated_at timestamp
76
  CREATE OR REPLACE FUNCTION update_updated_at_column()
77
  RETURNS TRIGGER AS $$
 
99
  CREATE TRIGGER update_chat_sessions_last_activity
100
  BEFORE UPDATE ON chat_sessions
101
  FOR EACH ROW
102
+ EXECUTE FUNCTION update_last_activity_column();
103
+
104
+ -- Create trigger to automatically update updated_at for user_personas
105
+ CREATE TRIGGER update_user_personas_updated_at
106
+ BEFORE UPDATE ON user_personas
107
+ FOR EACH ROW
108
+ EXECUTE FUNCTION update_updated_at_column();