Yassine Mhirsi commited on
Commit
6e8d513
·
1 Parent(s): 2ed0ab3

feat: Implement user management service with Supabase integration, including user registration, retrieval, and name update endpoints.

Browse files
config.py CHANGED
@@ -39,6 +39,10 @@ GROQ_TTS_FORMAT = "wav"
39
  # **Chat Model**
40
  GROQ_CHAT_MODEL = "llama3-70b-8192"
41
 
 
 
 
 
42
  # ============ API META ============
43
  API_TITLE = "NLP Debater - Voice Chatbot"
44
  API_DESCRIPTION = "NLP stance detection, KPA, and Groq STT/TTS chatbot"
@@ -80,4 +84,5 @@ logger.info(f" HF Label Model : {HUGGINGFACE_LABEL_MODEL_ID}")
80
  logger.info(f" GROQ STT Model : {GROQ_STT_MODEL}")
81
  logger.info(f" GROQ TTS Model : {GROQ_TTS_MODEL}")
82
  logger.info(f" GROQ Chat Model : {GROQ_CHAT_MODEL}")
 
83
  logger.info("="*60)
 
39
  # **Chat Model**
40
  GROQ_CHAT_MODEL = "llama3-70b-8192"
41
 
42
+ # ============ SUPABASE ============
43
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
44
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
45
+
46
  # ============ API META ============
47
  API_TITLE = "NLP Debater - Voice Chatbot"
48
  API_DESCRIPTION = "NLP stance detection, KPA, and Groq STT/TTS chatbot"
 
84
  logger.info(f" GROQ STT Model : {GROQ_STT_MODEL}")
85
  logger.info(f" GROQ TTS Model : {GROQ_TTS_MODEL}")
86
  logger.info(f" GROQ Chat Model : {GROQ_CHAT_MODEL}")
87
+ logger.info(f" Supabase URL : {'✓ Configured' if SUPABASE_URL else '✗ Not configured'}")
88
  logger.info("="*60)
models/__init__.py CHANGED
@@ -36,6 +36,14 @@ from .topic import (
36
  BatchTopicResponse,
37
  )
38
 
 
 
 
 
 
 
 
 
39
  # Import MCP-related schemas
