Skip to content

Commit bfa1df9

Browse files
committed
Add workflow management API - Resolves PLT-2503
1 parent 47a1eb6 commit bfa1df9

File tree

17 files changed

+7321
-0
lines changed

17 files changed

+7321
-0
lines changed

libs/labelbox/src/labelbox/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,4 @@
101101
from labelbox.schema.taskstatus import TaskStatus
102102
from labelbox.schema.api_key import ApiKey
103103
from labelbox.schema.timeunit import TimeUnit
104+
from labelbox.schema.workflow import ProjectWorkflow

libs/labelbox/src/labelbox/schema/project.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
ProjectOverview,
6060
ProjectOverviewDetailed,
6161
)
62+
from labelbox.schema.workflow import ProjectWorkflow
6263
from labelbox.schema.resource_tag import ResourceTag
6364
from labelbox.schema.task import Task
6465
from labelbox.schema.task_queue import TaskQueue
@@ -1702,6 +1703,45 @@ def get_labeling_service_dashboard(self) -> LabelingServiceDashboard:
17021703
"""
17031704
return LabelingServiceDashboard.get(self.client, self.uid)
17041705

1706+
def get_workflow(self):
1707+
"""Get the workflow configuration for this project.
1708+
1709+
Workflows are automatically created when projects are created.
1710+
1711+
Returns:
1712+
ProjectWorkflow: A ProjectWorkflow object containing the project workflow information.
1713+
"""
1714+
warnings.warn(
1715+
"Workflow Management is currently in alpha and its behavior may change in future releases.",
1716+
)
1717+
1718+
return ProjectWorkflow.get_workflow(self.client, self.uid)
1719+
1720+
def clone_workflow_from(self, source_project_id: str) -> "ProjectWorkflow":
1721+
"""Clones a workflow from another project to this project.
1722+
1723+
Args:
1724+
source_project_id (str): The ID of the project to clone the workflow from
1725+
1726+
Returns:
1727+
ProjectWorkflow: The cloned workflow in this project
1728+
"""
1729+
warnings.warn(
1730+
"Workflow Management is currently in alpha and its behavior may change in future releases.",
1731+
)
1732+
1733+
# Get the source workflow
1734+
source_workflow = ProjectWorkflow.get_workflow(
1735+
self.client, source_project_id
1736+
)
1737+
1738+
# Use copy_workflow_structure to clone the workflow
1739+
return ProjectWorkflow.copy_workflow_structure(
1740+
source_workflow=source_workflow,
1741+
target_client=self.client,
1742+
target_project_id=self.uid,
1743+
)
1744+
17051745

17061746
class ProjectMember(DbObject):
17071747
user = Relationship.ToOne("User", cache=True)
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""
2+
This module contains classes for managing project workflows in Labelbox.
3+
It provides strongly-typed classes for nodes, edges, and workflow configuration.
4+
"""
5+
6+
# Import all workflow classes to expose them at the package level
7+
from labelbox.schema.workflow.enums import (
8+
WorkflowDefinitionId,
9+
NodeOutput,
10+
NodeInput,
11+
MatchFilters,
12+
Scope,
13+
FilterField,
14+
FilterOperator,
15+
IndividualAssignment,
16+
)
17+
from labelbox.schema.workflow.base import (
18+
BaseWorkflowNode,
19+
NodePosition,
20+
)
21+
22+
# Import nodes from nodes.py (excluding LogicNode)
23+
from labelbox.schema.workflow.nodes import (
24+
InitialLabelingNode,
25+
InitialReworkNode,
26+
ReviewNode,
27+
ReworkNode,
28+
DoneNode,
29+
CustomReworkNode,
30+
UnknownWorkflowNode,
31+
)
32+
33+
# Import LogicNode from its own module
34+
from labelbox.schema.workflow.logic_node import LogicNode
35+
36+
# Import AutoQANode from its own module
37+
from labelbox.schema.workflow.autoqa_node import AutoQANode
38+
39+
from labelbox.schema.workflow.edges import (
40+
WorkflowEdge,
41+
WorkflowEdgeFactory,
42+
)
43+
from labelbox.schema.workflow.graph import ProjectWorkflowGraph
44+
45+
# Import from monolithic workflow.py file
46+
from labelbox.schema.workflow.workflow import ProjectWorkflow, NodeType
47+
48+
# Import from monolithic project_filter.py file
49+
from labelbox.schema.workflow.project_filter import (
50+
ProjectWorkflowFilter,
51+
created_by,
52+
labeled_by,
53+
annotation,
54+
dataset,
55+
issue_category,
56+
sample,
57+
metadata,
58+
model_prediction,
59+
natural_language,
60+
labeling_time,
61+
review_time,
62+
labeled_at,
63+
consensus_average,
64+
batch,
65+
feature_consensus_average,
66+
MetadataCondition,
67+
ModelPredictionCondition,
68+
m_condition,
69+
mp_condition,
70+
convert_to_api_format,
71+
)
72+
73+
# Re-export key classes at the module level
74+
__all__ = [
75+
# Core workflow components
76+
"WorkflowDefinitionId",
77+
"NodeOutput",
78+
"NodeInput",
79+
"MatchFilters",
80+
"Scope",
81+
"FilterField",
82+
"FilterOperator",
83+
"IndividualAssignment",
84+
"BaseWorkflowNode",
85+
"NodePosition",
86+
"InitialLabelingNode",
87+
"InitialReworkNode",
88+
"ReviewNode",
89+
"ReworkNode",
90+
"LogicNode",
91+
"DoneNode",
92+
"CustomReworkNode",
93+
"AutoQANode",
94+
"UnknownWorkflowNode",
95+
"WorkflowEdge",
96+
"WorkflowEdgeFactory",
97+
"ProjectWorkflow",
98+
"NodeType",
99+
"ProjectWorkflowGraph",
100+
"ProjectWorkflowFilter",
101+
# Filter construction functions
102+
"created_by",
103+
"labeled_by",
104+
"annotation",
105+
"sample",
106+
"dataset",
107+
"issue_category",
108+
"model_prediction",
109+
"natural_language",
110+
"labeled_at",
111+
"labeling_time",
112+
"review_time",
113+
"consensus_average",
114+
"batch",
115+
"feature_consensus_average",
116+
"metadata",
117+
"MetadataCondition",
118+
"ModelPredictionCondition",
119+
"m_condition",
120+
"mp_condition",
121+
# Utility functions
122+
"convert_to_api_format",
123+
]
124+
125+
# Define a mapping of node types for workflow creation
126+
NODE_TYPE_MAP = {
127+
WorkflowDefinitionId.InitialLabelingTask: InitialLabelingNode,
128+
WorkflowDefinitionId.InitialReworkTask: InitialReworkNode,
129+
WorkflowDefinitionId.ReviewTask: ReviewNode,
130+
WorkflowDefinitionId.SendToRework: ReworkNode,
131+
WorkflowDefinitionId.Logic: LogicNode,
132+
WorkflowDefinitionId.Done: DoneNode,
133+
WorkflowDefinitionId.CustomReworkTask: CustomReworkNode,
134+
WorkflowDefinitionId.AutoQA: AutoQANode,
135+
WorkflowDefinitionId.Unknown: UnknownWorkflowNode,
136+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"""AutoQA node for automated quality assessment with pass/fail routing.
2+
3+
This module contains the AutoQANode class which performs automated quality assessment
4+
using configured evaluators and score thresholds.
5+
"""
6+
7+
from typing import Dict, List, Any, Optional, Literal
8+
from pydantic import Field, model_validator, field_validator
9+
10+
from labelbox.schema.workflow.base import BaseWorkflowNode
11+
from labelbox.schema.workflow.enums import (
12+
WorkflowDefinitionId,
13+
NodeOutput,
14+
)
15+
16+
# Constants for this module
17+
DEFAULT_FILTER_LOGIC_AND = "and"
18+
19+
20+
class AutoQANode(BaseWorkflowNode):
21+
"""
22+
Automated Quality Assessment node with pass/fail routing.
23+
24+
This node performs automated quality assessment using configured evaluators
25+
and score thresholds. Work that meets the quality criteria is routed to the
26+
"if" output (passed), while work that fails is routed to the "else" output.
27+
28+
Attributes:
29+
label (str): Display name for the node (default: "Label Score (AutoQA)")
30+
filters (List[Dict[str, Any]]): Filter conditions for the AutoQA node
31+
filter_logic (str): Logic for combining filters ("and" or "or", default: "and")
32+
custom_fields (Dict[str, Any]): Additional custom configuration
33+
definition_id (WorkflowDefinitionId): Node type identifier (read-only)
34+
node_config (List[Dict[str, Any]]): API configuration for evaluator settings
35+
evaluator_id (Optional[str]): ID of the evaluator for AutoQA assessment
36+
scope (Optional[str]): Scope setting for AutoQA ("any" or "all")
37+
score_name (Optional[str]): Name of the score metric for evaluation
38+
score_threshold (Optional[float]): Threshold score for pass/fail determination
39+
40+
Inputs:
41+
Default: Must have exactly one input connection
42+
43+
Outputs:
44+
If: Route for work that passes quality assessment (score >= threshold)
45+
Else: Route for work that fails quality assessment (score < threshold)
46+
47+
AutoQA Configuration:
48+
- evaluator_id: Specifies which evaluator to use for assessment
49+
- scope: Determines evaluation scope ("any" or "all" annotations)
50+
- score_name: The specific score metric to evaluate
51+
- score_threshold: Minimum score required to pass
52+
- Automatically syncs configuration with API format
53+
54+
Validation:
55+
- Must have exactly one input connection
56+
- Both passed and failed outputs can be connected
57+
- AutoQA settings are automatically converted to API configuration
58+
- Evaluator and scoring parameters are validated
59+
60+
Example:
61+
>>> autoqa = AutoQANode(
62+
... label="Quality Gate",
63+
... evaluator_id="evaluator-123",
64+
... scope="all",
65+
... score_name="accuracy",
66+
... score_threshold=0.85
67+
... )
68+
>>> # Route high-quality work to done, low-quality to review
69+
>>> workflow.add_edge(autoqa, done_node, NodeOutput.If)
70+
>>> workflow.add_edge(autoqa, review_node, NodeOutput.Else)
71+
72+
Quality Assessment:
73+
AutoQA nodes enable automated quality control by evaluating work
74+
against trained models or rule-based evaluators. This reduces manual
75+
review overhead while maintaining quality standards.
76+
77+
Note:
78+
AutoQA requires properly configured evaluators and score thresholds.
79+
The evaluation results determine automatic routing without human intervention.
80+
"""
81+
82+
label: str = Field(default="Label Score (AutoQA)")
83+
filters: List[Dict[str, Any]] = Field(
84+
default_factory=lambda: [],
85+
description="Contains the filters for the AutoQA node",
86+
)
87+
filter_logic: Literal["and", "or"] = Field(
88+
default=DEFAULT_FILTER_LOGIC_AND, alias="filterLogic"
89+
)
90+
custom_fields: Dict[str, Any] = Field(
91+
default_factory=lambda: {},
92+
alias="customFields",
93+
)
94+
definition_id: WorkflowDefinitionId = Field(
95+
default=WorkflowDefinitionId.AutoQA,
96+
frozen=True,
97+
alias="definitionId",
98+
)
99+
node_config: List[Dict[str, Any]] = Field(
100+
default_factory=lambda: [],
101+
description="Contains evaluator_id, scope, score_name, score_threshold etc.",
102+
alias="config",
103+
)
104+
105+
# AutoQA-specific fields
106+
evaluator_id: Optional[str] = Field(
107+
default=None,
108+
description="ID of the evaluator for AutoQA",
109+
)
110+
scope: Optional[str] = Field(
111+
default=None,
112+
description="Scope setting for AutoQA (any/all)",
113+
)
114+
score_name: Optional[str] = Field(
115+
default=None,
116+
description="Name of the score for AutoQA",
117+
)
118+
score_threshold: Optional[float] = Field(
119+
default=None,
120+
description="Threshold score for AutoQA",
121+
)
122+
123+
@model_validator(mode="after")
124+
def sync_autoqa_config_with_node_config(self) -> "AutoQANode":
125+
"""Sync AutoQA-specific fields with node_config."""
126+
127+
# Clear existing AutoQA config
128+
self.node_config = [
129+
config
130+
for config in self.node_config
131+
if config.get("field")
132+
not in ["evaluator_id", "scope", "score_name", "score_threshold"]
133+
]
134+
135+
# Add evaluator_id if present
136+
if self.evaluator_id is not None:
137+
self.node_config.append(
138+
{
139+
"field": "evaluator_id",
140+
"value": self.evaluator_id,
141+
"metadata": None,
142+
}
143+
)
144+
145+
# Add scope if present
146+
if self.scope is not None:
147+
self.node_config.append(
148+
{"field": "scope", "value": self.scope, "metadata": None}
149+
)
150+
151+
# Add score_name if present
152+
if self.score_name is not None:
153+
self.node_config.append(
154+
{
155+
"field": "score_name",
156+
"value": self.score_name,
157+
"metadata": None,
158+
}
159+
)
160+
161+
# Add score_threshold if present
162+
if self.score_threshold is not None:
163+
self.node_config.append(
164+
{
165+
"field": "score_threshold",
166+
"value": self.score_threshold,
167+
"metadata": None,
168+
}
169+
)
170+
171+
return self
172+
173+
@field_validator("inputs")
174+
@classmethod
175+
def validate_inputs(cls, v) -> List[str]:
176+
"""Validate that AutoQA node has exactly one input."""
177+
if len(v) != 1:
178+
raise ValueError("AutoQA node must have exactly one input")
179+
return v
180+
181+
@property
182+
def supported_outputs(self) -> List[NodeOutput]:
183+
"""Returns the list of supported output types for this node."""
184+
return [NodeOutput.If, NodeOutput.Else] # Passed (if) and Failed (else)

0 commit comments

Comments
 (0)