synapse-sdk 2025.10.1__py3-none-any.whl → 2025.10.4__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 (54) hide show
  1. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  2. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  3. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  4. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  5. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  6. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md +560 -0
  7. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  8. synapse_sdk/devtools/docs/docs/plugins/plugins.md +12 -5
  9. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  10. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  11. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  12. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  13. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  14. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md +560 -0
  15. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  16. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current.json +16 -4
  17. synapse_sdk/devtools/docs/sidebars.ts +27 -1
  18. synapse_sdk/plugins/README.md +487 -80
  19. synapse_sdk/plugins/categories/export/actions/export/action.py +8 -3
  20. synapse_sdk/plugins/categories/export/actions/export/utils.py +108 -8
  21. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  22. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  23. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  24. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  25. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +145 -0
  26. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  27. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  28. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  29. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +97 -0
  30. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +250 -0
  31. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  32. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  33. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +284 -0
  34. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  35. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  36. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +87 -0
  37. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +127 -0
  38. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  39. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +2 -1
  40. synapse_sdk/plugins/categories/upload/actions/upload/models.py +134 -94
  41. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +2 -2
  42. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +106 -14
  43. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +113 -36
  44. synapse_sdk/plugins/categories/upload/templates/README.md +365 -0
  45. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.4.dist-info}/METADATA +1 -1
  46. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.4.dist-info}/RECORD +50 -22
  47. synapse_sdk/devtools/docs/docs/plugins/developing-upload-template.md +0 -1463
  48. synapse_sdk/devtools/docs/docs/plugins/upload-plugins.md +0 -1964
  49. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/developing-upload-template.md +0 -1463
  50. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md +0 -2077
  51. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.4.dist-info}/WHEEL +0 -0
  52. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.4.dist-info}/entry_points.txt +0 -0
  53. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.4.dist-info}/licenses/LICENSE +0 -0
  54. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1645 @@