40
  from .mcp_models import (
41
  ToolCallRequest,
@@ -72,6 +80,11 @@ __all__ = [
72
  "TopicResponse",
73
  "BatchTopicRequest",
74
  "BatchTopicResponse",
 
 
 
 
 
75
  # MCP schemas
76
  "ToolCallRequest",
77
  "ToolCallResponse",
 
36
  BatchTopicResponse,
37
  )
38
 
39
+ # Import user-related schemas
40
+ from .user import (
41
+ UserRegisterRequest,
42
+ UserResponse,
43
+ UserUpdateNameRequest,
44
+ UserGetRequest,
45
+ )
46
+
47
  # Import MCP-related schemas
48
  from .mcp_models import (
49
  ToolCallRequest,
 
80
  "TopicResponse",
81
  "BatchTopicRequest",
82
  "BatchTopicResponse",
83
+ # User schemas
84
+ "UserRegisterRequest",
85
+ "UserResponse",
86
+ "UserUpdateNameRequest",
87
+ "UserGetRequest",
88
  # MCP schemas
89
  "ToolCallRequest",
90
  "ToolCallResponse",
models/user.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pydantic models for user endpoints"""
2
+
3
+ from pydantic import BaseModel, Field, ConfigDict
4
+ from typing import Optional
5
+ from datetime import datetime
6
+
7
+
8
+ class UserRegisterRequest(BaseModel):
9
+ """Request model for user registration"""
10
+ model_config = ConfigDict(
11
+ json_schema_extra={
12
+ "example": {
13
+ "unique_id": "550e8400-e29b-41d4-a716-446655440000",
14
+ "name": "John Doe"
15
+ }
16
+ }
17
+ )
18
+
19
+ unique_id: str = Field(
20
+ ..., min_length=1, max_length=255,
21
+ description="Browser-generated unique identifier (stored in localStorage)"
22
+ )
23
+ name: Optional[str] = Field(
24
+ None, min_length=1, max_length=100,
25
+ description="User's display name (optional, will generate random if not provided)"
26
+ )
27
+
28
+
29
+ class UserResponse(BaseModel):
30
+ """Response model for user data"""
31
+ model_config = ConfigDict(
32
+ json_schema_extra={
33
+ "example": {
34
+ "id": "123e4567-e89b-12d3-a456-426614174000",
35
+ "unique_id": "550e8400-e29b-41d4-a716-446655440000",
36
+ "name": "John Doe",
37
+ "created_at": "2024-01-01T12:00:00Z",
38
+ "updated_at": "2024-01-01T12:00:00Z"
39
+ }
40
+ }
41
+ )
42
+
43
+ id: str = Field(..., description="User UUID")
44
+ unique_id: str = Field(..., description="Browser-generated unique identifier")
45
+ name: str = Field(..., description="User's display name")
46
+ created_at: str = Field(..., description="User creation timestamp")
47
+ updated_at: str = Field(..., description="User last update timestamp")
48
+
49
+
50
+ class UserUpdateNameRequest(BaseModel):
51
+ """Request model for updating user name"""
52
+ model_config = ConfigDict(
53
+ json_schema_extra={
54
+ "example": {
55
+ "name": "Jane Doe"
56
+ }
57
+ }
58
+ )
59
+
60
+ name: str = Field(
61
+ ..., min_length=1, max_length=100,
62
+ description="New display name"
63
+ )
64
+
65
+
66
+ class UserGetRequest(BaseModel):
67
+ """Request model for getting user by unique_id"""
68
+ model_config = ConfigDict(
69
+ json_schema_extra={
70
+ "example": {
71
+ "unique_id": "550e8400-e29b-41d4-a716-446655440000"
72
+ }
73
+ }
74
+ )
75
+
76
+ unique_id: str = Field(
77
+ ..., min_length=1, max_length=255,
78
+ description="Browser-generated unique identifier"
79
+ )
80
+
requirements.txt CHANGED
@@ -7,6 +7,7 @@ pydantic>=2.5.0
7
  # API Clients
8
  requests>=2.31.0
9
  groq>=0.9.0
 
10
 
11
  # LangChain
12
  langchain>=0.1.0
 
7
  # API Clients
8
  requests>=2.31.0
9
  groq>=0.9.0
10
+ supabase>=2.0.0
11
 
12
  # LangChain
13
  langchain>=0.1.0
routes/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
  """API route handlers"""
2
 
3
  from fastapi import APIRouter
4
- from . import root, health, stance, label, generate, topic
5
  from routes.tts_routes import router as audio_router
6
  # Create main router
7
  api_router = APIRouter()
@@ -13,6 +13,7 @@ api_router.include_router(stance.router, prefix="/stance")
13
  api_router.include_router(label.router, prefix="/label")
14
  api_router.include_router(generate.router, prefix="/generate")
15
  api_router.include_router(topic.router, prefix="/topic")
 
16
  api_router.include_router(audio_router)
17
 
18
  __all__ = ["api_router"]
 
1
  """API route handlers"""
2
 
3
  from fastapi import APIRouter
4
+ from . import root, health, stance, label, generate, topic, user
5
  from routes.tts_routes import router as audio_router
6
  # Create main router
7
  api_router = APIRouter()
 
13
  api_router.include_router(label.router, prefix="/label")
14
  api_router.include_router(generate.router, prefix="/generate")
15
  api_router.include_router(topic.router, prefix="/topic")
16
+ api_router.include_router(user.router, prefix="/user")
17
  api_router.include_router(audio_router)
18
 
19
  __all__ = ["api_router"]
routes/user.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """User management endpoints"""
2
+
3
+ from fastapi import APIRouter, HTTPException, Header
4
+ from typing import Optional
5
+ import logging
6
+
7
+ from services.user_service import user_service
8
+ from models.user import (
9
+ UserRegisterRequest,
10
+ UserResponse,
11
+ UserUpdateNameRequest,
12
+ UserGetRequest,
13
+ )
14
+
15
+ router = APIRouter()
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @router.post("/register", response_model=UserResponse, tags=["Users"])
20
+ async def register_user(request: UserRegisterRequest):
21
+ """
22
+ Register a new user or get existing user by unique_id
23
+
24
+ - **unique_id**: Browser-generated unique identifier (from localStorage)
25
+ - **name**: Optional display name (will generate random if not provided)
26
+
27
+ Returns user data including the user_id to store in localStorage
28
+ """
29
+ try:
30
+ user = user_service.register_or_get_user(
31
+ unique_id=request.unique_id,
32
+ name=request.name
33
+ )
34
+
35
+ logger.info(f"User registered/retrieved: {user.get('id')}")
36
+ return UserResponse(**user)
37
+
38
+ except ValueError as e:
39
+ logger.error(f"Validation error: {str(e)}")
40
+ raise HTTPException(status_code=400, detail=str(e))
41
+ except Exception as e:
42
+ logger.error(f"Registration error: {str(e)}")
43
+ raise HTTPException(status_code=500, detail=f"Registration failed: {str(e)}")
44
+
45
+
46
+ @router.get("/me", response_model=UserResponse, tags=["Users"])
47
+ async def get_current_user(x_user_id: Optional[str] = Header(None, alias="X-User-ID")):
48
+ """
49
+ Get current user by user_id from header
50
+
51
+ - **X-User-ID**: User UUID (sent in request header)
52
+
53
+ Returns user data
54
+ """
55
+ if not x_user_id:
56
+ raise HTTPException(status_code=400, detail="X-User-ID header is required")
57
+
58
+ try:
59
+ user = user_service.get_user_by_id(x_user_id)
60
+
61
+ if not user:
62
+ raise HTTPException(status_code=404, detail="User not found")
63
+
64
+ return UserResponse(**user)
65
+
66
+ except HTTPException:
67
+ raise
68
+ except Exception as e:
69
+ logger.error(f"Error getting user: {str(e)}")
70
+ raise HTTPException(status_code=500, detail=f"Failed to get user: {str(e)}")
71
+
72
+
73
+ @router.get("/by-unique-id", response_model=UserResponse, tags=["Users"])
74
+ async def get_user_by_unique_id(unique_id: str):
75
+ """
76
+ Get user by unique_id (query parameter)
77
+
78
+ - **unique_id**: Browser-generated unique identifier
79
+
80
+ Returns user data
81
+ """
82
+ try:
83
+ user = user_service.get_user_by_unique_id(unique_id)
84
+
85
+ if not user:
86
+ raise HTTPException(status_code=404, detail="User not found")
87
+
88
+ return UserResponse(**user)
89
+
90
+ except HTTPException:
91
+ raise
92
+ except Exception as e:
93
+ logger.error(f"Error getting user: {str(e)}")
94
+ raise HTTPException(status_code=500, detail=f"Failed to get user: {str(e)}")
95
+
96
+
97
+ @router.patch("/me/name", response_model=UserResponse, tags=["Users"])
98
+ async def update_user_name(
99
+ request: UserUpdateNameRequest,
100
+ x_user_id: Optional[str] = Header(None, alias="X-User-ID")
101
+ ):
102
+ """
103
+ Update user's display name
104
+
105
+ - **X-User-ID**: User UUID (sent in request header)
106
+ - **name**: New display name
107
+
108
+ Returns updated user data
109
+ """
110
+ if not x_user_id:
111
+ raise HTTPException(status_code=400, detail="X-User-ID header is required")
112
+
113
+ try:
114
+ user = user_service.update_user_name(
115
+ user_id=x_user_id,
116
+ name=request.name
117
+ )
118
+
119
+ logger.info(f"Updated user name: {x_user_id}")
120
+ return UserResponse(**user)
121
+
122
+ except ValueError as e:
123
+ logger.error(f"Validation error: {str(e)}")
124
+ raise HTTPException(status_code=400, detail=str(e))
125
+ except Exception as e:
126
+ logger.error(f"Update error: {str(e)}")
127
+ raise HTTPException(status_code=500, detail=f"Update failed: {str(e)}")
128
+
services/__init__.py CHANGED
@@ -8,6 +8,8 @@ from .generate_model_manager import GenerateModelManager, generate_model_manager
8
  from .stt_service import speech_to_text
9
  from .tts_service import text_to_speech
10
  from .topic_service import TopicService, topic_service
 
 
11
 
12
  __all__ = [
13
  "StanceModelManager",
@@ -18,6 +20,10 @@ __all__ = [
18
  "generate_model_manager",
19
  "TopicService",
20
  "topic_service",
 
 
 
 
21
 
22
  # NEW exports
23
  "speech_to_text",
 
8
  from .stt_service import speech_to_text
9
  from .tts_service import text_to_speech
10
  from .topic_service import TopicService, topic_service
11
+ from .database_service import DatabaseService, database_service
12
+ from .user_service import UserService, user_service
13
 
14
  __all__ = [
15
  "StanceModelManager",
 
20
  "generate_model_manager",
21
  "TopicService",
22
  "topic_service",
23
+ "DatabaseService",
24
+ "database_service",
25
+ "UserService",
26
+ "user_service",
27
 
28
  # NEW exports
29
  "speech_to_text",
services/database_service.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Database service for Supabase connection"""
2
+
3
+ import logging
4
+ from supabase import create_client, Client
5
+ from config import SUPABASE_URL, SUPABASE_KEY
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class DatabaseService:
11
+ """Service for managing Supabase database connection"""
12
+
13
+ def __init__(self):
14
+ self.client: Client | None = None
15
+ self.connected = False
16
+
17
+ def connect(self):
18
+ """Initialize Supabase client connection"""
19
+ if self.connected and self.client:
20
+ logger.info("Database already connected")
21
+ return
22
+
23
+ if not SUPABASE_URL or not SUPABASE_KEY:
24
+ raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set in environment variables")
25
+
26
+ try:
27
+ logger.info("Connecting to Supabase...")
28
+ self.client = create_client(SUPABASE_URL, SUPABASE_KEY)
29
+ self.connected = True
30
+ logger.info("✓ Successfully connected to Supabase")
31
+ except Exception as e:
32
+ logger.error(f"Error connecting to Supabase: {str(e)}")
33
+ raise RuntimeError(f"Failed to connect to Supabase: {str(e)}")
34
+
35
+ def get_client(self) -> Client:
36
+ """Get Supabase client instance"""
37
+ if not self.connected or not self.client:
38
+ self.connect()
39
+ return self.client
40
+
41
+ def is_connected(self) -> bool:
42
+ """Check if database is connected"""
43
+ return self.connected and self.client is not None
44
+
45
+
46
+ # Initialize singleton instance
47
+ database_service = DatabaseService()
48
+
services/user_service.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """User service for managing user operations"""
2
+
3
+ import logging
4
+ import uuid
5
+ from typing import Optional, Dict
6
+ from datetime import datetime
7
+
8
+ from services.database_service import database_service
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class UserService:
14
+ """Service for user CRUD operations"""
15
+
16
+ def __init__(self):
17
+ self.table_name = "users"
18
+
19
+ def _get_client(self):
20
+ """Get Supabase client"""
21
+ return database_service.get_client()
22
+
23
+ def create_user(self, unique_id: str, name: str) -> Dict:
24
+ """
25
+ Create a new user or get existing user by unique_id
26
+
27
+ Args:
28
+ unique_id: Browser-generated unique identifier
29
+ name: User's display name
30
+
31
+ Returns:
32
+ User dictionary with id, unique_id, name, created_at, updated_at
33
+ """
34
+ try:
35
+ client = self._get_client()
36
+
37
+ # Check if user already exists
38
+ existing_user = self.get_user_by_unique_id(unique_id)
39
+ if existing_user:
40
+ logger.info(f"User already exists with unique_id: {unique_id}")
41
+ return existing_user
42
+
43
+ # Create new user
44
+ user_data = {
45
+ "unique_id": unique_id,
46
+ "name": name,
47
+ "created_at": datetime.utcnow().isoformat(),
48
+ "updated_at": datetime.utcnow().isoformat()
49
+ }
50
+
51
+ result = client.table(self.table_name).insert(user_data).execute()
52
+
53
+ if result.data and len(result.data) > 0:
54
+ logger.info(f"Created new user: {result.data[0].get('id')}")
55
+ return result.data[0]
56
+ else:
57
+ raise RuntimeError("Failed to create user: no data returned")
58
+
59
+ except Exception as e:
60
+ logger.error(f"Error creating user: {str(e)}")
61
+ raise RuntimeError(f"Failed to create user: {str(e)}")
62
+
63
+ def get_user_by_unique_id(self, unique_id: str) -> Optional[Dict]:
64
+ """
65
+ Get user by unique_id
66
+
67
+ Args:
68
+ unique_id: Browser-generated unique identifier
69
+
70
+ Returns:
71
+ User dictionary or None if not found
72
+ """
73
+ try:
74
+ client = self._get_client()
75
+
76
+ result = client.table(self.table_name).select("*").eq("unique_id", unique_id).execute()
77
+
78
+ if result.data and len(result.data) > 0:
79
+ return result.data[0]
80
+ return None
81
+
82
+ except Exception as e:
83
+ logger.error(f"Error getting user by unique_id: {str(e)}")
84
+ raise RuntimeError(f"Failed to get user: {str(e)}")
85
+
86
+ def get_user_by_id(self, user_id: str) -> Optional[Dict]:
87
+ """
88
+ Get user by id (UUID)
89
+
90
+ Args:
91
+ user_id: User UUID
92
+
93
+ Returns:
94
+ User dictionary or None if not found
95
+ """
96
+ try:
97
+ client = self._get_client()
98
+
99
+ result = client.table(self.table_name).select("*").eq("id", user_id).execute()
100
+
101
+ if result.data and len(result.data) > 0:
102
+ return result.data[0]
103
+ return None
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error getting user by id: {str(e)}")
107
+ raise RuntimeError(f"Failed to get user: {str(e)}")
108
+
109
+ def update_user_name(self, user_id: str, name: str) -> Dict:
110
+ """
111
+ Update user's name
112
+
113
+ Args:
114
+ user_id: User UUID
115
+ name: New display name
116
+
117
+ Returns:
118
+ Updated user dictionary
119
+ """
120
+ try:
121
+ client = self._get_client()
122
+
123
+ update_data = {
124
+ "name": name,
125
+ "updated_at": datetime.utcnow().isoformat()
126
+ }
127
+
128
+ result = client.table(self.table_name).update(update_data).eq("id", user_id).execute()
129
+
130
+ if result.data and len(result.data) > 0:
131
+ logger.info(f"Updated user name: {user_id}")
132
+ return result.data[0]
133
+ else:
134
+ raise RuntimeError("Failed to update user: user not found")
135
+
136
+ except Exception as e:
137
+ logger.error(f"Error updating user name: {str(e)}")
138
+ raise RuntimeError(f"Failed to update user: {str(e)}")
139
+
140
+ def register_or_get_user(self, unique_id: str, name: Optional[str] = None) -> Dict:
141
+ """
142
+ Register a new user or get existing user by unique_id
143
+ If name is not provided, generates a random one
144
+
145
+ Args:
146
+ unique_id: Browser-generated unique identifier
147
+ name: Optional user's display name (generates random if not provided)
148
+
149
+ Returns:
150
+ User dictionary
151
+ """
152
+ # Generate random name if not provided
153
+ if not name:
154
+ name = f"User_{uuid.uuid4().hex[:8]}"
155
+
156
+ # Check if user exists
157
+ existing_user = self.get_user_by_unique_id(unique_id)
158
+ if existing_user:
159
+ return existing_user
160
+
161
+ # Create new user
162
+ return self.create_user(unique_id, name)
163
+
164
+
165
+ # Initialize singleton instance
166
+ user_service = UserService()
167
+