digitalkin 0.2.12__py3-none-any.whl → 0.2.14__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.
Files changed (42) hide show
  1. digitalkin/__version__.py +1 -1
  2. digitalkin/grpc_servers/_base_server.py +15 -17
  3. digitalkin/grpc_servers/module_server.py +9 -10
  4. digitalkin/grpc_servers/module_servicer.py +199 -85
  5. digitalkin/grpc_servers/registry_server.py +3 -6
  6. digitalkin/grpc_servers/registry_servicer.py +18 -19
  7. digitalkin/grpc_servers/utils/exceptions.py +4 -0
  8. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +3 -5
  9. digitalkin/logger.py +45 -1
  10. digitalkin/models/module/__init__.py +2 -1
  11. digitalkin/models/module/module.py +1 -0
  12. digitalkin/models/module/module_types.py +1 -0
  13. digitalkin/modules/_base_module.py +124 -7
  14. digitalkin/modules/archetype_module.py +11 -1
  15. digitalkin/modules/job_manager/base_job_manager.py +181 -0
  16. digitalkin/modules/job_manager/job_manager_models.py +44 -0
  17. digitalkin/modules/job_manager/single_job_manager.py +285 -0
  18. digitalkin/modules/job_manager/taskiq_broker.py +214 -0
  19. digitalkin/modules/job_manager/taskiq_job_manager.py +286 -0
  20. digitalkin/modules/tool_module.py +2 -1
  21. digitalkin/modules/trigger_module.py +3 -1
  22. digitalkin/services/cost/default_cost.py +8 -4
  23. digitalkin/services/cost/grpc_cost.py +15 -7
  24. digitalkin/services/filesystem/default_filesystem.py +2 -4
  25. digitalkin/services/filesystem/grpc_filesystem.py +8 -5
  26. digitalkin/services/setup/__init__.py +1 -0
  27. digitalkin/services/setup/default_setup.py +10 -12
  28. digitalkin/services/setup/grpc_setup.py +8 -10
  29. digitalkin/services/storage/default_storage.py +11 -5
  30. digitalkin/services/storage/grpc_storage.py +23 -8
  31. digitalkin/utils/arg_parser.py +5 -48
  32. digitalkin/utils/development_mode_action.py +51 -0
  33. {digitalkin-0.2.12.dist-info → digitalkin-0.2.14.dist-info}/METADATA +46 -15
  34. {digitalkin-0.2.12.dist-info → digitalkin-0.2.14.dist-info}/RECORD +41 -34
  35. {digitalkin-0.2.12.dist-info → digitalkin-0.2.14.dist-info}/WHEEL +1 -1
  36. modules/cpu_intensive_module.py +281 -0
  37. modules/minimal_llm_module.py +240 -58
  38. modules/storage_module.py +5 -6
  39. modules/text_transform_module.py +1 -1
  40. digitalkin/modules/job_manager.py +0 -177
  41. {digitalkin-0.2.12.dist-info → digitalkin-0.2.14.dist-info}/licenses/LICENSE +0 -0
  42. {digitalkin-0.2.12.dist-info → digitalkin-0.2.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,281 @@
1
+ """Simple module calling an LLM."""
2
+
3
+ import logging
4
+ from collections.abc import Callable
5
+ from typing import Any, ClassVar, Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from digitalkin.grpc_servers.utils.models import ClientConfig, SecurityMode, ServerConfig, ServerMode
10
+ from digitalkin.modules._base_module import BaseModule
11
+ from digitalkin.services.services_models import ServicesStrategy
12
+ from digitalkin.services.setup.setup_strategy import SetupData
13
+
14
+ # Configure logging with clear formatting
15
+ logging.basicConfig(
16
+ level=logging.DEBUG,
17
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
+ )
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class MessageInputPayload(BaseModel):
23
+ """Message trigger model for the CPU Archetype module."""
24
+
25
+ payload_type: Literal["message"] = "message"
26
+ user_prompt: str = Field(
27
+ ...,
28
+ title="User Prompt",
29
+ description="The prompt provided by the user for processing.",
30
+ )
31
+
32
+
33
+ class InputFile(BaseModel):
34
+ """File model for the CPU Archetype module."""
35
+
36
+ name: str = Field(
37
+ ...,
38
+ title="File Name",
39
+ description="The name of the file to be processed.",
40
+ )
41
+ content: bytes = Field(
42
+ ...,
43
+ title="File Content",
44
+ description="The content of the file to be processed.",
45
+ )
46
+
47
+ file_type: str = Field(
48
+ ...,
49
+ title="File Type",
50
+ description="The type of the file to be processed.",
51
+ )
52
+
53
+
54
+ class FileInputPayload(BaseModel):
55
+ """File input model for the CPU Archetype module."""
56
+
57
+ payload_type: Literal["file"] = "file"
58
+ files: list[InputFile] = Field(
59
+ ...,
60
+ title="Files",
61
+ description="List of files to be processed.",
62
+ )
63
+
64
+
65
+ class CPUInput(BaseModel):
66
+ """Input model defining what data the module expects."""
67
+
68
+ payload: MessageInputPayload | FileInputPayload = Field(
69
+ ...,
70
+ discriminator="payload_type",
71
+ title="Payload",
72
+ description="Either a message or list of file input.",
73
+ )
74
+
75
+
76
+ class MessageOutputPayload(BaseModel):
77
+ """Message output model for the CPU Archetype module."""
78
+
79
+ payload_type: Literal["message"] = "message"
80
+ user_response: str = Field(
81
+ ...,
82
+ title="User Response",
83
+ description="The response generated by the assistant based on the user prompt.",
84
+ )
85
+
86
+
87
+ class OutputFile(BaseModel):
88
+ """File model for the CPU Archetype module."""
89
+
90
+ name: str = Field(
91
+ ...,
92
+ title="File Name",
93
+ description="The name of the file to be processed.",
94
+ )
95
+ url: str | None = Field(
96
+ ...,
97
+ title="File URL",
98
+ description="The URL of the file to be processed.",
99
+ )
100
+
101
+ message: str | None = Field(
102
+ None,
103
+ title="Message",
104
+ description="Optional message associated with the file.",
105
+ )
106
+
107
+
108
+ class FileOutputPayload(BaseModel):
109
+ """File output model for the CPU Archetype module."""
110
+
111
+ payload_type: Literal["file"] = "file"
112
+ files: list[OutputFile] = Field(
113
+ ...,
114
+ title="Files",
115
+ description="List of files generated by the assistant.",
116
+ )
117
+
118
+
119
+ class CPUOutput(BaseModel):
120
+ """Output model defining what data the module produces."""
121
+
122
+ payload: MessageOutputPayload | FileOutputPayload = Field(
123
+ ...,
124
+ discriminator="payload_type",
125
+ title="Payload",
126
+ description="Either a message or file response.",
127
+ )
128
+
129
+
130
+ class CPUConfigSetup(BaseModel):
131
+ """Config Setup model definining data that will be pre-computed for each setup and module instance."""
132
+
133
+ files: list[str] = Field(
134
+ ...,
135
+ title="Files to embed",
136
+ description="List of files to embed in the setup lifecycle.",
137
+ )
138
+
139
+
140
+ class CPUSetup(BaseModel):
141
+ """Setup model defining module configuration parameters."""
142
+
143
+ model_name: str = Field(
144
+ ...,
145
+ title="Model Name",
146
+ description="The name of the CPU model to use for processing.",
147
+ )
148
+ developer_prompt: str = Field(
149
+ ...,
150
+ title="Developer Prompt",
151
+ description="The developer prompt new versions of system prompt, it defines the behavior of the assistant.",
152
+ )
153
+ temperature: float = Field(
154
+ 0.7,
155
+ title="Temperature",
156
+ description="Controls the randomness of the model's output. Higher values make output more random.",
157
+ )
158
+ max_tokens: int = Field(
159
+ 100,
160
+ title="Max Tokens",
161
+ description="The maximum number of tokens to generate in the response.",
162
+ )
163
+
164
+
165
+ class CPUToolSecret(BaseModel):
166
+ """Secret model defining module configuration parameters."""
167
+
168
+
169
+ server_config = ServerConfig(
170
+ host="[::]",
171
+ port=50151,
172
+ mode=ServerMode.ASYNC,
173
+ security=SecurityMode.INSECURE,
174
+ max_workers=10,
175
+ credentials=None,
176
+ )
177
+
178
+
179
+ client_config = ClientConfig(
180
+ host="[::]",
181
+ port=50151,
182
+ mode=ServerMode.ASYNC,
183
+ security=SecurityMode.INSECURE,
184
+ credentials=None,
185
+ )
186
+
187
+
188
+ class CPUIntensiveModule(BaseModule[CPUInput, CPUOutput, CPUSetup, CPUToolSecret, None]):
189
+ """A CPU endpoint tool module module."""
190
+
191
+ name = "CPUIntensiveModule"
192
+ description = "A module that interacts with CPU API to process text"
193
+
194
+ # Define the schema formats for the module
195
+ input_format = CPUInput
196
+ output_format = CPUOutput
197
+ setup_format = CPUSetup
198
+ secret_format = CPUToolSecret
199
+
200
+ # Define module metadata for discovery
201
+ metadata: ClassVar[dict[str, Any]] = {
202
+ "name": "CPUIntensiveModule",
203
+ "description": "Transforms input text using a streaming LLM response.",
204
+ "version": "1.0.0",
205
+ "tags": ["text", "transformation", "encryption", "streaming"],
206
+ }
207
+ # Define services_config_params with default values
208
+ services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]] = {}
209
+ services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]] = {
210
+ "storage": {
211
+ "config": {"chat_history": None},
212
+ "client_config": client_config,
213
+ },
214
+ "filesystem": {
215
+ "config": {},
216
+ "client_config": client_config,
217
+ },
218
+ "cost": {
219
+ "config": {},
220
+ "client_config": client_config,
221
+ },
222
+ }
223
+
224
+ async def initialize(self, setup_data: SetupData) -> None:
225
+ """Initialize the module capabilities.
226
+
227
+ This method is called when the module is loaded by the server.
228
+ Use it to set up module-specific resources or configurations.
229
+ """
230
+
231
+ async def run(
232
+ self,
233
+ input_data: CPUInput,
234
+ setup_data: CPUSetup,
235
+ callback: Callable,
236
+ ) -> None:
237
+ """Run the module.
238
+
239
+ Args:
240
+ input_data: Input data for the module
241
+ setup_data: Setup data for the module
242
+ callback: Callback function to report progress
243
+
244
+ Raises:
245
+ ValueError: If the payload type is unknown
246
+ """
247
+ # Validate the input data
248
+ input_model = self.input_format.model_validate(input_data)
249
+ self.setup_format.model_validate(setup_data)
250
+ logger.debug("Running with input data: %s", input_model)
251
+
252
+ if not hasattr(input_model, "payload"):
253
+ error_msg = "Input data is missing 'payload' field"
254
+ raise ValueError(error_msg)
255
+
256
+ if not hasattr(input_model.payload, "payload_type"):
257
+ error_msg = "Input payload is missing 'type' field"
258
+ raise ValueError(error_msg)
259
+
260
+ total = 0
261
+ input = MessageInputPayload.model_validate(input_model.payload).user_prompt
262
+
263
+ for i in range(int(input)):
264
+ total += i * i
265
+ if i % 100 == 0 or i == int(input) - 1:
266
+ message_output_payload = MessageOutputPayload(
267
+ payload_type="message",
268
+ user_response=f"result iteration {i}: {total}",
269
+ )
270
+ output_model = self.output_format.model_validate({"payload": message_output_payload})
271
+ await callback(output_data=output_model)
272
+ logger.info("Job %s completed", self.job_id)
273
+
274
+ async def cleanup(self) -> None:
275
+ """Clean up any resources when the module is stopped.
276
+
277
+ This method is called when the module is being shut down.
278
+ Use it to close connections, free resources, etc.
279
+ """
280
+ logger.info("Cleaning up module %s", self.metadata["name"])
281
+ # Release any resources here if needed.
@@ -1,44 +1,166 @@
1
1
  """Simple module calling an LLM."""
