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,54 @@
|
|
|
1
|
+
"""Pydantic models for cost service."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CostTypeEnum(Enum):
|
|
11
|
+
"""Enumeration of supported cost types."""
|
|
12
|
+
|
|
13
|
+
TOKEN_INPUT = "token_input"
|
|
14
|
+
TOKEN_OUTPUT = "token_output"
|
|
15
|
+
API_CALL = "api_call"
|
|
16
|
+
STORAGE = "storage"
|
|
17
|
+
TIME = "time"
|
|
18
|
+
CUSTOM = "custom"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CostConfig(BaseModel):
|
|
22
|
+
"""Pydantic model that defines a cost configuration.
|
|
23
|
+
|
|
24
|
+
:param cost_name: Name of the cost (unique identifier in the service).
|
|
25
|
+
:param cost_type: The type/category of the cost.
|
|
26
|
+
:param description: A short description of the cost.
|
|
27
|
+
:param unit: The unit of measurement (e.g. token, call, MB).
|
|
28
|
+
:param rate: The cost per unit (e.g. dollars per token).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
type: CostTypeEnum
|
|
33
|
+
description: str | None = None
|
|
34
|
+
unit: str
|
|
35
|
+
rate: float
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CostEvent(BaseModel):
|
|
39
|
+
"""Pydantic model that represents a cost event registered during service execution.
|
|
40
|
+
|
|
41
|
+
# DEPRECATED
|
|
42
|
+
:param cost_name: Identifier for the cost configuration.
|
|
43
|
+
:param cost_type: The type of cost.
|
|
44
|
+
:param usage: The amount or units consumed.
|
|
45
|
+
:param cost_amount: The computed cost amount; if not provided it is computed as usage*rate.
|
|
46
|
+
:param timestamp: The time when the cost event was recorded.
|
|
47
|
+
:param metadata: Additional contextual information about the cost event.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
usage: float
|
|
52
|
+
amount: float
|
|
53
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
54
|
+
metadata: dict[str, Any] | None = None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Registry data models."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RegistryModuleStatus(str, Enum):
|
|
9
|
+
"""Module status in the registry."""
|
|
10
|
+
|
|
11
|
+
UNSPECIFIED = "unspecified"
|
|
12
|
+
READY = "ready"
|
|
13
|
+
ACTIVE = "active"
|
|
14
|
+
ARCHIVED = "archived"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RegistryModuleType(str, Enum):
|
|
18
|
+
"""Module type in the registry."""
|
|
19
|
+
|
|
20
|
+
UNSPECIFIED = "unspecified"
|
|
21
|
+
ARCHETYPE = "archetype"
|
|
22
|
+
TOOL = "tool"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ModuleInfo(BaseModel):
|
|
26
|
+
"""Module information from registry."""
|
|
27
|
+
|
|
28
|
+
module_id: str
|
|
29
|
+
module_type: RegistryModuleType
|
|
30
|
+
address: str
|
|
31
|
+
port: int
|
|
32
|
+
version: str
|
|
33
|
+
name: str = ""
|
|
34
|
+
documentation: str | None = None
|
|
35
|
+
status: RegistryModuleStatus | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ModuleStatusInfo(BaseModel):
|
|
39
|
+
"""Module status response."""
|
|
40
|
+
|
|
41
|
+
module_id: str
|
|
42
|
+
status: RegistryModuleStatus
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Storage model."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseRole(str, Enum):
|
|
10
|
+
"""Officially supported Role Enum for chat messages."""
|
|
11
|
+
|
|
12
|
+
ASSISTANT = "assistant"
|
|
13
|
+
USER = "user"
|
|
14
|
+
SYSTEM = "system"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Role = BaseRole | str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseMessage(BaseModel):
|
|
21
|
+
"""Base Model representing a simple message in the chat history."""
|
|
22
|
+
|
|
23
|
+
role: Role = Field(..., description="Role of the message sender")
|
|
24
|
+
content: Any = Field(..., description="The content of the message | preferably a BaseModel.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChatHistory(BaseModel):
|
|
28
|
+
"""Storage chat history model for the OpenAI Archetype module."""
|
|
29
|
+
|
|
30
|
+
messages: list[BaseMessage] = Field(..., description="List of messages in the chat history")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FileModel(BaseModel):
|
|
34
|
+
"""File model."""
|
|
35
|
+
|
|
36
|
+
file_id: str = Field(..., description="ID of the file")
|
|
37
|
+
name: str = Field(..., description="Name of the file")
|
|
38
|
+
metadata: dict[str, Any] = Field(..., description="Metadata of the file")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FileHistory(BaseModel):
|
|
42
|
+
"""File history model."""
|
|
43
|
+
|
|
44
|
+
files: list[FileModel] = Field(..., description="List of files")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Module package for DigitalKin."""
|
|
2
|
+
|
|
3
|
+
from digitalkin.modules.archetype_module import ArchetypeModule
|
|
4
|
+
from digitalkin.modules.tool_module import ToolModule
|
|
5
|
+
from digitalkin.modules.trigger_handler import TriggerHandler
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ArchetypeModule",
|
|
9
|
+
"ToolModule",
|
|
10
|
+
"TriggerHandler",
|
|
11
|
+
]
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""BaseModule is the abstract base for all modules in the DigitalKin SDK."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable, Coroutine
|
|
8
|
+
from typing import Any, ClassVar, Generic
|
|
9
|
+
|
|
10
|
+
from digitalkin.grpc_servers.utils.utility_schema_extender import UtilitySchemaExtender
|
|
11
|
+
from digitalkin.logger import logger
|
|
12
|
+
from digitalkin.models.module.module import ModuleCodeModel, ModuleStatus
|
|
13
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
14
|
+
from digitalkin.models.module.module_types import (
|
|
15
|
+
DataModel,
|
|
16
|
+
InputModelT,
|
|
17
|
+
OutputModelT,
|
|
18
|
+
SecretModelT,
|
|
19
|
+
SetupModelT,
|
|
20
|
+
)
|
|
21
|
+
from digitalkin.models.module.utility import EndOfStreamOutput
|
|
22
|
+
from digitalkin.models.services.storage import BaseRole
|
|
23
|
+
from digitalkin.modules.trigger_handler import TriggerHandler
|
|
24
|
+
from digitalkin.services.services_config import ServicesConfig, ServicesStrategy
|
|
25
|
+
from digitalkin.utils.llm_ready_schema import llm_ready_schema
|
|
26
|
+
from digitalkin.utils.package_discover import ModuleDiscoverer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseModule( # noqa: PLR0904
|
|
30
|
+
ABC,
|
|
31
|
+
Generic[
|
|
32
|
+
InputModelT,
|
|
33
|
+
OutputModelT,
|
|
34
|
+
SetupModelT,
|
|
35
|
+
SecretModelT,
|
|
36
|
+
],
|
|
37
|
+
):
|
|
38
|
+
"""BaseModule is the abstract base for all modules in the DigitalKin SDK."""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
description: str
|
|
42
|
+
|
|
43
|
+
setup_format: type[SetupModelT]
|
|
44
|
+
input_format: type[InputModelT]
|
|
45
|
+
output_format: type[OutputModelT]
|
|
46
|
+
secret_format: type[SecretModelT]
|
|
47
|
+
metadata: ClassVar[dict[str, Any]]
|
|
48
|
+
|
|
49
|
+
context: ModuleContext
|
|
50
|
+
triggers_discoverer: ClassVar[ModuleDiscoverer]
|
|
51
|
+
|
|
52
|
+
# service config params
|
|
53
|
+
services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]] = {}
|
|
54
|
+
services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]] = {}
|
|
55
|
+
services_config: ServicesConfig
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_module_id(cls) -> str:
|
|
59
|
+
"""Get the module ID from environment variable or metadata.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The module_id from DIGITALKIN_MODULE_ID env var, or metadata module_id,
|
|
63
|
+
or "unknown" if neither exists.
|
|
64
|
+
"""
|
|
65
|
+
return os.environ.get("DIGITALKIN_MODULE_ID") or cls.metadata.get("module_id", "unknown")
|
|
66
|
+
|
|
67
|
+
def _init_strategies(self, mission_id: str, setup_id: str, setup_version_id: str) -> dict[str, Any]:
|
|
68
|
+
"""Initialize the services configuration.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
dict of services with name: Strategy
|
|
72
|
+
agent: AgentStrategy
|
|
73
|
+
cost: CostStrategy
|
|
74
|
+
filesystem: FilesystemStrategy
|
|
75
|
+
identity: IdentityStrategy
|
|
76
|
+
registry: RegistryStrategy
|
|
77
|
+
snapshot: SnapshotStrategy
|
|
78
|
+
storage: StorageStrategy
|
|
79
|
+
"""
|
|
80
|
+
logger.debug("Service initialisation: %s", self.services_config_strategies.keys())
|
|
81
|
+
return {
|
|
82
|
+
service_name: self.services_config.init_strategy(
|
|
83
|
+
service_name,
|
|
84
|
+
mission_id,
|
|
85
|
+
setup_id,
|
|
86
|
+
setup_version_id,
|
|
87
|
+
)
|
|
88
|
+
for service_name in self.services_config.valid_strategy_names()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
job_id: str,
|
|
94
|
+
mission_id: str,
|
|
95
|
+
setup_id: str,
|
|
96
|
+
setup_version_id: str,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Initialize the module."""
|
|
99
|
+
self._status = ModuleStatus.CREATED
|
|
100
|
+
|
|
101
|
+
# Initialize minimum context
|
|
102
|
+
self.context = ModuleContext(
|
|
103
|
+
# Initialize services configuration
|
|
104
|
+
**self._init_strategies(mission_id, setup_id, setup_version_id),
|
|
105
|
+
session={
|
|
106
|
+
"setup_id": setup_id,
|
|
107
|
+
"mission_id": mission_id,
|
|
108
|
+
"setup_version_id": setup_version_id,
|
|
109
|
+
"job_id": job_id,
|
|
110
|
+
},
|
|
111
|
+
callbacks={"logger": logger},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def status(self) -> ModuleStatus:
|
|
116
|
+
"""Get the module status.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The module status
|
|
120
|
+
"""
|
|
121
|
+
return self._status
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
async def get_secret_format(cls, *, llm_format: bool) -> str:
|
|
125
|
+
"""Get the JSON schema of the secret format model.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
129
|
+
references and simplified structure.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The JSON schema of the secret format as a JSON string.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
NotImplementedError: If the `secret_format` class attribute is not defined.
|
|
136
|
+
"""
|
|
137
|
+
if cls.secret_format is not None:
|
|
138
|
+
if llm_format:
|
|
139
|
+
return json.dumps(llm_ready_schema(cls.secret_format), indent=2)
|
|
140
|
+
return json.dumps(cls.secret_format.model_json_schema(), indent=2)
|
|
141
|
+
msg = f"{cls.__name__}' class does not define a 'secret_format'."
|
|
142
|
+
raise NotImplementedError(msg)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
async def get_input_format(cls, *, llm_format: bool) -> str:
|
|
146
|
+
"""Get the JSON schema of the input format model.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
150
|
+
references and simplified structure.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The JSON schema of the input format as a JSON string.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
NotImplementedError: If the `input_format` class attribute is not defined.
|
|
157
|
+
"""
|
|
158
|
+
if cls.input_format is None:
|
|
159
|
+
msg = f"{cls.__name__}' class does not define an 'input_format'."
|
|
160
|
+
raise NotImplementedError(msg)
|
|
161
|
+
|
|
162
|
+
extended_model = UtilitySchemaExtender.create_extended_input_model(cls.input_format)
|
|
163
|
+
|
|
164
|
+
if llm_format:
|
|
165
|
+
return json.dumps(llm_ready_schema(extended_model), indent=2)
|
|
166
|
+
return json.dumps(extended_model.model_json_schema(), indent=2)
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
async def get_output_format(cls, *, llm_format: bool) -> str:
|
|
170
|
+
"""Get the JSON schema of the output format model.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
174
|
+
references and simplified structure.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The JSON schema of the output format as a JSON string.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
NotImplementedError: If the `output_format` class attribute is not defined.
|
|
181
|
+
"""
|
|
182
|
+
if cls.output_format is None:
|
|
183
|
+
msg = f"'{cls.__name__}' class does not define an 'output_format'."
|
|
184
|
+
raise NotImplementedError(msg)
|
|
185
|
+
|
|
186
|
+
extended_model = UtilitySchemaExtender.create_extended_output_model(cls.output_format)
|
|
187
|
+
|
|
188
|
+
if llm_format:
|
|
189
|
+
return json.dumps(llm_ready_schema(extended_model), indent=2)
|
|
190
|
+
return json.dumps(extended_model.model_json_schema(), indent=2)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
async def get_config_setup_format(cls, *, llm_format: bool) -> str:
|
|
194
|
+
"""Gets the JSON schema of the config setup format model.
|
|
195
|
+
|
|
196
|
+
The config setup format is used only to initialize the module with configuration
|
|
197
|
+
data. It includes fields marked with `json_schema_extra={"config": True}` and
|
|
198
|
+
excludes hidden runtime fields.
|
|
199
|
+
|
|
200
|
+
Dynamic schema fields are always resolved when generating the schema, as this
|
|
201
|
+
method is typically called during module discovery or schema generation where
|
|
202
|
+
fresh values are needed.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
206
|
+
references and simplified structure.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The JSON schema of the config setup format as a JSON string.
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
NotImplementedError: If the `setup_format` class attribute is not defined.
|
|
213
|
+
"""
|
|
214
|
+
if cls.setup_format is not None:
|
|
215
|
+
setup_format = await cls.setup_format.get_clean_model(config_fields=True, hidden_fields=False, force=True)
|
|
216
|
+
if llm_format:
|
|
217
|
+
return json.dumps(llm_ready_schema(setup_format), indent=2)
|
|
218
|
+
return json.dumps(setup_format.model_json_schema(), indent=2)
|
|
219
|
+
msg = "'%s' class does not define an 'config_setup_format'."
|
|
220
|
+
raise NotImplementedError(msg)
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
async def get_setup_format(cls, *, llm_format: bool) -> str:
|
|
224
|
+
"""Gets the JSON schema of the setup format model.
|
|
225
|
+
|
|
226
|
+
The setup format is used at runtime and includes hidden fields but excludes
|
|
227
|
+
config-only fields. This is the schema used when running the module.
|
|
228
|
+
|
|
229
|
+
Dynamic schema fields are always resolved when generating the schema, as this
|
|
230
|
+
method is typically called during module discovery or schema generation where
|
|
231
|
+
fresh values are needed.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
235
|
+
references and simplified structure.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
The JSON schema of the setup format as a JSON string.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
NotImplementedError: If the `setup_format` class attribute is not defined.
|
|
242
|
+
"""
|
|
243
|
+
if cls.setup_format is not None:
|
|
244
|
+
setup_format = await cls.setup_format.get_clean_model(config_fields=False, hidden_fields=True, force=True)
|
|
245
|
+
if llm_format:
|
|
246
|
+
return json.dumps(llm_ready_schema(setup_format), indent=2)
|
|
247
|
+
return json.dumps(setup_format.model_json_schema(), indent=2)
|
|
248
|
+
msg = "'%s' class does not define an 'setup_format'."
|
|
249
|
+
raise NotImplementedError(msg)
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def create_config_setup_model(cls, config_setup_data: dict[str, Any]) -> SetupModelT:
|
|
253
|
+
"""Create the setup model from the setup data.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
config_setup_data: The setup data to create the model from.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
The setup model.
|
|
260
|
+
"""
|
|
261
|
+
return cls.setup_format(**config_setup_data)
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def create_input_model(cls, input_data: dict[str, Any]) -> InputModelT:
|
|
265
|
+
"""Create the input model from the input data.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
input_data: The input data to create the model from.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
The input model.
|
|
272
|
+
"""
|
|
273
|
+
return cls.input_format(**input_data)
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
async def create_setup_model(cls, setup_data: dict[str, Any], *, config_fields: bool = False) -> SetupModelT:
|
|
277
|
+
"""Create the setup model from the setup data.
|
|
278
|
+
|
|
279
|
+
Creates a filtered setup model instance based on the provided data.
|
|
280
|
+
Uses `get_clean_model()` internally to get the appropriate model class
|
|
281
|
+
with field filtering applied.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
setup_data: The setup data to create the model from.
|
|
285
|
+
config_fields: If True, include only fields with json_schema_extra["config"] == True.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
An instance of the setup model with the provided data.
|
|
289
|
+
"""
|
|
290
|
+
model_cls = await cls.setup_format.get_clean_model(config_fields=config_fields, hidden_fields=True)
|
|
291
|
+
return model_cls(**setup_data)
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def create_secret_model(cls, secret_data: dict[str, Any]) -> SecretModelT:
|
|
295
|
+
"""Create the secret model from the secret data.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
secret_data: The secret data to create the model from.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
The secret model.
|
|
302
|
+
"""
|
|
303
|
+
return cls.secret_format(**secret_data)
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def create_output_model(cls, output_data: dict[str, Any]) -> OutputModelT:
|
|
307
|
+
"""Create the output model from the output data.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
output_data: The output data to create the model from.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
The output model.
|
|
314
|
+
"""
|
|
315
|
+
return cls.output_format(**output_data)
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def discover(cls) -> None:
|
|
319
|
+
"""Discover and register all TriggerHandler subclasses in the specified package or current directory.
|
|
320
|
+
|
|
321
|
+
Dynamically import all Python modules in the specified package or current directory,
|
|
322
|
+
triggering class registrations for subclasses of TriggerHandler whose names end with 'Trigger'.
|
|
323
|
+
|
|
324
|
+
If a package is provided, all .py files within its path are imported; otherwise, the current
|
|
325
|
+
working directory is searched. For each imported module, any class matching the criteria is
|
|
326
|
+
registered via cls.register(). Errors during import are logged at debug level.
|
|
327
|
+
|
|
328
|
+
Built-in healthcheck handlers (ping, services, status) are automatically registered
|
|
329
|
+
to provide standard healthcheck functionality for all modules.
|
|
330
|
+
"""
|
|
331
|
+
from digitalkin.models.module.utility import UtilityRegistry # noqa: PLC0415
|
|
332
|
+
|
|
333
|
+
cls.triggers_discoverer.discover_modules()
|
|
334
|
+
|
|
335
|
+
# Auto-register built-in SDK triggers (healthcheck, etc.)
|
|
336
|
+
for trigger_cls in UtilityRegistry.get_builtin_triggers():
|
|
337
|
+
cls.triggers_discoverer.register_trigger(trigger_cls)
|
|
338
|
+
|
|
339
|
+
logger.debug("discovered: %s", cls.triggers_discoverer)
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def register(cls, handler_cls: type[TriggerHandler]) -> type[TriggerHandler]:
|
|
343
|
+
"""Dynamically register the trigger class.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
handler_cls: type of the trigger handler to register.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
type of the trigger handler.
|
|
350
|
+
"""
|
|
351
|
+
return cls.triggers_discoverer.register_trigger(handler_cls)
|
|
352
|
+
|
|
353
|
+
async def run_config_setup( # noqa: PLR6301
|
|
354
|
+
self,
|
|
355
|
+
context: ModuleContext, # noqa: ARG002
|
|
356
|
+
config_setup_data: SetupModelT,
|
|
357
|
+
) -> SetupModelT:
|
|
358
|
+
"""Run config setup the module.
|
|
359
|
+
|
|
360
|
+
The config setup is used to initialize the setup with configuration data.
|
|
361
|
+
This method is typically used to set up the module with necessary configuration before running it,
|
|
362
|
+
especially for processing data like files.
|
|
363
|
+
The function needs to save the setup in the storage.
|
|
364
|
+
The module will be initialize with the setup and not the config setup.
|
|
365
|
+
This method is optional, the config setup and setup can be the same.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
The updated setup model after running the config setup.
|
|
369
|
+
"""
|
|
370
|
+
return config_setup_data
|
|
371
|
+
|
|
372
|
+
@abstractmethod
|
|
373
|
+
async def initialize(self, context: ModuleContext, setup_data: SetupModelT) -> None:
|
|
374
|
+
"""Initialize the module."""
|
|
375
|
+
raise NotImplementedError
|
|
376
|
+
|
|
377
|
+
async def run(
|
|
378
|
+
self,
|
|
379
|
+
input_data: InputModelT,
|
|
380
|
+
setup_data: SetupModelT,
|
|
381
|
+
) -> None:
|
|
382
|
+
"""Run the module with the given input and setup data.
|
|
383
|
+
|
|
384
|
+
This method validates the input data, determines the protocol from the input,
|
|
385
|
+
and dispatches the request to the corresponding trigger handler. The trigger handler
|
|
386
|
+
is responsible for processing the input and invoking the callback with the result.
|
|
387
|
+
|
|
388
|
+
Triggers:
|
|
389
|
+
- The method is triggered when a module run is requested with specific input and setup data.
|
|
390
|
+
- The protocol specified in the input determines which trigger handler is invoked.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
input_data (InputModelT): The input data to be processed by the module.
|
|
394
|
+
setup_data (SetupModelT): The setup or configuration data required for the module.
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
ValueError: If no handler for the protocol is found.
|
|
398
|
+
"""
|
|
399
|
+
input_instance = self.input_format.model_validate(input_data)
|
|
400
|
+
handler_instance = self.triggers_discoverer.get_trigger(
|
|
401
|
+
input_instance.root.protocol,
|
|
402
|
+
input_instance.root,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
await handler_instance.handle(
|
|
406
|
+
input_instance.root,
|
|
407
|
+
setup_data,
|
|
408
|
+
self.context,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
@abstractmethod
|
|
412
|
+
async def cleanup(self) -> None:
|
|
413
|
+
"""Run the module."""
|
|
414
|
+
raise NotImplementedError
|
|
415
|
+
|
|
416
|
+
async def _run_lifecycle(
|
|
417
|
+
self,
|
|
418
|
+
input_data: InputModelT,
|
|
419
|
+
setup_data: SetupModelT,
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Run the module lifecycle.
|
|
422
|
+
|
|
423
|
+
Raises:
|
|
424
|
+
asyncio.CancelledError: If the module is cancelled
|
|
425
|
+
"""
|
|
426
|
+
try:
|
|
427
|
+
logger.info("Starting module %s", self.name, extra=self.context.session.current_ids())
|
|
428
|
+
await self.run(input_data, setup_data)
|
|
429
|
+
logger.info("Module %s finished", self.name, extra=self.context.session.current_ids())
|
|
430
|
+
except asyncio.CancelledError:
|
|
431
|
+
self._status = ModuleStatus.CANCELLED
|
|
432
|
+
logger.error("Module %s cancelled", self.name, extra=self.context.session.current_ids())
|
|
433
|
+
except Exception:
|
|
434
|
+
self._status = ModuleStatus.FAILED
|
|
435
|
+
logger.exception("Error inside module %s", self.name, extra=self.context.session.current_ids())
|
|
436
|
+
else:
|
|
437
|
+
self._status = ModuleStatus.STOPPING
|
|
438
|
+
|
|
439
|
+
async def start(
|
|
440
|
+
self,
|
|
441
|
+
input_data: InputModelT,
|
|
442
|
+
setup_data: SetupModelT,
|
|
443
|
+
callback: Callable[[OutputModelT | ModuleCodeModel], Coroutine[Any, Any, None]],
|
|
444
|
+
done_callback: Callable | None = None,
|
|
445
|
+
) -> None:
|
|
446
|
+
"""Start the module."""
|
|
447
|
+
try:
|
|
448
|
+
self.context.callbacks.send_message = callback
|
|
449
|
+
logger.info(f"Inititalize module {self.context.session.job_id}")
|
|
450
|
+
await self.initialize(self.context, setup_data)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
self._status = ModuleStatus.FAILED
|
|
453
|
+
short_description = "Error initializing module"
|
|
454
|
+
logger.exception("%s: %s", short_description, e)
|
|
455
|
+
await callback(
|
|
456
|
+
ModuleCodeModel(
|
|
457
|
+
code="Error",
|
|
458
|
+
short_description=short_description,
|
|
459
|
+
message=str(e),
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
if done_callback is not None:
|
|
463
|
+
await done_callback(None)
|
|
464
|
+
await self.stop()
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
logger.debug("Init the discovered input handlers.")
|
|
469
|
+
self.triggers_discoverer.init_handlers(self.context)
|
|
470
|
+
logger.debug(f"Run lifecycle {self.context.session.job_id}")
|
|
471
|
+
await self._run_lifecycle(input_data, setup_data)
|
|
472
|
+
except Exception:
|
|
473
|
+
self._status = ModuleStatus.FAILED
|
|
474
|
+
logger.exception("Error during module lifecyle")
|
|
475
|
+
finally:
|
|
476
|
+
await self.stop()
|
|
477
|
+
|
|
478
|
+
async def stop(self) -> None:
|
|
479
|
+
"""Stop the module."""
|
|
480
|
+
logger.info("Stopping module %s | job_id=%s", self.name, self.context.session.job_id)
|
|
481
|
+
try:
|
|
482
|
+
self._status = ModuleStatus.STOPPING
|
|
483
|
+
logger.debug("Module %s stopped", self.name)
|
|
484
|
+
await self.cleanup()
|
|
485
|
+
await self.context.callbacks.send_message(
|
|
486
|
+
DataModel(
|
|
487
|
+
root=EndOfStreamOutput(),
|
|
488
|
+
annotations={"role": BaseRole.SYSTEM},
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
self._status = ModuleStatus.STOPPED
|
|
492
|
+
logger.debug("Module %s cleaned", self.name)
|
|
493
|
+
except Exception:
|
|
494
|
+
self._status = ModuleStatus.FAILED
|
|
495
|
+
logger.exception("Error stopping module")
|
|
496
|
+
|
|
497
|
+
async def start_config_setup(
|
|
498
|
+
self,
|
|
499
|
+
config_setup_data: SetupModelT,
|
|
500
|
+
callback: Callable[[SetupModelT | ModuleCodeModel], Coroutine[Any, Any, None]],
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Start the module."""
|
|
503
|
+
try:
|
|
504
|
+
logger.info("Run Config Setup lifecycle", extra=self.context.session.current_ids())
|
|
505
|
+
self._status = ModuleStatus.RUNNING
|
|
506
|
+
self.context.callbacks.set_config_setup = callback
|
|
507
|
+
content = await self.run_config_setup(self.context, config_setup_data)
|
|
508
|
+
|
|
509
|
+
wrapper = config_setup_data.model_dump()
|
|
510
|
+
wrapper["content"] = content.model_dump()
|
|
511
|
+
setup_model = await self.create_setup_model(wrapper)
|
|
512
|
+
await callback(setup_model)
|
|
513
|
+
self._status = ModuleStatus.STOPPING
|
|
514
|
+
except Exception:
|
|
515
|
+
logger.error("Error during module lifecyle")
|
|
516
|
+
self._status = ModuleStatus.FAILED
|
|
517
|
+
logger.exception("Error during module lifecyle", extra=self.context.session.current_ids())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""ArchetypeModule extends BaseModule to implement specific module types."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
from digitalkin.models.module.module_types import (
|
|
6
|
+
InputModelT,
|
|
7
|
+
OutputModelT,
|
|
8
|
+
SecretModelT,
|
|
9
|
+
SetupModelT,
|
|
10
|
+
)
|
|
11
|
+
from digitalkin.modules._base_module import BaseModule
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArchetypeModule(
|
|
15
|
+
BaseModule[
|
|
16
|
+
InputModelT,
|
|
17
|
+
OutputModelT,
|
|
18
|
+
SetupModelT,
|
|
19
|
+
SecretModelT,
|
|
20
|
+
],
|
|
21
|
+
ABC,
|
|
22
|
+
):
|
|
23
|
+
"""ArchetypeModule extends BaseModule to implement specific module types."""
|