Skip to content

Commit 142b601

Browse files
authored
[PLT-1347] Vb/request labelin service (#1761)
1 parent 4e78990 commit 142b601

File tree

4 files changed

+155
-39
lines changed

4 files changed

+155
-39
lines changed

libs/labelbox/src/labelbox/client.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ def execute(self,
145145
files=None,
146146
timeout=60.0,
147147
experimental=False,
148-
error_log_key="message"):
148+
error_log_key="message",
149+
raise_return_resource_not_found=False):
149150
""" Sends a request to the server for the execution of the
150151
given query.
151152
@@ -297,9 +298,13 @@ def get_error_status_code(error: dict) -> int:
297298
resource_not_found_error = check_errors(["RESOURCE_NOT_FOUND"],
298299
"extensions", "code")
299300
if resource_not_found_error is not None:
300-
# Return None and let the caller methods raise an exception
301-
# as they already know which resource type and ID was requested
302-
return None
301+
if raise_return_resource_not_found:
302+
raise labelbox.exceptions.ResourceNotFoundError(
303+
message=resource_not_found_error["message"])
304+
else:
305+
# Return None and let the caller methods raise an exception
306+
# as they already know which resource type and ID was requested
307+
return None
303308

304309
resource_conflict_error = check_errors(["RESOURCE_CONFLICT"],
305310
"extensions", "code")
@@ -875,12 +880,12 @@ def create_offline_model_evaluation_project(self, **kwargs) -> Project:
875880

876881
return self._create_project(**kwargs)
877882

878-
879-
def create_prompt_response_generation_project(self,
880-
dataset_id: Optional[str] = None,
881-
dataset_name: Optional[str] = None,
882-
data_row_count: int = 100,
883-
**kwargs) -> Project:
883+
def create_prompt_response_generation_project(
884+
self,
885+
dataset_id: Optional[str] = None,
886+
dataset_name: Optional[str] = None,
887+
data_row_count: int = 100,
888+
**kwargs) -> Project:
884889
"""
885890
Use this method exclusively to create a prompt and response generation project.
886891
@@ -915,8 +920,7 @@ def create_prompt_response_generation_project(self,
915920

916921
if dataset_id and dataset_name:
917922
raise ValueError(
918-
"Only provide a dataset_name or dataset_id, not both."
919-
)
923+
"Only provide a dataset_name or dataset_id, not both.")
920924

