synapse-sdk 2025.9.5__py3-none-any.whl → 2025.10.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synapse-sdk might be problematic. Click here for more details.

Files changed (78) hide show
  1. synapse_sdk/clients/base.py +129 -9
  2. synapse_sdk/devtools/docs/docs/api/clients/base.md +230 -8
  3. synapse_sdk/devtools/docs/docs/api/plugins/models.md +58 -3
  4. synapse_sdk/devtools/docs/docs/plugins/categories/neural-net-plugins/train-action-overview.md +663 -0
  5. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  6. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  7. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  8. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  9. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  10. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md +585 -0
  11. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  12. synapse_sdk/devtools/docs/docs/plugins/export-plugins.md +39 -0
  13. synapse_sdk/devtools/docs/docs/plugins/plugins.md +12 -5
  14. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/base.md +230 -8
  15. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/plugins/models.md +114 -0
  16. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/neural-net-plugins/train-action-overview.md +621 -0
  17. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  18. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  19. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  20. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  21. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  22. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md +585 -0
  23. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  24. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/export-plugins.md +39 -0
  25. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current.json +16 -4
  26. synapse_sdk/devtools/docs/sidebars.ts +45 -1
  27. synapse_sdk/plugins/README.md +487 -80
  28. synapse_sdk/plugins/categories/base.py +1 -0
  29. synapse_sdk/plugins/categories/export/actions/export/action.py +8 -3
  30. synapse_sdk/plugins/categories/export/actions/export/utils.py +108 -8
  31. synapse_sdk/plugins/categories/export/templates/config.yaml +18 -0
  32. synapse_sdk/plugins/categories/export/templates/plugin/export.py +97 -0
  33. synapse_sdk/plugins/categories/neural_net/actions/train.py +592 -22
  34. synapse_sdk/plugins/categories/neural_net/actions/tune.py +150 -3
  35. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  36. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  37. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  38. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  39. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +145 -0
  40. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  41. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  42. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  43. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +97 -0
  44. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +250 -0
  45. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  46. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  47. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +284 -0
  48. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  49. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  50. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +87 -0
  51. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +127 -0
  52. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  53. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +2 -1
  54. synapse_sdk/plugins/categories/upload/actions/upload/action.py +8 -1
  55. synapse_sdk/plugins/categories/upload/actions/upload/context.py +0 -1
  56. synapse_sdk/plugins/categories/upload/actions/upload/models.py +134 -94
  57. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +2 -2
  58. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +6 -2
  59. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +24 -9
  60. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +130 -18
  61. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +147 -37
  62. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +10 -5
  63. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +31 -6
  64. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +65 -37
  65. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +17 -2
  66. synapse_sdk/plugins/categories/upload/templates/README.md +394 -0
  67. synapse_sdk/plugins/models.py +62 -0
  68. synapse_sdk/utils/file/download.py +261 -0
  69. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/METADATA +15 -2
  70. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/RECORD +74 -43
  71. synapse_sdk/devtools/docs/docs/plugins/developing-upload-template.md +0 -1463
  72. synapse_sdk/devtools/docs/docs/plugins/upload-plugins.md +0 -1964
  73. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/developing-upload-template.md +0 -1463
  74. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md +0 -2077
  75. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/WHEEL +0 -0
  76. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/entry_points.txt +0 -0
  77. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/licenses/LICENSE +0 -0
  78. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1645 @@
