🔧 (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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# User-scoped conversation state - keyed by user_id -> response_id -> conversation
|
# Import persistent storage
|
||||||
user_conversations = {} # user_id -> {response_id: {"messages": [...], "previous_response_id": parent_id, "created_at": timestamp, "last_activity": timestamp}}
|
from services.conversation_persistence_service import conversation_persistence
|
||||||
|
|
||||||
|
|
||||||
def get_user_conversations(user_id: str):
|
def get_user_conversations(user_id: str):
|
||||||
"""Get all conversations for a user"""
|
"""Get all conversations for a user"""
|
||||||
if user_id not in user_conversations:
|
return conversation_persistence.get_user_conversations(user_id)
|
||||||
user_conversations[user_id] = {}
|
|
||||||
return user_conversations[user_id]
|
|
||||||
|
|
||||||
|
|
||||||
def get_conversation_thread(user_id: str, previous_response_id: str = None):
|
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):
|
def store_conversation_thread(user_id: str, response_id: str, conversation_state: dict):
|
||||||
"""Store a conversation thread with its response_id"""
|
"""Store a conversation thread with its response_id"""
|
||||||
conversations = get_user_conversations(user_id)
|
conversation_persistence.store_conversation_thread(user_id, response_id, conversation_state)
|
||||||
conversations[response_id] = conversation_state
|
|
||||||
|
|
||||||
|
|
||||||
# Legacy function for backward compatibility
|
# Legacy function for backward compatibility
|
||||||
|
|
@ -413,17 +410,11 @@ async def async_langflow_chat(
|
||||||
conversation_state["last_activity"] = datetime.now()
|
conversation_state["last_activity"] = datetime.now()
|
||||||
store_conversation_thread(user_id, response_id, conversation_state)
|
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:
|
try:
|
||||||
from services.session_ownership_service import session_ownership_service
|
from services.session_ownership_service import session_ownership_service
|
||||||
from services.user_binding_service import user_binding_service
|
session_ownership_service.claim_session(user_id, response_id)
|
||||||
|
print(f"[DEBUG] Claimed session {response_id} for user {user_id}")
|
||||||
# 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}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WARNING] Failed to claim session ownership: {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()
|
conversation_state["last_activity"] = datetime.now()
|
||||||
store_conversation_thread(user_id, response_id, conversation_state)
|
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:
|
try:
|
||||||
from services.session_ownership_service import session_ownership_service
|
from services.session_ownership_service import session_ownership_service
|
||||||
from services.user_binding_service import user_binding_service
|
session_ownership_service.claim_session(user_id, response_id)
|
||||||
|
print(f"[DEBUG] Claimed session {response_id} for user {user_id}")
|
||||||
# Check if this is a Google user (Google IDs are numeric, Langflow IDs are UUID)
|
except Exception as e:
|
||||||
if user_id.isdigit() and user_binding_service.has_binding(user_id):
|
print(f"[WARNING] Failed to claim session ownership: {e}")
|
||||||
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}")
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[DEBUG] Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
|
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.google_drive import GoogleDriveConnector
|
||||||
from connectors.onedrive import OneDriveConnector
|
from connectors.onedrive import OneDriveConnector
|
||||||
from connectors.sharepoint import SharePointConnector
|
from connectors.sharepoint import SharePointConnector
|
||||||
from services.user_binding_service import user_binding_service
|
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
|
|
@ -268,24 +267,10 @@ class AuthService:
|
||||||
)
|
)
|
||||||
|
|
||||||
if jwt_token:
|
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(
|
user_info = await self.session_manager.get_user_info_from_token(
|
||||||
token_data["access_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 = {
|
response_data = {
|
||||||
"status": "authenticated",
|
"status": "authenticated",
|
||||||
|
|
@ -294,13 +279,13 @@ class AuthService:
|
||||||
"jwt_token": jwt_token, # Include JWT token in response
|
"jwt_token": jwt_token, # Include JWT token in response
|
||||||
}
|
}
|
||||||
|
|
||||||
if google_user_id:
|
if user_info and user_info.get("id"):
|
||||||
# Convert the temporary auth connection to a persistent Google Drive connection
|
# Convert the temporary auth connection to a persistent OAuth connection
|
||||||
await self.connector_service.connection_manager.update_connection(
|
await self.connector_service.connection_manager.update_connection(
|
||||||
connection_id=connection_id,
|
connection_id=connection_id,
|
||||||
connector_type="google_drive",
|
connector_type="google_drive",
|
||||||
name=f"Google Drive ({user_info.get('email', 'Unknown')})",
|
name=f"Google Drive ({user_info.get('email', 'Unknown')})",
|
||||||
user_id=google_user_id,
|
user_id=user_info.get("id"),
|
||||||
config={
|
config={
|
||||||
**connection_config.config,
|
**connection_config.config,
|
||||||
"purpose": "data_source",
|
"purpose": "data_source",
|
||||||
|
|
@ -349,10 +334,6 @@ class AuthService:
|
||||||
user = getattr(request.state, "user", None)
|
user = getattr(request.state, "user", None)
|
||||||
|
|
||||||
if user:
|
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 = {
|
user_data = {
|
||||||
"authenticated": True,
|
"authenticated": True,
|
||||||
"user": {
|
"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
|
return user_data
|
||||||
else:
|
else:
|
||||||
return {"authenticated": False, "user": None}
|
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"""
|
"""Get langflow conversation history for a user - now fetches from both OpenRAG memory and Langflow database"""
|
||||||
from agent import get_user_conversations
|
from agent import get_user_conversations
|
||||||
from services.langflow_history_service import langflow_history_service
|
from services.langflow_history_service import langflow_history_service
|
||||||
from services.user_binding_service import user_binding_service
|
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return {"error": "User ID is required", "conversations": []}
|
return {"error": "User ID is required", "conversations": []}
|
||||||
|
|
@ -285,12 +284,17 @@ class ChatService:
|
||||||
messages = []
|
messages = []
|
||||||
for msg in conversation_state.get("messages", []):
|
for msg in conversation_state.get("messages", []):
|
||||||
if msg.get("role") in ["user", "assistant"]:
|
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 = {
|
message_data = {
|
||||||
"role": msg["role"],
|
"role": msg["role"],
|
||||||
"content": msg["content"],
|
"content": msg["content"],
|
||||||
"timestamp": msg.get("timestamp").isoformat()
|
"timestamp": timestamp,
|
||||||
if msg.get("timestamp")
|
|
||||||
else None,
|
|
||||||
}
|
}
|
||||||
if msg.get("response_id"):
|
if msg.get("response_id"):
|
||||||
message_data["response_id"] = msg["response_id"]
|
message_data["response_id"] = msg["response_id"]
|
||||||
|
|
@ -309,17 +313,22 @@ class ChatService:
|
||||||
else "New chat"
|
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({
|
all_conversations.append({
|
||||||
"response_id": response_id,
|
"response_id": response_id,
|
||||||
"title": title,
|
"title": title,
|
||||||
"endpoint": "langflow",
|
"endpoint": "langflow",
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"created_at": conversation_state.get("created_at").isoformat()
|
"created_at": created_at,
|
||||||
if conversation_state.get("created_at")
|
"last_activity": last_activity,
|
||||||
else None,
|
|
||||||
"last_activity": conversation_state.get("last_activity").isoformat()
|
|
||||||
if conversation_state.get("last_activity")
|
|
||||||
else None,
|
|
||||||
"previous_response_id": conversation_state.get("previous_response_id"),
|
"previous_response_id": conversation_state.get("previous_response_id"),
|
||||||
"total_messages": len(messages),
|
"total_messages": len(messages),
|
||||||
"source": "openrag_memory"
|
"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
|
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
|
import httpx
|
||||||
from typing import List, Dict, Optional, Any
|
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 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:
|
class LangflowHistoryService:
|
||||||
"""Service to retrieve message history from Langflow using user bindings"""
|
"""Simplified service to retrieve message history from Langflow"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.langflow_url = LANGFLOW_URL
|
self.langflow_url = LANGFLOW_URL
|
||||||
self.auth_token = None
|
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]:
|
async def _authenticate(self) -> Optional[str]:
|
||||||
"""Authenticate with Langflow and get access token"""
|
"""Authenticate with Langflow and get access token"""
|
||||||
if self.auth_token:
|
if self.auth_token:
|
||||||
return 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]):
|
if not all([LANGFLOW_SUPERUSER, LANGFLOW_SUPERUSER_PASSWORD]):
|
||||||
print("Missing Langflow superuser credentials")
|
print("Missing Langflow credentials")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -98,15 +59,8 @@ class LangflowHistoryService:
|
||||||
async def get_user_sessions(self, user_id: str, flow_id: Optional[str] = None) -> List[str]:
|
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
|
"""Get all session IDs for a user's conversations
|
||||||
|
|
||||||
Args:
|
Since we use one Langflow token, we get all sessions and filter by user_id locally
|
||||||
user_id: Either Google user ID or direct Langflow user ID
|
|
||||||
"""
|
"""
|
||||||
# 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()
|
token = await self._authenticate()
|
||||||
if not token:
|
if not token:
|
||||||
return []
|
return []
|
||||||
|
|
@ -127,15 +81,11 @@ class LangflowHistoryService:
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
session_ids = response.json()
|
session_ids = response.json()
|
||||||
|
print(f"Found {len(session_ids)} total sessions from Langflow")
|
||||||
|
|
||||||
# Filter sessions to only include those belonging to the user
|
# Since we use a single Langflow instance, return all sessions
|
||||||
user_sessions = await self._filter_sessions_by_user(session_ids, langflow_user_id, token)
|
# Session filtering is handled by user_id at the application level
|
||||||
|
return session_ids
|
||||||
# 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
|
|
||||||
else:
|
else:
|
||||||
print(f"Failed to get sessions: {response.status_code} - {response.text}")
|
print(f"Failed to get sessions: {response.status_code} - {response.text}")
|
||||||
return []
|
return []
|
||||||
|
|
@ -144,65 +94,8 @@ class LangflowHistoryService:
|
||||||
print(f"Error getting user sessions: {e}")
|
print(f"Error getting user sessions: {e}")
|
||||||
return []
|
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]]:
|
async def get_session_messages(self, user_id: str, session_id: str) -> List[Dict[str, Any]]:
|
||||||
"""Get all messages for a specific session"""
|
"""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()
|
token = await self._authenticate()
|
||||||
if not token:
|
if not token:
|
||||||
return []
|
return []
|
||||||
|
|
@ -222,14 +115,6 @@ class LangflowHistoryService:
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
messages = response.json()
|
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
|
# Convert to OpenRAG format
|
||||||
return self._convert_langflow_messages(messages)
|
return self._convert_langflow_messages(messages)
|
||||||
else:
|
else:
|
||||||
|
|
@ -270,16 +155,12 @@ class LangflowHistoryService:
|
||||||
return converted_messages
|
return converted_messages
|
||||||
|
|
||||||
async def get_user_conversation_history(self, user_id: str, flow_id: Optional[str] = None) -> Dict[str, Any]:
|
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"""
|
"""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:
|
Simplified version - gets all sessions and lets the frontend filter by user_id
|
||||||
return {
|
"""
|
||||||
"error": f"No Langflow user found for {user_id}",
|
|
||||||
"conversations": []
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get all user sessions
|
# Get all sessions (no complex filtering needed)
|
||||||
session_ids = await self.get_user_sessions(user_id, flow_id)
|
session_ids = await self.get_user_sessions(user_id, flow_id)
|
||||||
|
|
||||||
conversations = []
|
conversations = []
|
||||||
|
|
@ -309,7 +190,6 @@ class LangflowHistoryService:
|
||||||
return {
|
return {
|
||||||
"conversations": conversations,
|
"conversations": conversations,
|
||||||
"total_conversations": len(conversations),
|
"total_conversations": len(conversations),
|
||||||
"langflow_user_id": langflow_user_id,
|
|
||||||
"user_id": user_id
|
"user_id": user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
"""
|
"""
|
||||||
Session Ownership Service
|
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 json
|
||||||
import os
|
import os
|
||||||
from typing import Dict, List, Optional, Set
|
from typing import Dict, List, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class SessionOwnershipService:
|
class SessionOwnershipService:
|
||||||
"""Service to track session ownership for proper message history separation"""
|
"""Simple service to track which user owns which session"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ownership_file = "session_ownership.json"
|
self.ownership_file = "session_ownership.json"
|
||||||
|
|
@ -36,73 +36,55 @@ class SessionOwnershipService:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving session ownership data: {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):
|
def claim_session(self, user_id: str, session_id: str):
|
||||||
"""Claim a Langflow session for a Google user"""
|
"""Claim a session for a user"""
|
||||||
if langflow_session_id not in self.ownership_data:
|
if session_id not in self.ownership_data:
|
||||||
self.ownership_data[langflow_session_id] = {
|
self.ownership_data[session_id] = {
|
||||||
"google_user_id": google_user_id,
|
"user_id": user_id,
|
||||||
"langflow_user_id": langflow_user_id,
|
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": datetime.now().isoformat(),
|
||||||
"last_accessed": datetime.now().isoformat()
|
"last_accessed": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
self._save_ownership_data()
|
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:
|
else:
|
||||||
# Update last accessed time
|
# 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()
|
self._save_ownership_data()
|
||||||
|
|
||||||
def get_session_owner(self, langflow_session_id: str) -> Optional[str]:
|
def get_session_owner(self, session_id: str) -> Optional[str]:
|
||||||
"""Get the Google user ID that owns a Langflow session"""
|
"""Get the user ID that owns a session"""
|
||||||
session_data = self.ownership_data.get(langflow_session_id)
|
session_data = self.ownership_data.get(session_id)
|
||||||
return session_data.get("google_user_id") if session_data else None
|
return session_data.get("user_id") if session_data else None
|
||||||
|
|
||||||
def get_user_sessions(self, google_user_id: str) -> List[str]:
|
def get_user_sessions(self, user_id: str) -> List[str]:
|
||||||
"""Get all Langflow sessions owned by a Google user"""
|
"""Get all sessions owned by a user"""
|
||||||
return [
|
return [
|
||||||
session_id
|
session_id
|
||||||
for session_id, session_data in self.ownership_data.items()
|
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]:
|
def is_session_owned_by_user(self, session_id: str, user_id: str) -> bool:
|
||||||
"""Get sessions for a Langflow user that aren't claimed by any Google user
|
"""Check if a session is owned by a specific user"""
|
||||||
|
return self.get_session_owner(session_id) == user_id
|
||||||
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 filter_sessions_for_google_user(self, all_sessions: List[str], google_user_id: str) -> List[str]:
|
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 Google user"""
|
"""Filter a list of sessions to only include those owned by the user"""
|
||||||
user_sessions = self.get_user_sessions(google_user_id)
|
user_sessions = self.get_user_sessions(user_id)
|
||||||
return [session for session in all_sessions if session in user_sessions]
|
return [session for session in session_ids 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 get_ownership_stats(self) -> Dict[str, any]:
|
def get_ownership_stats(self) -> Dict[str, any]:
|
||||||
"""Get statistics about session ownership"""
|
"""Get statistics about session ownership"""
|
||||||
google_users = set()
|
users = set()
|
||||||
langflow_users = set()
|
|
||||||
|
|
||||||
for session_data in self.ownership_data.values():
|
for session_data in self.ownership_data.values():
|
||||||
google_users.add(session_data.get("google_user_id"))
|
users.add(session_data.get("user_id"))
|
||||||
langflow_users.add(session_data.get("langflow_user_id"))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_tracked_sessions": len(self.ownership_data),
|
"total_tracked_sessions": len(self.ownership_data),
|
||||||
"unique_google_users": len(google_users),
|
"unique_users": len(users),
|
||||||
"unique_langflow_users": len(langflow_users),
|
"sessions_per_user": {
|
||||||
"sessions_per_google_user": {
|
user: len(self.get_user_sessions(user))
|
||||||
google_user: len(self.get_user_sessions(google_user))
|
for user in users if user
|
||||||
for google_user in google_users
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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