921925
if data_row_count <= 0:
922926
raise ValueError("data_row_count must be a positive integer.")
@@ -928,7 +932,9 @@ def create_prompt_response_generation_project(self,
928932
append_to_existing_dataset = False
929933
dataset_name_or_id = dataset_name
930934

931-
if "media_type" in kwargs and kwargs.get("media_type") not in [MediaType.LLMPromptCreation, MediaType.LLMPromptResponseCreation]:
935+
if "media_type" in kwargs and kwargs.get("media_type") not in [
936+
MediaType.LLMPromptCreation, MediaType.LLMPromptResponseCreation
937+
]:
932938
raise ValueError(
933939
"media_type must be either LLMPromptCreation or LLMPromptResponseCreation"
934940
)
@@ -949,8 +955,7 @@ def create_response_creation_project(self, **kwargs) -> Project:
949955
Returns:
950956
Project: The created project
951957
"""
952-
kwargs[
953-
"media_type"] = MediaType.Text # Only Text is supported
958+
kwargs["media_type"] = MediaType.Text # Only Text is supported
954959
kwargs[
955960
"editor_task_type"] = EditorTaskType.ResponseCreation.value # Special editor task type for response creation projects
956961

@@ -1005,7 +1010,8 @@ def _create_project(self, **kwargs) -> Project:
10051010

10061011
if quality_modes and quality_mode:
10071012
raise ValueError(
1008-
"Cannot use both quality_modes and quality_mode at the same time. Use one or the other.")
1013+
"Cannot use both quality_modes and quality_mode at the same time. Use one or the other."
1014+
)
10091015

10101016
if not quality_modes and not quality_mode:
10111017
logger.info("Defaulting quality modes to Benchmark and Consensus.")
@@ -1021,12 +1027,11 @@ def _create_project(self, **kwargs) -> Project:
10211027
if quality_mode:
10221028
quality_modes_set = {quality_mode}
10231029

1024-
if (
1025-
quality_modes_set is None
1026-
or len(quality_modes_set) == 0
1027-
or quality_modes_set == {QualityMode.Benchmark, QualityMode.Consensus}
1028-
):
1029-
data["auto_audit_number_of_labels"] = CONSENSUS_AUTO_AUDIT_NUMBER_OF_LABELS
1030+
if (quality_modes_set is None or len(quality_modes_set) == 0 or
1031+
quality_modes_set
1032+
== {QualityMode.Benchmark, QualityMode.Consensus}):
1033+
data[
1034+
"auto_audit_number_of_labels"] = CONSENSUS_AUTO_AUDIT_NUMBER_OF_LABELS
10301035
data["auto_audit_percentage"] = CONSENSUS_AUTO_AUDIT_PERCENTAGE
10311036
data["is_benchmark_enabled"] = True
10321037
data["is_consensus_enabled"] = True
@@ -1297,10 +1302,12 @@ def create_ontology_from_feature_schemas(
12971302
f"Tool `{tool}` not in list of supported tools.")
12981303
elif 'type' in feature_schema.normalized:
12991304
classification = feature_schema.normalized['type']
1300-
if classification in Classification.Type._value2member_map_.keys():
1305+
if classification in Classification.Type._value2member_map_.keys(
1306+
):
13011307
Classification.Type(classification)
13021308
classifications.append(feature_schema.normalized)
1303-
elif classification in PromptResponseClassification.Type._value2member_map_.keys():
1309+
elif classification in PromptResponseClassification.Type._value2member_map_.keys(
1310+
):
13041311
PromptResponseClassification.Type(classification)
13051312
classifications.append(feature_schema.normalized)
13061313
else:
@@ -1518,7 +1525,8 @@ def create_ontology(self,
15181525
raise get_media_type_validation_error(media_type)
15191526

15201527
if ontology_kind and OntologyKind.is_supported(ontology_kind):
1521-
media_type = OntologyKind.evaluate_ontology_kind_with_media_type(ontology_kind, media_type)
1528+
media_type = OntologyKind.evaluate_ontology_kind_with_media_type(
1529+
ontology_kind, media_type)
15221530
editor_task_type_value = EditorTaskTypeMapper.to_editor_task_type(
15231531
ontology_kind, media_type).value
15241532
elif ontology_kind:

libs/labelbox/src/labelbox/schema/labeling_service.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010

1111
Cuid = Annotated[str, Field(min_length=25, max_length=25)]
1212

13+
1314
class LabelingServiceStatus(Enum):
14-
Accepted = 'ACCEPTED',
15-
Calibration = 'CALIBRATION',
16-
Complete = 'COMPLETE',
17-
Production = 'PRODUCTION',
18-
Requested = 'REQUESTED',
15+
Accepted = 'ACCEPTED'
16+
Calibration = 'CALIBRATION'
17+
Complete = 'COMPLETE'
18+
Production = 'PRODUCTION'
19+
Requested = 'REQUESTED'
1920
SetUp = 'SET_UP'
2021

2122

@@ -40,7 +41,7 @@ class Config(_CamelCaseMixin.Config):
4041
@classmethod
4142
def start(cls, client, project_id: Cuid) -> 'LabelingService':
4243
"""
43-
Starts the labeling service for the project. This is equivalent to a UI acction to Request Specialized Labelers
44+
Starts the labeling service for the project. This is equivalent to a UI action to Request Specialized Labelers
4445
4546
Returns:
4647
LabelingService: The labeling service for the project.
@@ -58,6 +59,34 @@ def start(cls, client, project_id: Cuid) -> 'LabelingService':
5859
raise Exception("Failed to start labeling service")
5960
return cls.get(client, project_id)
6061

62+
def request(self) -> 'LabelingService':
63+
"""
64+
Creates a request to labeling service to start labeling for the project.
65+
Our back end will validate that the project is ready for labeling and then request the labeling service.
66+
67+
Returns:
68+
LabelingService: The labeling service for the project.
69+
Raises:
70+
ResourceNotFoundError: If ontology is not associated with the project
71+
or if any projects required prerequisites are missing.
72+
73+
"""
74+
75+
query_str = """mutation ValidateAndRequestProjectBoostWorkforcePyApi($projectId: ID!) {
76+
validateAndRequestProjectBoostWorkforce(
77+
data: { projectId: $projectId }
78+
) {
79+
success
80+
}
81+
}
82+
"""
83+
result = self.client.execute(query_str, {"projectId": self.project_id},
84+
raise_return_resource_not_found=True)
85+
success = result["validateAndRequestProjectBoostWorkforce"]["success"]
86+
if not success:
87+
raise Exception("Failed to start labeling service")
88+
return LabelingService.get(self.client, self.project_id)
89+
6190
@classmethod
6291
def get(cls, client, project_id: Cuid) -> 'LabelingService':
6392
"""

libs/labelbox/tests/conftest.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@ def rest_url(environ: str) -> str:
130130
def testing_api_key(environ: Environ) -> str:
131131
keys = [
132132
f"LABELBOX_TEST_API_KEY_{environ.value.upper()}",
133-
"LABELBOX_TEST_API_KEY",
134-
"LABELBOX_API_KEY"
133+
"LABELBOX_TEST_API_KEY", "LABELBOX_API_KEY"
135134
]
136135
for key in keys:
137136
value = os.environ.get(key)
@@ -318,11 +317,7 @@ def environ() -> Environ:
318317
'prod' or 'staging'
319318
Make sure to set LABELBOX_TEST_ENVIRON in .github/workflows/python-package.yaml
320319
"""
321-
keys = [
322-
"LABELBOX_TEST_ENV",
323-
"LABELBOX_TEST_ENVIRON",
324-
"LABELBOX_ENV"
325-
]
320+
keys = ["LABELBOX_TEST_ENV", "LABELBOX_TEST_ENVIRON", "LABELBOX_ENV"]
326321
for key in keys:
327322
value = os.environ.get(key)
328323
if value is not None:
@@ -742,6 +737,23 @@ def configured_batch_project_with_multiple_datarows(project, dataset, data_rows,
742737
label.delete()
743738

744739

740+
@pytest.fixture
741+
def configured_batch_project_for_labeling_service(project,
742+
data_row_and_global_key):
743+
"""Project with a batch having multiple datarows
744+
Project contains an ontology with 1 bbox tool
745+
Additionally includes a create_label method for any needed extra labels
746+
"""
747+
global_keys = [data_row_and_global_key[1]]
748+
749+
batch_name = f'batch {uuid.uuid4()}'
750+
project.create_batch(batch_name, global_keys=global_keys)
751+
752+
_setup_ontology(project)
753+
754+
yield project
755+
756+
745757
# NOTE this is nice heuristics, also there is this logic _wait_until_data_rows_are_processed in Project
746758
# in case we still have flakiness in the future, we can use it
747759
@pytest.fixture

libs/labelbox/tests/integration/test_labeling_service.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from labelbox.exceptions import ResourceNotFoundError
3+
from labelbox.exceptions import LabelboxError, ResourceNotFoundError
44
from labelbox.schema.labeling_service import LabelingServiceStatus
55

66

@@ -23,3 +23,70 @@ def test_start_labeling_service(project):
2323

2424
labeling_service_status = project.get_labeling_service_status()
2525
assert labeling_service_status == LabelingServiceStatus.SetUp
26+
27+
28+
def test_request_labeling_service(
29+
configured_batch_project_for_labeling_service):
30+
project = configured_batch_project_for_labeling_service
31+
32+
project.upsert_instructions('tests/integration/media/sample_pdf.pdf')
33+
34+
labeling_service = project.request_labeling_service(
35+
) # project fixture is an Image type project
36+
labeling_service.request()
37+
assert project.get_labeling_service_status(
38+
) == LabelingServiceStatus.Requested
39+
40+
41+
def test_request_labeling_service_moe_offline_project(
42+
rand_gen, offline_chat_evaluation_project, chat_evaluation_ontology,
43+
offline_conversational_data_row, model_config):
44+
project = offline_chat_evaluation_project
45+
project.connect_ontology(chat_evaluation_ontology)
46+
47+
project.create_batch(
48+
rand_gen(str),
49+
[offline_conversational_data_row.uid], # sample of data row objects
50+
)
51+
52+
project.upsert_instructions('tests/integration/media/sample_pdf.pdf')
53+
54+
labeling_service = project.request_labeling_service()
55+
labeling_service.request()
56+
assert project.get_labeling_service_status(
57+
) == LabelingServiceStatus.Requested
58+
59+
60+
def test_request_labeling_service_moe_project(
61+
rand_gen, live_chat_evaluation_project_with_new_dataset,
62+
chat_evaluation_ontology, model_config):
63+
project = live_chat_evaluation_project_with_new_dataset
64+
project.connect_ontology(chat_evaluation_ontology)
65+
66+
project.upsert_instructions('tests/integration/media/sample_pdf.pdf')
67+
68+
labeling_service = project.request_labeling_service()
69+
with pytest.raises(
70+
LabelboxError,
71+
match=
72+
'[{"errorType":"PROJECT_MODEL_CONFIG","errorMessage":"Project model config is not completed"}]'
73+
):
74+
labeling_service.request()
75+
project.add_model_config(model_config.uid)
76+
project.set_project_model_setup_complete()
77+
78+
labeling_service.request()
79+
assert project.get_labeling_service_status(
80+
) == LabelingServiceStatus.Requested
81+
82+
83+
def test_request_labeling_service_incomplete_requirements(project, ontology):
84+
labeling_service = project.request_labeling_service(
85+
) # project fixture is an Image type project
86+
with pytest.raises(ResourceNotFoundError,
87+
match="Associated ontology id could not be found"
88+
): # No labeling service by default
89+
labeling_service.request()
90+
project.connect_ontology(ontology)
91+
with pytest.raises(LabelboxError):
92+
labeling_service.request()

0 commit comments

Comments
 (0)