digitalkin 0.3.2.dev2__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.
- base_server/__init__.py +1 -0
- base_server/mock/__init__.py +5 -0
- base_server/mock/mock_pb2.py +39 -0
- base_server/mock/mock_pb2_grpc.py +102 -0
- base_server/server_async_insecure.py +125 -0
- base_server/server_async_secure.py +143 -0
- base_server/server_sync_insecure.py +103 -0
- base_server/server_sync_secure.py +122 -0
- digitalkin/__init__.py +8 -0
- digitalkin/__version__.py +8 -0
- digitalkin/core/__init__.py +1 -0
- digitalkin/core/common/__init__.py +9 -0
- digitalkin/core/common/factories.py +156 -0
- digitalkin/core/job_manager/__init__.py +1 -0
- digitalkin/core/job_manager/base_job_manager.py +288 -0
- digitalkin/core/job_manager/single_job_manager.py +354 -0
- digitalkin/core/job_manager/taskiq_broker.py +311 -0
- digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
- digitalkin/core/task_manager/__init__.py +1 -0
- digitalkin/core/task_manager/base_task_manager.py +539 -0
- digitalkin/core/task_manager/local_task_manager.py +108 -0
- digitalkin/core/task_manager/remote_task_manager.py +87 -0
- digitalkin/core/task_manager/surrealdb_repository.py +266 -0
- digitalkin/core/task_manager/task_executor.py +249 -0
- digitalkin/core/task_manager/task_session.py +406 -0
- digitalkin/grpc_servers/__init__.py +1 -0
- digitalkin/grpc_servers/_base_server.py +486 -0
- digitalkin/grpc_servers/module_server.py +208 -0
- digitalkin/grpc_servers/module_servicer.py +516 -0
- digitalkin/grpc_servers/utils/__init__.py +1 -0
- digitalkin/grpc_servers/utils/exceptions.py +29 -0
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
- digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
- digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
- digitalkin/logger.py +157 -0
- digitalkin/mixins/__init__.py +19 -0
- digitalkin/mixins/base_mixin.py +10 -0
- digitalkin/mixins/callback_mixin.py +24 -0
- digitalkin/mixins/chat_history_mixin.py +110 -0
- digitalkin/mixins/cost_mixin.py +76 -0
- digitalkin/mixins/file_history_mixin.py +93 -0
- digitalkin/mixins/filesystem_mixin.py +46 -0
- digitalkin/mixins/logger_mixin.py +51 -0
- digitalkin/mixins/storage_mixin.py +79 -0
- digitalkin/models/__init__.py +8 -0
- digitalkin/models/core/__init__.py +1 -0
- digitalkin/models/core/job_manager_models.py +36 -0
- digitalkin/models/core/task_monitor.py +70 -0
- digitalkin/models/grpc_servers/__init__.py +1 -0
- digitalkin/models/grpc_servers/models.py +275 -0
- digitalkin/models/grpc_servers/types.py +24 -0
- digitalkin/models/module/__init__.py +25 -0
- digitalkin/models/module/module.py +40 -0
- digitalkin/models/module/module_context.py +149 -0
- digitalkin/models/module/module_types.py +393 -0
- digitalkin/models/module/utility.py +146 -0
- digitalkin/models/services/__init__.py +10 -0
- digitalkin/models/services/cost.py +54 -0
- digitalkin/models/services/registry.py +42 -0
- digitalkin/models/services/storage.py +44 -0
- digitalkin/modules/__init__.py +11 -0
- digitalkin/modules/_base_module.py +517 -0
- digitalkin/modules/archetype_module.py +23 -0
- digitalkin/modules/tool_module.py +23 -0
- digitalkin/modules/trigger_handler.py +48 -0
- digitalkin/modules/triggers/__init__.py +12 -0
- digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
- digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
- digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
- digitalkin/py.typed +0 -0
- digitalkin/services/__init__.py +30 -0
- digitalkin/services/agent/__init__.py +6 -0
- digitalkin/services/agent/agent_strategy.py +19 -0
- digitalkin/services/agent/default_agent.py +13 -0
- digitalkin/services/base_strategy.py +22 -0
- digitalkin/services/communication/__init__.py +7 -0
- digitalkin/services/communication/communication_strategy.py +76 -0
- digitalkin/services/communication/default_communication.py +101 -0
- digitalkin/services/communication/grpc_communication.py +223 -0
- digitalkin/services/cost/__init__.py +14 -0
- digitalkin/services/cost/cost_strategy.py +100 -0
- digitalkin/services/cost/default_cost.py +114 -0
- digitalkin/services/cost/grpc_cost.py +138 -0
- digitalkin/services/filesystem/__init__.py +7 -0
- digitalkin/services/filesystem/default_filesystem.py +417 -0
- digitalkin/services/filesystem/filesystem_strategy.py +252 -0
- digitalkin/services/filesystem/grpc_filesystem.py +317 -0
- digitalkin/services/identity/__init__.py +6 -0
- digitalkin/services/identity/default_identity.py +15 -0
- digitalkin/services/identity/identity_strategy.py +14 -0
- digitalkin/services/registry/__init__.py +27 -0
- digitalkin/services/registry/default_registry.py +141 -0
- digitalkin/services/registry/exceptions.py +47 -0
- digitalkin/services/registry/grpc_registry.py +306 -0
- digitalkin/services/registry/registry_models.py +43 -0
- digitalkin/services/registry/registry_strategy.py +98 -0
- digitalkin/services/services_config.py +200 -0
- digitalkin/services/services_models.py +65 -0
- digitalkin/services/setup/__init__.py +1 -0
- digitalkin/services/setup/default_setup.py +219 -0
- digitalkin/services/setup/grpc_setup.py +343 -0
- digitalkin/services/setup/setup_strategy.py +145 -0
- digitalkin/services/snapshot/__init__.py +6 -0
- digitalkin/services/snapshot/default_snapshot.py +39 -0
- digitalkin/services/snapshot/snapshot_strategy.py +30 -0
- digitalkin/services/storage/__init__.py +7 -0
- digitalkin/services/storage/default_storage.py +228 -0
- digitalkin/services/storage/grpc_storage.py +214 -0
- digitalkin/services/storage/storage_strategy.py +273 -0
- digitalkin/services/user_profile/__init__.py +12 -0
- digitalkin/services/user_profile/default_user_profile.py +55 -0
- digitalkin/services/user_profile/grpc_user_profile.py +69 -0
- digitalkin/services/user_profile/user_profile_strategy.py +40 -0
- digitalkin/utils/__init__.py +29 -0
- digitalkin/utils/arg_parser.py +92 -0
- digitalkin/utils/development_mode_action.py +51 -0
- digitalkin/utils/dynamic_schema.py +483 -0
- digitalkin/utils/llm_ready_schema.py +75 -0
- digitalkin/utils/package_discover.py +357 -0
- digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
- digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
- digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
- digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
- digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
- modules/__init__.py +0 -0
- modules/cpu_intensive_module.py +280 -0
- modules/dynamic_setup_module.py +338 -0
- modules/minimal_llm_module.py +347 -0
- modules/text_transform_module.py +203 -0
- services/filesystem_module.py +200 -0
- services/storage_module.py +206 -0
|
@@ -0,0 +1,280 @@
|
|
|
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 digitalkin.grpc_servers.utils.models import ClientConfig, SecurityMode, ServerConfig, ServerMode
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
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
|
+
"client_config": client_config,
|
|
216
|
+
},
|
|
217
|
+
"cost": {
|
|
218
|
+
"config": {},
|
|
219
|
+
"client_config": client_config,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async def initialize(self, setup_data: SetupData) -> None:
|
|
224
|
+
"""Initialize the module capabilities.
|
|
225
|
+
|
|
226
|
+
This method is called when the module is loaded by the server.
|
|
227
|
+
Use it to set up module-specific resources or configurations.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
async def run(
|
|
231
|
+
self,
|
|
232
|
+
input_data: CPUInput,
|
|
233
|
+
setup_data: CPUSetup,
|
|
234
|
+
callback: Callable,
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Run the module.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
input_data: Input data for the module
|
|
240
|
+
setup_data: Setup data for the module
|
|
241
|
+
callback: Callback function to report progress
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If the payload type is unknown
|
|
245
|
+
"""
|
|
246
|
+
# Validate the input data
|
|
247
|
+
input_model = self.input_format.model_validate(input_data)
|
|
248
|
+
self.setup_format.model_validate(setup_data)
|
|
249
|
+
logger.debug("Running with input data: %s", input_model)
|
|
250
|
+
|
|
251
|
+
if not hasattr(input_model, "payload"):
|
|
252
|
+
error_msg = "Input data is missing 'payload' field"
|
|
253
|
+
raise ValueError(error_msg)
|
|
254
|
+
|
|
255
|
+
if not hasattr(input_model.payload, "payload_type"):
|
|
256
|
+
error_msg = "Input payload is missing 'type' field"
|
|
257
|
+
raise ValueError(error_msg)
|
|
258
|
+
|
|
259
|
+
total = 0
|
|
260
|
+
input = MessageInputPayload.model_validate(input_model.payload).user_prompt
|
|
261
|
+
|
|
262
|
+
for i in range(int(input)):
|
|
263
|
+
total += i * i
|
|
264
|
+
if i % 100 == 0 or i == int(input) - 1:
|
|
265
|
+
message_output_payload = MessageOutputPayload(
|
|
266
|
+
payload_type="message",
|
|
267
|
+
user_response=f"result iteration {i}: {total}",
|
|
268
|
+
)
|
|
269
|
+
output_model = self.output_format.model_validate({"payload": message_output_payload})
|
|
270
|
+
await callback(output_data=output_model)
|
|
271
|
+
logger.info("Job %s completed", self.job_id)
|
|
272
|
+
|
|
273
|
+
async def cleanup(self) -> None:
|
|
274
|
+
"""Clean up any resources when the module is stopped.
|
|
275
|
+
|
|
276
|
+
This method is called when the module is being shut down.
|
|
277
|
+
Use it to close connections, free resources, etc.
|
|
278
|
+
"""
|
|
279
|
+
logger.info("Cleaning up module %s", self.metadata["name"])
|
|
280
|
+
# Release any resources here if needed.
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Example module demonstrating dynamic schema fields in SetupModel.
|
|
2
|
+
|
|
3
|
+
This example shows how to use the Dynamic metadata class with async fetchers
|
|
4
|
+
to populate field schemas (like enums) at runtime. This is useful when the
|
|
5
|
+
available options come from external sources like databases or APIs.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# Start the module server
|
|
9
|
+
python examples/modules/dynamic_setup_module.py
|
|
10
|
+
|
|
11
|
+
# Or import and use in your own code
|
|
12
|
+
from examples.modules.dynamic_setup_module import DynamicSetupModule
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Annotated, Any, ClassVar
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
22
|
+
from digitalkin.models.module.module_types import DataModel, DataTrigger, SetupModel
|
|
23
|
+
from digitalkin.modules._base_module import BaseModule
|
|
24
|
+
from digitalkin.services.services_models import ServicesStrategy
|
|
25
|
+
from digitalkin.utils import Dynamic
|
|
26
|
+
|
|
27
|
+
# Configure logging
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
level=logging.DEBUG,
|
|
30
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
31
|
+
)
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Simulated External Services (replace with real implementations)
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MockModelRegistry:
|
|
41
|
+
"""Simulates an external model registry service.
|
|
42
|
+
|
|
43
|
+
In a real application, this would be a connection to a database,
|
|
44
|
+
API service, or configuration management system.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_models: ClassVar[list[str]] = ["gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"]
|
|
48
|
+
_languages: ClassVar[list[str]] = ["en", "fr", "de", "es", "it", "pt"]
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
async def fetch_available_models(cls) -> list[str]:
|
|
52
|
+
"""Fetch available models from the registry.
|
|
53
|
+
|
|
54
|
+
Simulates an async API call with a small delay.
|
|
55
|
+
"""
|
|
56
|
+
await asyncio.sleep(0.1) # Simulate network latency
|
|
57
|
+
logger.info("Fetched %d models from registry", len(cls._models))
|
|
58
|
+
return cls._models.copy()
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
async def fetch_supported_languages(cls) -> list[str]:
|
|
62
|
+
"""Fetch supported languages from the registry."""
|
|
63
|
+
await asyncio.sleep(0.05) # Simulate network latency
|
|
64
|
+
logger.info("Fetched %d languages from registry", len(cls._languages))
|
|
65
|
+
return cls._languages.copy()
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_default_model(cls) -> str:
|
|
69
|
+
"""Get the default model (sync fetcher example)."""
|
|
70
|
+
return cls._models[0] if cls._models else "gpt-4"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Dynamic Fetcher Functions
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def fetch_models() -> list[str]:
|
|
79
|
+
"""Async fetcher for available model names.
|
|
80
|
+
|
|
81
|
+
This function is called when SetupModel.get_clean_model(force=True)
|
|
82
|
+
is invoked, typically during module initialization or schema refresh.
|
|
83
|
+
"""
|
|
84
|
+
return await MockModelRegistry.fetch_available_models()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def fetch_languages() -> list[str]:
|
|
88
|
+
"""Async fetcher for supported languages."""
|
|
89
|
+
return await MockModelRegistry.fetch_supported_languages()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_temperature_range() -> dict[str, float]:
|
|
93
|
+
"""Sync fetcher example returning min/max for temperature.
|
|
94
|
+
|
|
95
|
+
Demonstrates that fetchers can return any JSON-serializable value,
|
|
96
|
+
not just lists for enums.
|
|
97
|
+
"""
|
|
98
|
+
return {"minimum": 0.0, "maximum": 2.0}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# Setup Model with Dynamic Fields
|
|
103
|
+
# =============================================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class DynamicAgentSetup(SetupModel):
|
|
107
|
+
"""Setup model demonstrating dynamic schema fields.
|
|
108
|
+
|
|
109
|
+
Fields marked with Dynamic(...) will have their schema values
|
|
110
|
+
refreshed at runtime when get_clean_model(force=True) is called.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
model_name: The LLM model to use. Enum values fetched from registry.
|
|
114
|
+
language: Output language. Enum values fetched dynamically.
|
|
115
|
+
temperature: Sampling temperature. Static field for comparison.
|
|
116
|
+
max_tokens: Maximum tokens to generate.
|
|
117
|
+
system_prompt: The system prompt for the model.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# Dynamic field: enum values fetched asynchronously from model registry
|
|
121
|
+
model_name: Annotated[str, Dynamic(enum=fetch_models)] = Field(
|
|
122
|
+
default="gpt-4",
|
|
123
|
+
title="Model Name",
|
|
124
|
+
description="The LLM model to use for generation.",
|
|
125
|
+
json_schema_extra={
|
|
126
|
+
"config": True, # Shown in initial configuration
|
|
127
|
+
"ui:widget": "select",
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Dynamic field: language options fetched asynchronously
|
|
132
|
+
language: Annotated[str, Dynamic(enum=fetch_languages)] = Field(
|
|
133
|
+
default="en",
|
|
134
|
+
title="Output Language",
|
|
135
|
+
description="The language for generated responses.",
|
|
136
|
+
json_schema_extra={
|
|
137
|
+
"config": True,
|
|
138
|
+
"ui:widget": "select",
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Static field: no dynamic fetcher, values defined at class time
|
|
143
|
+
temperature: float = Field(
|
|
144
|
+
default=0.7,
|
|
145
|
+
ge=0.0,
|
|
146
|
+
le=2.0,
|
|
147
|
+
title="Temperature",
|
|
148
|
+
description="Controls randomness. Higher values = more creative.",
|
|
149
|
+
json_schema_extra={"config": True},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Static field with hidden flag (runtime-only, not in initial config)
|
|
153
|
+
max_tokens: int = Field(
|
|
154
|
+
default=1024,
|
|
155
|
+
ge=1,
|
|
156
|
+
le=4096,
|
|
157
|
+
title="Max Tokens",
|
|
158
|
+
description="Maximum tokens in the response.",
|
|
159
|
+
json_schema_extra={"hidden": True},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Static field without any special flags
|
|
163
|
+
system_prompt: str = Field(
|
|
164
|
+
default="You are a helpful assistant.",
|
|
165
|
+
title="System Prompt",
|
|
166
|
+
description="The system prompt defining assistant behavior.",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# Input/Output Models (Using DataModel/DataTrigger pattern)
|
|
172
|
+
# =============================================================================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class MessageInputTrigger(DataTrigger):
|
|
176
|
+
"""Message input trigger following DigitalKin DataTrigger pattern.
|
|
177
|
+
|
|
178
|
+
The protocol field determines which trigger handler processes this input.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
protocol: str = "message"
|
|
182
|
+
content: str = Field(default="", description="The user message content.")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class DynamicModuleInput(DataModel[MessageInputTrigger]):
|
|
186
|
+
"""Input model following DigitalKin DataModel pattern.
|
|
187
|
+
|
|
188
|
+
Wraps the trigger in a root field with optional annotations.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
root: MessageInputTrigger = Field(default_factory=MessageInputTrigger)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class MessageOutputTrigger(DataTrigger):
|
|
195
|
+
"""Message output trigger following DigitalKin DataTrigger pattern."""
|
|
196
|
+
|
|
197
|
+
protocol: str = "message"
|
|
198
|
+
content: str = Field(default="", description="The generated response.")
|
|
199
|
+
model_used: str = Field(default="", description="The model that generated this response.")
|
|
200
|
+
language: str = Field(default="", description="The output language.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class DynamicModuleOutput(DataModel[MessageOutputTrigger]):
|
|
204
|
+
"""Output model following DigitalKin DataModel pattern."""
|
|
205
|
+
|
|
206
|
+
root: MessageOutputTrigger = Field(default_factory=MessageOutputTrigger)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class DynamicModuleSecret(BaseModel):
|
|
210
|
+
"""Secret model (empty for this example)."""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# Module Implementation
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class DynamicSetupModule(
|
|
219
|
+
BaseModule[
|
|
220
|
+
DynamicModuleInput,
|
|
221
|
+
DynamicModuleOutput,
|
|
222
|
+
DynamicAgentSetup,
|
|
223
|
+
DynamicModuleSecret,
|
|
224
|
+
]
|
|
225
|
+
):
|
|
226
|
+
"""Example module demonstrating dynamic schema in SetupModel.
|
|
227
|
+
|
|
228
|
+
This module shows how to:
|
|
229
|
+
1. Define setup fields with Dynamic() metadata for runtime enum fetching
|
|
230
|
+
2. Mix static and dynamic fields in the same SetupModel
|
|
231
|
+
3. Use async fetchers that simulate external service calls
|
|
232
|
+
4. Follow DigitalKin's DataModel/DataTrigger pattern for I/O
|
|
233
|
+
|
|
234
|
+
The key integration point is in the gRPC servicer, which calls
|
|
235
|
+
SetupModel.get_clean_model(force=True) to refresh dynamic values
|
|
236
|
+
before returning schema information to clients.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
name = "DynamicSetupModule"
|
|
240
|
+
description = "Demonstrates dynamic schema fields in module setup"
|
|
241
|
+
|
|
242
|
+
# Schema format definitions
|
|
243
|
+
input_format = DynamicModuleInput
|
|
244
|
+
output_format = DynamicModuleOutput
|
|
245
|
+
setup_format = DynamicAgentSetup
|
|
246
|
+
secret_format = DynamicModuleSecret
|
|
247
|
+
|
|
248
|
+
# Module metadata
|
|
249
|
+
metadata: ClassVar[dict[str, Any]] = {
|
|
250
|
+
"name": "DynamicSetupModule",
|
|
251
|
+
"description": "Example module with dynamic setup schema",
|
|
252
|
+
"version": "1.0.0",
|
|
253
|
+
"tags": ["example", "dynamic-schema"],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Services configuration (empty for this example)
|
|
257
|
+
services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]] = {}
|
|
258
|
+
services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]] = {}
|
|
259
|
+
|
|
260
|
+
async def initialize(self, context: ModuleContext, setup_data: DynamicAgentSetup) -> None:
|
|
261
|
+
"""Initialize the module with setup data.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
context: The module context with services and session info.
|
|
265
|
+
setup_data: The validated setup configuration.
|
|
266
|
+
"""
|
|
267
|
+
logger.info(
|
|
268
|
+
"Initializing DynamicSetupModule with model=%s, language=%s",
|
|
269
|
+
setup_data.model_name,
|
|
270
|
+
setup_data.language,
|
|
271
|
+
)
|
|
272
|
+
self.setup = setup_data
|
|
273
|
+
|
|
274
|
+
async def cleanup(self) -> None:
|
|
275
|
+
"""Clean up resources."""
|
|
276
|
+
logger.info("Cleaning up DynamicSetupModule")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# =============================================================================
|
|
280
|
+
# Demonstration Script
|
|
281
|
+
# =============================================================================
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def demonstrate_dynamic_schema() -> None:
|
|
285
|
+
"""Demonstrate the dynamic schema functionality."""
|
|
286
|
+
# 1. Show schema WITHOUT force (dynamic fields not resolved)
|
|
287
|
+
|
|
288
|
+
model_no_force = await DynamicAgentSetup.get_clean_model(
|
|
289
|
+
config_fields=True,
|
|
290
|
+
hidden_fields=False,
|
|
291
|
+
force=False,
|
|
292
|
+
)
|
|
293
|
+
schema_no_force = model_no_force.model_json_schema()
|
|
294
|
+
|
|
295
|
+
# Check if enum is present
|
|
296
|
+
model_name_schema = schema_no_force.get("properties", {}).get("model_name", {})
|
|
297
|
+
if "enum" in model_name_schema:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# 2. Show schema WITH force (dynamic fields resolved)
|
|
301
|
+
|
|
302
|
+
model_with_force = await DynamicAgentSetup.get_clean_model(
|
|
303
|
+
config_fields=True,
|
|
304
|
+
hidden_fields=False,
|
|
305
|
+
force=True,
|
|
306
|
+
)
|
|
307
|
+
schema_with_force = model_with_force.model_json_schema()
|
|
308
|
+
|
|
309
|
+
# Check enum values after force
|
|
310
|
+
model_name_schema = schema_with_force.get("properties", {}).get("model_name", {})
|
|
311
|
+
if "enum" in model_name_schema:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
language_schema = schema_with_force.get("properties", {}).get("language", {})
|
|
315
|
+
if "enum" in language_schema:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
# 3. Show that static json_schema_extra is preserved
|
|
319
|
+
|
|
320
|
+
# 4. Show field filtering
|
|
321
|
+
|
|
322
|
+
# Config fields only (hidden excluded)
|
|
323
|
+
await DynamicAgentSetup.get_clean_model(
|
|
324
|
+
config_fields=True,
|
|
325
|
+
hidden_fields=False,
|
|
326
|
+
force=False,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# All fields including hidden
|
|
330
|
+
await DynamicAgentSetup.get_clean_model(
|
|
331
|
+
config_fields=True,
|
|
332
|
+
hidden_fields=True,
|
|
333
|
+
force=False,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
asyncio.run(demonstrate_dynamic_schema())
|