2
2
 
3
3
  import logging
4
+ import os
4
5
  from collections.abc import Callable
5
- from typing import Any, ClassVar
6
+ from typing import Any, ClassVar, Literal
6
7
 
7
- import grpc
8
8
  import openai
9
- from pydantic import BaseModel
9
+ from pydantic import BaseModel, Field
10
10
 
11
- from digitalkin.grpc_servers.utils.models import SecurityMode, ClientConfig, ServerMode
11
+ from digitalkin.grpc_servers.utils.models import ClientConfig, SecurityMode, ServerMode
12
12
  from digitalkin.modules._base_module import BaseModule
13
- from digitalkin.services.setup.setup_strategy import SetupData
13
+ from digitalkin.services.services_models import ServicesStrategy
14
14
 
15
15
  # Configure logging with clear formatting
16
16
  logging.basicConfig(
17
- level=logging.INFO,
17
+ level=logging.DEBUG,
18
18
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
19
19
  )
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
22
 
23
- # Define schema models using Pydantic
24
- class OpenAIToolInput(BaseModel):
23
+ class MessageInputPayload(BaseModel):
24
+ """Message trigger model for the OpenAI Archetype module."""
25
+
26
+ payload_type: Literal["message"] = "message"
27
+ user_prompt: str = Field(
28
+ ...,
29
+ title="User Prompt",
30
+ description="The prompt provided by the user for processing.",
31
+ )
32
+
33
+
34
+ class InputFile(BaseModel):
35
+ """File model for the OpenAI Archetype module."""
36
+
37
+ name: str = Field(
38
+ ...,
39
+ title="File Name",
40
+ description="The name of the file to be processed.",
41
+ )
42
+ content: bytes = Field(
43
+ ...,
44
+ title="File Content",
45
+ description="The content of the file to be processed.",
46
+ )
47
+
48
+ file_type: str = Field(
49
+ ...,
50
+ title="File Type",
51
+ description="The type of the file to be processed.",
52
+ )
53
+
54
+
55
+ class FileInputPayload(BaseModel):
56
+ """File input model for the OpenAI Archetype module."""
57
+
58
+ payload_type: Literal["file"] = "file"
59
+ files: list[InputFile] = Field(
60
+ ...,
61
+ title="Files",
62
+ description="List of files to be processed.",
63
+ )
64
+
65
+
66
+ class OpenAIInput(BaseModel):
25
67
  """Input model defining what data the module expects."""
