Skip to content

Commit e6267da

Browse files
committed
Add workflow management
1 parent 777bfc0 commit e6267da

File tree

18 files changed

+5991
-0
lines changed

18 files changed

+5991
-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: 36 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,41 @@ 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+
from labelbox.schema.workflow import ProjectWorkflow
1715+
1716+
return ProjectWorkflow.get_workflow(self.client, self.uid)
1717+
1718+
def clone_workflow_from(self, source_project_id: str) -> "ProjectWorkflow":
1719+
"""Clones a workflow from another project to this project.
1720+
1721+
Args:
1722+
source_project_id (str): The ID of the project to clone the workflow from
1723+
1724+
Returns:
1725+
ProjectWorkflow: The cloned workflow in this project
1726+
"""
1727+
from labelbox.schema.workflow import ProjectWorkflow
1728+
1729+
# Get the source workflow
1730+
source_workflow = ProjectWorkflow.get_workflow(
1731+
self.client, source_project_id
1732+
)
1733+
1734+
# Use copy_workflow_structure to clone the workflow
1735+
return ProjectWorkflow.copy_workflow_structure(
1736+
source_workflow=source_workflow,
1737+
target_client=self.client,
1738+
target_project_id=self.uid,
1739+
)
1740+
17051741

17061742
class ProjectMember(DbObject):
17071743
user = Relationship.ToOne("User", cache=True)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
FilterField,
11+
FilterOperator,
12+
)
13+
from labelbox.schema.workflow.filters import (
14+
Filter,
15+
UserFilter,
16+
MetadataFilter,
17+
TimeRangeFilter,
18+
DatasetFilter,
19+
AnnotationFilter,
20+
NLSearchFilter,
21+
)
22+
from labelbox.schema.workflow.base import (
23+
BaseWorkflowNode,
24+
NodePosition,
25+
)
26+
from labelbox.schema.workflow.nodes import (
27+
InitialLabelingNode,
28+
InitialReworkNode,
29+
ReviewNode,
30+
ReworkNode,
31+
LogicNode,
32+
DoneNode,
33+
CustomReworkNode,
34+
UnknownWorkflowNode,
35+
)
36+
from labelbox.schema.workflow.edges import (
37+
WorkflowEdge,
38+
WorkflowEdgeFactory,
39+
)
40+
from labelbox.schema.workflow.graph import ProjectWorkflowGraph
41+
from labelbox.schema.workflow.project_filter import ProjectWorkflowFilter
42+
from labelbox.schema.workflow.workflow import ProjectWorkflow
43+
44+
# Re-export key classes at the module level
45+
__all__ = [
46+
"WorkflowDefinitionId",
47+
"NodeOutput",
48+
"FilterField",
49+
"FilterOperator",
50+
"BaseWorkflowNode",
51+
"NodePosition",
52+
"Filter",
53+
"UserFilter",
54+
"MetadataFilter",
55+
"TimeRangeFilter",
56+
"DatasetFilter",
57+
"AnnotationFilter",
58+
"NLSearchFilter",
59+
"InitialLabelingNode",
60+
"InitialReworkNode",
61+
"ReviewNode",
62+
"ReworkNode",
63+
"LogicNode",
64+
"DoneNode",
65+
"CustomReworkNode",
66+
"UnknownWorkflowNode",
67+
"WorkflowEdge",
68+
"WorkflowEdgeFactory",
69+
"ProjectWorkflow",
70+
"ProjectWorkflowGraph",
71+
"ProjectWorkflowFilter",
72+
]
73+
74+
# Define a mapping of node types for backward compatibility
75+
NODE_TYPE_MAP = {
76+
WorkflowDefinitionId.InitialLabelingTask: InitialLabelingNode,
77+
WorkflowDefinitionId.InitialReworkTask: InitialReworkNode,
78+
WorkflowDefinitionId.ReviewTask: ReviewNode,
79+
WorkflowDefinitionId.SendToRework: ReworkNode,
80+
WorkflowDefinitionId.Logic: LogicNode,
81+
WorkflowDefinitionId.Done: DoneNode,
82+
WorkflowDefinitionId.CustomReworkTask: CustomReworkNode,
83+
WorkflowDefinitionId.Unknown: UnknownWorkflowNode,
84+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Base classes for Project Workflow nodes and common utilities."""
2+
3+
import logging
4+
from typing import Dict, List, Any, Optional
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
from labelbox.schema.workflow.enums import WorkflowDefinitionId, NodeOutput
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class NodePosition(BaseModel):
13+
"""Position of a node in the workflow canvas."""
14+
15+
x: float
16+
y: float
17+
18+
19+
class BaseWorkflowNode(BaseModel):
20+
"""Base class for all workflow node types."""
21+
22+
id: str
23+
position: NodePosition
24+
raw_data: Dict[str, Any] = Field(
25+
default_factory=dict,
26+
description="Store the original dict for reference",
27+
)
28+
inputs: List[str] = Field(
29+
default_factory=list, description="Input connection identifiers"
30+
)
31+
output_if: Optional[str] = Field(
32+
default=None, description="Primary/if output connection identifier"
33+
)
34+
output_else: Optional[str] = Field(
35+
default=None, description="Secondary/else output connection identifier"
36+
)
37+
definition_id: WorkflowDefinitionId = Field(
38+
default=WorkflowDefinitionId.Unknown,
39+
frozen=True,
40+
alias="definitionId",
41+
)
42+
model_config = ConfigDict(
43+
arbitrary_types_allowed=True,
44+
populate_by_name=True,
45+
)
46+
47+
def _update_node_in_workflow(self) -> None:
48+
"""Update the node data in the workflow's config."""
49+
workflow = self.raw_data.get("_workflow")
50+
if workflow and hasattr(workflow, "config"):
51+
for node_data in workflow.config.get("nodes", []):
52+
if node_data.get("id") == self.id:
53+
# Update the workflow config with current node state
54+
if hasattr(self, "label"):
55+
node_data["label"] = self.label
56+
if hasattr(self, "custom_fields"):
57+
node_data["customFields"] = self.custom_fields
58+
if (
59+
hasattr(self, "instructions")
60+
and self.instructions is not None
61+
):
62+
# Ensure customFields exists
63+
if "customFields" not in node_data:
64+
node_data["customFields"] = {}
65+
node_data["customFields"]["description"] = (
66+
self.instructions
67+
)
68+
break
69+
70+
@property
71+
def name(self) -> Optional[str]:
72+
"""Get the node's name (label)."""
73+
return self.raw_data.get("label")
74+
75+
@name.setter
76+
def name(self, value: str) -> None:
77+
"""Set the node's name (label)."""
78+
if hasattr(self, "label"):
79+
self.label = value
80+
self.raw_data["label"] = value
81+
self._update_node_in_workflow()
82+
83+
@property
84+
def supported_outputs(self) -> List[NodeOutput]:
85+
"""Returns the list of supported output types for this node."""
86+
return []
87+
88+
@property
89+
def config(self) -> Dict[str, Any]:
90+
"""Returns the node's configuration."""
91+
return self.raw_data
92+
93+
def __repr__(self):
94+
"""String representation of the node."""
95+
return f"{self.__class__.__name__}(id={self.id}, name={self.name})"

0 commit comments

Comments
 (0)