service-forge 0.1.18__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 service-forge might be problematic. Click here for more details.

Files changed (83) hide show
  1. service_forge/api/deprecated_websocket_api.py +86 -0
  2. service_forge/api/deprecated_websocket_manager.py +425 -0
  3. service_forge/api/http_api.py +152 -0
  4. service_forge/api/http_api_doc.py +455 -0
  5. service_forge/api/kafka_api.py +126 -0
  6. service_forge/api/routers/feedback/feedback_router.py +148 -0
  7. service_forge/api/routers/service/service_router.py +127 -0
  8. service_forge/api/routers/websocket/websocket_manager.py +83 -0
  9. service_forge/api/routers/websocket/websocket_router.py +78 -0
  10. service_forge/api/task_manager.py +141 -0
  11. service_forge/current_service.py +14 -0
  12. service_forge/db/__init__.py +1 -0
  13. service_forge/db/database.py +237 -0
  14. service_forge/db/migrations/feedback_migration.py +154 -0
  15. service_forge/db/models/__init__.py +0 -0
  16. service_forge/db/models/feedback.py +33 -0
  17. service_forge/llm/__init__.py +67 -0
  18. service_forge/llm/llm.py +56 -0
  19. service_forge/model/__init__.py +0 -0
  20. service_forge/model/feedback.py +30 -0
  21. service_forge/model/websocket.py +13 -0
  22. service_forge/proto/foo_input.py +5 -0
  23. service_forge/service.py +280 -0
  24. service_forge/service_config.py +44 -0
  25. service_forge/sft/cli.py +91 -0
  26. service_forge/sft/cmd/config_command.py +67 -0
  27. service_forge/sft/cmd/deploy_service.py +123 -0
  28. service_forge/sft/cmd/list_tars.py +41 -0
  29. service_forge/sft/cmd/service_command.py +149 -0
  30. service_forge/sft/cmd/upload_service.py +36 -0
  31. service_forge/sft/config/injector.py +129 -0
  32. service_forge/sft/config/injector_default_files.py +131 -0
  33. service_forge/sft/config/sf_metadata.py +30 -0
  34. service_forge/sft/config/sft_config.py +200 -0
  35. service_forge/sft/file/__init__.py +0 -0
  36. service_forge/sft/file/ignore_pattern.py +80 -0
  37. service_forge/sft/file/sft_file_manager.py +107 -0
  38. service_forge/sft/kubernetes/kubernetes_manager.py +257 -0
  39. service_forge/sft/util/assert_util.py +25 -0
  40. service_forge/sft/util/logger.py +16 -0
  41. service_forge/sft/util/name_util.py +8 -0
  42. service_forge/sft/util/yaml_utils.py +57 -0
  43. service_forge/storage/__init__.py +5 -0
  44. service_forge/storage/feedback_storage.py +245 -0
  45. service_forge/utils/__init__.py +0 -0
  46. service_forge/utils/default_type_converter.py +12 -0
  47. service_forge/utils/register.py +39 -0
  48. service_forge/utils/type_converter.py +99 -0
  49. service_forge/utils/workflow_clone.py +124 -0
  50. service_forge/workflow/__init__.py +1 -0
  51. service_forge/workflow/context.py +14 -0
  52. service_forge/workflow/edge.py +24 -0
  53. service_forge/workflow/node.py +184 -0
  54. service_forge/workflow/nodes/__init__.py +8 -0
  55. service_forge/workflow/nodes/control/if_node.py +29 -0
  56. service_forge/workflow/nodes/control/switch_node.py +28 -0
  57. service_forge/workflow/nodes/input/console_input_node.py +26 -0
  58. service_forge/workflow/nodes/llm/query_llm_node.py +41 -0
  59. service_forge/workflow/nodes/nested/workflow_node.py +28 -0
  60. service_forge/workflow/nodes/output/kafka_output_node.py +27 -0
  61. service_forge/workflow/nodes/output/print_node.py +29 -0
  62. service_forge/workflow/nodes/test/if_console_input_node.py +33 -0
  63. service_forge/workflow/nodes/test/time_consuming_node.py +62 -0
  64. service_forge/workflow/port.py +89 -0
  65. service_forge/workflow/trigger.py +28 -0
  66. service_forge/workflow/triggers/__init__.py +6 -0
  67. service_forge/workflow/triggers/a2a_api_trigger.py +257 -0
  68. service_forge/workflow/triggers/fast_api_trigger.py +201 -0
  69. service_forge/workflow/triggers/kafka_api_trigger.py +47 -0
  70. service_forge/workflow/triggers/once_trigger.py +23 -0
  71. service_forge/workflow/triggers/period_trigger.py +29 -0
  72. service_forge/workflow/triggers/websocket_api_trigger.py +189 -0
  73. service_forge/workflow/workflow.py +227 -0
  74. service_forge/workflow/workflow_callback.py +141 -0
  75. service_forge/workflow/workflow_config.py +66 -0
  76. service_forge/workflow/workflow_event.py +15 -0
  77. service_forge/workflow/workflow_factory.py +246 -0
  78. service_forge/workflow/workflow_group.py +51 -0
  79. service_forge/workflow/workflow_type.py +52 -0
  80. service_forge-0.1.18.dist-info/METADATA +98 -0
  81. service_forge-0.1.18.dist-info/RECORD +83 -0
  82. service_forge-0.1.18.dist-info/WHEEL +4 -0
  83. service_forge-0.1.18.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+ from loguru import logger