1
+ ---
2
+ id: to-task-action-development
3
+ title: ToTask 액션 - SDK 개발자 가이드
4
+ sidebar_position: 3
5
+ ---
6
+
7
+ # ToTask 액션 - SDK 개발자 가이드
8
+
9
+ :::info 대상 독자
10
+ 이 가이드는 ToTaskAction 프레임워크, 오케스트레이터, 전략 및 워크플로우 엔진을 작업하는 **SDK 핵심 개발자**를 위한 것입니다.
11
+
12
+ AnnotationToTask 템플릿을 사용하여 커스텀 pre-annotation 플러그인을 생성하는 **플러그인 개발자**라면 [ToTask 템플릿 개발](./to-task-template-development.md) 가이드를 참조하세요.
13
+ :::
14
+
15
+ 이 가이드는 ToTask 액션 아키텍처에 대한 포괄적인 기술 문서를 제공하며, 설계 패턴, 워크플로우 실행, 커스텀 전략 구현을 위한 확장 포인트를 포함합니다.
16
+
17
+ ## 아키텍처 개요
18
+
19
+ ToTask 액션은 네 가지 핵심 설계 패턴을 사용하는 현대적이고 유지보수 가능한 아키텍처로 구축되었습니다:
20
+
21
+ ### 설계 패턴
22
+
23
+ #### 1. 전략 패턴 (Strategy Pattern)
24
+
25
+ 검증, 주석, 메트릭 및 데이터 추출을 위한 플러그형 알고리즘을 가능하게 합니다. 구성에 따라 런타임에 다른 전략을 선택할 수 있습니다.
26
+
27
+ #### 2. 퍼사드 패턴 (Facade Pattern)
28
+
29
+ `ToTaskOrchestrator`는 복잡한 7단계 워크플로우에 대한 단순화된 인터페이스를 제공하며, 오케스트레이션, 오류 관리 및 롤백을 처리합니다.
30
+
31
+ #### 3. 팩토리 패턴 (Factory Pattern)
32
+
33
+ `ToTaskStrategyFactory`는 런타임 파라미터에 따라 적절한 전략 인스턴스를 생성하여 전략 생성과 사용을 분리합니다.
34
+
35
+ #### 4. 컨텍스트 패턴 (Context Pattern)
36
+
37
+ `ToTaskContext`는 실행 전반에 걸쳐 워크플로우 컴포넌트 간의 공유 상태와 통신 채널을 유지합니다.
38
+
39
+ ## 핵심 컴포넌트
40
+
41
+ ### 컴포넌트 다이어그램
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
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py`
135
+
136
+ 주석 액션의 진입점입니다.
137
+
138
+ **책임:**
139
+
140
+ - `ToTaskParams` 모델을 사용한 파라미터 검증
141
+ - 실행 컨텍스트 생성
142
+ - 오케스트레이터 인스턴스화 및 실행
143
+ - 최상위 오류 처리
144
+
145
+ **주요 속성:**
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
+ **주요 메서드:**
171
+
172
+ ```python
173
+ def start(self) -> Dict[str, Any]:
174
+ """ToTask 액션 워크플로우 실행"""
175
+ try:
176
+ # 실행 컨텍스트 생성
177
+ context = self.get_context()
178
+
179
+ # 오케스트레이터 생성 및 실행
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
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py`
197
+
198
+ 7단계 워크플로우를 오케스트레이션하는 퍼사드입니다.
199
+
200
+ **책임:**
201
+
202
+ - 워크플로우 단계를 순차적으로 실행
203
+ - 롤백을 위해 완료된 단계 추적
204
+ - 오류 처리 및 롤백 트리거
205
+ - 전략 인스턴스 관리
206
+
207
+ **주요 메서드:**
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
+ # 검증 전략 초기화
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
+ # 메트릭 전략 초기화
222
+ self.metrics_strategy = self.factory.create_metrics_strategy()
223
+
224
+ def execute_workflow(self) -> Dict[str, Any]:
225
+ """완전한 7단계 워크플로우 실행"""
226
+ try:
227
+ # 1단계: 프로젝트 검증
228
+ self._execute_step('project_validation', self._validate_project)
229
+
230
+ # 2단계: 작업 검증
231
+ self._execute_step('task_validation', self._validate_tasks)
232
+
233
+ # 3단계: 주석 방법 결정
234
+ self._execute_step('method_determination', self._determine_annotation_method)
235
+
236
+ # 4단계: 주석 방법 검증
237
+ self._execute_step('method_validation', self._validate_annotation_method)
238
+
239
+ # 5단계: 처리 초기화
240
+ self._execute_step('processing_initialization', self._initialize_processing)
241
+
242
+ # 6단계: 모든 작업 처리
243
+ self._execute_step('task_processing', self._process_all_tasks)
244
+
245
+ # 7단계: 마무리
246
+ self._execute_step('finalization', self._finalize_processing)
247
+
248
+ return self.context.metrics
249
+
250
+ except Exception as e:
251
+ # 오류 발생 시 롤백
252
+ self._rollback_completed_steps()
253
+ raise
254
+
255
+ def _execute_step(self, step_name: str, step_func: callable):
256
+ """워크플로우 단계 실행 및 완료 추적"""
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
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py`
267
+
268
+ 워크플로우 컴포넌트를 위한 공유 실행 컨텍스트입니다.
269
+
270
+ **주요 속성:**
271
+
272
+ ```python
273
+ class ToTaskContext:
274
+ def __init__(self, params: Dict, client: Any, logger: Any):
275
+ # 구성
276
+ self.params = params
277
+ self.client = client
278
+ self.logger = logger
279
+
280
+ # 프로젝트 데이터
281
+ self.project: Optional[Dict] = None
282
+ self.data_collection: Optional[Dict] = None
283
+
284
+ # 작업 데이터
285
+ self.task_ids: List[int] = []
286
+
287
+ # 실행 상태
288
+ self.annotation_method: Optional[AnnotationMethod] = None
289
+ self.metrics = MetricsRecord(success=0, failed=0, total=0)
290
+
291
+ # 정리 추적
292
+ self.temp_files: List[str] = []
293
+ self.rollback_actions: List[callable] = []
294
+ ```
295
+
296
+ **헬퍼 메서드:**
297
+
298
+ ```python
299
+ def add_temp_file(self, file_path: str):
300
+ """정리를 위한 임시 파일 등록"""
301
+ self.temp_files.append(file_path)
302
+
303
+ def add_rollback_action(self, action: callable):
304
+ """롤백 액션 등록"""
305
+ self.rollback_actions.append(action)
306
+
307
+ def update_metrics(self, success_count: int, failed_count: int, total_count: int):
308
+ """메트릭 카운터 업데이트"""
309
+ self.metrics.success = success_count
310
+ self.metrics.failed = failed_count
311
+ self.metrics.total = total_count
312
+ ```
313
+
314
+ ### ToTaskStrategyFactory
315
+
316
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py`
317
+
318
+ 전략 인스턴스 생성을 위한 팩토리입니다.
319
+
320
+ **구현:**
321
+
322
+ ```python
323
+ class ToTaskStrategyFactory:
324
+ def create_validation_strategy(
325
+ self, strategy_type: str
326
+ ) -> ValidationStrategy:
327
+ """타입별 검증 전략 생성"""
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
+ """방법별 주석 전략 생성"""
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
+ """메트릭 전략 생성"""
353
+ return ToTaskMetricsStrategy()
354
+
355
+ def create_extraction_strategy(self) -> DataExtractionStrategy:
356
+ """데이터 추출 전략 생성"""
357
+ return FileDataExtractionStrategy()
358
+ ```
359
+
360
+ ## 전략 아키텍처
361
+
362
+ ### 전략 기본 클래스
363
+
364
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py`
365
+
366
+ #### ValidationStrategy
367
+
368
+ ```python
369
+ class ValidationStrategy(ABC):
370
+ """검증 전략의 기본 클래스"""
371
+
372
+ @abstractmethod
373
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
374
+ """
375
+ 검증 수행
376
+
377
+ Args:
378
+ context: 실행 컨텍스트
379
+
380
+ Returns:
381
+ 'success' (bool) 및 선택적 'error' (str)가 있는 딕셔너리
382
+ """
383
+ pass
384
+ ```
385
+
386
+ #### AnnotationStrategy
387
+
388
+ ```python
389
+ class AnnotationStrategy(ABC):
390
+ """주석 전략의 기본 클래스"""
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
+ 단일 작업 주석 처리
402
+
403
+ Args:
404
+ context: 실행 컨텍스트
405
+ task_id: 작업 ID
406
+ task_data: API의 작업 데이터
407
+ **kwargs: 추가 파라미터
408
+
409
+ Returns:
410
+ 'success' (bool) 및 선택적 'error' (str)가 있는 딕셔너리
411
+ """
412
+ pass
413
+ ```
414
+
415
+ #### MetricsStrategy
416
+
417
+ ```python
418
+ class MetricsStrategy(ABC):
419
+ """메트릭 추적의 기본 클래스"""
420
+
421
+ @abstractmethod
422
+ def update_progress(self, context: ToTaskContext, current: int, total: int):
423
+ """진행 백분율 업데이트"""
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
+ """개별 작업 결과 기록"""
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
+ """집계 메트릭 업데이트"""
446
+ pass
447
+
448
+ @abstractmethod
449
+ def finalize_metrics(self, context: ToTaskContext):
450
+ """최종 메트릭 마무리 및 기록"""
451
+ pass
452
+ ```
453
+
454
+ #### DataExtractionStrategy
455
+
456
+ ```python
457
+ class DataExtractionStrategy(ABC):
458
+ """데이터 추출의 기본 클래스"""
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
+ 작업에서 필요한 데이터 추출
468
+
469
+ Args:
470
+ context: 실행 컨텍스트
471
+ task_data: API의 작업 데이터
472
+
473
+ Returns:
474
+ (data_url, original_name) 튜플
475
+ """
476
+ pass
477
+ ```
478
+
479
+ ### 검증 전략 구현
480
+
481
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py`
482
+
483
+ #### ProjectValidationStrategy
484
+
485
+ ```python
486
+ class ProjectValidationStrategy(ValidationStrategy):
487
+ """프로젝트 및 데이터 컬렉션 검증"""
488
+
489
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
490
+ try:
491
+ # 프로젝트 가져오기
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
+ # 데이터 컬렉션 확인
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
+ # 데이터 컬렉션 세부정보 가져오기
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
+ """작업 검증 및 검색"""
537
+
538
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
539
+ try:
540
+ # 필터와 일치하는 작업 가져오기
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
+ # 작업 ID 저장
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
+ """대상 사양 이름이 존재하는지 검증"""
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
+ # 데이터 컬렉션 파일 사양 가져오기
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
+ ### 주석 전략 구현
613
+
614
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py`
615
+
616
+ #### FileAnnotationStrategy
617
+
618
+ ```python
619
+ class FileAnnotationStrategy(AnnotationStrategy):
620
+ """JSON 파일을 사용한 작업 주석"""
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
+ # 대상 파일 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
+ # JSON 데이터 다운로드
643
+ json_data = self._download_json_data(url)
644
+
645
+ # 작업 객체로 변환
646
+ task_object = self._convert_to_task_object(json_data)
647
+
648
+ # 작업 업데이트
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
+ """URL에서 JSON 다운로드 및 파싱"""
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
+ """JSON 데이터를 작업 객체 형식으로 변환"""
681
+ # 필요에 따라 검증 및 변환
682
+ return json_data
683
+ ```
684
+
685
+ #### InferenceAnnotationStrategy
686
+
687
+ ```python
688
+ class InferenceAnnotationStrategy(AnnotationStrategy):
689
+ """모델 추론을 사용한 작업 주석"""
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
+ # 전처리기 정보 가져오기
700
+ preprocessor_id = context.params['pre_processor']
701
+ preprocessor_info = self._get_preprocessor_info(
702
+ context, preprocessor_id
703
+ )
704
+
705
+ # 전처리기가 실행 중인지 확인
706
+ self._ensure_preprocessor_running(
707
+ context, preprocessor_info['code']
708
+ )
709
+
710
+ # 주 이미지 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
+ # 전처리기 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
+ # 추론 결과를 작업 객체로 변환
728
+ task_object = self._convert_inference_to_task_object(result)
729
+
730
+ # 작업 업데이트
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
+ """전처리기 세부정보 가져오기"""
758
+ return context.client.get_plugin_release(preprocessor_id)
759
+
760
+ def _ensure_preprocessor_running(
761
+ self, context: ToTaskContext, preprocessor_code: str
762
+ ):
763
+ """전처리기가 배포되고 실행 중인지 확인"""
764
+ # Ray Serve 애플리케이션 확인
765
+ apps = context.client.list_serve_applications(
766
+ agent=context.params['agent']
767
+ )
768
+
769
+ # 코드로 앱 찾기
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
+ # 전처리기 배포
777
+ context.logger.log_message_with_code(
778
+ LogCode.DEPLOYING_PREPROCESSOR,
779
+ preprocessor_code
780
+ )
781
+ # 배포 로직...
782
+
783
+ def _extract_primary_image_url(self, task_data: Dict[str, Any]) -> Optional[str]:
784
+ """작업 데이터에서 주 이미지 URL 추출"""
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
+ """추론을 위한 전처리기 API 호출"""
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
+ """추론 결과를 작업 객체 형식으로 변환"""
817
+ # 모델 출력을 작업 객체 구조로 변환
818
+ return result
819
+ ```
820
+
821
+ ## 워크플로우 실행
822
+
823
+ ### 7단계 워크플로우
824
+
825
+ ```mermaid
826
+ flowchart TD
827
+ A[ToTask 액션 시작] --> B[파라미터 및 Run 검증]
828
+ B --> C[ToTaskContext 생성]
829
+ C --> D[ToTaskOrchestrator 생성]
830
+ D --> E[워크플로우 실행 시작]
831
+
832
+ E --> F[1단계: 프로젝트 검증]
833
+ F -->|성공| G[2단계: 작업 검증]
834
+ F -->|실패| Z[롤백 및 오류]
835
+
836
+ G -->|성공| H[3단계: 방법 결정]
837
+ G -->|실패| Z
838
+
839
+ H --> I{방법 타입?}
840
+ I -->|파일| J[4단계: 타겟 사양 검증]
841
+ I -->|추론| K[4단계: 전처리기 검증]
842
+
843
+ J -->|성공| L[5단계: 처리 초기화]
844
+ K -->|성공| L
845
+ J -->|실패| Z
846
+ K -->|실패| Z
847
+
848
+ L --> M[6단계: 작업 처리 루프]
849
+
850
+ M --> N{더 많은 작업?}
851
+ N -->|예| O[작업 데이터 가져오기]
852
+ N -->|아니오| T[7단계: 마무리]
853
+
854
+ O --> P{주석 방법?}
855
+ P -->|파일| Q[파일 주석 전략 실행]
856
+ P -->|추론| R[추론 주석 전략 실행]
857
+
858
+ Q --> S{성공?}
859
+ R --> S
860
+
861
+ S -->|예| U[성공 카운트 증가]
862
+ S -->|아니오| V[실패 카운트 증가]
863
+ S -->|치명적| Z
864
+
865
+ U --> W[진행 및 메트릭 업데이트]
866
+ V --> W
867
+
868
+ W --> N
869
+
870
+ T --> X[최종 메트릭 집계]
871
+ X --> Y[성공 결과 반환]
872
+
873
+ Z --> ZA[완료된 단계 롤백]
874
+ ZA --> ZB[임시 파일 정리]
875
+ ZB --> ZC[오류 예외 발생]
876
+ ```
877
+
878
+ ### 단계 세부사항
879
+
880
+ #### 1단계: 프로젝트 검증
881
+
882
+ ```python
883
+ def _validate_project(self):
884
+ """프로젝트 및 데이터 컬렉션 검증"""
885
+ result = self.project_validation.validate(self.context)
886
+
887
+ if not result['success']:
888
+ raise PreAnnotationToTaskFailed(result.get('error'))
889
+
890
+ # 이제 프로젝트와 데이터 컬렉션을 컨텍스트에서 사용 가능
891
+ self.context.logger.log_message_with_code(
892
+ LogCode.PROJECT_VALIDATION_COMPLETE
893
+ )
894
+ ```
895
+
896
+ #### 2단계: 작업 검증
897
+
898
+ ```python
899
+ def _validate_tasks(self):
900
+ """작업 검증 및 검색"""
901
+ result = self.task_validation.validate(self.context)
902
+
903
+ if not result['success']:
904
+ raise PreAnnotationToTaskFailed(result.get('error'))
905
+
906
+ # 이제 작업 ID가 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
+ #### 3단계: 방법 결정
915
+
916
+ ```python
917
+ def _determine_annotation_method(self):
918
+ """파라미터에서 주석 방법 결정"""
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
+ #### 4단계: 방법 검증
937
+
938
+ ```python
939
+ def _validate_annotation_method(self):
940
+ """방법별 요구사항 검증"""
941
+ if self.context.annotation_method == AnnotationMethod.FILE:
942
+ # 대상 사양 검증
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
+ # 전처리기 검증
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
+ # 추가 전처리기 검증...
958
+ ```
959
+
960
+ #### 5단계: 처리 초기화
961
+
962
+ ```python
963
+ def _initialize_processing(self):
964
+ """메트릭 및 진행 추적 초기화"""
965
+ total_tasks = len(self.context.task_ids)
966
+
967
+ # 메트릭 재설정
968
+ self.context.update_metrics(
969
+ success_count=0,
970
+ failed_count=0,
971
+ total_count=total_tasks
972
+ )
973
+
974
+ # 진행 초기화
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
+ #### 6단계: 작업 처리
988
+
989
+ ```python
990
+ def _process_all_tasks(self):
991
+ """주석 전략으로 모든 작업 처리"""
992
+ # 주석 전략 생성
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
+ # 작업 쿼리 파라미터
1002
+ task_params = {
1003
+ 'expand': 'data_unit,data_unit.files'
1004
+ }
1005
+
1006
+ # 각 작업 처리
1007
+ for index, task_id in enumerate(self.context.task_ids, start=1):
1008
+ try:
1009
+ # 작업 데이터 가져오기
1010
+ task_data = self.context.client.get_task(
1011
+ task_id,
1012
+ params=task_params
1013
+ )
1014
+
1015
+ # 주석 전략 실행
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
+ # 카운터 업데이트
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
+ # 치명적 오류는 처리 중지
1044
+ raise PreAnnotationToTaskFailed(
1045
+ f'Critical error processing task {task_id}: {str(e)}'
1046
+ )
1047
+
1048
+ except Exception as e:
1049
+ # 작업 수준 오류는 처리 계속
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
+ # 진행 및 메트릭 업데이트
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
+ #### 7단계: 마무리
1079
+
1080
+ ```python
1081
+ def _finalize_processing(self):
1082
+ """메트릭 마무리 및 정리"""
1083
+ self.metrics_strategy.finalize_metrics(self.context)
1084
+
1085
+ # 완료 기록
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
+ ## 오류 처리 및 롤백
1094
+
1095
+ ### 롤백 메커니즘
1096
+
1097
+ ```python
1098
+ def _rollback_completed_steps(self):
1099
+ """완료된 모든 워크플로우 단계 롤백"""
1100
+ self.context.logger.log_message_with_code(
1101
+ LogCode.ROLLBACK_STARTED,
1102
+ len(self.steps_completed)
1103
+ )
1104
+
1105
+ # 역순으로 단계 롤백
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
+ # 커스텀 롤백 액션 실행
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
+ # 임시 파일 정리
1133
+ self._cleanup_temp_files()
1134
+ ```
1135
+
1136
+ ### 단계별 롤백 메서드
1137
+
1138
+ ```python
1139
+ def _rollback_project_validation(self):
1140
+ """프로젝트 검증 롤백"""
1141
+ self.context.project = None
1142
+ self.context.data_collection = None
1143
+
1144
+ def _rollback_task_validation(self):
1145
+ """작업 검증 롤백"""
1146
+ self.context.task_ids = []
1147
+
1148
+ def _rollback_processing_initialization(self):
1149
+ """처리 초기화 롤백"""
1150
+ self.context.metrics = MetricsRecord(success=0, failed=0, total=0)
1151
+
1152
+ def _rollback_task_processing(self):
1153
+ """작업 처리 롤백"""
1154
+ # 정리는 임시 파일로 처리됨
1155
+ pass
1156
+
1157
+ def _cleanup_temp_files(self):
1158
+ """모든 임시 파일 제거"""
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
+ ### 오류 레벨
1172
+
1173
+ **작업 수준 오류:**
1174
+
1175
+ - 개별 작업 처리 실패
1176
+ - 메트릭에 기록되고 카운트됨
1177
+ - 워크플로우는 나머지 작업과 계속됨
1178
+
1179
+ **치명적 오류:**
1180
+
1181
+ - 시스템 수준 실패
1182
+ - 즉시 워크플로우 종료 트리거
1183
+ - 롤백 프로세스 시작
1184
+
1185
+ **워크플로우 오류:**
1186
+
1187
+ - 단계 검증 실패
1188
+ - 완료된 단계 롤백
1189
+ - 호출자에게 예외 전파
1190
+
1191
+ ## ToTask 액션 확장
1192
+
1193
+ ### 커스텀 검증 전략 생성
1194
+
1195
+ ```python
1196
+ from synapse_sdk.plugins.categories.pre_annotation.actions.to_task.strategies.base import ValidationStrategy
1197
+
1198
+ class CustomBusinessRuleValidationStrategy(ValidationStrategy):
1199
+ """비즈니스별 규칙에 대한 커스텀 검증"""
1200
+
1201
+ def validate(self, context: ToTaskContext) -> Dict[str, Any]:
1202
+ """커스텀 비즈니스 규칙 검증"""
1203
+ try:
1204
+ # 작업 가져오기
1205
+ tasks = context.client.list_tasks(
1206
+ project=context.params['project'],
1207
+ **context.params['task_filters']
1208
+ )
1209
+
1210
+ # 커스텀 검증 로직
1211
+ for task in tasks:
1212
+ # 비즈니스 규칙 확인
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
+ # 작업 ID 저장
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
+ """작업이 비즈니스 규칙을 충족하는지 확인"""
1232
+ # 비즈니스 로직 구현
1233
+ return True
1234
+ ```
1235
+
1236
+ ### 커스텀 주석 전략 생성
1237
+
1238
+ ```python
1239
+ from synapse_sdk.plugins.categories.pre_annotation.actions.to_task.strategies.base import AnnotationStrategy
1240
+
1241
+ class ExternalAPIAnnotationStrategy(AnnotationStrategy):
1242
+ """외부 API 서비스를 사용한 주석"""
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
+ """외부 API를 사용한 작업 처리"""
1252
+ try:
1253
+ # 이미지 URL 추출
1254
+ image_url = self._extract_image_url(task_data)
1255
+
1256
+ # 외부 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
+ # 작업 객체로 변환
1267
+ task_object = self._convert_annotations(annotations)
1268
+
1269
+ # 작업 업데이트
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
+ """작업 데이터에서 이미지 URL 추출"""
1291
+ # 구현...
1292
+ pass
1293
+
1294
+ def _call_external_api(
1295
+ self, api_url: str, api_key: str, image_url: str
1296
+ ) -> Dict[str, Any]:
1297
+ """외부 주석 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
+ """외부 형식을 작업 객체로 변환"""
1311
+ # 구현...
1312
+ pass
1313
+ ```
1314
+
1315
+ ### 커스텀 전략 통합
1316
+
1317
+ 커스텀 전략을 지원하도록 팩토리 업데이트:
1318
+
1319
+ ```python
1320
+ class CustomToTaskStrategyFactory(ToTaskStrategyFactory):
1321
+ """커스텀 전략을 포함한 확장 팩토리"""
1322
+
1323
+ def create_validation_strategy(
1324
+ self, strategy_type: str
1325
+ ) -> ValidationStrategy:
1326
+ """커스텀 타입을 포함한 검증 전략 생성"""
1327
+ strategies = {
1328
+ 'project': ProjectValidationStrategy,
1329
+ 'task': TaskValidationStrategy,
1330
+ 'target_spec': TargetSpecValidationStrategy,
1331
+ 'business_rules': CustomBusinessRuleValidationStrategy, # 커스텀
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
+ """커스텀 방법을 포함한 주석 전략 생성"""
1344
+ if method == AnnotationMethod.FILE:
1345
+ return FileAnnotationStrategy()
1346
+ elif method == AnnotationMethod.INFERENCE:
1347
+ return InferenceAnnotationStrategy()
1348
+ elif method == 'external_api': # 커스텀
1349
+ return ExternalAPIAnnotationStrategy()
1350
+ else:
1351
+ raise ValueError(f'Unknown annotation method: {method}')
1352
+ ```
1353
+
1354
+ ## API 레퍼런스
1355
+
1356
+ ### 모델 및 Enum
1357
+
1358
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py`
1359
+
1360
+ #### ToTaskParams
1361
+
1362
+ ```python
1363
+ class ToTaskParams(BaseModel):
1364
+ """ToTask 액션의 파라미터"""
1365
+
1366
+ name: str = Field(..., description="액션 이름 (공백 불가)")
1367
+ description: Optional[str] = Field(None, description="액션 설명")
1368
+ project: int = Field(..., description="프로젝트 ID")
1369
+ agent: int = Field(..., description="에이전트 ID")
1370
+ task_filters: Dict[str, Any] = Field(..., description="작업 필터 기준")
1371
+ method: str = Field(..., description="주석 방법: 'file' 또는 'inference'")
1372
+ target_specification_name: Optional[str] = Field(
1373
+ None,
1374
+ description="파일 사양 이름 (파일 방법에 필수)"
1375
+ )
1376
+ model: Optional[int] = Field(None, description="모델 ID")
1377
+ pre_processor: Optional[int] = Field(
1378
+ None,
1379
+ description="전처리기 ID (추론 방법에 필수)"
1380
+ )
1381
+ pre_processor_params: Dict[str, Any] = Field(
1382
+ default_factory=dict,
1383
+ description="전처리기 구성 파라미터"
1384
+ )
1385
+
1386
+ @validator('name')
1387
+ def validate_name(cls, v):
1388
+ """이름에 공백이 없는지 검증"""
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
+ """방법이 지원되는지 검증"""
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
+ """ToTask 액션의 결과"""
1406
+
1407
+ status: JobStatus = Field(..., description="작업 상태")
1408
+ message: str = Field(..., description="결과 메시지")
1409
+ ```
1410
+
1411
+ #### MetricsRecord
1412
+
1413
+ ```python
1414
+ class MetricsRecord(BaseModel):
1415
+ """메트릭 추적 레코드"""
1416
+
1417
+ success: int = Field(0, description="성공적으로 처리된 수")
1418
+ failed: int = Field(0, description="처리 실패 수")
1419
+ total: int = Field(0, description="총 작업 수")
1420
+ ```
1421
+
1422
+ **파일:** `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py`
1423
+
1424
+ #### AnnotationMethod
1425
+
1426
+ ```python
1427
+ class AnnotationMethod(str, Enum):
1428
+ """지원되는 주석 방법"""
1429
+
1430
+ FILE = 'file'
1431
+ INFERENCE = 'inference'
1432
+ ```
1433
+
1434
+ #### LogCode
1435
+
1436
+ ```python
1437
+ class LogCode(str, Enum):
1438
+ """타입 안전 로깅 코드"""
1439
+
1440
+ # 워크플로우 코드
1441
+ TO_TASK_STARTED = 'to_task_started'
1442
+ STEP_COMPLETED = 'step_completed'
1443
+ TO_TASK_COMPLETED = 'to_task_completed'
1444
+
1445
+ # 검증 코드
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
+ # 처리 코드
1454
+ PROCESSING_INITIALIZED = 'processing_initialized'
1455
+ ANNOTATING_DATA = 'annotating_data'
1456
+ ANNOTATION_COMPLETED = 'annotation_completed'
1457
+ TASK_PROCESSING_FAILED = 'task_processing_failed'
1458
+
1459
+ # 추론 코드
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
+ # 롤백 코드
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
+ # 메트릭 코드
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
+ ## 테스트
1479
+
1480
+ ### 단위 테스트 전략
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
+ """프로젝트 검증 전략 테스트"""
1489
+
1490
+ def test_validate_success(self):
1491
+ """성공적인 검증 테스트"""
1492
+ # 설정
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
+ # 실행
1504
+ strategy = ProjectValidationStrategy()
1505
+ result = strategy.validate(context)
1506
+
1507
+ # 확인
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
+ """프로젝트를 찾을 수 없을 때 검증 실패 테스트"""
1514
+ # 설정
1515
+ context = Mock()
1516
+ context.params = {'project': 999}
1517
+ context.client.get_project.return_value = None
1518
+
1519
+ # 실행
1520
+ strategy = ProjectValidationStrategy()
1521
+ result = strategy.validate(context)
1522
+
1523
+ # 확인
1524
+ assert result['success'] is False
1525
+ assert 'not found' in result['error']
1526
+
1527
+ def test_validate_no_data_collection(self):
1528
+ """데이터 컬렉션이 없을 때 검증 실패 테스트"""
1529
+ # 설정
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
+ # 실행
1538
+ strategy = ProjectValidationStrategy()
1539
+ result = strategy.validate(context)
1540
+
1541
+ # 확인
1542
+ assert result['success'] is False
1543
+ assert 'no data collection' in result['error']
1544
+ ```
1545
+
1546
+ ### 통합 테스트
1547
+
1548
+ ```python
1549
+ class TestToTaskIntegration:
1550
+ """ToTask 워크플로우 통합 테스트"""
1551
+
1552
+ @pytest.fixture
1553
+ def mock_context(self):
1554
+ """모의 컨텍스트 생성"""
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
+ """파일 방법으로 완전한 워크플로우 테스트"""
1568
+ # 오케스트레이터 설정
1569
+ orchestrator = ToTaskOrchestrator(mock_context)
1570
+
1571
+ # 검증 응답 모의
1572
+ mock_context.client.get_project.return_value = {
1573
+ 'id': 123,
1574
+ 'data_collection': 456
1575
+ }
1576
+
1577
+ # 워크플로우 실행
1578
+ result = orchestrator.execute_workflow()
1579
+
1580
+ # 모든 단계 완료 확인
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
+ # 메트릭 확인
1586
+ assert result.total == 3
1587
+ ```
1588
+
1589
+ ## 마이그레이션 가이드
1590
+
1591
+ ### 레거시 구현에서
1592
+
1593
+ 레거시 ToTask 구현에서 마이그레이션하는 경우:
1594
+
1595
+ **주요 변경사항:**
1596
+
1597
+ 1. **전략 기반 아키텍처** - 단일 로직을 전략으로 대체
1598
+ 2. **오케스트레이션된 워크플로우** - 임의 실행 대신 정의된 7단계
1599
+ 3. **자동 롤백** - 내장된 오류 복구
1600
+ 4. **타입 안전 로깅** - 문자열 메시지 대신 LogCode enum
1601
+
1602
+ **마이그레이션 단계:**
1603
+
1604
+ 1. `ToTaskParams` 모델을 사용하도록 파라미터 검증 업데이트
1605
+ 2. 커스텀 검증 로직을 검증 전략으로 대체
1606
+ 3. 주석 로직을 주석 전략으로 마이그레이션
1607
+ 4. 오케스트레이터 롤백을 사용하도록 오류 처리 업데이트
1608
+ 5. LogCode 기반 메시지로 로깅 대체
1609
+
1610
+ ## 모범 사례
1611
+
1612
+ ### 성능
1613
+
1614
+ 1. 추론을 위한 **적절한 배치 크기 사용**
1615
+ 2. 처리 오버헤드를 줄이기 위해 **작업을 효율적으로 필터링**
1616
+ 3. 적절한 경우 커스텀 전략에 **캐싱 구현**
1617
+ 4. 대규모 작업 세트에 대한 **메모리 사용량 모니터링**
1618
+
1619
+ ### 신뢰성
1620
+
1621
+ 1. 처리 전 **모든 입력 검증**
1622
+ 2. 커스텀 전략에 **적절한 오류 처리 구현**
1623
+ 3. 정리 작업을 위해 **롤백 액션 사용**
1624
+ 4. 디버깅을 위한 **상세한 오류 메시지 기록**
1625
+
1626
+ ### 유지보수성
1627
+
1628
+ 1. 새로운 기능을 위해 **전략 패턴 따르기**
1629
+ 2. **타입 힌트 일관성 있게 사용**
1630
+ 3. 커스텀 전략을 **철저히 문서화**
1631
+ 4. 모든 전략에 대해 **포괄적인 테스트 작성**
1632
+
1633
+ ## 관련 문서
1634
+
1635
+ - [ToTask 개요](./to-task-overview.md) - 사용자 가이드
1636
+ - [Pre-annotation 플러그인 개요](./pre-annotation-plugin-overview.md) - 카테고리 개요
1637
+ - 플러그인 개발 가이드 - 일반 플러그인 개발
1638
+ - 전략 패턴 - 설계 패턴 세부사항
1639
+
1640
+ ## 소스 코드 레퍼런스
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`