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.
- service_forge/api/deprecated_websocket_api.py +86 -0
- service_forge/api/deprecated_websocket_manager.py +425 -0
- service_forge/api/http_api.py +152 -0
- service_forge/api/http_api_doc.py +455 -0
- service_forge/api/kafka_api.py +126 -0
- service_forge/api/routers/feedback/feedback_router.py +148 -0
- service_forge/api/routers/service/service_router.py +127 -0
- service_forge/api/routers/websocket/websocket_manager.py +83 -0
- service_forge/api/routers/websocket/websocket_router.py +78 -0
- service_forge/api/task_manager.py +141 -0
- service_forge/current_service.py +14 -0
- service_forge/db/__init__.py +1 -0
- service_forge/db/database.py +237 -0
- service_forge/db/migrations/feedback_migration.py +154 -0
- service_forge/db/models/__init__.py +0 -0
- service_forge/db/models/feedback.py +33 -0
- service_forge/llm/__init__.py +67 -0
- service_forge/llm/llm.py +56 -0
- service_forge/model/__init__.py +0 -0
- service_forge/model/feedback.py +30 -0
- service_forge/model/websocket.py +13 -0
- service_forge/proto/foo_input.py +5 -0
- service_forge/service.py +280 -0
- service_forge/service_config.py +44 -0
- service_forge/sft/cli.py +91 -0
- service_forge/sft/cmd/config_command.py +67 -0
- service_forge/sft/cmd/deploy_service.py +123 -0
- service_forge/sft/cmd/list_tars.py +41 -0
- service_forge/sft/cmd/service_command.py +149 -0
- service_forge/sft/cmd/upload_service.py +36 -0
- service_forge/sft/config/injector.py +129 -0
- service_forge/sft/config/injector_default_files.py +131 -0
- service_forge/sft/config/sf_metadata.py +30 -0
- service_forge/sft/config/sft_config.py +200 -0
- service_forge/sft/file/__init__.py +0 -0
- service_forge/sft/file/ignore_pattern.py +80 -0
- service_forge/sft/file/sft_file_manager.py +107 -0
- service_forge/sft/kubernetes/kubernetes_manager.py +257 -0
- service_forge/sft/util/assert_util.py +25 -0
- service_forge/sft/util/logger.py +16 -0
- service_forge/sft/util/name_util.py +8 -0
- service_forge/sft/util/yaml_utils.py +57 -0
- service_forge/storage/__init__.py +5 -0
- service_forge/storage/feedback_storage.py +245 -0
- service_forge/utils/__init__.py +0 -0
- service_forge/utils/default_type_converter.py +12 -0
- service_forge/utils/register.py +39 -0
- service_forge/utils/type_converter.py +99 -0
- service_forge/utils/workflow_clone.py +124 -0
- service_forge/workflow/__init__.py +1 -0
- service_forge/workflow/context.py +14 -0
- service_forge/workflow/edge.py +24 -0
- service_forge/workflow/node.py +184 -0
- service_forge/workflow/nodes/__init__.py +8 -0
- service_forge/workflow/nodes/control/if_node.py +29 -0
- service_forge/workflow/nodes/control/switch_node.py +28 -0
- service_forge/workflow/nodes/input/console_input_node.py +26 -0
- service_forge/workflow/nodes/llm/query_llm_node.py +41 -0
- service_forge/workflow/nodes/nested/workflow_node.py +28 -0
- service_forge/workflow/nodes/output/kafka_output_node.py +27 -0
- service_forge/workflow/nodes/output/print_node.py +29 -0
- service_forge/workflow/nodes/test/if_console_input_node.py +33 -0
- service_forge/workflow/nodes/test/time_consuming_node.py +62 -0
- service_forge/workflow/port.py +89 -0
- service_forge/workflow/trigger.py +28 -0
- service_forge/workflow/triggers/__init__.py +6 -0
- service_forge/workflow/triggers/a2a_api_trigger.py +257 -0
- service_forge/workflow/triggers/fast_api_trigger.py +201 -0
- service_forge/workflow/triggers/kafka_api_trigger.py +47 -0
- service_forge/workflow/triggers/once_trigger.py +23 -0
- service_forge/workflow/triggers/period_trigger.py +29 -0
- service_forge/workflow/triggers/websocket_api_trigger.py +189 -0
- service_forge/workflow/workflow.py +227 -0
- service_forge/workflow/workflow_callback.py +141 -0
- service_forge/workflow/workflow_config.py +66 -0
- service_forge/workflow/workflow_event.py +15 -0
- service_forge/workflow/workflow_factory.py +246 -0
- service_forge/workflow/workflow_group.py +51 -0
- service_forge/workflow/workflow_type.py +52 -0
- service_forge-0.1.18.dist-info/METADATA +98 -0
- service_forge-0.1.18.dist-info/RECORD +83 -0
- service_forge-0.1.18.dist-info/WHEEL +4 -0
- 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
|