🔧 (agent.py): Import and use conversation_persistence_service to handle user conversations storage and retrieval
🔧 (conversation_persistence_service.py): Create a service to persist chat conversations to disk for server restarts
This commit is contained in:
parent
c87877bb80
commit
18b4059b56
7 changed files with 211 additions and 509 deletions
43
src/agent.py
43
src/agent.py
|
|
@ -2,15 +2,13 @@ from utils.logging_config import get_logger
|
|||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# User-scoped conversation state - keyed by user_id -> response_id -> conversation
|
||||
user_conversations = {} # user_id -> {response_id: {"messages": [...], "previous_response_id": parent_id, "created_at": timestamp, "last_activity": timestamp}}
|
||||
# Import persistent storage
|
||||
from services.conversation_persistence_service import conversation_persistence
|
||||
|
||||
|
||||
def get_user_conversations(user_id: str):
|
||||
"""Get all conversations for a user"""
|
||||
if user_id not in user_conversations:
|
||||
user_conversations[user_id] = {}
|
||||
return user_conversations[user_id]
|
||||
return conversation_persistence.get_user_conversations(user_id)
|
||||
|
||||
|
||||
def get_conversation_thread(user_id: str, previous_response_id: str = None):
|
||||
|
|
@ -44,8 +42,7 @@ def get_conversation_thread(user_id: str, previous_response_id: str = None):
|
|||
|
||||
def store_conversation_thread(user_id: str, response_id: str, conversation_state: dict):
|
||||
"""Store a conversation thread with its response_id"""
|
||||
conversations = get_user_conversations(user_id)
|
||||
conversations[response_id] = conversation_state
|
||||
conversation_persistence.store_conversation_thread(user_id, response_id, conversation_state)
|
||||
|
||||
|
||||
# Legacy function for backward compatibility
|
||||
|
|
@ -413,17 +410,11 @@ async def async_langflow_chat(
|
|||
conversation_state["last_activity"] = datetime.now()
|
||||
store_conversation_thread(user_id, response_id, conversation_state)
|
||||
|
||||
# Claim session ownership if this is a Google user
|
||||
# Claim session ownership for this user
|
||||
try:
|
||||
from services.session_ownership_service import session_ownership_service
|
||||
from services.user_binding_service import user_binding_service
|
||||
|
||||
# Check if this is a Google user (Google IDs are numeric, Langflow IDs are UUID)
|
||||
if user_id.isdigit() and user_binding_service.has_binding(user_id):
|
||||
langflow_user_id = user_binding_service.get_langflow_user_id(user_id)
|
||||
if langflow_user_id:
|
||||
session_ownership_service.claim_session(user_id, response_id, langflow_user_id)
|
||||
print(f"[DEBUG] Claimed session {response_id} for Google user {user_id}")
|
||||
session_ownership_service.claim_session(user_id, response_id)
|
||||
print(f"[DEBUG] Claimed session {response_id} for user {user_id}")
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Failed to claim session ownership: {e}")
|
||||
|
||||
|
|
@ -502,19 +493,13 @@ async def async_langflow_chat_stream(
|
|||
conversation_state["last_activity"] = datetime.now()
|
||||
store_conversation_thread(user_id, response_id, conversation_state)
|
||||
|
||||
# Claim session ownership if this is a Google user
|
||||
try:
|
||||
from services.session_ownership_service import session_ownership_service
|
||||
from services.user_binding_service import user_binding_service
|
||||
|
||||
# Check if this is a Google user (Google IDs are numeric, Langflow IDs are UUID)
|
||||
if user_id.isdigit() and user_binding_service.has_binding(user_id):
|
||||
langflow_user_id = user_binding_service.get_langflow_user_id(user_id)
|
||||
if langflow_user_id:
|
||||
session_ownership_service.claim_session(user_id, response_id, langflow_user_id)
|
||||
print(f"[DEBUG] Claimed session {response_id} for Google user {user_id} (streaming)")
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Failed to claim session ownership (streaming): {e}")
|
||||
# Claim session ownership for this user
|
||||
try:
|
||||
from services.session_ownership_service import session_ownership_service
|
||||
session_ownership_service.claim_session(user_id, response_id)
|
||||
print(f"[DEBUG] Claimed session {response_id} for user {user_id}")
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Failed to claim session ownership: {e}")
|
||||
|
||||
print(
|
||||
f"[DEBUG] Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ from connectors.sharepoint.oauth import SharePointOAuth
|
|||
from connectors.google_drive import GoogleDriveConnector
|
||||
from connectors.onedrive import OneDriveConnector
|
||||
from connectors.sharepoint import SharePointConnector
|
||||
from services.user_binding_service import user_binding_service
|
||||
|
||||
|
||||
class AuthService:
|
||||
|
|
@ -268,24 +267,10 @@ class AuthService:
|
|||
)
|
||||
|
||||
if jwt_token:
|
||||
# Get the user info to create a persistent Google Drive connection
|
||||
# Get the user info to create a persistent connector connection
|
||||
user_info = await self.session_manager.get_user_info_from_token(
|
||||
token_data["access_token"]
|
||||
)
|
||||
google_user_id = user_info["id"] if user_info else None
|
||||
|
||||
# Create or update user binding between Google ID and Langflow ID
|
||||
if google_user_id and user_info:
|
||||
try:
|
||||
print(f"[DEBUG] Creating/updating user binding for Google ID: {google_user_id}")
|
||||
binding_created = await user_binding_service.ensure_binding(google_user_id, user_info)
|
||||
if binding_created:
|
||||
print(f"[DEBUG] Successfully ensured user binding for Google ID: {google_user_id}")
|
||||
else:
|
||||
print(f"[DEBUG] Failed to create user binding for Google ID: {google_user_id}")
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Failed to create user binding for Google ID {google_user_id}: {e}")
|
||||
# Don't fail authentication if binding creation fails
|
||||
|
||||
response_data = {
|
||||
"status": "authenticated",
|
||||
|
|
@ -294,13 +279,13 @@ class AuthService:
|
|||
"jwt_token": jwt_token, # Include JWT token in response
|
||||
}
|
||||
|
||||
if google_user_id:
|
||||
# Convert the temporary auth connection to a persistent Google Drive connection
|
||||
if user_info and user_info.get("id"):
|
||||
# Convert the temporary auth connection to a persistent OAuth connection
|
||||
await self.connector_service.connection_manager.update_connection(
|
||||
connection_id=connection_id,
|
||||
connector_type="google_drive",
|
||||
name=f"Google Drive ({user_info.get('email', 'Unknown')})",
|
||||
user_id=google_user_id,
|
||||
user_id=user_info.get("id"),
|
||||
config={
|
||||
**connection_config.config,
|
||||
"purpose": "data_source",
|
||||
|
|
@ -349,10 +334,6 @@ class AuthService:
|
|||
user = getattr(request.state, "user", None)
|
||||
|
||||
if user:
|
||||
# Get user binding info if available
|
||||
binding_info = user_binding_service.get_binding_info(user.user_id)
|
||||
langflow_user_id = user_binding_service.get_langflow_user_id(user.user_id)
|
||||
|
||||
user_data = {
|
||||
"authenticated": True,
|
||||
"user": {
|
||||
|
|
@ -367,13 +348,6 @@ class AuthService:
|
|||
},
|
||||
}
|
||||
|
||||
# Add binding information if available
|
||||
if langflow_user_id:
|
||||
user_data["user"]["langflow_user_id"] = langflow_user_id
|
||||
if binding_info:
|
||||
user_data["user"]["binding_created_at"] = binding_info.get("created_at")
|
||||
user_data["user"]["binding_last_updated"] = binding_info.get("last_updated")
|
||||
|
||||
return user_data
|
||||
else:
|
||||
return {"authenticated": False, "user": None}
|
||||
|
|
|
|||
|
|
@ -269,7 +269,6 @@ class ChatService:
|
|||
"""Get langflow conversation history for a user - now fetches from both OpenRAG memory and Langflow database"""
|
||||
from agent import get_user_conversations
|
||||
from services.langflow_history_service import langflow_history_service
|
||||
from services.user_binding_service import user_binding_service
|
||||
|
||||
if not user_id:
|
||||
return {"error": "User ID is required", "conversations": []}
|
||||
|
|
@ -285,12 +284,17 @@ class ChatService:
|
|||
messages = []
|
||||
for msg in conversation_state.get("messages", []):
|
||||
if msg.get("role") in ["user", "assistant"]:
|
||||
# Handle timestamp - could be datetime object or string
|
||||
timestamp = msg.get("timestamp")
|
||||
if timestamp:
|
||||
if hasattr(timestamp, 'isoformat'):
|
||||
timestamp = timestamp.isoformat()
|
||||
# else it's already a string
|
||||
|
||||
message_data = {
|
||||
"role": msg["role"],
|
||||
"content": msg["content"],
|
||||
"timestamp": msg.get("timestamp").isoformat()
|
||||
if msg.get("timestamp")
|
||||
else None,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
if msg.get("response_id"):
|
||||
message_data["response_id"] = msg["response_id"]
|
||||
|
|
@ -309,17 +313,22 @@ class ChatService:
|
|||
else "New chat"
|
||||
)
|
||||
|
||||
# Handle conversation timestamps - could be datetime objects or strings
|
||||
created_at = conversation_state.get("created_at")
|
||||
if created_at and hasattr(created_at, 'isoformat'):
|
||||
created_at = created_at.isoformat()
|
||||
|
||||
last_activity = conversation_state.get("last_activity")
|
||||
if last_activity and hasattr(last_activity, 'isoformat'):
|
||||
last_activity = last_activity.isoformat()
|
||||
|
||||
all_conversations.append({
|
||||
"response_id": response_id,
|
||||
"title": title,
|
||||
"endpoint": "langflow",
|
||||
"messages": messages,
|
||||
"created_at": conversation_state.get("created_at").isoformat()
|
||||
if conversation_state.get("created_at")
|
||||
else None,
|
||||
"last_activity": conversation_state.get("last_activity").isoformat()
|
||||
if conversation_state.get("last_activity")
|
||||
else None,
|
||||
"created_at": created_at,
|
||||
"last_activity": last_activity,
|
||||
"previous_response_id": conversation_state.get("previous_response_id"),
|
||||
"total_messages": len(messages),
|
||||
"source": "openrag_memory"
|
||||
|
|
|
|||
126
src/services/conversation_persistence_service.py
Normal file
126
src/services/conversation_persistence_service.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
Conversation Persistence Service
|
||||
Simple service to persist chat conversations to disk so they survive server restarts
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
import threading
|
||||
|
||||
|
||||
class ConversationPersistenceService:
|
||||
"""Simple service to persist conversations to disk"""
|
||||
|
||||
def __init__(self, storage_file: str = "conversations.json"):
|
||||
self.storage_file = storage_file
|
||||
self.lock = threading.Lock()
|
||||
self._conversations = self._load_conversations()
|
||||
|
||||
def _load_conversations(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Load conversations from disk"""
|
||||
if os.path.exists(self.storage_file):
|
||||
try:
|
||||
with open(self.storage_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
print(f"Loaded {self._count_total_conversations(data)} conversations from {self.storage_file}")
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"Error loading conversations from {self.storage_file}: {e}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _save_conversations(self):
|
||||
"""Save conversations to disk"""
|
||||
try:
|
||||
with self.lock:
|
||||
with open(self.storage_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self._conversations, f, indent=2, ensure_ascii=False, default=str)
|
||||
print(f"Saved {self._count_total_conversations(self._conversations)} conversations to {self.storage_file}")
|
||||
except Exception as e:
|
||||
print(f"Error saving conversations to {self.storage_file}: {e}")
|
||||
|
||||
def _count_total_conversations(self, data: Dict[str, Any]) -> int:
|
||||
"""Count total conversations across all users"""
|
||||
total = 0
|
||||
for user_conversations in data.values():
|
||||
if isinstance(user_conversations, dict):
|
||||
total += len(user_conversations)
|
||||
return total
|
||||
|
||||
def get_user_conversations(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get all conversations for a user"""
|
||||
if user_id not in self._conversations:
|
||||
self._conversations[user_id] = {}
|
||||
return self._conversations[user_id]
|
||||
|
||||
def _serialize_datetime(self, obj: Any) -> Any:
|
||||
"""Recursively convert datetime objects to ISO strings for JSON serialization"""
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, dict):
|
||||
return {key: self._serialize_datetime(value) for key, value in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [self._serialize_datetime(item) for item in obj]
|
||||
else:
|
||||
return obj
|
||||
|
||||
def store_conversation_thread(self, user_id: str, response_id: str, conversation_state: Dict[str, Any]):
|
||||
"""Store a conversation thread and persist to disk"""
|
||||
if user_id not in self._conversations:
|
||||
self._conversations[user_id] = {}
|
||||
|
||||
# Recursively convert datetime objects to strings for JSON serialization
|
||||
serialized_conversation = self._serialize_datetime(conversation_state)
|
||||
|
||||
self._conversations[user_id][response_id] = serialized_conversation
|
||||
|
||||
# Save to disk (we could optimize this with batching if needed)
|
||||
self._save_conversations()
|
||||
|
||||
def get_conversation_thread(self, user_id: str, response_id: str) -> Dict[str, Any]:
|
||||
"""Get a specific conversation thread"""
|
||||
user_conversations = self.get_user_conversations(user_id)
|
||||
return user_conversations.get(response_id, {})
|
||||
|
||||
def delete_conversation_thread(self, user_id: str, response_id: str):
|
||||
"""Delete a specific conversation thread"""
|
||||
if user_id in self._conversations and response_id in self._conversations[user_id]:
|
||||
del self._conversations[user_id][response_id]
|
||||
self._save_conversations()
|
||||
print(f"Deleted conversation {response_id} for user {user_id}")
|
||||
|
||||
def clear_user_conversations(self, user_id: str):
|
||||
"""Clear all conversations for a user"""
|
||||
if user_id in self._conversations:
|
||||
del self._conversations[user_id]
|
||||
self._save_conversations()
|
||||
print(f"Cleared all conversations for user {user_id}")
|
||||
|
||||
def get_storage_stats(self) -> Dict[str, Any]:
|
||||
"""Get statistics about stored conversations"""
|
||||
total_users = len(self._conversations)
|
||||
total_conversations = self._count_total_conversations(self._conversations)
|
||||
|
||||
user_stats = {}
|
||||
for user_id, conversations in self._conversations.items():
|
||||
user_stats[user_id] = {
|
||||
'conversation_count': len(conversations),
|
||||
'latest_activity': max(
|
||||
(conv.get('last_activity', '') for conv in conversations.values()),
|
||||
default=''
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
'total_users': total_users,
|
||||
'total_conversations': total_conversations,
|
||||
'storage_file': self.storage_file,
|
||||
'file_exists': os.path.exists(self.storage_file),
|
||||
'user_stats': user_stats
|
||||
}
|
||||
|
||||
|
||||
# Global instance
|
||||
conversation_persistence = ConversationPersistenceService()
|
||||
|
|
@ -1,72 +1,33 @@
|
|||
"""
|
||||
Langflow Message History Service
|
||||
Retrieves message history from Langflow's database using user bindings
|
||||
Simplified service that retrieves message history from Langflow using a single token
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
from typing import List, Dict, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
from config.settings import LANGFLOW_URL, LANGFLOW_KEY, LANGFLOW_SUPERUSER, LANGFLOW_SUPERUSER_PASSWORD
|
||||
from services.user_binding_service import user_binding_service
|
||||
from services.session_ownership_service import session_ownership_service
|
||||
|
||||
|
||||
class LangflowHistoryService:
|
||||
"""Service to retrieve message history from Langflow using user bindings"""
|
||||
"""Simplified service to retrieve message history from Langflow"""
|
||||
|
||||
def __init__(self):
|
||||
self.langflow_url = LANGFLOW_URL
|
||||
self.auth_token = None
|
||||
|
||||
def _resolve_langflow_user_id(self, user_id: str) -> Optional[str]:
|
||||
"""Resolve user_id to Langflow user ID
|
||||
|
||||
Args:
|
||||
user_id: Either Google user ID or direct Langflow user ID
|
||||
|
||||
Returns:
|
||||
Langflow user ID or None
|
||||
"""
|
||||
# First, check if this is already a Langflow user ID by checking UUID format
|
||||
if self._is_uuid_format(user_id):
|
||||
print(f"User ID {user_id} appears to be a Langflow UUID, using directly")
|
||||
return user_id
|
||||
|
||||
# Otherwise, try to get Langflow user ID from Google binding
|
||||
langflow_user_id = user_binding_service.get_langflow_user_id(user_id)
|
||||
if langflow_user_id:
|
||||
print(f"Found Langflow binding for Google user {user_id}: {langflow_user_id}")
|
||||
return langflow_user_id
|
||||
|
||||
print(f"No Langflow user ID found for {user_id}")
|
||||
return None
|
||||
|
||||
def _is_uuid_format(self, user_id: str) -> bool:
|
||||
"""Check if string looks like a UUID (Langflow user ID format vs Google numeric ID)"""
|
||||
# Langflow IDs are UUID v4, Google IDs are purely numeric
|
||||
return not user_id.isdigit()
|
||||
|
||||
def _filter_sessions_by_ownership(self, session_ids: List[str], user_id: str, langflow_user_id: str) -> List[str]:
|
||||
"""Filter sessions based on user type and ownership"""
|
||||
if self._is_uuid_format(user_id):
|
||||
# Direct Langflow user - show all sessions for this Langflow user
|
||||
print(f"[DEBUG] Direct Langflow user - showing all {len(session_ids)} sessions")
|
||||
return session_ids
|
||||
else:
|
||||
# Google OAuth user - only show sessions they own
|
||||
owned_sessions = session_ownership_service.filter_sessions_for_google_user(session_ids, user_id)
|
||||
print(f"[DEBUG] Google user {user_id} owns {len(owned_sessions)} out of {len(session_ids)} total sessions")
|
||||
return owned_sessions
|
||||
|
||||
async def _authenticate(self) -> Optional[str]:
|
||||
"""Authenticate with Langflow and get access token"""
|
||||
if self.auth_token:
|
||||
return self.auth_token
|
||||
|
||||
# Try using LANGFLOW_KEY first if available
|
||||
if LANGFLOW_KEY:
|
||||
self.auth_token = LANGFLOW_KEY
|
||||
return self.auth_token
|
||||
|
||||
if not all([LANGFLOW_SUPERUSER, LANGFLOW_SUPERUSER_PASSWORD]):
|
||||
print("Missing Langflow superuser credentials")
|
||||
print("Missing Langflow credentials")
|
||||
return None
|
||||
|
||||
try:
|
||||
|
|
@ -98,15 +59,8 @@ class LangflowHistoryService:
|
|||
async def get_user_sessions(self, user_id: str, flow_id: Optional[str] = None) -> List[str]:
|
||||
"""Get all session IDs for a user's conversations
|
||||
|
||||
Args:
|
||||
user_id: Either Google user ID or direct Langflow user ID
|
||||
Since we use one Langflow token, we get all sessions and filter by user_id locally
|
||||
"""
|
||||
# Determine the Langflow user ID
|
||||
langflow_user_id = self._resolve_langflow_user_id(user_id)
|
||||
if not langflow_user_id:
|
||||
print(f"No Langflow user found for user: {user_id}")
|
||||
return []
|
||||
|
||||
token = await self._authenticate()
|
||||
if not token:
|
||||
return []
|
||||
|
|
@ -127,15 +81,11 @@ class LangflowHistoryService:
|
|||
|
||||
if response.status_code == 200:
|
||||
session_ids = response.json()
|
||||
print(f"Found {len(session_ids)} total sessions from Langflow")
|
||||
|
||||
# Filter sessions to only include those belonging to the user
|
||||
user_sessions = await self._filter_sessions_by_user(session_ids, langflow_user_id, token)
|
||||
|
||||
# Apply ownership-based filtering for Google users
|
||||
filtered_sessions = self._filter_sessions_by_ownership(user_sessions, user_id, langflow_user_id)
|
||||
|
||||
print(f"Found {len(filtered_sessions)} sessions for user {user_id} (Langflow ID: {langflow_user_id})")
|
||||
return filtered_sessions
|
||||
# Since we use a single Langflow instance, return all sessions
|
||||
# Session filtering is handled by user_id at the application level
|
||||
return session_ids
|
||||
else:
|
||||
print(f"Failed to get sessions: {response.status_code} - {response.text}")
|
||||
return []
|
||||
|
|
@ -144,65 +94,8 @@ class LangflowHistoryService:
|
|||
print(f"Error getting user sessions: {e}")
|
||||
return []
|
||||
|
||||
async def _filter_sessions_by_user(self, session_ids: List[str], langflow_user_id: str, token: str) -> List[str]:
|
||||
"""Filter session IDs to only include those belonging to the specified user"""
|
||||
user_sessions = []
|
||||
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
for session_id in session_ids:
|
||||
# Get a sample message from this session to check flow ownership
|
||||
response = await client.get(
|
||||
f"{self.langflow_url.rstrip('/')}/api/v1/monitor/messages",
|
||||
headers=headers,
|
||||
params={
|
||||
"session_id": session_id,
|
||||
"order_by": "timestamp"
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
messages = response.json()
|
||||
if messages and len(messages) > 0:
|
||||
# Check if this session belongs to the user via flow ownership
|
||||
flow_id = messages[0].get('flow_id')
|
||||
if flow_id and await self._is_user_flow(flow_id, langflow_user_id, token):
|
||||
user_sessions.append(session_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error filtering sessions by user: {e}")
|
||||
|
||||
return user_sessions
|
||||
|
||||
async def _is_user_flow(self, flow_id: str, langflow_user_id: str, token: str) -> bool:
|
||||
"""Check if a flow belongs to the specified user"""
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.langflow_url.rstrip('/')}/api/v1/flows/{flow_id}",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
flow_data = response.json()
|
||||
return flow_data.get('user_id') == langflow_user_id
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking flow ownership: {e}")
|
||||
|
||||
return False
|
||||
|
||||
async def get_session_messages(self, user_id: str, session_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get all messages for a specific session"""
|
||||
# Verify user has access to this session
|
||||
langflow_user_id = self._resolve_langflow_user_id(user_id)
|
||||
if not langflow_user_id:
|
||||
return []
|
||||
|
||||
token = await self._authenticate()
|
||||
if not token:
|
||||
return []
|
||||
|
|
@ -222,14 +115,6 @@ class LangflowHistoryService:
|
|||
|
||||
if response.status_code == 200:
|
||||
messages = response.json()
|
||||
|
||||
# Verify user owns this session (security check)
|
||||
if messages and len(messages) > 0:
|
||||
flow_id = messages[0].get('flow_id')
|
||||
if not await self._is_user_flow(flow_id, langflow_user_id, token):
|
||||
print(f"User {user_id} does not own session {session_id}")
|
||||
return []
|
||||
|
||||
# Convert to OpenRAG format
|
||||
return self._convert_langflow_messages(messages)
|
||||
else:
|
||||
|
|
@ -270,16 +155,12 @@ class LangflowHistoryService:
|
|||
return converted_messages
|
||||
|
||||
async def get_user_conversation_history(self, user_id: str, flow_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get all conversation history for a user, organized by session"""
|
||||
langflow_user_id = self._resolve_langflow_user_id(user_id)
|
||||
if not langflow_user_id:
|
||||
return {
|
||||
"error": f"No Langflow user found for {user_id}",
|
||||
"conversations": []
|
||||
}
|
||||
|
||||
"""Get all conversation history for a user, organized by session
|
||||
|
||||
Simplified version - gets all sessions and lets the frontend filter by user_id
|
||||
"""
|
||||
try:
|
||||
# Get all user sessions
|
||||
# Get all sessions (no complex filtering needed)
|
||||
session_ids = await self.get_user_sessions(user_id, flow_id)
|
||||
|
||||
conversations = []
|
||||
|
|
@ -309,7 +190,6 @@ class LangflowHistoryService:
|
|||
return {
|
||||
"conversations": conversations,
|
||||
"total_conversations": len(conversations),
|
||||
"langflow_user_id": langflow_user_id,
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
"""
|
||||
Session Ownership Service
|
||||
Tracks which Google user owns which Langflow session to properly separate message history
|
||||
Simple service that tracks which user owns which session
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, List, Optional, Set
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SessionOwnershipService:
|
||||
"""Service to track session ownership for proper message history separation"""
|
||||
"""Simple service to track which user owns which session"""
|
||||
|
||||
def __init__(self):
|
||||
self.ownership_file = "session_ownership.json"
|
||||
|
|
@ -36,73 +36,55 @@ class SessionOwnershipService:
|
|||
except Exception as e:
|
||||
print(f"Error saving session ownership data: {e}")
|
||||
|
||||
def claim_session(self, google_user_id: str, langflow_session_id: str, langflow_user_id: str):
|
||||
"""Claim a Langflow session for a Google user"""
|
||||
if langflow_session_id not in self.ownership_data:
|
||||
self.ownership_data[langflow_session_id] = {
|
||||
"google_user_id": google_user_id,
|
||||
"langflow_user_id": langflow_user_id,
|
||||
def claim_session(self, user_id: str, session_id: str):
|
||||
"""Claim a session for a user"""
|
||||
if session_id not in self.ownership_data:
|
||||
self.ownership_data[session_id] = {
|
||||
"user_id": user_id,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"last_accessed": datetime.now().isoformat()
|
||||
}
|
||||
self._save_ownership_data()
|
||||
print(f"Claimed session {langflow_session_id} for Google user {google_user_id}")
|
||||
print(f"Claimed session {session_id} for user {user_id}")
|
||||
else:
|
||||
# Update last accessed time
|
||||
self.ownership_data[langflow_session_id]["last_accessed"] = datetime.now().isoformat()
|
||||
self.ownership_data[session_id]["last_accessed"] = datetime.now().isoformat()
|
||||
self._save_ownership_data()
|
||||
|
||||
def get_session_owner(self, langflow_session_id: str) -> Optional[str]:
|
||||
"""Get the Google user ID that owns a Langflow session"""
|
||||
session_data = self.ownership_data.get(langflow_session_id)
|
||||
return session_data.get("google_user_id") if session_data else None
|
||||
def get_session_owner(self, session_id: str) -> Optional[str]:
|
||||
"""Get the user ID that owns a session"""
|
||||
session_data = self.ownership_data.get(session_id)
|
||||
return session_data.get("user_id") if session_data else None
|
||||
|
||||
def get_user_sessions(self, google_user_id: str) -> List[str]:
|
||||
"""Get all Langflow sessions owned by a Google user"""
|
||||
def get_user_sessions(self, user_id: str) -> List[str]:
|
||||
"""Get all sessions owned by a user"""
|
||||
return [
|
||||
session_id
|
||||
for session_id, session_data in self.ownership_data.items()
|
||||
if session_data.get("google_user_id") == google_user_id
|
||||
if session_data.get("user_id") == user_id
|
||||
]
|
||||
|
||||
def get_unowned_sessions_for_langflow_user(self, langflow_user_id: str) -> Set[str]:
|
||||
"""Get sessions for a Langflow user that aren't claimed by any Google user
|
||||
|
||||
This requires querying the Langflow database to get all sessions for the user,
|
||||
then filtering out the ones that are already claimed.
|
||||
"""
|
||||
# This will be implemented when we have access to all sessions for a Langflow user
|
||||
claimed_sessions = set()
|
||||
for session_data in self.ownership_data.values():
|
||||
if session_data.get("langflow_user_id") == langflow_user_id:
|
||||
claimed_sessions.add(session_data.get("google_user_id"))
|
||||
return claimed_sessions
|
||||
def is_session_owned_by_user(self, session_id: str, user_id: str) -> bool:
|
||||
"""Check if a session is owned by a specific user"""
|
||||
return self.get_session_owner(session_id) == user_id
|
||||
|
||||
def filter_sessions_for_google_user(self, all_sessions: List[str], google_user_id: str) -> List[str]:
|
||||
"""Filter a list of sessions to only include those owned by the Google user"""
|
||||
user_sessions = self.get_user_sessions(google_user_id)
|
||||
return [session for session in all_sessions if session in user_sessions]
|
||||
|
||||
def is_session_owned_by_google_user(self, langflow_session_id: str, google_user_id: str) -> bool:
|
||||
"""Check if a session is owned by a specific Google user"""
|
||||
return self.get_session_owner(langflow_session_id) == google_user_id
|
||||
def filter_sessions_for_user(self, session_ids: List[str], user_id: str) -> List[str]:
|
||||
"""Filter a list of sessions to only include those owned by the user"""
|
||||
user_sessions = self.get_user_sessions(user_id)
|
||||
return [session for session in session_ids if session in user_sessions]
|
||||
|
||||
def get_ownership_stats(self) -> Dict[str, any]:
|
||||
"""Get statistics about session ownership"""
|
||||
google_users = set()
|
||||
langflow_users = set()
|
||||
|
||||
users = set()
|
||||
for session_data in self.ownership_data.values():
|
||||
google_users.add(session_data.get("google_user_id"))
|
||||
langflow_users.add(session_data.get("langflow_user_id"))
|
||||
users.add(session_data.get("user_id"))
|
||||
|
||||
return {
|
||||
"total_tracked_sessions": len(self.ownership_data),
|
||||
"unique_google_users": len(google_users),
|
||||
"unique_langflow_users": len(langflow_users),
|
||||
"sessions_per_google_user": {
|
||||
google_user: len(self.get_user_sessions(google_user))
|
||||
for google_user in google_users
|
||||
"unique_users": len(users),
|
||||
"sessions_per_user": {
|
||||
user: len(self.get_user_sessions(user))
|
||||
for user in users if user
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,254 +0,0 @@
|
|||
"""
|
||||
User Binding Service
|
||||
Manages mappings between Google OAuth user IDs and Langflow user IDs
|
||||
Uses verified Langflow API endpoints: /api/v1/login and /api/v1/users/whoami
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Optional, Any
|
||||
import httpx
|
||||
from config.settings import LANGFLOW_URL, LANGFLOW_KEY
|
||||
|
||||
USER_BINDINGS_FILE = "user_bindings.json"
|
||||
|
||||
class UserBindingService:
|
||||
def __init__(self):
|
||||
self.bindings_file = USER_BINDINGS_FILE
|
||||
self.bindings = self._load_bindings()
|
||||
|
||||
def _load_bindings(self) -> Dict[str, Any]:
|
||||
"""Load user bindings from JSON file"""
|
||||
try:
|
||||
if os.path.exists(self.bindings_file):
|
||||
with open(self.bindings_file, 'r') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error loading user bindings: {e}")
|
||||
return {}
|
||||
|
||||
def _save_bindings(self):
|
||||
"""Save user bindings to JSON file"""
|
||||
try:
|
||||
with open(self.bindings_file, 'w') as f:
|
||||
json.dump(self.bindings, f, indent=2)
|
||||
print(f"Saved user bindings to {self.bindings_file}")
|
||||
except Exception as e:
|
||||
print(f"Error saving user bindings: {e}")
|
||||
|
||||
def get_langflow_user_id(self, google_user_id: str) -> Optional[str]:
|
||||
"""Get Langflow user ID from Google user ID"""
|
||||
return self.bindings.get(google_user_id, {}).get('langflow_user_id')
|
||||
|
||||
def get_google_user_id(self, langflow_user_id: str) -> Optional[str]:
|
||||
"""Get Google user ID from Langflow user ID (reverse lookup)"""
|
||||
for google_id, binding in self.bindings.items():
|
||||
if binding.get('langflow_user_id') == langflow_user_id:
|
||||
return google_id
|
||||
return None
|
||||
|
||||
def create_binding(self, google_user_id: str, langflow_user_id: str, google_user_info: Dict[str, Any]):
|
||||
"""Create a new binding between Google and Langflow user IDs"""
|
||||
self.bindings[google_user_id] = {
|
||||
'langflow_user_id': langflow_user_id,
|
||||
'google_user_info': {
|
||||
'email': google_user_info.get('email'),
|
||||
'name': google_user_info.get('name'),
|
||||
'picture': google_user_info.get('picture'),
|
||||
'verified_email': google_user_info.get('verified_email')
|
||||
},
|
||||
'created_at': __import__('datetime').datetime.now().isoformat(),
|
||||
'last_updated': __import__('datetime').datetime.now().isoformat()
|
||||
}
|
||||
self._save_bindings()
|
||||
print(f"Created binding: Google ID {google_user_id} -> Langflow ID {langflow_user_id}")
|
||||
|
||||
def update_binding(self, google_user_id: str, google_user_info: Dict[str, Any]):
|
||||
"""Update existing binding with fresh Google user info"""
|
||||
if google_user_id in self.bindings:
|
||||
self.bindings[google_user_id]['google_user_info'] = {
|
||||
'email': google_user_info.get('email'),
|
||||
'name': google_user_info.get('name'),
|
||||
'picture': google_user_info.get('picture'),
|
||||
'verified_email': google_user_info.get('verified_email')
|
||||
}
|
||||
self.bindings[google_user_id]['last_updated'] = __import__('datetime').datetime.now().isoformat()
|
||||
self._save_bindings()
|
||||
print(f"Updated binding for Google ID {google_user_id}")
|
||||
|
||||
def has_binding(self, google_user_id: str) -> bool:
|
||||
"""Check if a binding exists for the Google user ID"""
|
||||
return google_user_id in self.bindings
|
||||
|
||||
async def get_langflow_user_info(self, langflow_access_token: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get current user info from Langflow /me endpoint"""
|
||||
if not LANGFLOW_URL:
|
||||
print("LANGFLOW_URL not configured")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Use the correct Langflow endpoint based on source code analysis
|
||||
endpoint = "/api/v1/users/whoami"
|
||||
|
||||
headers = {}
|
||||
if langflow_access_token:
|
||||
headers["Authorization"] = f"Bearer {langflow_access_token}"
|
||||
elif LANGFLOW_KEY:
|
||||
# Try with global Langflow API key if available
|
||||
headers["Authorization"] = f"Bearer {LANGFLOW_KEY}"
|
||||
headers["x-api-key"] = LANGFLOW_KEY
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
url = f"{LANGFLOW_URL.rstrip('/')}{endpoint}"
|
||||
print(f"Getting Langflow user info from: {url}")
|
||||
|
||||
response = await client.get(url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
user_data = response.json()
|
||||
print(f"Successfully got Langflow user data")
|
||||
return user_data
|
||||
else:
|
||||
print(f"Langflow /whoami endpoint returned: {response.status_code} - {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting Langflow user info: {e}")
|
||||
return None
|
||||
|
||||
async def authenticate_with_langflow(self) -> Optional[str]:
|
||||
"""Authenticate with Langflow using superuser credentials to get access token"""
|
||||
if not LANGFLOW_URL:
|
||||
return None
|
||||
|
||||
try:
|
||||
from config.settings import LANGFLOW_SUPERUSER, LANGFLOW_SUPERUSER_PASSWORD
|
||||
|
||||
if not LANGFLOW_SUPERUSER or not LANGFLOW_SUPERUSER_PASSWORD:
|
||||
print("Langflow superuser credentials not configured")
|
||||
return None
|
||||
|
||||
# Try to login to Langflow
|
||||
login_data = {
|
||||
"username": LANGFLOW_SUPERUSER,
|
||||
"password": LANGFLOW_SUPERUSER_PASSWORD
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Use the correct Langflow login endpoint based on source code analysis
|
||||
endpoint = "/api/v1/login"
|
||||
url = f"{LANGFLOW_URL.rstrip('/')}{endpoint}"
|
||||
|
||||
# Try form-encoded data first (standard OAuth2 flow)
|
||||
try:
|
||||
response = await client.post(
|
||||
url,
|
||||
data=login_data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
access_token = result.get('access_token')
|
||||
if access_token:
|
||||
print(f"Successfully authenticated with Langflow via {endpoint}")
|
||||
return access_token
|
||||
else:
|
||||
print(f"Langflow login returned: {response.status_code} - {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error with form login: {e}")
|
||||
|
||||
# If form login didn't work, try JSON (fallback)
|
||||
try:
|
||||
response = await client.post(
|
||||
url,
|
||||
json=login_data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
access_token = result.get('access_token')
|
||||
if access_token:
|
||||
print(f"Successfully authenticated with Langflow via {endpoint} (JSON)")
|
||||
return access_token
|
||||
else:
|
||||
print(f"Langflow login (JSON) returned: {response.status_code} - {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error with JSON login: {e}")
|
||||
|
||||
print("Failed to authenticate with Langflow")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error authenticating with Langflow: {e}")
|
||||
return None
|
||||
|
||||
async def ensure_binding(self, google_user_id: str, google_user_info: Dict[str, Any]) -> bool:
|
||||
"""Ensure a binding exists for the Google user, create if needed"""
|
||||
if self.has_binding(google_user_id):
|
||||
# Update existing binding with fresh Google info
|
||||
self.update_binding(google_user_id, google_user_info)
|
||||
return True
|
||||
|
||||
# No binding exists, try to create one
|
||||
try:
|
||||
# First authenticate with Langflow
|
||||
langflow_token = await self.authenticate_with_langflow()
|
||||
if not langflow_token:
|
||||
print("Could not authenticate with Langflow to create binding")
|
||||
return False
|
||||
|
||||
# Get Langflow user info
|
||||
langflow_user_info = await self.get_langflow_user_info(langflow_token)
|
||||
if not langflow_user_info:
|
||||
print("Could not get Langflow user info")
|
||||
return False
|
||||
|
||||
# Extract Langflow user ID (try different possible fields)
|
||||
langflow_user_id = None
|
||||
for id_field in ['id', 'user_id', 'sub', 'username']:
|
||||
if id_field in langflow_user_info:
|
||||
langflow_user_id = str(langflow_user_info[id_field])
|
||||
break
|
||||
|
||||
if not langflow_user_id:
|
||||
print(f"Could not extract Langflow user ID from: {langflow_user_info}")
|
||||
return False
|
||||
|
||||
# Create the binding
|
||||
self.create_binding(google_user_id, langflow_user_id, google_user_info)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating binding for Google user {google_user_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_binding_info(self, google_user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get complete binding information for a Google user ID"""
|
||||
return self.bindings.get(google_user_id)
|
||||
|
||||
def list_all_bindings(self) -> Dict[str, Any]:
|
||||
"""Get all user bindings (for admin purposes)"""
|
||||
return self.bindings.copy()
|
||||
|
||||
def is_langflow_user_id(self, user_id: str) -> bool:
|
||||
"""Check if user_id appears to be a Langflow UUID (vs Google numeric ID)"""
|
||||
# Langflow IDs are UUID v4, Google IDs are purely numeric
|
||||
return not user_id.isdigit()
|
||||
|
||||
def get_user_type(self, user_id: str) -> str:
|
||||
"""Determine user type: 'google_oauth', 'langflow_direct', or 'unknown'"""
|
||||
if self.has_binding(user_id):
|
||||
return "google_oauth"
|
||||
elif self.is_langflow_user_id(user_id):
|
||||
return "langflow_direct"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
# Global instance
|
||||
user_binding_service = UserBindingService()
|
||||
Loading…
Add table
Reference in a new issue