26
68
 
27
- prompt: str
69
+ payload: MessageInputPayload | FileInputPayload = Field(
70
+ ...,
71
+ discriminator="payload_type",
72
+ title="Payload",
73
+ description="Either a message or list of file input.",
74
+ )
75
+
76
+
77
+ class MessageOutputPayload(BaseModel):
78
+ """Message output model for the OpenAI Archetype module."""
79
+
80
+ payload_type: Literal["message"] = "message"
81
+ user_response: str = Field(
82
+ ...,
83
+ title="User Response",
84
+ description="The response generated by the assistant based on the user prompt.",
85
+ )
28
86
 
29
87
 
30
- class OpenAIToolOutput(BaseModel):
88
+ class OutputFile(BaseModel):
89
+ """File model for the OpenAI Archetype module."""
90
+
91
+ name: str = Field(
92
+ ...,
93
+ title="File Name",
94
+ description="The name of the file to be processed.",
95
+ )
96
+ url: str | None = Field(
97
+ ...,
98
+ title="File URL",
99
+ description="The URL of the file to be processed.",
100
+ )
101
+
102
+ message: str | None = Field(
103
+ None,
104
+ title="Message",
105
+ description="Optional message associated with the file.",
106
+ )
107
+
108
+
109
+ class FileOutputPayload(BaseModel):
110
+ """File output model for the OpenAI Archetype module."""
111
+
112
+ payload_type: Literal["file"] = "file"
113
+ files: list[OutputFile] = Field(
114
+ ...,
115
+ title="Files",
116
+ description="List of files generated by the assistant.",
117
+ )
118
+
119
+
120
+ class OpenAIOutput(BaseModel):
31
121
  """Output model defining what data the module produces."""
