|
13 | 13 | ProjectBoostWorkforce, |
14 | 14 | ) |
15 | 15 | from labelbox.pagination import PaginatedCollection |
| 16 | +from labelbox.orm.model import Entity |
16 | 17 |
|
17 | 18 | logger = logging.getLogger(__name__) |
18 | 19 |
|
@@ -153,6 +154,238 @@ def get_project_owner(self) -> Optional[ProjectBoostWorkforce]: |
153 | 154 | client=self.client, project_id=self.project.uid |
154 | 155 | ) |
155 | 156 |
|
| 157 | + def _get_user_labels(self, user_id: str): |
| 158 | + """Get all labels created by a user in this project. |
| 159 | + |
| 160 | + Args: |
| 161 | + user_id: ID of the user |
| 162 | + |
| 163 | + Returns: |
| 164 | + List of Label objects |
| 165 | + |
| 166 | + Raises: |
| 167 | + Exception: If labels cannot be retrieved |
| 168 | + """ |
| 169 | + labels = list(self.project.labels(created_by=user_id)) |
| 170 | + logger.info( |
| 171 | + "Found %d labels created by user %s in project %s", |
| 172 | + len(labels), |
| 173 | + user_id, |
| 174 | + self.project.uid |
| 175 | + ) |
| 176 | + return labels |
| 177 | + |
| 178 | + def _create_trust_safety_case(self, user_id: str, event_metadata: dict) -> bool: |
| 179 | + """Create a Trust & Safety case for a user. |
| 180 | + |
| 181 | + Args: |
| 182 | + user_id: ID of the user being reported |
| 183 | + event_metadata: JSON metadata about the event |
| 184 | + |
| 185 | + Returns: |
| 186 | + True if case was created successfully |
| 187 | + |
| 188 | + Raises: |
| 189 | + Exception: If T&S case creation fails |
| 190 | + """ |
| 191 | + mutation = """mutation CreateTrustAndSafetyCasePyApi( |
| 192 | + $subjectUserId: String! |
| 193 | + $eventType: CaseEventGqlType! |
| 194 | + $severity: CaseSeverityGqlType! |
| 195 | + $eventMetadata: Json! |
| 196 | + ) { |
| 197 | + createTrustAndSafetyCase(input: { |
| 198 | + subjectUserId: $subjectUserId |
| 199 | + eventType: $eventType |
| 200 | + severity: $severity |
| 201 | + eventMetadata: $eventMetadata |
| 202 | + }) { |
| 203 | + success |
| 204 | + } |
| 205 | + }""" |
| 206 | + |
| 207 | + params = { |
| 208 | + "subjectUserId": user_id, |
| 209 | + "eventType": "manual", |
| 210 | + "severity": "high", |
| 211 | + "eventMetadata": event_metadata, |
| 212 | + } |
| 213 | + |
| 214 | + result = self.client.execute(mutation, params) |
| 215 | + success = result["createTrustAndSafetyCase"]["success"] |
| 216 | + |
| 217 | + if success: |
| 218 | + logger.info( |
| 219 | + "Created T&S case for user %s in project %s", |
| 220 | + user_id, |
| 221 | + self.project.uid |
| 222 | + ) |
| 223 | + |
| 224 | + return success |
| 225 | + |
| 226 | + def _remove_user_from_project(self, user_id: str) -> None: |
| 227 | + """Remove a user from this project. |
| 228 | + |
| 229 | + Args: |
| 230 | + user_id: ID of the user to remove |
| 231 | + |
| 232 | + Raises: |
| 233 | + ValueError: If user not found in project |
| 234 | + Exception: If removal fails |
| 235 | + """ |
| 236 | + # Check if user is in project members |
| 237 | + user_found = False |
| 238 | + for member in self.project.members(): |
| 239 | + if member.user().uid == user_id: |
| 240 | + user_found = True |
| 241 | + break |
| 242 | + |
| 243 | + if not user_found: |
| 244 | + logger.warning("User %s not found in project %s members", user_id, self.project.uid) |
| 245 | + raise ValueError(f"User {user_id} not found in project members") |
| 246 | + |
| 247 | + # Remove user using deleteProjectMemberships mutation |
| 248 | + result = self.client.delete_project_memberships( |
| 249 | + project_id=self.project.uid, |
| 250 | + user_ids=[user_id] |
| 251 | + ) |
| 252 | + |
| 253 | + if not result.get("success"): |
| 254 | + error_message = result.get("errorMessage", "Unknown error") |
| 255 | + logger.error("Failed to remove user: %s", error_message) |
| 256 | + raise Exception(f"Failed to remove user: {error_message}") |
| 257 | + |
| 258 | + logger.info( |
| 259 | + "Removed user %s from project %s", |
| 260 | + user_id, |
| 261 | + self.project.uid |
| 262 | + ) |
| 263 | + |
| 264 | + def _delete_user_labels(self, labels) -> int: |
| 265 | + """Delete a list of labels. |
| 266 | + |
| 267 | + Args: |
| 268 | + labels: List of Label objects to delete |
| 269 | + |
| 270 | + Returns: |
| 271 | + Number of labels deleted |
| 272 | + |
| 273 | + Raises: |
| 274 | + Exception: If deletion fails |
| 275 | + """ |
| 276 | + if not labels: |
| 277 | + return 0 |
| 278 | + |
| 279 | + Entity.Label.bulk_delete(labels) |
| 280 | + logger.info( |
| 281 | + "Deleted %d labels in project %s", |
| 282 | + len(labels), |
| 283 | + self.project.uid |
| 284 | + ) |
| 285 | + return len(labels) |
| 286 | + |
| 287 | + def report_fraud( |
| 288 | + self, |
| 289 | + user_id: str, |
| 290 | + reason: str, |
| 291 | + custom_metadata: dict = None |
| 292 | + ) -> dict: |
| 293 | + """Report potential fraud by a user in this project. |
| 294 | +
|
| 295 | + This method performs the following actions: |
| 296 | + 1. Gets all labels created by the user in this project |
| 297 | + 2. Creates a Trust & Safety case for the user (MANUAL event type, HIGH severity) |
| 298 | + 3. Removes the user from the project (prevents creating more labels) |
| 299 | + 4. Deletes all the user's labels |
| 300 | + |
| 301 | + Args: |
| 302 | + user_id (str): The ID of the user to report for fraud. |
| 303 | + reason (str): Reason for reporting fraud (e.g., "Spam labels", "Low quality work"). |
| 304 | + custom_metadata (dict, optional): Additional metadata to include in the T&S case. |
| 305 | + Will be merged with automatic metadata (project_id, reason, label_count, label_ids). |
| 306 | + |
| 307 | + Returns: |
| 308 | + dict: A dictionary containing: |
| 309 | + - ts_case_id: Status of T&S case creation ("created" if successful) |
| 310 | + - labels_found: Number of labels found by the user |
| 311 | + - user_removed: Whether the user was successfully removed |
| 312 | + - labels_deleted: Number of labels deleted |
| 313 | + - error: Any error message if any step failed |
| 314 | + |
| 315 | + Example: |
| 316 | + >>> from alignerr import AlignerrWorkspace |
| 317 | + >>> from labelbox import Client |
| 318 | + >>> |
| 319 | + >>> client = Client(api_key="YOUR_API_KEY") |
| 320 | + >>> workspace = AlignerrWorkspace.from_labelbox(client) |
| 321 | + >>> project = workspace.project_builder().from_existing(project_id) |
| 322 | + >>> |
| 323 | + >>> # Report fraud with reason |
| 324 | + >>> result = project.report_fraud(user_id, reason="Spam labels detected") |
| 325 | + >>> print(f"Removed user: {result['user_removed']}, Deleted {result['labels_deleted']} labels") |
| 326 | + >>> |
| 327 | + >>> # With additional custom metadata |
| 328 | + >>> result = project.report_fraud( |
| 329 | + >>> user_id, |
| 330 | + >>> reason="Production quality issues", |
| 331 | + >>> custom_metadata={"ticket_id": "TICKET-123", "reviewer": "john@example.com"} |
| 332 | + >>> ) |
| 333 | + """ |
| 334 | + result = { |
| 335 | + "ts_case_id": None, |
| 336 | + "labels_found": 0, |
| 337 | + "user_removed": False, |
| 338 | + "labels_deleted": 0, |
| 339 | + "error": None, |
| 340 | + } |
| 341 | + |
| 342 | + # Step 1: Get all labels cteated by this user in this project |
| 343 | + try: |
| 344 | + labels_to_delete = self._get_user_labels(user_id) |
| 345 | + result["labels_found"] = len(labels_to_delete) |
| 346 | + except Exception as e: |
| 347 | + logger.error("Failed to get labels: %s", str(e)) |
| 348 | + result["error"] = f"Failed to get labels: {str(e)}" |
| 349 | + return result |
| 350 | + |
| 351 | + # Step 2: Create T&S case with label information |
| 352 | + try: |
| 353 | + event_metadata = { |
| 354 | + "project_id": self.project.uid, |
| 355 | + "reason": reason, |
| 356 | + "label_count": len(labels_to_delete), |
| 357 | + "label_ids": [label.uid for label in labels_to_delete], |
| 358 | + } |
| 359 | + if custom_metadata: |
| 360 | + event_metadata.update(custom_metadata) |
| 361 | + |
| 362 | + ts_case_created = self._create_trust_safety_case(user_id, event_metadata) |
| 363 | + if ts_case_created: |
| 364 | + result["ts_case_id"] = "created" |
| 365 | + except Exception as e: |
| 366 | + logger.error("Failed to create T&S case: %s", str(e)) |
| 367 | + result["error"] = f"Failed to create T&S case: {str(e)}" |
| 368 | + return result |
| 369 | + |
| 370 | + # Step 3: Remove user from project (prevent creating more labels) |
| 371 | + try: |
| 372 | + self._remove_user_from_project(user_id) |
| 373 | + result["user_removed"] = True |
| 374 | + except Exception as e: |
| 375 | + logger.error("Failed to remove user from project: %s", str(e)) |
| 376 | + result["error"] = f"Failed to remove user: {str(e)}" |
| 377 | + return result |
| 378 | + |
| 379 | + # Step 4: Delete all labels by this user |
| 380 | + try: |
| 381 | + result["labels_deleted"] = self._delete_user_labels(labels_to_delete) |
| 382 | + except Exception as e: |
| 383 | + logger.error("Failed to delete labels: %s", str(e)) |
| 384 | + result["error"] = f"Failed to delete labels: {str(e)}" |
| 385 | + return result |
| 386 | + |
| 387 | + return result |
| 388 | + |
156 | 389 |
|
157 | 390 | class AlignerrWorkspace: |
158 | 391 | def __init__(self, client: "Client"): |
|
0 commit comments