3
+ from service_forge.workflow.trigger import Trigger
4
+ from typing import AsyncIterator, Any
5
+ from service_forge.workflow.port import Port
6
+ from google.protobuf.message import Message
7
+ from google.protobuf.json_format import MessageToJson
8
+ from fastapi import FastAPI
9
+ from a2a.types import (
10
+ AgentCapabilities,
11
+ AgentCard,
12
+ AgentSkill,
13
+ )
14
+ from a2a.server.apps import A2AStarletteApplication
15
+ from a2a.server.request_handlers import DefaultRequestHandler
16
+ from a2a.server.tasks import InMemoryTaskStore
17
+ from a2a.server.agent_execution import AgentExecutor, RequestContext
18
+ from a2a.server.events import EventQueue
19
+ from a2a.utils import new_agent_text_message
20
+ from a2a.utils.constants import DEFAULT_RPC_URL, EXTENDED_AGENT_CARD_PATH, AGENT_CARD_WELL_KNOWN_PATH
21
+
22
+ import json
23
+ import uuid
24
+ import asyncio
25
+ from service_forge.workflow.workflow_event import WorkflowResult
26
+
27
+ class A2AAgentExecutor(AgentExecutor):
28
+ def __init__(self, trigger: A2AAPITrigger):
29
+ self.trigger = trigger
30
+
31
+ @staticmethod
32
+ def serialize_result(result: Any) -> str:
33
+ if isinstance(result, Message):
34
+ return MessageToJson(
35
+ result,
36
+ preserving_proto_field_name=True
37
+ )
38
+ return json.dumps(result)
39
+
40
+ async def send_event(self, event_queue: EventQueue, item: WorkflowResult) -> None:
41
+ if item.is_error:
42
+ result = {
43
+ 'event': 'error',
44
+ 'detail': str(item.result)
45
+ }
46
+ await event_queue.enqueue_event(new_agent_text_message(json.dumps(result)))
47
+
48
+ if item.is_end:
49
+ result = {
50
+ 'event': 'end',
51
+ 'detail': self.serialize_result(item.result)
52
+ }
53
+ await event_queue.enqueue_event(new_agent_text_message(json.dumps(result)))
54
+
55
+ result = {
56
+ 'event': 'data',
57
+ 'data': self.serialize_result(item.result)
58
+ }
59
+ await event_queue.enqueue_event(new_agent_text_message(json.dumps(result)))
60
+
61
+ async def execute(
62
+ self,
63
+ context: RequestContext,
64
+ event_queue: EventQueue,
65
+ ) -> None:
66
+ task_id = uuid.uuid4()
67
+ self.trigger.result_queues[task_id] = asyncio.Queue()
68
+
69
+ self.trigger.trigger_queue.put_nowait({
70
+ 'id': task_id,
71
+ 'context': context,
72
+ })
73
+
74
+ # TODO: support stream output
75
+ if False:
76
+ self.trigger.stream_queues[task_id] = asyncio.Queue()
77
+ while True:
78
+ item = await self.trigger.stream_queues[task_id].get()
79
+ await self.send_event(event_queue, item)
80
+
81
+ if item.is_error or item.is_end:
82
+ break
83
+
84
+ if task_id in self.trigger.stream_queues:
85
+ del self.trigger.stream_queues[task_id]
86
+ else:
87
+ result = await self.trigger.result_queues[task_id].get()
88
+ await self.send_event(event_queue, result)
89
+
90
+ if task_id in self.trigger.result_queues:
91
+ del self.trigger.result_queues[task_id]
92
+
93
+ async def cancel(
94
+ self, context: RequestContext, event_queue: EventQueue
95
+ ) -> None:
96
+ raise Exception('cancel not supported')
97
+
98
+
99
+ class A2AAPITrigger(Trigger):
100
+ DEFAULT_INPUT_PORTS = [
101
+ Port("app", FastAPI),
102
+ Port("path", str),
103
+ Port("skill_id", str, is_extended=True),
104
+ Port("skill_name", str, is_extended=True),
105
+ Port("skill_description", str, is_extended=True),
106
+ Port("skill_tags", list[str], is_extended=True),
107
+ Port("skill_examples", list[str], is_extended=True),
108
+ Port("agent_name", str),
109
+ Port("agent_url", str),
110
+ Port("agent_description", str),
111
+ Port("agent_version", str),
112
+ ]
113
+
114
+ DEFAULT_OUTPUT_PORTS = [
115
+ Port("trigger", bool),
116
+ Port("context", RequestContext),
117
+ ]
118
+
119
+ def __init__(self, name: str):
120
+ super().__init__(name)
121
+ self.events = {}
122
+ self.is_setup_handler = False
123
+ self.agent_card: AgentCard | None = None
124
+
125
+ @staticmethod
126
+ def serialize_result(result: Any):
127
+ if isinstance(result, Message):
128
+ return MessageToJson(
129
+ result,
130
+ preserving_proto_field_name=True
131
+ )
132
+ return result
133
+
134
+ def _setup_handler(
135
+ self,
136
+ app: FastAPI,
137
+ path: str,
138
+ skill_id: list[tuple[int, str]],
139
+ skill_name: list[tuple[int, str]],
140
+ skill_description: list[tuple[int, str]],
141
+ skill_tags: list[tuple[int, list[str]]],
142
+ skill_examples: list[tuple[int, list[str]]],
143
+ agent_name: str,
144
+ agent_url: str,
145
+ agent_description: str,
146
+ agent_version: str,
147
+ ) -> None:
148
+
149
+ skills_config = []
150
+ for i in range(len(skill_id)):
151
+ skills_config.append({
152
+ 'id': '',
153
+ 'name': '',
154
+ 'description': '',
155
+ 'tags': [],
156
+ 'examples': [],
157
+ })
158
+
159
+ for i in range(len(skill_id)):
160
+ skills_config[skill_id[i][0]]['id'] = skill_id[i][1]
161
+ skills_config[skill_name[i][0]]['name'] = skill_name[i][1]
162
+ skills_config[skill_description[i][0]]['description'] = skill_description[i][1]
163
+ skills_config[skill_tags[i][0]]['tags'] = skill_tags[i][1]
164
+ skills_config[skill_examples[i][0]]['examples'] = skill_examples[i][1]
165
+
166
+ skills = []
167
+ for config in skills_config:
168
+ skills.append(AgentSkill(
169
+ id=config['id'],
170
+ name=config['name'],
171
+ description=config['description'],
172
+ tags=config['tags'],
173
+ examples=config['examples'],
174
+ ))
175
+
176
+ agent_card = AgentCard(
177
+ name=agent_name,
178
+ description=agent_description,
179
+ url=agent_url,
180
+ version=agent_version,
181
+ default_input_modes=['text'],
182
+ default_output_modes=['text'],
183
+ capabilities=AgentCapabilities(streaming=True),
184
+ skills=skills,
185
+ supports_authenticated_extended_card=False,
186
+ )
187
+
188
+ self.agent_card = agent_card
189
+
190
+ request_handler = DefaultRequestHandler(
191
+ agent_executor=A2AAgentExecutor(self),
192
+ task_store=InMemoryTaskStore(),
193
+ )
194
+
195
+ try:
196
+ server = A2AStarletteApplication(
197
+ agent_card=agent_card,
198
+ http_handler=request_handler,
199
+ )
200
+
201
+ server.add_routes_to_app(
202
+ app,
203
+ agent_card_url="/a2a" + path + AGENT_CARD_WELL_KNOWN_PATH,
204
+ rpc_url="/a2a" + path + DEFAULT_RPC_URL,
205
+ extended_agent_card_url="/a2a" + path + EXTENDED_AGENT_CARD_PATH,
206
+ )
207
+
208
+ except Exception as e:
209
+ logger.error(f"Error adding A2A routes: {e}")
210
+ raise
211
+
212
+ async def _run(
213
+ self,
214
+ app: FastAPI,
215
+ path: str,
216
+ skill_id: list[tuple[int, str]],
217
+ skill_name: list[tuple[int, str]],
218
+ skill_description: list[tuple[int, str]],
219
+ skill_tags: list[tuple[int, list[str]]],
220
+ skill_examples: list[tuple[int, list[str]]],
221
+ agent_name: str,
222
+ agent_url: str,
223
+ agent_description: str,
224
+ agent_version: str,
225
+ ) -> AsyncIterator[bool]:
226
+ if len(skill_id) != len(skill_name) or len(skill_id) != len(skill_description) or len(skill_id) != len(skill_tags) or len(skill_id) != len(skill_examples):
227
+ raise ValueError("skill_id, skill_name, skill_description, skill_tags, skill_examples must have the same length")
228
+
229
+ if not self.is_setup_handler:
230
+ self._setup_handler(
231
+ app,
232
+ path,
233
+ skill_id,
234
+ skill_name,
235
+ skill_description,
236
+ skill_tags,
237
+ skill_examples,
238
+ agent_name,
239
+ agent_url,
240
+ agent_description,
241
+ agent_version,
242
+ )
243
+ self.is_setup_handler = True
244
+
245
+ logger.info(f"A2A Trigger {self.name} is running")
246
+
247
+ while True:
248
+ try:
249
+ trigger = await self.trigger_queue.get()
250
+ self.prepare_output_edges('context', trigger['context'])
251
+ yield self.trigger(trigger['id'])
252
+ except Exception as e:
253
+ logger.error(f"Error in A2AAPITrigger._run: {e}")
254
+ continue
255
+
256
+ async def _stop(self) -> AsyncIterator[bool]:
257
+ pass
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+ import uuid
3
+ import asyncio
4
+ import json
5
+ from loguru import logger
6
+ from service_forge.workflow.trigger import Trigger
7
+ from typing import AsyncIterator, Any
8
+ from fastapi import FastAPI, Request
9
+ from fastapi.responses import StreamingResponse
10
+ from service_forge.workflow.port import Port
11
+ from service_forge.utils.default_type_converter import type_converter
12
+ from service_forge.api.routers.websocket.websocket_manager import websocket_manager
13
+ from fastapi import HTTPException
14
+ from google.protobuf.message import Message
15
+ from google.protobuf.json_format import MessageToJson
16
+
17
+ class FastAPITrigger(Trigger):
18
+ DEFAULT_INPUT_PORTS = [
19
+ Port("app", FastAPI),
20
+ Port("path", str),
21
+ Port("method", str),
22
+ Port("data_type", type),
23
+ Port("is_stream", bool),
24
+ ]
25
+
26
+ DEFAULT_OUTPUT_PORTS = [
27
+ Port("trigger", bool),
28
+ Port("user_id", int),
29
+ Port("data", Any),
30
+ ]
31
+
32
+ def __init__(self, name: str):
33
+ super().__init__(name)
34
+ self.events = {}
35
+ self.is_setup_route = False
36
+ self.app = None
37
+ self.route_path = None
38
+ self.route_method = None
39
+
40
+ @staticmethod
41
+ def serialize_result(result: Any):
42
+ if isinstance(result, Message):
43
+ return MessageToJson(
44
+ result,
45
+ preserving_proto_field_name=True
46
+ )
47
+ return result
48
+
49
+ async def handle_request(
50
+ self,
51
+ request: Request,
52
+ data_type: type,
53
+ extract_data_fn: callable[[Request], dict],
54
+ is_stream: bool,
55
+ ):
56
+ task_id = uuid.uuid4()
57
+ self.result_queues[task_id] = asyncio.Queue()
58
+
59
+ body_data = await extract_data_fn(request)
60
+ converted_data = data_type(**body_data)
61
+
62
+ client_id = (
63
+ body_data.get("client_id")
64
+ or request.query_params.get("client_id")
65
+ or request.headers.get("X-Client-ID")
66
+ )
67
+ if client_id:
68
+ workflow_name = getattr(self.workflow, "name", "Unknown")
69
+ steps = len(self.workflow.nodes) if hasattr(self.workflow, "nodes") else 1
70
+ websocket_manager.create_task_with_client(task_id, client_id, workflow_name, steps)
71
+
72
+ self.trigger_queue.put_nowait({
73
+ "id": task_id,
74
+ "user_id": getattr(request.state, "user_id", None),
75
+ "data": converted_data,
76
+ })
77
+
78
+ if is_stream:
79
+ self.stream_queues[task_id] = asyncio.Queue()
80
+
81
+ async def generate_sse():
82
+ try:
83
+ while True:
84
+ item = await self.stream_queues[task_id].get()
85
+
86
+ if item.is_error:
87
+ yield f"event: error\ndata: {json.dumps({'detail': str(item.result)})}\n\n"
88
+ break
89
+
90
+ if item.is_end:
91
+ # TODO: send the result?
92
+ break
93
+
94
+ # TODO: modify
95
+ serialized = self.serialize_result(item.result)
96
+ if isinstance(serialized, str):
97
+ data = serialized
98
+ else:
99
+ data = json.dumps(serialized)
100
+
101
+ yield f"data: {data}\n\n"
102
+
103
+ except Exception as e:
104
+ yield f"event: error\ndata: {json.dumps({'detail': str(e)})}\n\n"
105
+ finally:
106
+ if task_id in self.stream_queues:
107
+ del self.stream_queues[task_id]
108
+ if task_id in self.result_queues:
109
+ del self.result_queues[task_id]
110
+
111
+ return StreamingResponse(
112
+ generate_sse(),
113
+ media_type="text/event-stream",
114
+ headers={
115
+ "Cache-Control": "no-cache",
116
+ "Connection": "keep-alive",
117
+ "X-Accel-Buffering": "no",
118
+ }
119
+ )
120
+ else:
121
+ result = await self.result_queues[task_id].get()
122
+ del self.result_queues[task_id]
123
+
124
+ if result.is_error:
125
+ if isinstance(result.result, HTTPException):
126
+ raise result.result
127
+ else:
128
+ raise HTTPException(status_code=500, detail=str(result.result))
129
+
130
+ return self.serialize_result(result.result)
131
+
132
+ def _setup_route(self, app: FastAPI, path: str, method: str, data_type: type, is_stream: bool) -> None:
133
+ async def get_data(request: Request) -> dict:
134
+ return dict(request.query_params)
135
+
136
+ async def body_data(request: Request) -> dict:
137
+ raw = await request.body()
138
+ if not raw:
139
+ return {}
140
+ return json.loads(raw.decode("utf-8"))
141
+
142
+ extractor = get_data if method == "GET" else body_data
143
+
144
+ async def handler(request: Request):
145
+ return await self.handle_request(request, data_type, extractor, is_stream)
146
+
147
+ # Save route information for cleanup
148
+ self.app = app
149
+ self.route_path = path
150
+ self.route_method = method.upper()
151
+
152
+ if method == "GET":
153
+ app.get(path)(handler)
154
+ elif method == "POST":
155
+ app.post(path)(handler)
156
+ elif method == "PUT":
157
+ app.put(path)(handler)
158
+ elif method == "DELETE":
159
+ app.delete(path)(handler)
160
+ else:
161
+ raise ValueError(f"Invalid method {method}")
162
+
163
+ async def _run(self, app: FastAPI, path: str, method: str, data_type: type, is_stream: bool = False) -> AsyncIterator[bool]:
164
+ if not self.is_setup_route:
165
+ self._setup_route(app, path, method, data_type, is_stream)
166
+ self.is_setup_route = True
167
+
168
+ while True:
169
+ try:
170
+ trigger = await self.trigger_queue.get()
171
+ self.prepare_output_edges(self.get_output_port_by_name('user_id'), trigger['user_id'])
172
+ self.prepare_output_edges(self.get_output_port_by_name('data'), trigger['data'])
173
+ yield self.trigger(trigger['id'])
174
+ except Exception as e:
175
+ logger.error(f"Error in FastAPITrigger._run: {e}")
176
+ continue
177
+
178
+ async def _stop(self) -> AsyncIterator[bool]:
179
+ if self.is_setup_route:
180
+ # Remove the route from the app
181
+ if self.app and self.route_path and self.route_method:
182
+ # Find and remove matching route
183
+ routes_to_remove = []
184
+ for route in self.app.routes:
185
+ if hasattr(route, "path") and hasattr(route, "methods"):
186
+ if route.path == self.route_path and self.route_method in route.methods:
187
+ routes_to_remove.append(route)
188
+
189
+ # Remove found routes
190
+ for route in routes_to_remove:
191
+ try:
192
+ self.app.routes.remove(route)
193
+ logger.info(f"Removed route {self.route_method} {self.route_path} from FastAPI app")
194
+ except ValueError:
195
+ logger.warning(f"Route {self.route_method} {self.route_path} not found in app.routes")
196
+
197
+ # Reset route information
198
+ self.app = None
199
+ self.route_path = None
200
+ self.route_method = None
201
+ self.is_setup_route = False
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+ import uuid
3
+ from typing import Any
4
+ from service_forge.workflow.trigger import Trigger
5
+ from typing import AsyncIterator
6
+ from service_forge.workflow.port import Port
7
+ from service_forge.api.kafka_api import KafkaApp
8
+
9
+ class KafkaAPITrigger(Trigger):
10
+ DEFAULT_INPUT_PORTS = [
11
+ Port("app", KafkaApp),
12
+ Port("topic", str),
13
+ Port("data_type", type),
14
+ Port("group_id", str),
15
+ ]
16
+
17
+ DEFAULT_OUTPUT_PORTS = [
18
+ Port("trigger", bool),
19
+ Port("data", Any),
20
+ ]
21
+
22
+ def __init__(self, name: str):
23
+ super().__init__(name)
24
+ self.events = {}
25
+ self.is_setup_kafka_input = False
26
+
27
+ def _setup_kafka_input(self, app: KafkaApp, topic: str, data_type: type, group_id: str) -> None:
28
+ @app.kafka_input(topic, data_type, group_id)
29
+ async def handle_message(data):
30
+ task_id = uuid.uuid4()
31
+ self.trigger_queue.put_nowait({
32
+ "id": task_id,
33
+ "data": data,
34
+ })
35
+
36
+ async def _run(self, app: KafkaApp, topic: str, data_type: type, group_id: str) -> AsyncIterator[bool]:
37
+ if not self.is_setup_kafka_input:
38
+ self._setup_kafka_input(app, topic, data_type, group_id)
39
+ self.is_setup_kafka_input = True
40
+
41
+ while True:
42
+ trigger = await self.trigger_queue.get()
43
+ self.prepare_output_edges(self.get_output_port_by_name('data'), trigger['data'])
44
+ yield self.trigger(trigger['id'])
45
+
46
+ async def _stop(self) -> AsyncIterator[bool]:
47
+ pass
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+ from service_forge.workflow.node import Node
3
+ from service_forge.workflow.port import Port
4
+ from service_forge.workflow.trigger import Trigger
5
+ from typing import AsyncIterator
6
+ import uuid
7
+
8
+ class OnceTrigger(Trigger):
9
+ DEFAULT_INPUT_PORTS = [
10
+ ]
11
+
12
+ DEFAULT_OUTPUT_PORTS = [
13
+ Port("trigger", bool),
14
+ ]
15
+
16
+ def __init__(self, name: str):
17
+ super().__init__(name)
18
+
19
+ async def _run(self) -> AsyncIterator[bool]:
20
+ yield self.trigger(uuid.uuid4())
21
+
22
+ async def _stop(self) -> AsyncIterator[bool]:
23
+ pass
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ from service_forge.workflow.node import Node
4
+ from service_forge.workflow.port import Port
5
+ from service_forge.workflow.trigger import Trigger
6
+ from typing import AsyncIterator
7
+ import uuid
8
+
9
+ class PeriodTrigger(Trigger):
10
+ DEFAULT_INPUT_PORTS = [
11
+ Port("TRIGGER", bool),
12
+ Port("period", float),
13
+ Port("times", int),
14
+ ]
15
+
16
+ DEFAULT_OUTPUT_PORTS = [
17
+ Port("trigger", bool),
18
+ ]
19
+
20
+ def __init__(self, name: str):
21
+ super().__init__(name)
22
+
23
+ async def _run(self, times: int, period: float) -> AsyncIterator[bool]:
24
+ for _ in range(times):
25
+ await asyncio.sleep(period)
26
+ yield uuid.uuid4()
27
+
28
+ async def _stop(self) -> AsyncIterator[bool]:
29
+ pass