32
122
 
33
- response: str
123
+ payload: MessageOutputPayload | FileOutputPayload = Field(
124
+ ...,
125
+ discriminator="payload_type",
126
+ title="Payload",
127
+ description="Either a message or file response.",
128
+ )
34
129
 
35
130
 
36
- class OpenAIToolSetup(BaseModel):
131
+ class OpenAISetup(BaseModel):
37
132
  """Setup model defining module configuration parameters."""
38
133
 
39
- openai_key: str
40
- model_name: str
41
- dev_prompt: str
134
+ model_name: str = Field(
135
+ ...,
136
+ title="Model Name",
137
+ description="The name of the OpenAI model to use for processing.",
138
+ )
139
+ developer_prompt: str = Field(
140
+ ...,
141
+ title="Developer Prompt",
142
+ description="The developer prompt new versions of system prompt, it defines the behavior of the assistant.",
143
+ )
144
+ temperature: float = Field(
145
+ 0.7,
146
+ title="Temperature",
147
+ description="Controls the randomness of the model's output. Higher values make output more random.",
148
+ )
149
+ max_tokens: int = Field(
150
+ 100,
151
+ title="Max Tokens",
152
+ description="The maximum number of tokens to generate in the response.",
153
+ )
154
+
155
+
156
+ class OpenAIConfigSetup(BaseModel):
157
+ """Setup model defining module configuration parameters."""
158
+
159
+ rag_files: list[bytes] = Field(
160
+ ...,
161
+ title="RAG Files",
162
+ description="Files used for retrieval-augmented generation (RAG) with the OpenAI module.",
163
+ )
42
164
 
