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

message and session management

Browse files
Files changed (4) hide show
  1. ai_chat.py +56 -12
  2. database.py +102 -1
  3. main.py +18 -4
  4. supabase_setup.sql +53 -4
ai_chat.py CHANGED
@@ -2,52 +2,96 @@ 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
 
6
- def chat_without_memory(state):
7
- """Chat function without memory - stateless responses"""
8
  user_message = state["user_message"]
9
  user_info = state.get("user_info", {})
 
 
 
 
 
 
 
 
 
10
 
11
  # Add system message with user context
12
  system_message = "You are a helpful and concise assistant."
13
  if user_info.get("name") and user_info["name"] != "Unknown":
14
  system_message += f" The user's name is {user_info['name']}."
15
 
16
- messages = [
17
- {"role": "system", "content": system_message},
18
- {"role": "user", "content": user_message}
19
- ]
 
 
 
 
 
20
 
21
  try:
22
  if not OPENAI_API_KEY:
23
  return {"response": "Sorry, AI chat is not available. Please check your OpenAI API key configuration."}
24
 
25
  response = llm.invoke(messages)
26
- return {"response": response.content}
 
 
 
 
 
 
 
 
 
 
27
  except Exception as e:
28
- print(f"Error in chat_without_memory: {e}")
29
  return {"response": "Sorry, something went wrong: " + str(e)}
30
 
31
  class ChatState(TypedDict):
32
  user_message: str
33
  response: str
34
  user_info: dict
 
 
 
 
35
 
36
  # --- Build LangGraph ---
37
  graph = StateGraph(ChatState)
38
- graph.add_node("chat", RunnableLambda(chat_without_memory))
39
  graph.set_entry_point("chat")
40
  graph.add_edge("chat", END)
41
  chat_graph = graph.compile()
42
 
43
- async def process_message(user_message: str, user_info: dict = None):
44
- """Process a message through the AI chat system"""
45
  if user_info is None:
46
  user_info = {}
47
 
 
 
 
 
 
 
48
  result = await chat_graph.ainvoke({
49
  "user_message": user_message,
50
- "user_info": user_info
 
 
 
 
51
  })
52
 
 
 
 
 
 
53
  return result["response"]
 
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
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 = []
17
+ if session_id:
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
+
33
+ # Add current user message
34
+ messages.append({"role": "user", "content": user_message})
35
 
36
  try:
37
  if not OPENAI_API_KEY:
38
  return {"response": "Sorry, AI chat is not available. Please check your OpenAI API key configuration."}
39
 
40
  response = llm.invoke(messages)
41
+ ai_response = response.content
42
+
43
+ # Save messages to database (this will be handled by the async wrapper)
44
+ return {
45
+ "response": ai_response,
46
+ "user_message": user_message,
47
+ "ai_response": ai_response,
48
+ "session_id": session_id,
49
+ "wa_id": wa_id,
50
+ "wamid": wamid
51
+ }
52
  except Exception as e:
53
+ print(f"Error in chat_with_session_memory: {e}")
54
  return {"response": "Sorry, something went wrong: " + str(e)}
55
 
56
  class ChatState(TypedDict):
57
  user_message: str
58
  response: str
59
  user_info: dict
60
+ session_id: str
61
+ wa_id: str
62
+ wamid: str
63
+ session_messages: list
64
 
65
  # --- Build LangGraph ---
66
  graph = StateGraph(ChatState)
67
+ graph.add_node("chat", RunnableLambda(chat_with_session_memory))
68
  graph.set_entry_point("chat")