1
+ ---
2
+ id: to-task-action-development
3
+ title: ToTask Action - SDK Developer Guide
4
+ sidebar_position: 3
5
+ ---
6
+
7
+ # ToTask Action - SDK Developer Guide
8
+
9
+ :::info Audience
10
+ This guide is for **SDK core developers** working on the ToTaskAction framework, orchestrator, strategies, and workflow engine.
11
+
12
+ If you're a **plugin developer** creating custom pre-annotation plugins using the AnnotationToTask template, see the [ToTask Template Development](./to-task-template-development.md) guide instead.
13
+ :::
14
+
15
+ This guide provides comprehensive technical documentation for the ToTask action architecture, including design patterns, workflow execution, and extension points for custom strategy implementations.
16
+
17
+ ## Architecture Overview
18
+
19
+ The ToTask action is built on a modern, maintainable architecture using four key design patterns:
20
+
21
+ ### Design Patterns
22
+
23
+ #### 1. Strategy Pattern
24
+
25
+ Enables pluggable algorithms for validation, annotation, metrics, and data extraction. Different strategies can be selected at runtime based on configuration.
26
+
27
+ #### 2. Facade Pattern
28
+
29
+ `ToTaskOrchestrator` provides a simplified interface to the complex 7-stage workflow, handling orchestration, error management, and rollback.
30
+
31
+ #### 3. Factory Pattern
32
+
33
+ `ToTaskStrategyFactory` creates appropriate strategy instances based on runtime parameters, decoupling strategy creation from usage.
34
+
35
+ #### 4. Context Pattern
36
+
37
+ `ToTaskContext` maintains shared state and communication channels between workflow components throughout execution.
38
+
39
+ ## Core Components
40
+
41
+ ### Component Diagram
42
+
43
+ ```mermaid
44
+ classDiagram
45
+ class ToTaskAction {
46
+ +name: str = "to_task"
47
+ +category: PluginCategory.PRE_ANNOTATION
48
+ +method: RunMethod.JOB
49
+ +run_class: ToTaskRun
50
+ +params_model: ToTaskParams
51
+ +progress_categories: dict
52
+ +metrics_categories: dict
53
+
54
+ +start() Dict
55
+ +get_context() ToTaskContext
56
+ }
57
+
58
+ class ToTaskOrchestrator {
59
+ +context: ToTaskContext
60
+ +factory: ToTaskStrategyFactory
61
+ +steps_completed: List[str]
62
+
63
+ +execute_workflow() Dict
64
+ +_execute_step(step_name, step_func)
65
+ +_validate_project()
66
+ +_validate_tasks()
67
+ +_determine_annotation_method()
68
+ +_validate_annotation_method()
69
+ +_initialize_processing()
70
+ +_process_all_tasks()
71
+ +_finalize_processing()
72
+ +_rollback_completed_steps()
73
+ }
74
+
75
+ class ToTaskContext {
76
+ +params: Dict
77
+ +client: BackendClient
78
+ +logger: Any
79
+ +project: Dict
80
+ +data_collection: Dict
81
+ +task_ids: List[int]
82
+ +metrics: MetricsRecord
83
+ +annotation_method: AnnotationMethod
84
+ +temp_files: List[str]
85
+ +rollback_actions: List[callable]
86
+
87
+ +add_temp_file(file_path)
88
+ +add_rollback_action(action)
89
+ +update_metrics(success, failed, total)
90
+ }
91
+
92
+ class ToTaskStrategyFactory {
93
+ +create_validation_strategy(type) ValidationStrategy
94
+ +create_annotation_strategy(method) AnnotationStrategy
95
+ +create_metrics_strategy() MetricsStrategy
96
+ +create_extraction_strategy() DataExtractionStrategy
97
+ }
98
+
99
+ class ValidationStrategy {
100
+ <<abstract>>
101
+ +validate(context) Dict
102
+ }
103
+
104
+ class AnnotationStrategy {
105
+ <<abstract>>
106
+ +process_task(context, task_id, task_data) Dict
107
+ }
108
+
109
+ class MetricsStrategy {
110
+ <<abstract>>
111
+ +update_progress(context, current, total)
112
+ +record_task_result(context, task_id, success, error)
113
+ +update_metrics(context, total, success, failed)
114
+ +finalize_metrics(context)
115
+ }
116
+
117
+ class DataExtractionStrategy {
118
+ <<abstract>>
119
+ +extract_data(context, task_data) Tuple
120
+ }
121
+
122
+ ToTaskAction --> ToTaskOrchestrator : creates
123
+ ToTaskAction --> ToTaskContext : creates
124
+ ToTaskOrchestrator --> ToTaskContext : uses
125
+ ToTaskOrchestrator --> ToTaskStrategyFactory : uses
126
+ ToTaskStrategyFactory --> ValidationStrategy : creates
127
+ ToTaskStrategyFactory --> AnnotationStrategy : creates
128
+ ToTaskStrategyFactory --> MetricsStrategy : creates
129
+ ToTaskStrategyFactory --> DataExtractionStrategy : creates
130
+ ```
131
+
132
+ ### ToTaskAction
133
+
134
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py`
135
+
136
+ Entry point for the annotation action.
137
+
138
+ **Responsibilities:**
139
+
140
+ - Parameter validation using `ToTaskParams` model
141
+ - Execution context creation
142
+ - Orchestrator instantiation and execution
143
+ - Top-level error handling
144
+
145
+ **Key Attributes:**
146
+
147
+ ```python
148
+ class ToTaskAction(BaseAction):
149
+ name = 'to_task'
150
+ category = PluginCategory.PRE_ANNOTATION
151
+ method = RunMethod.JOB
152
+ run_class = ToTaskRun
153
+ params_model = ToTaskParams
154
+
155
+ progress_categories = {
156
+ 'annotate_task_data': {
157
+ 'label': 'Annotating Task Data',
158
+ 'weight': 1.0
159
+ }
160
+ }
161
+
162
+ metrics_categories = {
163
+ 'annotate_task_data': {
164
+ 'label': 'Task Annotation Metrics',
165
+ 'metrics': ['success', 'failed', 'stand_by']
166
+ }
167
+ }
168
+ ```
169
+
170
+ **Main Method:**
171
+
172
+ ```python
173
+ def start(self) -> Dict[str, Any]:
174
+ """Execute the ToTask action workflow"""
175
+ try:
176
+ # Create execution context
177
+ context = self.get_context()
178
+
179
+ # Create and execute orchestrator
180
+ orchestrator = ToTaskOrchestrator(context)
181
+ result = orchestrator.execute_workflow()
182
+
183
+ return {
184
+ 'status': JobStatus.SUCCEEDED,
185
+ 'message': f'Successfully processed {result["total"]} tasks'
186
+ }
187
+ except Exception as e:
188
+ return {
189
+ 'status': JobStatus.FAILED,
190
+ 'message': str(e)
191
+ }
192
+ ```
193
+
194
+ ### ToTaskOrchestrator
195
+
196
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py`
197
+
198
+ Facade that orchestrates the 7-stage workflow.
199
+
200
+ **Responsibilities:**
201
+
202
+ - Execute workflow stages in sequence
203
+ - Track completed steps for rollback
204
+ - Handle errors and trigger rollback
205
+ - Manage strategy instances
206
+
207
+ **Key Methods:**
208
+
209
+ ```python
210
+ class ToTaskOrchestrator:
211
+ def __init__(self, context: ToTaskContext):
212
+ self.context = context
213
+ self.factory = ToTaskStrategyFactory()
214
+ self.steps_completed = []
215
+
216
+ # Initialize validation strategies
217
+ self.project_validation = self.factory.create_validation_strategy('project')
218
+ self.task_validation = self.factory.create_validation_strategy('task')
219
+ self.target_spec_validation = self.factory.create_validation_strategy('target_spec')
220
+
221
+ # Initialize metrics strategy
222
+ self.metrics_strategy = self.factory.create_metrics_strategy()
223
+
224
+ def execute_workflow(self) -> Dict[str, Any]:
225
+ """Execute the complete 7-stage workflow"""
226
+ try:
227
+ # Stage 1: Validate project
228
+ self._execute_step('project_validation', self._validate_project)
229
+
230
+ # Stage 2: Validate tasks
231
+ self._execute_step('task_validation', self._validate_tasks)
232
+
233
+ # Stage 3: Determine annotation method
234
+ self._execute_step('method_determination', self._determine_annotation_method)
235
+
236
+ # Stage 4: Validate annotation method
237
+ self._execute_step('method_validation', self._validate_annotation_method)
238
+
239
+ # Stage 5: Initialize processing
240
+ self._execute_step('processing_initialization', self._initialize_processing)
241
+
242
+ # Stage 6: Process all tasks
243
+ self._execute_step('task_processing', self._process_all_tasks)
244
+
245
+ # Stage 7: Finalize
246
+ self._execute_step('finalization', self._finalize_processing)
247
+
248
+ return self.context.metrics
249
+
250
+ except Exception as e:
251
+ # Rollback on any error
252
+ self._rollback_completed_steps()
253
+ raise
254
+
255
+ def _execute_step(self, step_name: str, step_func: callable):
256
+ """Execute a workflow step and track completion"""
257
+ step_func()
258
+ self.steps_completed.append(step_name)
259
+ self.context.logger.log_message_with_code(
260
+ LogCode.STEP_COMPLETED, step_name
261
+ )
262
+ ```
263
+
264
+ ### ToTaskContext
265
+
266
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py`
267
+
268
+ Shared execution context for workflow components.
269
+
270
+ **Key Attributes:**
271
+
272
+ ```python
273
+ class ToTaskContext:
274
+ def __init__(self, params: Dict, client: Any, logger: Any):
275
+ # Configuration
276
+ self.params = params
277
+ self.client = client
278
+ self.logger = logger
279
+
280
+ # Project data
281
+ self.project: Optional[Dict] = None
282
+ self.data_collection: Optional[Dict] = None
283
+
284
+ # Task data
285
+ self.task_ids: List[int] = []
286
+
287
+ # Execution state
288
+ self.annotation_method: Optional[AnnotationMethod] = None
289
+ self.metrics = MetricsRecord(success=0, failed=0, total=0)
290
+
291
+ # Cleanup tracking
292
+ self.temp_files: List[str] = []
293
+ self.rollback_actions: List[callable] = []
294
+ ```
295
+
296
+ **Helper Methods:**
297
+
298
+ ```python
299
+ def add_temp_file(self, file_path: str):
300
+ """Register a temporary file for cleanup"""
301
+ self.temp_files.append(file_path)
302
+
303
+ def add_rollback_action(self, action: callable):
304
+ """Register a rollback action"""
305
+ self.rollback_actions.append(action)
306
+
307
+ def update_metrics(self, success_count: int, failed_count: int, total_count: int):
308
+ """Update metrics counters"""
309
+ self.metrics.success = success_count
310
+ self.metrics.failed = failed_count
311
+ self.metrics.total = total_count
312
+ ```
313
+
314
+ ### ToTaskStrategyFactory
315
+
316
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py`
317
+
318
+ Factory for creating strategy instances.
319
+
320
+ **Implementation:**
321
+
322
+ ```python
323
+ class ToTaskStrategyFactory:
324
+ def create_validation_strategy(
325
+ self, strategy_type: str
326
+ ) -> ValidationStrategy:
327
+ """Create validation strategy by type"""
328
+ strategies = {
329
+ 'project': ProjectValidationStrategy,
330
+ 'task': TaskValidationStrategy,
331
+ 'target_spec': TargetSpecValidationStrategy
332
+ }
333
+
334
+ strategy_class = strategies.get(strategy_type)
335
+ if not strategy_class:
336
+ raise ValueError(f'Unknown validation strategy: {strategy_type}')
337
+
338
+ return strategy_class()
339
+
340
+ def create_annotation_strategy(
341
+ self, method: AnnotationMethod
342
+ ) -> AnnotationStrategy:
343
+ """Create annotation strategy by method"""
344
+ if method == AnnotationMethod.FILE:
345
+ return FileAnnotationStrategy()
346
+ elif method == AnnotationMethod.INFERENCE:
347
+ return InferenceAnnotationStrategy()
348
+ else:
349
+ raise ValueError(f'Unknown annotation method: {method}')
350
+
351
+ def create_metrics_strategy(self) -> MetricsStrategy:
352
+ """Create metrics strategy"""
353
+ return ToTaskMetricsStrategy()
354
+
355
+ def create_extraction_strategy(self) -> DataExtractionStrategy:
356
+ """Create data extraction strategy"""
357
+ return FileDataExtractionStrategy()
358
+ ```
359
+
360
+ ## Strategy Architecture
361
+
362
+ ### Strategy Base Classes
363
+
364
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py`
365
+
366
+ #### ValidationStrategy
367
+
368
+ ```python
369
+ class ValidationStrategy(ABC):
370
+ """Base class for validation strategies"""
371
+
372
+ @abstractmethod
373
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
374
+ """
375
+ Perform validation
376
+
377
+ Args:
378
+ context: Execution context
379
+
380
+ Returns:
381
+ Dict with 'success' (bool) and optional 'error' (str)
382
+ """
383
+ pass
384
+ ```
385
+
386
+ #### AnnotationStrategy
387
+
388
+ ```python
389
+ class AnnotationStrategy(ABC):
390
+ """Base class for annotation strategies"""
391
+
392
+ @abstractmethod
393
+ def process_task(
394
+ self,
395
+ context: ToTaskContext,
396
+ task_id: int,
397
+ task_data: Dict[str, Any],
398
+ **kwargs
399
+ ) -> Dict[str, Any]:
400
+ """
401
+ Process a single task annotation
402
+
403
+ Args:
404
+ context: Execution context
405
+ task_id: Task ID
406
+ task_data: Task data from API
407
+ **kwargs: Additional parameters
408
+
409
+ Returns:
410
+ Dict with 'success' (bool) and optional 'error' (str)
411
+ """
412
+ pass
413
+ ```
414
+
415
+ #### MetricsStrategy
416
+
417
+ ```python
418
+ class MetricsStrategy(ABC):
419
+ """Base class for metrics tracking"""
420
+
421
+ @abstractmethod
422
+ def update_progress(self, context: ToTaskContext, current: int, total: int):
423
+ """Update progress percentage"""
424
+ pass
425
+
426
+ @abstractmethod
427
+ def record_task_result(
428
+ self,
429
+ context: ToTaskContext,
430
+ task_id: int,
431
+ success: bool,
432
+ error: Optional[str] = None
433
+ ):
434
+ """Record individual task result"""
435
+ pass
436
+
437
+ @abstractmethod
438
+ def update_metrics(
439
+ self,
440
+ context: ToTaskContext,
441
+ total: int,
442
+ success: int,
443
+ failed: int
444
+ ):
445
+ """Update aggregate metrics"""
446
+ pass
447
+
448
+ @abstractmethod
449
+ def finalize_metrics(self, context: ToTaskContext):
450
+ """Finalize and log final metrics"""
451
+ pass
452
+ ```
453
+
454
+ #### DataExtractionStrategy
455
+
456
+ ```python
457
+ class DataExtractionStrategy(ABC):
458
+ """Base class for data extraction"""
459
+
460
+ @abstractmethod
461
+ def extract_data(
462
+ self,
463
+ context: ToTaskContext,
464
+ task_data: Dict[str, Any]
465
+ ) -> Tuple[Optional[str], Optional[str]]:
466
+ """
467
+ Extract required data from task
468
+
469
+ Args:
470
+ context: Execution context
471
+ task_data: Task data from API
472
+
473
+ Returns:
474
+ Tuple of (data_url, original_name)
475
+ """
476
+ pass
477
+ ```
478
+
479
+ ### Validation Strategy Implementations
480
+
481
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py`
482
+
483
+ #### ProjectValidationStrategy
484
+
485
+ ```python
486
+ class ProjectValidationStrategy(ValidationStrategy):
487
+ """Validates project and data collection"""
488
+
489
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
490
+ try:
491
+ # Get project
492
+ project_id = context.params['project']
493
+ project = context.client.get_project(project_id)
494
+
495
+ if not project:
496
+ return {
497
+ 'success': False,
498
+ 'error': f'Project {project_id} not found'
499
+ }
500
+
501
+ context.project = project
502
+
503
+ # Check data collection
504
+ data_collection_id = project.get('data_collection')
505
+ if not data_collection_id:
506
+ return {
507
+ 'success': False,
508
+ 'error': 'Project has no data collection'
509
+ }
510
+
511
+ # Get data collection details
512
+ data_collection = context.client.get_data_collection(
513
+ data_collection_id
514
+ )
515
+ context.data_collection = data_collection
516
+
517
+ context.logger.log_message_with_code(
518
+ LogCode.PROJECT_VALIDATED,
519
+ project_id,
520
+ data_collection_id
521
+ )
522
+
523
+ return {'success': True}
524
+
525
+ except Exception as e:
526
+ return {
527
+ 'success': False,
528
+ 'error': f'Project validation failed: {str(e)}'
529
+ }
530
+ ```
531
+
532
+ #### TaskValidationStrategy
533
+
534
+ ```python
535
+ class TaskValidationStrategy(ValidationStrategy):
536
+ """Validates and retrieves tasks"""
537
+
538
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
539
+ try:
540
+ # Get tasks matching filters
541
+ project_id = context.params['project']
542
+ task_filters = context.params['task_filters']
543
+
544
+ tasks = context.client.list_tasks(
545
+ project=project_id,
546
+ **task_filters
547
+ )
548
+
549
+ if not tasks:
550
+ return {
551
+ 'success': False,
552
+ 'error': 'No tasks found matching filters'
553
+ }
554
+
555
+ # Store task IDs
556
+ context.task_ids = [task['id'] for task in tasks]
557
+
558
+ context.logger.log_message_with_code(
559
+ LogCode.TASKS_VALIDATED,
560
+ len(context.task_ids)
561
+ )
562
+
563
+ return {'success': True}
564
+
565
+ except Exception as e:
566
+ return {
567
+ 'success': False,
568
+ 'error': f'Task validation failed: {str(e)}'
569
+ }
570
+ ```
571
+
572
+ #### TargetSpecValidationStrategy
573
+
574
+ ```python
575
+ class TargetSpecValidationStrategy(ValidationStrategy):
576
+ """Validates target specification name exists"""
577
+
578
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
579
+ try:
580
+ target_spec_name = context.params.get('target_specification_name')
581
+
582
+ if not target_spec_name:
583
+ return {
584
+ 'success': False,
585
+ 'error': 'target_specification_name is required for file method'
586
+ }
587
+
588
+ # Get data collection file specifications
589
+ file_specs = context.data_collection.get('file_specifications', [])
590
+ spec_names = [spec['name'] for spec in file_specs]
591
+
592
+ if target_spec_name not in spec_names:
593
+ return {
594
+ 'success': False,
595
+ 'error': f'Target specification "{target_spec_name}" not found'
596
+ }
597
+
598
+ context.logger.log_message_with_code(
599
+ LogCode.TARGET_SPEC_VALIDATED,
600
+ target_spec_name
601
+ )
602
+
603
+ return {'success': True}
604
+
605
+ except Exception as e:
606
+ return {
607
+ 'success': False,
608
+ 'error': f'Target spec validation failed: {str(e)}'
609
+ }
610
+ ```
611
+
612
+ ### Annotation Strategy Implementations
613
+
614
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py`
615
+
616
+ #### FileAnnotationStrategy
617
+
618
+ ```python
619
+ class FileAnnotationStrategy(AnnotationStrategy):
620
+ """Annotates tasks using JSON files"""
621
+
622
+ def process_task(
623
+ self,
624
+ context: ToTaskContext,
625
+ task_id: int,
626
+ task_data: Dict[str, Any],
627
+ **kwargs
628
+ ) -> Dict[str, Any]:
629
+ try:
630
+ # Extract target file URL
631
+ extraction_strategy = FileDataExtractionStrategy()
632
+ url, original_name = extraction_strategy.extract_data(
633
+ context, task_data
634
+ )
635
+
636
+ if not url:
637
+ return {
638
+ 'success': False,
639
+ 'error': 'No file URL found for target specification'
640
+ }
641
+
642
+ # Download JSON data
643
+ json_data = self._download_json_data(url)
644
+
645
+ # Convert to task object
646
+ task_object = self._convert_to_task_object(json_data)
647
+
648
+ # Update task
649
+ result = context.client.update_task(
650
+ task_id,
651
+ data={'data': task_object}
652
+ )
653
+
654
+ if result:
655
+ context.logger.log_message_with_code(
656
+ LogCode.ANNOTATION_COMPLETED,
657
+ task_id
658
+ )
659
+ return {'success': True}
660
+ else:
661
+ return {
662
+ 'success': False,
663
+ 'error': 'Failed to update task'
664
+ }
665
+
666
+ except Exception as e:
667
+ return {
668
+ 'success': False,
669
+ 'error': f'File annotation failed: {str(e)}'
670
+ }
671
+
672
+ def _download_json_data(self, url: str) -> Dict[str, Any]:
673
+ """Download and parse JSON from URL"""
674
+ import requests
675
+ response = requests.get(url, timeout=30)
676
+ response.raise_for_status()
677
+ return response.json()
678
+
679
+ def _convert_to_task_object(self, json_data: Dict[str, Any]) -> Dict[str, Any]:
680
+ """Convert JSON data to task object format"""
681
+ # Validate and transform as needed
682
+ return json_data
683
+ ```
684
+
685
+ #### InferenceAnnotationStrategy
686
+
687
+ ```python
688
+ class InferenceAnnotationStrategy(AnnotationStrategy):
689
+ """Annotates tasks using model inference"""
690
+
691
+ def process_task(
692
+ self,
693
+ context: ToTaskContext,
694
+ task_id: int,
695
+ task_data: Dict[str, Any],
696
+ **kwargs
697
+ ) -> Dict[str, Any]:
698
+ try:
699
+ # Get preprocessor info
700
+ preprocessor_id = context.params['pre_processor']
701
+ preprocessor_info = self._get_preprocessor_info(
702
+ context, preprocessor_id
703
+ )
704
+
705
+ # Ensure preprocessor is running
706
+ self._ensure_preprocessor_running(
707
+ context, preprocessor_info['code']
708
+ )
709
+
710
+ # Extract primary image URL
711
+ image_url = self._extract_primary_image_url(task_data)
712
+
713
+ if not image_url:
714
+ return {
715
+ 'success': False,
716
+ 'error': 'No primary image found'
717
+ }
718
+
719
+ # Call preprocessor API
720
+ inference_params = context.params.get('pre_processor_params', {})
721
+ result = self._call_preprocessor_api(
722
+ preprocessor_info,
723
+ image_url,
724
+ inference_params
725
+ )
726
+
727
+ # Convert inference result to task object
728
+ task_object = self._convert_inference_to_task_object(result)
729
+
730
+ # Update task
731
+ update_result = context.client.update_task(
732
+ task_id,
733
+ data={'data': task_object}
734
+ )
735
+
736
+ if update_result:
737
+ context.logger.log_message_with_code(
738
+ LogCode.INFERENCE_COMPLETED,
739
+ task_id
740
+ )
741
+ return {'success': True}
742
+ else:
743
+ return {
744
+ 'success': False,
745
+ 'error': 'Failed to update task with inference results'
746
+ }
747
+
748
+ except Exception as e:
749
+ return {
750
+ 'success': False,
751
+ 'error': f'Inference annotation failed: {str(e)}'
752
+ }
753
+
754
+ def _get_preprocessor_info(
755
+ self, context: ToTaskContext, preprocessor_id: int
756
+ ) -> Dict[str, Any]:
757
+ """Get preprocessor details"""
758
+ return context.client.get_plugin_release(preprocessor_id)
759
+
760
+ def _ensure_preprocessor_running(
761
+ self, context: ToTaskContext, preprocessor_code: str
762
+ ):
763
+ """Ensure preprocessor is deployed and running"""
764
+ # Check Ray Serve applications
765
+ apps = context.client.list_serve_applications(
766
+ agent=context.params['agent']
767
+ )
768
+
769
+ # Find app by code
770
+ app = next(
771
+ (a for a in apps if a['code'] == preprocessor_code),
772
+ None
773
+ )
774
+
775
+ if not app or app['status'] != 'RUNNING':
776
+ # Deploy preprocessor
777
+ context.logger.log_message_with_code(
778
+ LogCode.DEPLOYING_PREPROCESSOR,
779
+ preprocessor_code
780
+ )
781
+ # Deployment logic here...
782
+
783
+ def _extract_primary_image_url(self, task_data: Dict[str, Any]) -> Optional[str]:
784
+ """Extract primary image URL from task data"""
785
+ data_unit = task_data.get('data_unit', {})
786
+ files = data_unit.get('files', [])
787
+
788
+ for file_info in files:
789
+ if file_info.get('is_primary'):
790
+ return file_info.get('url')
791
+
792
+ return None
793
+
794
+ def _call_preprocessor_api(
795
+ self,
796
+ preprocessor_info: Dict[str, Any],
797
+ image_url: str,
798
+ params: Dict[str, Any]
799
+ ) -> Dict[str, Any]:
800
+ """Call preprocessor API for inference"""
801
+ import requests
802
+
803
+ api_url = preprocessor_info['api_endpoint']
804
+ payload = {
805
+ 'image_url': image_url,
806
+ **params
807
+ }
808
+
809
+ response = requests.post(api_url, json=payload, timeout=60)
810
+ response.raise_for_status()
811
+ return response.json()
812
+
813
+ def _convert_inference_to_task_object(
814
+ self, result: Dict[str, Any]
815
+ ) -> Dict[str, Any]:
816
+ """Convert inference results to task object format"""
817
+ # Transform model output to task object structure
818
+ return result
819
+ ```
820
+
821
+ ## Workflow Execution
822
+
823
+ ### 7-Stage Workflow
824
+
825
+ ```mermaid
826
+ flowchart TD
827
+ A[ToTask Action Start] --> B[Parameter & Run Validation]
828
+ B --> C[Create ToTaskContext]
829
+ C --> D[Create ToTaskOrchestrator]
830
+ D --> E[Execute Workflow]
831
+
832
+ E --> F[Stage 1: Project Validation]
833
+ F -->|Success| G[Stage 2: Task Validation]
834
+ F -->|Failure| Z[Rollback & Error]
835
+
836
+ G -->|Success| H[Stage 3: Method Determination]
837
+ G -->|Failure| Z
838
+
839
+ H --> I{Method Type?}
840
+ I -->|File| J[Stage 4: Target Spec Validation]
841
+ I -->|Inference| K[Stage 4: Preprocessor Validation]
842
+
843
+ J -->|Success| L[Stage 5: Initialize Processing]
844
+ K -->|Success| L
845
+ J -->|Failure| Z
846
+ K -->|Failure| Z
847
+
848
+ L --> M[Stage 6: Task Processing Loop]
849
+
850
+ M --> N{More Tasks?}
851
+ N -->|Yes| O[Fetch Task Data]
852
+ N -->|No| T[Stage 7: Finalization]
853
+
854
+ O --> P{Annotation Method?}
855
+ P -->|File| Q[File Annotation Strategy]
856
+ P -->|Inference| R[Inference Annotation Strategy]
857
+
858
+ Q --> S{Success?}
859
+ R --> S
860
+
861
+ S -->|Yes| U[Increment Success Count]
862
+ S -->|No| V[Increment Failed Count]
863
+ S -->|Critical| Z
864
+
865
+ U --> W[Update Progress & Metrics]
866
+ V --> W
867
+
868
+ W --> N
869
+
870
+ T --> X[Aggregate Final Metrics]
871
+ X --> Y[Return Success Result]
872
+
873
+ Z --> ZA[Rollback Completed Steps]
874
+ ZA --> ZB[Cleanup Temp Files]
875
+ ZB --> ZC[Raise Error Exception]
876
+ ```
877
+
878
+ ### Stage Details
879
+
880
+ #### Stage 1: Project Validation
881
+
882
+ ```python
883
+ def _validate_project(self):
884
+ """Validate project and data collection"""
885
+ result = self.project_validation.validate(self.context)
886
+
887
+ if not result['success']:
888
+ raise PreAnnotationToTaskFailed(result.get('error'))
889
+
890
+ # Project and data collection now available in context
891
+ self.context.logger.log_message_with_code(
892
+ LogCode.PROJECT_VALIDATION_COMPLETE
893
+ )
894
+ ```
895
+
896
+ #### Stage 2: Task Validation
897
+
898
+ ```python
899
+ def _validate_tasks(self):
900
+ """Validate and retrieve tasks"""
901
+ result = self.task_validation.validate(self.context)
902
+
903
+ if not result['success']:
904
+ raise PreAnnotationToTaskFailed(result.get('error'))
905
+
906
+ # Task IDs now available in context.task_ids
907
+ task_count = len(self.context.task_ids)
908
+ self.context.logger.log_message_with_code(
909
+ LogCode.TASKS_FOUND,
910
+ task_count
911
+ )
912
+ ```
913
+
914
+ #### Stage 3: Method Determination
915
+
916
+ ```python
917
+ def _determine_annotation_method(self):
918
+ """Determine annotation method from parameters"""
919
+ method = self.context.params.get('method')
920
+
921
+ if method == 'file':
922
+ self.context.annotation_method = AnnotationMethod.FILE
923
+ elif method == 'inference':
924
+ self.context.annotation_method = AnnotationMethod.INFERENCE
925
+ else:
926
+ raise PreAnnotationToTaskFailed(
927
+ f'Unsupported annotation method: {method}'
928
+ )
929
+
930
+ self.context.logger.log_message_with_code(
931
+ LogCode.METHOD_DETERMINED,
932
+ self.context.annotation_method
933
+ )
934
+ ```
935
+
936
+ #### Stage 4: Method Validation
937
+
938
+ ```python
939
+ def _validate_annotation_method(self):
940
+ """Validate method-specific requirements"""
941
+ if self.context.annotation_method == AnnotationMethod.FILE:
942
+ # Validate target specification
943
+ result = self.target_spec_validation.validate(self.context)
944
+
945
+ if not result['success']:
946
+ raise PreAnnotationToTaskFailed(result.get('error'))
947
+
948
+ elif self.context.annotation_method == AnnotationMethod.INFERENCE:
949
+ # Validate preprocessor
950
+ preprocessor_id = self.context.params.get('pre_processor')
951
+
952
+ if not preprocessor_id:
953
+ raise PreAnnotationToTaskFailed(
954
+ 'pre_processor is required for inference method'
955
+ )
956
+
957
+ # Additional preprocessor validation...
958
+ ```
959
+
960
+ #### Stage 5: Processing Initialization
961
+
962
+ ```python
963
+ def _initialize_processing(self):
964
+ """Initialize metrics and progress tracking"""
965
+ total_tasks = len(self.context.task_ids)
966
+
967
+ # Reset metrics
968
+ self.context.update_metrics(
969
+ success_count=0,
970
+ failed_count=0,
971
+ total_count=total_tasks
972
+ )
973
+
974
+ # Initialize progress
975
+ self.metrics_strategy.update_progress(
976
+ self.context,
977
+ current=0,
978
+ total=total_tasks
979
+ )
980
+
981
+ self.context.logger.log_message_with_code(
982
+ LogCode.PROCESSING_INITIALIZED,
983
+ total_tasks
984
+ )
985
+ ```
986
+
987
+ #### Stage 6: Task Processing
988
+
989
+ ```python
990
+ def _process_all_tasks(self):
991
+ """Process all tasks with annotation strategy"""
992
+ # Create annotation strategy
993
+ annotation_strategy = self.factory.create_annotation_strategy(
994
+ self.context.annotation_method
995
+ )
996
+
997
+ success_count = 0
998
+ failed_count = 0
999
+ total_tasks = len(self.context.task_ids)
1000
+
1001
+ # Task query parameters
1002
+ task_params = {
1003
+ 'expand': 'data_unit,data_unit.files'
1004
+ }
1005
+
1006
+ # Process each task
1007
+ for index, task_id in enumerate(self.context.task_ids, start=1):
1008
+ try:
1009
+ # Fetch task data
1010
+ task_data = self.context.client.get_task(
1011
+ task_id,
1012
+ params=task_params
1013
+ )
1014
+
1015
+ # Execute annotation strategy
1016
+ result = annotation_strategy.process_task(
1017
+ self.context,
1018
+ task_id,
1019
+ task_data,
1020
+ target_specification_name=self.context.params.get(
1021
+ 'target_specification_name'
1022
+ )
1023
+ )
1024
+
1025
+ # Update counters
1026
+ if result['success']:
1027
+ success_count += 1
1028
+ self.metrics_strategy.record_task_result(
1029
+ self.context,
1030
+ task_id,
1031
+ success=True
1032
+ )
1033
+ else:
1034
+ failed_count += 1
1035
+ self.metrics_strategy.record_task_result(
1036
+ self.context,
1037
+ task_id,
1038
+ success=False,
1039
+ error=result.get('error')
1040
+ )
1041
+
1042
+ except CriticalError as e:
1043
+ # Critical errors stop processing
1044
+ raise PreAnnotationToTaskFailed(
1045
+ f'Critical error processing task {task_id}: {str(e)}'
1046
+ )
1047
+
1048
+ except Exception as e:
1049
+ # Task-level errors continue processing
1050
+ failed_count += 1
1051
+ self.context.logger.log_message_with_code(
1052
+ LogCode.TASK_PROCESSING_FAILED,
1053
+ task_id,
1054
+ str(e)
1055
+ )
1056
+
1057
+ # Update progress and metrics
1058
+ self.metrics_strategy.update_progress(
1059
+ self.context,
1060
+ current=index,
1061
+ total=total_tasks
1062
+ )
1063
+
1064
+ self.context.update_metrics(
1065
+ success_count=success_count,
1066
+ failed_count=failed_count,
1067
+ total_count=total_tasks
1068
+ )
1069
+
1070
+ self.metrics_strategy.update_metrics(
1071
+ self.context,
1072
+ total=total_tasks,
1073
+ success=success_count,
1074
+ failed=failed_count
1075
+ )
1076
+ ```
1077
+
1078
+ #### Stage 7: Finalization
1079
+
1080
+ ```python
1081
+ def _finalize_processing(self):
1082
+ """Finalize metrics and cleanup"""
1083
+ self.metrics_strategy.finalize_metrics(self.context)
1084
+
1085
+ # Log completion
1086
+ self.context.logger.log_message_with_code(
1087
+ LogCode.TO_TASK_COMPLETED,
1088
+ self.context.metrics.success,
1089
+ self.context.metrics.failed
1090
+ )
1091
+ ```
1092
+
1093
+ ## Error Handling and Rollback
1094
+
1095
+ ### Rollback Mechanism
1096
+
1097
+ ```python
1098
+ def _rollback_completed_steps(self):
1099
+ """Rollback all completed workflow steps"""
1100
+ self.context.logger.log_message_with_code(
1101
+ LogCode.ROLLBACK_STARTED,
1102
+ len(self.steps_completed)
1103
+ )
1104
+
1105
+ # Rollback steps in reverse order
1106
+ for step in reversed(self.steps_completed):
1107
+ try:
1108
+ rollback_method = getattr(self, f'_rollback_{step}', None)
1109
+ if rollback_method:
1110
+ rollback_method()
1111
+ self.context.logger.log_message_with_code(
1112
+ LogCode.STEP_ROLLED_BACK,
1113
+ step
1114
+ )
1115
+ except Exception as e:
1116
+ self.context.logger.log_message_with_code(
1117
+ LogCode.ROLLBACK_FAILED,
1118
+ step,
1119
+ str(e)
1120
+ )
1121
+
1122
+ # Execute custom rollback actions
1123
+ for action in reversed(self.context.rollback_actions):
1124
+ try:
1125
+ action()
1126
+ except Exception as e:
1127
+ self.context.logger.log_message_with_code(
1128
+ LogCode.ROLLBACK_ACTION_FAILED,
1129
+ str(e)
1130
+ )
1131
+
1132
+ # Cleanup temp files
1133
+ self._cleanup_temp_files()
1134
+ ```
1135
+
1136
+ ### Step-Specific Rollback Methods
1137
+
1138
+ ```python
1139
+ def _rollback_project_validation(self):
1140
+ """Rollback project validation"""
1141
+ self.context.project = None
1142
+ self.context.data_collection = None
1143
+
1144
+ def _rollback_task_validation(self):
1145
+ """Rollback task validation"""
1146
+ self.context.task_ids = []
1147
+
1148
+ def _rollback_processing_initialization(self):
1149
+ """Rollback processing initialization"""
1150
+ self.context.metrics = MetricsRecord(success=0, failed=0, total=0)
1151
+
1152
+ def _rollback_task_processing(self):
1153
+ """Rollback task processing"""
1154
+ # Cleanup is handled by temp files
1155
+ pass
1156
+
1157
+ def _cleanup_temp_files(self):
1158
+ """Remove all temporary files"""
1159
+ for file_path in self.context.temp_files:
1160
+ try:
1161
+ if os.path.exists(file_path):
1162
+ os.remove(file_path)
1163
+ except Exception as e:
1164
+ self.context.logger.log_message_with_code(
1165
+ LogCode.TEMP_FILE_CLEANUP_FAILED,
1166
+ file_path,
1167
+ str(e)
1168
+ )
1169
+ ```
1170
+
1171
+ ### Error Levels
1172
+
1173
+ **Task-Level Errors:**
1174
+
1175
+ - Individual task processing failures
1176
+ - Logged and counted in metrics
1177
+ - Workflow continues with remaining tasks
1178
+
1179
+ **Critical Errors:**
1180
+
1181
+ - System-level failures
1182
+ - Trigger immediate workflow termination
1183
+ - Initiate rollback process
1184
+
1185
+ **Workflow Errors:**
1186
+
1187
+ - Stage validation failures
1188
+ - Rollback completed stages
1189
+ - Propagate exception to caller
1190
+
1191
+ ## Extending the ToTask Action
1192
+
1193
+ ### Creating Custom Validation Strategies
1194
+
1195
+ ```python
1196
+ from synapse_sdk.plugins.categories.pre_annotation.actions.to_task.strategies.base import ValidationStrategy
1197
+
1198
+ class CustomBusinessRuleValidationStrategy(ValidationStrategy):
1199
+ """Custom validation for business-specific rules"""
1200
+
1201
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
1202
+ """Validate custom business rules"""
1203
+ try:
1204
+ # Get tasks
1205
+ tasks = context.client.list_tasks(
1206
+ project=context.params['project'],
1207
+ **context.params['task_filters']
1208
+ )
1209
+
1210
+ # Custom validation logic
1211
+ for task in tasks:
1212
+ # Check business rules
1213
+ if not self._meets_business_rules(task):
1214
+ return {
1215
+ 'success': False,
1216
+ 'error': f'Task {task["id"]} violates business rules'
1217
+ }
1218
+
1219
+ # Store task IDs
1220
+ context.task_ids = [task['id'] for task in tasks]
1221
+
1222
+ return {'success': True}
1223
+
1224
+ except Exception as e:
1225
+ return {
1226
+ 'success': False,
1227
+ 'error': f'Custom validation failed: {str(e)}'
1228
+ }
1229
+
1230
+ def _meets_business_rules(self, task: Dict[str, Any]) -> bool:
1231
+ """Check if task meets business rules"""
1232
+ # Implement your business logic
1233
+ return True
1234
+ ```
1235
+
1236
+ ### Creating Custom Annotation Strategies
1237
+
1238
+ ```python
1239
+ from synapse_sdk.plugins.categories.pre_annotation.actions.to_task.strategies.base import AnnotationStrategy
1240
+
1241
+ class ExternalAPIAnnotationStrategy(AnnotationStrategy):
1242
+ """Annotate using external API service"""
1243
+
1244
+ def process_task(
1245
+ self,
1246
+ context: ToTaskContext,
1247
+ task_id: int,
1248
+ task_data: Dict[str, Any],
1249
+ **kwargs
1250
+ ) -> Dict[str, Any]:
1251
+ """Process task using external API"""
1252
+ try:
1253
+ # Extract image URL
1254
+ image_url = self._extract_image_url(task_data)
1255
+
1256
+ # Call external API
1257
+ api_url = context.params.get('external_api_url')
1258
+ api_key = context.params.get('external_api_key')
1259
+
1260
+ annotations = self._call_external_api(
1261
+ api_url,
1262
+ api_key,
1263
+ image_url
1264
+ )
1265
+
1266
+ # Convert to task object
1267
+ task_object = self._convert_annotations(annotations)
1268
+
1269
+ # Update task
1270
+ result = context.client.update_task(
1271
+ task_id,
1272
+ data={'data': task_object}
1273
+ )
1274
+
1275
+ if result:
1276
+ return {'success': True}
1277
+ else:
1278
+ return {
1279
+ 'success': False,
1280
+ 'error': 'Failed to update task'
1281
+ }
1282
+
1283
+ except Exception as e:
1284
+ return {
1285
+ 'success': False,
1286
+ 'error': f'External API annotation failed: {str(e)}'
1287
+ }
1288
+
1289
+ def _extract_image_url(self, task_data: Dict[str, Any]) -> str:
1290
+ """Extract image URL from task data"""
1291
+ # Implementation...
1292
+ pass
1293
+
1294
+ def _call_external_api(
1295
+ self, api_url: str, api_key: str, image_url: str
1296
+ ) -> Dict[str, Any]:
1297
+ """Call external annotation API"""
1298
+ import requests
1299
+
1300
+ response = requests.post(
1301
+ api_url,
1302
+ headers={'Authorization': f'Bearer {api_key}'},
1303
+ json={'image_url': image_url},
1304
+ timeout=30
1305
+ )
1306
+ response.raise_for_status()
1307
+ return response.json()
1308
+
1309
+ def _convert_annotations(self, annotations: Dict[str, Any]) -> Dict[str, Any]:
1310
+ """Convert external format to task object"""
1311
+ # Implementation...
1312
+ pass
1313
+ ```
1314
+
1315
+ ### Integrating Custom Strategies
1316
+
1317
+ Update the factory to support custom strategies:
1318
+
1319
+ ```python
1320
+ class CustomToTaskStrategyFactory(ToTaskStrategyFactory):
1321
+ """Extended factory with custom strategies"""
1322
+
1323
+ def create_validation_strategy(
1324
+ self, strategy_type: str
1325
+ ) -> ValidationStrategy:
1326
+ """Create validation strategy including custom types"""
1327
+ strategies = {
1328
+ 'project': ProjectValidationStrategy,
1329
+ 'task': TaskValidationStrategy,
1330
+ 'target_spec': TargetSpecValidationStrategy,
1331
+ 'business_rules': CustomBusinessRuleValidationStrategy, # Custom
1332
+ }
1333
+
1334
+ strategy_class = strategies.get(strategy_type)
1335
+ if not strategy_class:
1336
+ raise ValueError(f'Unknown validation strategy: {strategy_type}')
1337
+
1338
+ return strategy_class()
1339
+
1340
+ def create_annotation_strategy(
1341
+ self, method: AnnotationMethod
1342
+ ) -> AnnotationStrategy:
1343
+ """Create annotation strategy including custom methods"""
1344
+ if method == AnnotationMethod.FILE:
1345
+ return FileAnnotationStrategy()
1346
+ elif method == AnnotationMethod.INFERENCE:
1347
+ return InferenceAnnotationStrategy()
1348
+ elif method == 'external_api': # Custom
1349
+ return ExternalAPIAnnotationStrategy()
1350
+ else:
1351
+ raise ValueError(f'Unknown annotation method: {method}')
1352
+ ```
1353
+
1354
+ ## API Reference
1355
+
1356
+ ### Models and Enums
1357
+
1358
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py`
1359
+
1360
+ #### ToTaskParams
1361
+
1362
+ ```python
1363
+ class ToTaskParams(BaseModel):
1364
+ """Parameters for ToTask action"""
1365
+
1366
+ name: str = Field(..., description="Action name (no whitespace)")
1367
+ description: Optional[str] = Field(None, description="Action description")
1368
+ project: int = Field(..., description="Project ID")
1369
+ agent: int = Field(..., description="Agent ID")
1370
+ task_filters: Dict[str, Any] = Field(..., description="Task filter criteria")
1371
+ method: str = Field(..., description="Annotation method: 'file' or 'inference'")
1372
+ target_specification_name: Optional[str] = Field(
1373
+ None,
1374
+ description="File specification name (required for file method)"
1375
+ )
1376
+ model: Optional[int] = Field(None, description="Model ID")
1377
+ pre_processor: Optional[int] = Field(
1378
+ None,
1379
+ description="Pre-processor ID (required for inference method)"
1380
+ )
1381
+ pre_processor_params: Dict[str, Any] = Field(
1382
+ default_factory=dict,
1383
+ description="Pre-processor configuration parameters"
1384
+ )
1385
+
1386
+ @validator('name')
1387
+ def validate_name(cls, v):
1388
+ """Validate name has no whitespace"""
1389
+ if ' ' in v:
1390
+ raise ValueError('Name must not contain whitespace')
1391
+ return v
1392
+
1393
+ @validator('method')
1394
+ def validate_method(cls, v):
1395
+ """Validate method is supported"""
1396
+ if v not in ['file', 'inference']:
1397
+ raise ValueError('Method must be "file" or "inference"')
1398
+ return v
1399
+ ```
1400
+
1401
+ #### ToTaskResult
1402
+
1403
+ ```python
1404
+ class ToTaskResult(BaseModel):
1405
+ """Result from ToTask action"""
1406
+
1407
+ status: JobStatus = Field(..., description="Job status")
1408
+ message: str = Field(..., description="Result message")
1409
+ ```
1410
+
1411
+ #### MetricsRecord
1412
+
1413
+ ```python
1414
+ class MetricsRecord(BaseModel):
1415
+ """Metrics tracking record"""
1416
+
1417
+ success: int = Field(0, description="Successfully processed count")
1418
+ failed: int = Field(0, description="Failed processing count")
1419
+ total: int = Field(0, description="Total tasks count")
1420
+ ```
1421
+
1422
+ **File:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py`
1423
+
1424
+ #### AnnotationMethod
1425
+
1426
+ ```python
1427
+ class AnnotationMethod(str, Enum):
1428
+ """Supported annotation methods"""
1429
+
1430
+ FILE = 'file'
1431
+ INFERENCE = 'inference'
1432
+ ```
1433
+
1434
+ #### LogCode
1435
+
1436
+ ```python
1437
+ class LogCode(str, Enum):
1438
+ """Type-safe logging codes"""
1439
+
1440
+ # Workflow codes
1441
+ TO_TASK_STARTED = 'to_task_started'
1442
+ STEP_COMPLETED = 'step_completed'
1443
+ TO_TASK_COMPLETED = 'to_task_completed'
1444
+
1445
+ # Validation codes
1446
+ PROJECT_VALIDATED = 'project_validated'
1447
+ TASKS_VALIDATED = 'tasks_validated'
1448
+ TARGET_SPEC_VALIDATED = 'target_spec_validated'
1449
+ INVALID_PROJECT_RESPONSE = 'invalid_project_response'
1450
+ NO_DATA_COLLECTION = 'no_data_collection'
1451
+ TARGET_SPEC_NOT_FOUND = 'target_spec_not_found'
1452
+
1453
+ # Processing codes
1454
+ PROCESSING_INITIALIZED = 'processing_initialized'
1455
+ ANNOTATING_DATA = 'annotating_data'
1456
+ ANNOTATION_COMPLETED = 'annotation_completed'
1457
+ TASK_PROCESSING_FAILED = 'task_processing_failed'
1458
+
1459
+ # Inference codes
1460
+ ANNOTATING_INFERENCE_DATA = 'annotating_inference_data'
1461
+ DEPLOYING_PREPROCESSOR = 'deploying_preprocessor'
1462
+ INFERENCE_COMPLETED = 'inference_completed'
1463
+ INFERENCE_PROCESSING_FAILED = 'inference_processing_failed'
1464
+
1465
+ # Rollback codes
1466
+ ROLLBACK_STARTED = 'rollback_started'
1467
+ STEP_ROLLED_BACK = 'step_rolled_back'
1468
+ ROLLBACK_FAILED = 'rollback_failed'
1469
+ ROLLBACK_ACTION_FAILED = 'rollback_action_failed'
1470
+
1471
+ # Metrics codes
1472
+ PROGRESS_UPDATE_FAILED = 'progress_update_failed'
1473
+ METRICS_RECORDING_FAILED = 'metrics_recording_failed'
1474
+ METRICS_UPDATE_FAILED = 'metrics_update_failed'
1475
+ METRICS_FINALIZATION_FAILED = 'metrics_finalization_failed'
1476
+ ```
1477
+
1478
+ ## Testing
1479
+
1480
+ ### Unit Testing Strategies
1481
+
1482
+ ```python
1483
+ import pytest
1484
+ from unittest.mock import Mock, patch
1485
+ from synapse_sdk.plugins.categories.pre_annotation.actions.to_task.strategies.validation import ProjectValidationStrategy
1486
+
1487
+ class TestProjectValidationStrategy:
1488
+ """Test project validation strategy"""
1489
+
1490
+ def test_validate_success(self):
1491
+ """Test successful validation"""
1492
+ # Setup
1493
+ context = Mock()
1494
+ context.params = {'project': 123}
1495
+ context.client.get_project.return_value = {
1496
+ 'id': 123,
1497
+ 'data_collection': 456
1498
+ }
1499
+ context.client.get_data_collection.return_value = {
1500
+ 'id': 456
1501
+ }
1502
+
1503
+ # Execute
1504
+ strategy = ProjectValidationStrategy()
1505
+ result = strategy.validate(context)
1506
+
1507
+ # Assert
1508
+ assert result['success'] is True
1509
+ assert context.project == {'id': 123, 'data_collection': 456}
1510
+ assert context.data_collection == {'id': 456}
1511
+
1512
+ def test_validate_no_project(self):
1513
+ """Test validation fails when project not found"""
1514
+ # Setup
1515
+ context = Mock()
1516
+ context.params = {'project': 999}
1517
+ context.client.get_project.return_value = None
1518
+
1519
+ # Execute
1520
+ strategy = ProjectValidationStrategy()
1521
+ result = strategy.validate(context)
1522
+
1523
+ # Assert
1524
+ assert result['success'] is False
1525
+ assert 'not found' in result['error']
1526
+
1527
+ def test_validate_no_data_collection(self):
1528
+ """Test validation fails when no data collection"""
1529
+ # Setup
1530
+ context = Mock()
1531
+ context.params = {'project': 123}
1532
+ context.client.get_project.return_value = {
1533
+ 'id': 123,
1534
+ 'data_collection': None
1535
+ }
1536
+
1537
+ # Execute
1538
+ strategy = ProjectValidationStrategy()
1539
+ result = strategy.validate(context)
1540
+
1541
+ # Assert
1542
+ assert result['success'] is False
1543
+ assert 'no data collection' in result['error']
1544
+ ```
1545
+
1546
+ ### Integration Testing
1547
+
1548
+ ```python
1549
+ class TestToTaskIntegration:
1550
+ """Integration tests for ToTask workflow"""
1551
+
1552
+ @pytest.fixture
1553
+ def mock_context(self):
1554
+ """Create mock context"""
1555
+ context = Mock(spec=ToTaskContext)
1556
+ context.params = {
1557
+ 'project': 123,
1558
+ 'agent': 1,
1559
+ 'task_filters': {'status': 'pending'},
1560
+ 'method': 'file',
1561
+ 'target_specification_name': 'annotations'
1562
+ }
1563
+ context.task_ids = [1, 2, 3]
1564
+ return context
1565
+
1566
+ def test_full_workflow_file_method(self, mock_context):
1567
+ """Test complete workflow with file method"""
1568
+ # Setup orchestrator
1569
+ orchestrator = ToTaskOrchestrator(mock_context)
1570
+
1571
+ # Mock validation responses
1572
+ mock_context.client.get_project.return_value = {
1573
+ 'id': 123,
1574
+ 'data_collection': 456
1575
+ }
1576
+
1577
+ # Execute workflow
1578
+ result = orchestrator.execute_workflow()
1579
+
1580
+ # Verify all stages completed
1581
+ assert 'project_validation' in orchestrator.steps_completed
1582
+ assert 'task_validation' in orchestrator.steps_completed
1583
+ assert 'finalization' in orchestrator.steps_completed
1584
+
1585
+ # Verify metrics
1586
+ assert result.total == 3
1587
+ ```
1588
+
1589
+ ## Migration Guide
1590
+
1591
+ ### From Legacy Implementation
1592
+
1593
+ If migrating from a legacy ToTask implementation:
1594
+
1595
+ **Key Changes:**
1596
+
1597
+ 1. **Strategy-based architecture** - Replace monolithic logic with strategies
1598
+ 2. **Orchestrated workflow** - 7 defined stages instead of ad-hoc execution
1599
+ 3. **Automatic rollback** - Built-in error recovery
1600
+ 4. **Type-safe logging** - LogCode enum instead of string messages
1601
+
1602
+ **Migration Steps:**
1603
+
1604
+ 1. Update parameter validation to use `ToTaskParams` model
1605
+ 2. Replace custom validation logic with validation strategies
1606
+ 3. Migrate annotation logic to annotation strategies
1607
+ 4. Update error handling to use orchestrator rollback
1608
+ 5. Replace logging with LogCode-based messages
1609
+
1610
+ ## Best Practices
1611
+
1612
+ ### Performance
1613
+
1614
+ 1. **Use appropriate batch sizes** for inference
1615
+ 2. **Filter tasks efficiently** to reduce processing overhead
1616
+ 3. **Implement caching** in custom strategies when appropriate
1617
+ 4. **Monitor memory usage** for large task sets
1618
+
1619
+ ### Reliability
1620
+
1621
+ 1. **Validate all inputs** before processing
1622
+ 2. **Implement proper error handling** in custom strategies
1623
+ 3. **Use rollback actions** for cleanup operations
1624
+ 4. **Log detailed error messages** for debugging
1625
+
1626
+ ### Maintainability
1627
+
1628
+ 1. **Follow strategy pattern** for new functionality
1629
+ 2. **Use type hints** consistently
1630
+ 3. **Document custom strategies** thoroughly
1631
+ 4. **Write comprehensive tests** for all strategies
1632
+
1633
+ ## Related Documentation
1634
+
1635
+ - [ToTask Overview](./to-task-overview.md) - User guide
1636
+ - [Pre-annotation Plugin Overview](./pre-annotation-plugin-overview.md) - Category overview
1637
+ - Plugin Development Guide - General plugin development
1638
+ - Strategy Pattern - Design pattern details
1639
+
1640
+ ## Source Code References
1641
+
1642
+ - Action: `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py`
1643
+ - Orchestrator: `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py`
1644
+ - Strategies: `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/`
1645
+ - Models: `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py`