🔧 (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:
cristhianzl 2025-09-05 16:03:06 -03:00
parent c87877bb80
commit 18b4059b56
7 changed files with 211 additions and 509 deletions

View file

@ -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}"

View file

@ -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}

View file

@ -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"

View 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()

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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()