69
  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({
84
  "user_message": user_message,
85
+ "user_info": user_info,
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)
95
+ await save_message(session_id, wa_id, f"{wamid}_ai", "assistant", result["response"])
96
+
97
  return result["response"]
database.py CHANGED
@@ -1,4 +1,4 @@
1
- from datetime import datetime
2
  from config import supabase
3
 
4
  async def get_or_create_user(wa_id: str, name: str = None) -> dict:
@@ -42,6 +42,107 @@ async def update_user_activity(wa_id: str):
42
  except Exception as e:
43
  print(f"Error updating user activity: {e}")
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  async def get_user(wa_id: str):
46
  """Get user by WhatsApp ID"""
47
  if not supabase:
 
1
+ from datetime import datetime, timedelta
2
  from config import supabase
3
 
4
  async def get_or_create_user(wa_id: str, name: str = None) -> dict:
 
42
  except Exception as e:
43
  print(f"Error updating user activity: {e}")
44
 
45
+ # --- Session Management ---
46
+ async def get_active_session(wa_id: str) -> dict:
47
+ """Get the active session for a user (not ended and within 3 hours)"""
48
+ if not supabase:
49
+ return None
50
+
51
+ try:
52
+ # Calculate 3 hours ago
53
+ three_hours_ago = datetime.utcnow() - timedelta(hours=3)
54
+
55
+ response = supabase.table("chat_sessions").select("*").eq("wa_id", wa_id).eq("ended", False).gte("last_activity", three_hours_ago.isoformat()).order("last_activity", desc=True).limit(1).execute()
56
+
57
+ return response.data[0] if response.data else None
58
+ except Exception as e:
59
+ print(f"Error getting active session: {e}")
60
+ return None
61
+
62
+ async def create_new_session(wa_id: str) -> dict:
63
+ """Create a new chat session for a user"""
64
+ if not supabase:
65
+ return {"id": "local_session", "wa_id": wa_id, "started_at": datetime.utcnow().isoformat(), "last_activity": datetime.utcnow().isoformat(), "ended": False}
66
+
67
+ try:
68
+ new_session = {
69
+ "wa_id": wa_id,
70
+ "started_at": datetime.utcnow().isoformat(),
71
+ "last_activity": datetime.utcnow().isoformat(),
72
+ "ended": False
73
+ }
74
+ response = supabase.table("chat_sessions").insert(new_session).execute()
75
+ return response.data[0] if response.data else new_session
76
+ except Exception as e:
77
+ print(f"Error creating new session: {e}")
78
+ return {"id": "local_session", "wa_id": wa_id, "started_at": datetime.utcnow().isoformat(), "last_activity": datetime.utcnow().isoformat(), "ended": False}
79
+
80
+ async def update_session_activity(session_id: str):
81
+ """Update session's last activity timestamp"""
82
+ if not supabase or session_id == "local_session":
83
+ return
84
+
85
+ try:
86
+ supabase.table("chat_sessions").update({"last_activity": datetime.utcnow().isoformat()}).eq("id", session_id).execute()
87
+ except Exception as e:
88
+ print(f"Error updating session activity: {e}")
89
+
90
+ async def end_session(session_id: str):
91
+ """Mark a session as ended"""
92
+ if not supabase or session_id == "local_session":
93
+ return
94
+
95
+ try:
96
+ supabase.table("chat_sessions").update({"ended": True}).eq("id", session_id).execute()
97
+ except Exception as e:
98
+ print(f"Error ending session: {e}")
99
+
100
+ # --- Message Management ---
101
+ async def save_message(session_id: str, wa_id: str, wamid: str, role: str, content: str):
102
+ """Save a message to the database"""
103
+ if not supabase or session_id == "local_session":
104
+ return
105
+
106
+ try:
107
+ message = {
108
+ "session_id": session_id,
109
+ "wa_id": wa_id,
110
+ "wamid": wamid,
111
+ "role": role,
112
+ "content": content,
113
+ "created_at": datetime.utcnow().isoformat()
114
+ }
115
+ supabase.table("messages").insert(message).execute()
116
+ except Exception as e:
117
+ print(f"Error saving message: {e}")
118
+
119
+ async def get_session_messages(session_id: str, limit: int = 10) -> list:
120
+ """Get recent messages for a session (for memory context)"""
121
+ if not supabase or session_id == "local_session":
122
+ return []
123
+
124
+ try:
125
+ response = supabase.table("messages").select("*").eq("session_id", session_id).order("created_at", desc=True).limit(limit).execute()
126
+ # Reverse to get chronological order
127
+ messages = response.data[::-1] if response.data else []
128
+ return messages
129
+ except Exception as e:
130
+ print(f"Error getting session messages: {e}")
131
+ return []
132
+
133
+ # --- Session Management Helper ---
134
+ async def get_or_create_active_session(wa_id: str) -> dict:
135
+ """Get active session or create new one if needed"""
136
+ active_session = await get_active_session(wa_id)
137
+
138
+ if active_session:
139
+ # Update activity
140
+ await update_session_activity(active_session["id"])
141
+ return active_session
142
+ else:
143
+ # Create new session
144
+ return await create_new_session(wa_id)
145
+
146
  async def get_user(wa_id: str):
147
  """Get user by WhatsApp ID"""
148
  if not supabase:
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
7
  from whatsapp import send_whatsapp_message
8
  from ai_chat import process_message
9
  from api_routes import router
@@ -35,6 +35,7 @@ async def receive_message(req: Request):
35
 
36
  if msg.get("type") == "text":
37
  user_message = msg.get("text", {}).get("body", "")
 
38
 
39
  # Get or create user in database
40
  user_info = await get_or_create_user(wa_id, contact_name)
@@ -42,13 +43,26 @@ async def receive_message(req: Request):
42
  # Update user activity
43
  await update_user_activity(wa_id)
44
 
45
- # Process with AI including user context
46
- ai_response = await process_message(user_message, user_info)
 
 
 
 
 
 
 
 
 
47
 
48
  # Send response back to WhatsApp
49
  await send_whatsapp_message(wa_id, ai_response)
50
 
51
- return JSONResponse({"status": "replied", "user_id": wa_id})
 
 
 
 
52
 
53
  except Exception as e:
54
  print("Error processing message:", e)
 
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
7
  from whatsapp import send_whatsapp_message
8
  from ai_chat import process_message
9
  from api_routes import router
 
35
 
36
  if msg.get("type") == "text":
37
  user_message = msg.get("text", {}).get("body", "")
38
+ wamid = msg.get("id") # Meta message ID
39
 
40
  # Get or create user in database
41
  user_info = await get_or_create_user(wa_id, contact_name)
 
43
  # Update user activity
44
  await update_user_activity(wa_id)
45
 
46
+ # Get or create active session
47
+ session = await get_or_create_active_session(wa_id)
48
+
49
+ # Process with AI including session memory
50
+ ai_response = await process_message(
51
+ user_message=user_message,
52
+ user_info=user_info,
53
+ session_id=session["id"],
54
+ wa_id=wa_id,
55
+ wamid=wamid
56
+ )
57
 
58
  # Send response back to WhatsApp
59
  await send_whatsapp_message(wa_id, ai_response)
60
 
61
+ return JSONResponse({
62
+ "status": "replied",
63
+ "user_id": wa_id,
64
+ "session_id": session["id"]
65
+ })
66
 
67
  except Exception as e:
68
  print("Error processing message:", e)
supabase_setup.sql CHANGED
@@ -6,17 +6,52 @@ CREATE TABLE IF NOT EXISTS users (
6
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
7
  );
8
 
9
- -- Create index for better query performance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at DESC);
11
  CREATE INDEX IF NOT EXISTS idx_users_updated_at ON users(updated_at DESC);
 
 
 
 
 
 
 