43
165
 
44
166
  class OpenAIToolSecret(BaseModel):
@@ -54,47 +176,80 @@ client_config = ClientConfig(
54
176
  )
55
177
 
56
178
 
57
- class OpenAIToolModule(BaseModule[OpenAIToolInput, OpenAIToolOutput, OpenAIToolSetup, OpenAIToolSecret]):
179
+ class OpenAIToolModule(
180
+ BaseModule[
181
+ OpenAIInput,
182
+ OpenAIOutput,
183
+ OpenAISetup,
184
+ OpenAIToolSecret,
185
+ OpenAIConfigSetup,
186
+ ]
187
+ ):
58
188
  """A openAI endpoint tool module module."""
59
189
 
60
190
  name = "OpenAIToolModule"
61
191
  description = "A module that interacts with OpenAI API to process text"
62
192
 
63
193
  # Define the schema formats for the module
64
- input_format = OpenAIToolInput
65
- output_format = OpenAIToolOutput
66
- setup_format = OpenAIToolSetup
194
+ config_setup_format = OpenAIConfigSetup
195
+ input_format = OpenAIInput
196
+ output_format = OpenAIOutput
197
+ setup_format = OpenAISetup
67
198
  secret_format = OpenAIToolSecret
68
199
 
69
200
  openai_client: openai.OpenAI
70
201
 
71
202
  # Define module metadata for discovery
72
203
  metadata: ClassVar[dict[str, Any]] = {
73
- "name": "Minimal_LLM_Tool",
204
+ "name": "OpenAIToolModule",
74
205
  "description": "Transforms input text using a streaming LLM response.",
75
206
  "version": "1.0.0",
76
207
  "tags": ["text", "transformation", "encryption", "streaming"],
77
208
  }
78
209
  # Define services_config_params with default values
79
- services_config_strategies = {}
80
- services_config_params = {
210
+ services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]] = {}
211
+ services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]] = {
81
212
  "storage": {
82
- "config": {"setups": OpenAIToolSetup},
213
+ "config": {"setups": OpenAISetup},
83
214
  "client_config": client_config,
84
215
  },
85
216
  "filesystem": {
86
217
  "config": {},
87
218
  "client_config": client_config,
88
219
  },
220
+ "cost": {
221
+ "config": {},
222
+ "client_config": client_config,
223
+ },
89
224
  }
90
225
 
91
- async def initialize(self, setup_data: SetupData) -> None:
226
+ async def run_config_setup(
227
+ self,
228
+ config_setup_data: OpenAIConfigSetup,
229
+ setup_data: OpenAISetup,
230
+ callback: Callable,
231
+ ) -> None:
232
+ """Configure the module with additional setup data.
233
+
234
+ Args:
235
+ config_setup_data: Additional configuration content.
236
+ setup_data: Initial setup data for the module.
237
+ callback: Function to send output data back to the client.
238
+ """
239
+ logger.info("Configuring OpenAIToolModule with additional setup data. %s", config_setup_data)
240
+
241
+ # Here you can process config_content and update setup_data as needed
242
+ # For now, we just return the original setup_data
243
+ setup_data.developer_prompt = "| + |".join(f.decode("utf-8") for f in config_setup_data.rag_files)
244
+ await callback(setup_data)
245
+
246
+ async def initialize(self, setup_data: OpenAISetup) -> None:
92
247
  """Initialize the module capabilities.
93
248
 
94
249
  This method is called when the module is loaded by the server.
95
250
  Use it to set up module-specific resources or configurations.
96
251
  """
97
- self.openai_client = openai.OpenAI(api_key=setup_data.current_setup_version.content["openai_key"])
252
+ self.client: openai.AsyncOpenAI = openai.AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
98
253
  # Define what capabilities this module provides
99
254
  self.capabilities = ["text-processing", "streaming", "transformation"]