12
 
13
  -- Enable Row Level Security (RLS) - optional but recommended
14
  ALTER TABLE users ENABLE ROW LEVEL SECURITY;
 
 
15
 
16
- -- Create policy to allow all operations (you can restrict this based on your needs)
17
  CREATE POLICY "Allow all operations on users" ON users
18
  FOR ALL USING (true);
19
 
 
 
 
 
 
 
20
  -- Optional: Create a function to automatically update the updated_at timestamp
21
  CREATE OR REPLACE FUNCTION update_updated_at_column()
22
  RETURNS TRIGGER AS $$
@@ -26,8 +61,22 @@ BEGIN
26
  END;
27
  $$ language 'plpgsql';
28
 
29
- -- Create trigger to automatically update updated_at
30
  CREATE TRIGGER update_users_updated_at
31
  BEFORE UPDATE ON users
32
  FOR EACH ROW
33
- EXECUTE FUNCTION update_updated_at_column();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
7
  );
8
 
9
+ -- Create chat_sessions table
10
+ CREATE TABLE IF NOT EXISTS chat_sessions (
11
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ wa_id TEXT NOT NULL REFERENCES users(wa_id) ON DELETE CASCADE,
13
+ started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
14
+ last_activity TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
15
+ ended BOOLEAN DEFAULT FALSE
16
+ );
17
+
18
+ -- Create messages table
19
+ CREATE TABLE IF NOT EXISTS messages (
20
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
21
+ session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
22
+ wa_id TEXT NOT NULL REFERENCES users(wa_id) ON DELETE CASCADE,
23
+ wamid TEXT NOT NULL, -- Meta message ID
24
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
25
+ content TEXT NOT NULL,
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);
32
+ CREATE INDEX IF NOT EXISTS idx_chat_sessions_wa_id ON chat_sessions(wa_id);
33
+ CREATE INDEX IF NOT EXISTS idx_chat_sessions_last_activity ON chat_sessions(last_activity DESC);
34
+ CREATE INDEX IF NOT EXISTS idx_chat_sessions_ended ON chat_sessions(ended);
35
+ 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
47
  FOR ALL USING (true);
48
 
49
+ CREATE POLICY "Allow all operations on chat_sessions" ON chat_sessions
50
+ FOR ALL USING (true);
51
+
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 $$
 
61
  END;
62
  $$ language 'plpgsql';
63
 
64
+ -- Create trigger to automatically update updated_at for users
65
  CREATE TRIGGER update_users_updated_at
66
  BEFORE UPDATE ON users
67
  FOR EACH ROW
68
+ EXECUTE FUNCTION update_updated_at_column();
69
+
70
+ -- Create trigger to automatically update last_activity for chat_sessions
71
+ CREATE OR REPLACE FUNCTION update_last_activity_column()
72
+ RETURNS TRIGGER AS $$
73
+ BEGIN
74
+ NEW.last_activity = NOW();
75
+ RETURN NEW;
76
+ END;
77
+ $$ language 'plpgsql';
78
+
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();