100
255
  logger.info(
@@ -103,10 +258,10 @@ class OpenAIToolModule(BaseModule[OpenAIToolInput, OpenAIToolOutput, OpenAIToolS
103
258
  self.capabilities,
104
259
  )
105
260
 
106
- async def run(
261
+ async def run_message(
107
262
  self,
108
- input_data: dict[str, Any],
109
- setup_data: SetupData,
263
+ input_model: MessageInputPayload,
264
+ setup_model: OpenAISetup,
110
265
  callback: Callable,
111
266
  ) -> None:
112
267
  """Process input text and stream LLM responses.
@@ -122,37 +277,64 @@ class OpenAIToolModule(BaseModule[OpenAIToolInput, OpenAIToolOutput, OpenAIToolS
122
277
  openai.APIConnectionError: If an API connection error occurs.
123
278
  Exception: For any unexpected runtime errors.
124
279
  """
125
- logger.info(
126
- "Running job %s with prompt: '%s' on model: %s",
127
- self.job_id,
128
- input_data["prompt"],
129
- setup_data.current_setup_version.content["model_name"],
280
+ # response = await self.client.responses.create(
281
+ # model=setup_model.model_name,
282
+ # instructions=setup_model.developer_prompt,
283
+ # temperature=setup_model.temperature,
284
+ # max_output_tokens=setup_model.max_tokens,
285
+ # input=input_model.user_prompt,
286
+ # )
287
+ # logger.info("Recieved answer from OpenAI: %s", response)
288
+
289
+ # Get and save the output data
290
+ message_output_payload = MessageOutputPayload(
291
+ payload_type="message",
292
+ user_response="Mock data",
293
+ # user_response=response.output_text,
130
294
  )
131
- try:
132
- response = self.openai_client.responses.create(
133
- model=setup_data.current_setup_version.content["model_name"],
134
- tools=[{"type": "web_search_preview"}],
135
- instructions=setup_data.current_setup_version.content["dev_prompt"],
136
- input=input_data["prompt"],
137
- )
138
- if not response.output_text:
139
- raise openai.APIConnectionError
140
- output_data = OpenAIToolOutput(response=response.output_text).model_dump()
141
-
142
- except openai.AuthenticationError as _:
143
- message = "Authentication Error, OPENAI auth token was never set."
144
- logger.exception(message)
145
- output_data = {
146
- "error": {
147
- "code": grpc.StatusCode.UNAUTHENTICATED,
148
- "error_message": message,
149
- }
150
- }
151
- except openai.APIConnectionError as _:
152
- message = "API Error, please try again."
153
- logger.exception(message)
154
- output_data = {"error": {"code": grpc.StatusCode.UNAVAILABLE, "error_message": message}}
155
- await callback(job_id=self.job_id, output_data=output_data)
295
+ output_model = self.output_format.model_validate({"payload": message_output_payload})
296
+ await callback(output_data=output_model)
297
+
298
+ async def run(
299
+ self,
300
+ input_data: OpenAIInput,
301
+ setup_data: OpenAISetup,
302
+ callback: Callable,
303
+ ) -> None:
304
+ """Run the module.
305
+
306
+ Args:
307
+ input_data: Input data for the module
308
+ setup_data: Setup data for the module
309
+ callback: Callback function to report progress
310
+
311
+ Raises:
312
+ ValueError: If the payload type is unknown
313
+ """
314
+ # Validate the input data
315
+ input_model = self.input_format.model_validate(input_data)
316
+ setup_model = self.setup_format.model_validate(setup_data)
317
+ logger.debug("Running with input data: %s", input_model)
318
+
319
+ if not hasattr(input_model, "payload"):
320
+ error_msg = "Input data is missing 'payload' field"
321
+ raise ValueError(error_msg)
322
+
323
+ if not hasattr(input_model.payload, "payload_type"):
324
+ error_msg = "Input payload is missing 'type' field"
325
+ raise ValueError(error_msg)
326
+
327
+ if input_model.payload.payload_type == "message":
328
+ # Validate against MessageInputPayload
329
+ message_payload = MessageInputPayload.model_validate(input_model.payload)
330
+ await self.run_message(message_payload, setup_model, callback)
331
+ elif input_model.payload.payload_type == "file":
332
+ # Validate against FileInputPayload
333
+ file_payload = FileInputPayload.model_validate(input_model.payload)
334
+ await self.run_file(file_payload, setup_model, callback)
335
+ else:
336
+ error_msg = f"Unknown input type '{input_model.payload.payload_type}'. Expected 'message' or 'file'."
337
+ raise ValueError(error_msg)
156
338
  logger.info("Job %s completed", self.job_id)
157
339
 
158
340
  async def cleanup(self) -> None:
modules/storage_module.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import asyncio
4
4
  import datetime
5
5
  from collections.abc import Callable
6
- from typing import Any
6
+ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  from pydantic import BaseModel, Field
9
9
 
@@ -12,7 +12,9 @@ from digitalkin.models.module import ModuleStatus
12
12
  from digitalkin.modules.archetype_module import ArchetypeModule
13
13
  from digitalkin.services.services_config import ServicesConfig
14
14
  from digitalkin.services.services_models import ServicesMode
15
- from digitalkin.services.storage.storage_strategy import StorageRecord
15
+
16
+ if TYPE_CHECKING:
17
+ from digitalkin.services.storage.storage_strategy import StorageRecord
16
18
 
17
19
 
18
20
  class ExampleInput(BaseModel):
@@ -120,10 +122,7 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
120
122
 
121
123
  # Store the output data in storage
122
124
  storage_id = self.storage.store(
123
- collection="example",
124
- record_id=f"example_outputs",
125
- data=output_data.model_dump(),
126
- data_type="OUTPUT"
125
+ collection="example", record_id="example_outputs", data=output_data.model_dump(), data_type="OUTPUT"
127
126
  )
128
127
 
129
128
  logger.info("Stored output data with ID: %s", storage_id)