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,97 @@
|
|
|
1
|
+
"""Utility schema extender for gRPC API responses.
|
|
2
|
+
|
|
3
|
+
This module extends module schemas with SDK utility protocols for API responses.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Annotated, Union, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, create_model
|
|
9
|
+
|
|
10
|
+
from digitalkin.models.module.module_types import DataModel
|
|
11
|
+
from digitalkin.models.module.utility import (
|
|
12
|
+
EndOfStreamOutput,
|
|
13
|
+
HealthcheckPingInput,
|
|
14
|
+
HealthcheckPingOutput,
|
|
15
|
+
HealthcheckServicesInput,
|
|
16
|
+
HealthcheckServicesOutput,
|
|
17
|
+
HealthcheckStatusInput,
|
|
18
|
+
HealthcheckStatusOutput,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UtilitySchemaExtender:
|
|
23
|
+
"""Extends module schemas with SDK utility protocols for API responses.
|
|
24
|
+
|
|
25
|
+
This class provides methods to create extended Pydantic models that include
|
|
26
|
+
both user-defined protocols and SDK utility protocols in their schemas.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_output_protocols = (
|
|
30
|
+
EndOfStreamOutput,
|
|
31
|
+
HealthcheckPingOutput,
|
|
32
|
+
HealthcheckServicesOutput,
|
|
33
|
+
HealthcheckStatusOutput,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_input_protocols = (
|
|
37
|
+
HealthcheckPingInput,
|
|
38
|
+
HealthcheckServicesInput,
|
|
39
|
+
HealthcheckStatusInput,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def _extract_union_types(cls, annotation: type) -> tuple:
|
|
44
|
+
"""Extract individual types from a Union or Annotated[Union, ...] annotation.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A tuple of individual types contained in the Union.
|
|
48
|
+
"""
|
|
49
|
+
if get_origin(annotation) is Annotated:
|
|
50
|
+
inner_args = get_args(annotation)
|
|
51
|
+
if inner_args:
|
|
52
|
+
return cls._extract_union_types(inner_args[0])
|
|
53
|
+
if get_origin(annotation) is Union:
|
|
54
|
+
return get_args(annotation)
|
|
55
|
+
return (annotation,)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def create_extended_output_model(cls, base_model: type[DataModel]) -> type[DataModel]:
|
|
59
|
+
"""Create an extended output model that includes utility output protocols.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
base_model: The module's output_format class (a DataModel subclass).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A new DataModel subclass with root typed as Union[original_types, utility_types].
|
|
66
|
+
"""
|
|
67
|
+
original_annotation = base_model.model_fields["root"].annotation
|
|
68
|
+
original_types = cls._extract_union_types(original_annotation)
|
|
69
|
+
extended_types = (*original_types, *cls._output_protocols)
|
|
70
|
+
extended_root = Annotated[extended_types, Field(discriminator="protocol")] # type: ignore[valid-type]
|
|
71
|
+
return create_model(
|
|
72
|
+
f"{base_model.__name__}Utilities",
|
|
73
|
+
__base__=DataModel,
|
|
74
|
+
root=(extended_root, ...),
|
|
75
|
+
annotations=(dict[str, str], Field(default={})),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def create_extended_input_model(cls, base_model: type[DataModel]) -> type[DataModel]:
|
|
80
|
+
"""Create an extended input model that includes utility input protocols.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
base_model: The module's input_format class (a DataModel subclass).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A new DataModel subclass with root typed as Union[original_types, utility_types].
|
|
87
|
+
"""
|
|
88
|
+
original_annotation = base_model.model_fields["root"].annotation
|
|
89
|
+
original_types = cls._extract_union_types(original_annotation)
|
|
90
|
+
extended_types = (*original_types, *cls._input_protocols)
|
|
91
|
+
extended_root = Annotated[extended_types, Field(discriminator="protocol")] # type: ignore[valid-type]
|
|
92
|
+
return create_model(
|
|
93
|
+
f"{base_model.__name__}Utilities",
|
|
94
|
+
__base__=DataModel,
|
|
95
|
+
root=(extended_root, ...),
|
|
96
|
+
annotations=(dict[str, str], Field(default={})),
|
|
97
|
+
)
|
digitalkin/logger.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""This module sets up a logger."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, ClassVar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ColorJSONFormatter(logging.Formatter):
|
|
12
|
+
"""Color JSON formatter for development (pretty-printed with colors)."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, is_production: bool = False) -> None:
|
|
15
|
+
"""Initialize the formatter.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
is_production: Whether the application is running in production.
|
|
19
|
+
"""
|
|
20
|
+
self.is_production = is_production
|
|
21
|
+
super().__init__()
|
|
22
|
+
|
|
23
|
+
grey = "\x1b[38;20m"
|
|
24
|
+
green = "\x1b[32;20m"
|
|
25
|
+
blue = "\x1b[34;20m"
|
|
26
|
+
yellow = "\x1b[33;20m"
|
|
27
|
+
red = "\x1b[31;20m"
|
|
28
|
+
bold_red = "\x1b[31;1m"
|
|
29
|
+
reset = "\x1b[0m"
|
|
30
|
+
|
|
31
|
+
COLORS: ClassVar[dict[int, str]] = {
|
|
32
|
+
logging.DEBUG: grey,
|
|
33
|
+
logging.INFO: green,
|
|
34
|
+
logging.WARNING: yellow,
|
|
35
|
+
logging.ERROR: red,
|
|
36
|
+
logging.CRITICAL: bold_red,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
40
|
+
"""Format the log record as colored JSON for development.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
record: The log record to format.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: The colored JSON formatted log record.
|
|
47
|
+
"""
|
|
48
|
+
log_obj: dict[str, Any] = {
|
|
49
|
+
"timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
|
|
50
|
+
"level": record.levelname.lower(),
|
|
51
|
+
"message": record.getMessage(),
|
|
52
|
+
"module": record.module,
|
|
53
|
+
"location": f"{record.pathname}:{record.lineno}:{record.funcName}",
|
|
54
|
+
}
|
|
55
|
+
# Add exception info if present
|
|
56
|
+
if record.exc_info:
|
|
57
|
+
log_obj["exception"] = self.formatException(record.exc_info)
|
|
58
|
+
|
|
59
|
+
# Add any extra fields
|
|
60
|
+
skip_attrs = {
|
|
61
|
+
"name",
|
|
62
|
+
"msg",
|
|
63
|
+
"args",
|
|
64
|
+
"created",
|
|
65
|
+
"filename",
|
|
66
|
+
"funcName",
|
|
67
|
+
"levelname",
|
|
68
|
+
"levelno",
|
|
69
|
+
"lineno",
|
|
70
|
+
"module",
|
|
71
|
+
"msecs",
|
|
72
|
+
"message",
|
|
73
|
+
"pathname",
|
|
74
|
+
"process",
|
|
75
|
+
"processName",
|
|
76
|
+
"relativeCreated",
|
|
77
|
+
"thread",
|
|
78
|
+
"threadName",
|
|
79
|
+
"exc_info",
|
|
80
|
+
"exc_text",
|
|
81
|
+
"stack_info",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
extras = {key: value for key, value in record.__dict__.items() if key not in skip_attrs}
|
|
85
|
+
|
|
86
|
+
if extras:
|
|
87
|
+
log_obj["extra"] = extras
|
|
88
|
+
|
|
89
|
+
# Pretty print with color
|
|
90
|
+
color = self.COLORS.get(record.levelno, self.grey)
|
|
91
|
+
if self.is_production:
|
|
92
|
+
log_obj["message"] = f"{color}{log_obj.get('message', '')}{self.reset}"
|
|
93
|
+
return json.dumps(log_obj, default=str, separators=(",", ":"))
|
|
94
|
+
json_str = json.dumps(log_obj, indent=2, default=str)
|
|
95
|
+
json_str = json_str.replace("\\n", "\n")
|
|
96
|
+
return f"{color}{json_str}{self.reset}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def setup_logger(
|
|
100
|
+
name: str,
|
|
101
|
+
level: int = logging.INFO,
|
|
102
|
+
additional_loggers: dict[str, int] | None = None,
|
|
103
|
+
*,
|
|
104
|
+
is_production: bool | None = None,
|
|
105
|
+
configure_root: bool = True,
|
|
106
|
+
) -> logging.Logger:
|
|
107
|
+
"""Set up a logger with the ColorJSONFormatter.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
name: Name of the logger to create
|
|
111
|
+
level: Logging level (default: logging.INFO)
|
|
112
|
+
is_production: Whether running in production. If None, checks RAILWAY_SERVICE_NAME env var
|
|
113
|
+
configure_root: Whether to configure root logger (default: True)
|
|
114
|
+
additional_loggers: Dict of additional logger names and their levels to configure
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
logging.Logger: Configured logger instance
|
|
118
|
+
"""
|
|
119
|
+
# Determine if we're in production
|
|
120
|
+
if is_production is None:
|
|
121
|
+
is_production = os.getenv("RAILWAY_SERVICE_NAME") is not None
|
|
122
|
+
|
|
123
|
+
# Configure root logger if requested
|
|
124
|
+
if configure_root:
|
|
125
|
+
logging.basicConfig(
|
|
126
|
+
level=logging.DEBUG,
|
|
127
|
+
stream=sys.stdout,
|
|
128
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Configure additional loggers
|
|
132
|
+
if additional_loggers:
|
|
133
|
+
for logger_name, logger_level in additional_loggers.items():
|
|
134
|
+
logging.getLogger(logger_name).setLevel(logger_level)
|
|
135
|
+
|
|
136
|
+
# Create and configure the main logger
|
|
137
|
+
logger = logging.getLogger(name)
|
|
138
|
+
logger.setLevel(level)
|
|
139
|
+
# Only add handler if not already configured
|
|
140
|
+
if not logger.handlers:
|
|
141
|
+
ch = logging.StreamHandler()
|
|
142
|
+
ch.setLevel(level)
|
|
143
|
+
ch.setFormatter(ColorJSONFormatter(is_production=is_production))
|
|
144
|
+
logger.addHandler(ch)
|
|
145
|
+
logger.propagate = False
|
|
146
|
+
|
|
147
|
+
return logger
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
logger = setup_logger(
|
|
151
|
+
"digitalkin",
|
|
152
|
+
level=logging.INFO,
|
|
153
|
+
additional_loggers={
|
|
154
|
+
"grpc": logging.DEBUG,
|
|
155
|
+
"asyncio": logging.DEBUG,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Mixin definitions."""
|
|
2
|
+
|
|
3
|
+
from digitalkin.mixins.base_mixin import BaseMixin
|
|
4
|
+
from digitalkin.mixins.callback_mixin import UserMessageMixin
|
|
5
|
+
from digitalkin.mixins.chat_history_mixin import ChatHistoryMixin
|
|
6
|
+
from digitalkin.mixins.cost_mixin import CostMixin
|
|
7
|
+
from digitalkin.mixins.filesystem_mixin import FilesystemMixin
|
|
8
|
+
from digitalkin.mixins.logger_mixin import LoggerMixin
|
|
9
|
+
from digitalkin.mixins.storage_mixin import StorageMixin
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BaseMixin",
|
|
13
|
+
"ChatHistoryMixin",
|
|
14
|
+
"CostMixin",
|
|
15
|
+
"FilesystemMixin",
|
|
16
|
+
"LoggerMixin",
|
|
17
|
+
"StorageMixin",
|
|
18
|
+
"UserMessageMixin",
|
|
19
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Simple toolkit class with basic and simple API access in the Triggers."""
|
|
2
|
+
|
|
3
|
+
from digitalkin.mixins.chat_history_mixin import ChatHistoryMixin
|
|
4
|
+
from digitalkin.mixins.cost_mixin import CostMixin
|
|
5
|
+
from digitalkin.mixins.file_history_mixin import FileHistoryMixin
|
|
6
|
+
from digitalkin.mixins.logger_mixin import LoggerMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseMixin(CostMixin, ChatHistoryMixin, FileHistoryMixin, LoggerMixin):
|
|
10
|
+
"""Base Mixin to access to minimum Module Context functionnalities in the Triggers."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""User callback to send a message from the Trigger."""
|
|
2
|
+
|
|
3
|
+
from typing import Generic
|
|
4
|
+
|
|
5
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
6
|
+
from digitalkin.models.module.module_types import OutputModelT
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserMessageMixin(Generic[OutputModelT]):
|
|
10
|
+
"""Mixin providing callback operations through the callbacks .
|
|
11
|
+
|
|
12
|
+
This mixin wraps callback strategy calls to provide a cleaner API
|
|
13
|
+
for direct messaging in trigger handlers.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
async def send_message(context: ModuleContext, output: OutputModelT) -> None:
|
|
18
|
+
"""Send a message using the callbacks strategy.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
context: Module context containing the callbacks strategy.
|
|
22
|
+
output: Message to send with the Module defined output Type.
|
|
23
|
+
"""
|
|
24
|
+
await context.callbacks.send_message(output)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Context mixins providing ergonomic access to service strategies.
|
|
2
|
+
|
|
3
|
+
This module provides mixins that wrap service strategy calls with cleaner APIs,
|
|
4
|
+
following Django/FastAPI patterns where context is passed explicitly to each method.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Generic
|
|
8
|
+
|
|
9
|
+
from digitalkin.mixins.callback_mixin import UserMessageMixin
|
|
10
|
+
from digitalkin.mixins.logger_mixin import LoggerMixin
|
|
11
|
+
from digitalkin.mixins.storage_mixin import StorageMixin
|
|
12
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
13
|
+
from digitalkin.models.module.module_types import InputModelT, OutputModelT
|
|
14
|
+
from digitalkin.models.services.storage import BaseMessage, ChatHistory, Role
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChatHistoryMixin(UserMessageMixin, StorageMixin, LoggerMixin, Generic[InputModelT, OutputModelT]):
|
|
18
|
+
"""Mixin providing chat history operations through storage strategy.
|
|
19
|
+
|
|
20
|
+
This mixin provides a higher-level API for managing chat history,
|
|
21
|
+
using the storage strategy as the underlying persistence mechanism.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
CHAT_HISTORY_COLLECTION = "chat_history"
|
|
25
|
+
CHAT_HISTORY_RECORD_ID = "full_chat_history"
|
|
26
|
+
|
|
27
|
+
def _get_history_key(self, context: ModuleContext) -> str:
|
|
28
|
+
"""Get session-specific history key.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
context: Module context containing session information
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Unique history key for the current session
|
|
35
|
+
"""
|
|
36
|
+
# TODO: define mission-specific chat history key not dependant on mission_id
|
|
37
|
+
# or need customization by user
|
|
38
|
+
mission_id = getattr(context.session, "mission_id", None) or "default"
|
|
39
|
+
return f"{self.CHAT_HISTORY_RECORD_ID}_{mission_id}"
|
|
40
|
+
|
|
41
|
+
def load_chat_history(self, context: ModuleContext) -> ChatHistory:
|
|
42
|
+
"""Load chat history for the current session.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
context: Module context containing storage strategy
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Chat history object, empty if none exists or loading fails
|
|
49
|
+
"""
|
|
50
|
+
history_key = self._get_history_key(context)
|
|
51
|
+
|
|
52
|
+
if (raw_history := self.read_storage(context, self.CHAT_HISTORY_COLLECTION, history_key)) is not None:
|
|
53
|
+
return ChatHistory.model_validate(raw_history.data)
|
|
54
|
+
return ChatHistory(messages=[])
|
|
55
|
+
|
|
56
|
+
def append_chat_history_message(
|
|
57
|
+
self,
|
|
58
|
+
context: ModuleContext,
|
|
59
|
+
role: Role,
|
|
60
|
+
content: Any, # noqa: ANN401
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Append a message to chat history.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
context: Module context containing storage strategy
|
|
66
|
+
role: Message role (user, assistant, system)
|
|
67
|
+
content: Message content
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
StorageServiceError: If history update fails
|
|
71
|
+
"""
|
|
72
|
+
history_key = self._get_history_key(context)
|
|
73
|
+
chat_history = self.load_chat_history(context)
|
|
74
|
+
|
|
75
|
+
chat_history.messages.append(BaseMessage(role=role, content=content))
|
|
76
|
+
if len(chat_history.messages) == 1:
|
|
77
|
+
# Create new record
|
|
78
|
+
self.log_debug(context, f"Creating new chat history for session: {history_key}")
|
|
79
|
+
self.store_storage(
|
|
80
|
+
context,
|
|
81
|
+
self.CHAT_HISTORY_COLLECTION,
|
|
82
|
+
history_key,
|
|
83
|
+
chat_history.model_dump(),
|
|
84
|
+
data_type="OUTPUT",
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
self.log_debug(context, f"Updating chat history for session: {history_key}")
|
|
88
|
+
self.update_storage(
|
|
89
|
+
context,
|
|
90
|
+
self.CHAT_HISTORY_COLLECTION,
|
|
91
|
+
history_key,
|
|
92
|
+
chat_history.model_dump(),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def save_send_message(
|
|
96
|
+
self,
|
|
97
|
+
context: ModuleContext,
|
|
98
|
+
output: OutputModelT,
|
|
99
|
+
role: Role,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Save the output message to the chat history and send a response to the Module request.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: Module context containing storage strategy
|
|
105
|
+
role: Message role (user, assistant, system)
|
|
106
|
+
output: Message content as Pydantic Class
|
|
107
|
+
"""
|
|
108
|
+
# TO-DO: we should define a default output message type to ease user experience
|
|
109
|
+
self.append_chat_history_message(context=context, role=role, content=output.root)
|
|
110
|
+
await self.send_message(context=context, output=output)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Cost Mixin to ease trigger deveolpment."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
6
|
+
from digitalkin.services.cost.cost_strategy import CostData
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CostMixin:
|
|
10
|
+
"""Mixin providing cost tracking operations through the cost strategy.
|
|
11
|
+
|
|
12
|
+
This mixin wraps cost strategy calls to provide a cleaner API
|
|
13
|
+
for cost tracking in trigger handlers.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def add_cost(context: ModuleContext, name: str, cost_config_name: str, quantity: float) -> None:
|
|
18
|
+
"""Add a cost entry using the cost strategy.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
context: Module context containing the cost strategy
|
|
22
|
+
name: Name/identifier for this cost entry
|
|
23
|
+
cost_config_name: Name of the cost configuration to use
|
|
24
|
+
quantity: Quantity of units consumed
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
CostServiceError: If cost addition fails
|
|
28
|
+
"""
|
|
29
|
+
return context.cost.add(name, cost_config_name, quantity)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def get_cost(context: ModuleContext, name: str) -> list[CostData]:
|
|
33
|
+
"""Get cost entries for a specific name.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
context: Module context containing the cost strategy
|
|
37
|
+
name: Name/identifier to get costs for
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of cost data entries
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
CostServiceError: If cost retrieval fails
|
|
44
|
+
"""
|
|
45
|
+
return context.cost.get(name)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def get_costs(
|
|
49
|
+
context: ModuleContext,
|
|
50
|
+
names: list[str] | None = None,
|
|
51
|
+
cost_types: list[
|
|
52
|
+
Literal[
|
|
53
|
+
"TOKEN_INPUT",
|
|
54
|
+
"TOKEN_OUTPUT",
|
|
55
|
+
"API_CALL",
|
|
56
|
+
"STORAGE",
|
|
57
|
+
"TIME",
|
|
58
|
+
"OTHER",
|
|
59
|
+
]
|
|
60
|
+
]
|
|
61
|
+
| None = None,
|
|
62
|
+
) -> list[CostData]:
|
|
63
|
+
"""Get filtered cost entries.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
context: Module context containing the cost strategy
|
|
67
|
+
names: Optional list of names to filter by
|
|
68
|
+
cost_types: Optional list of cost types to filter by
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of filtered cost data entries
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
CostServiceError: If cost retrieval fails
|
|
75
|
+
"""
|
|
76
|
+
return context.cost.get_filtered(names, cost_types)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Context mixins providing ergonomic access to service strategies.
|
|
2
|
+
|
|
3
|
+
This module provides mixins that wrap service strategy calls with cleaner APIs,
|
|
4
|
+
following Django/FastAPI patterns where context is passed explicitly to each method.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from digitalkin.mixins.logger_mixin import LoggerMixin
|
|
8
|
+
from digitalkin.mixins.storage_mixin import StorageMixin
|
|
9
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
10
|
+
from digitalkin.models.services.storage import FileHistory, FileModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileHistoryMixin(StorageMixin, LoggerMixin):
|
|
14
|
+
"""Mixin providing File history operations through storage strategy.
|
|
15
|
+
|
|
16
|
+
This mixin provides a higher-level API for managing File history,
|
|
17
|
+
using the storage strategy as the underlying persistence mechanism.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
file_history_front: FileHistory = FileHistory(files=[])
|
|
21
|
+
FILE_HISTORY_COLLECTION = "file_history"
|
|
22
|
+
FILE_HISTORY_RECORD_ID = "full_file_history"
|
|
23
|
+
|
|
24
|
+
def _get_history_key(self, context: ModuleContext) -> str:
|
|
25
|
+
"""Get session-specific history key.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
context: Module context containing session information
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Unique history key for the current session
|
|
32
|
+
"""
|
|
33
|
+
# TODO: define mission-specific chat history key not dependant on mission_id
|
|
34
|
+
# or need customization by user
|
|
35
|
+
mission_id = getattr(context.session, "mission_id", None) or "default"
|
|
36
|
+
return f"{self.FILE_HISTORY_RECORD_ID}_{mission_id}"
|
|
37
|
+
|
|
38
|
+
def load_file_history(self, context: ModuleContext) -> FileHistory:
|
|
39
|
+
"""Load File history for the current session.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
context: Module context containing storage strategy
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
File history object, empty if none exists or loading fails
|
|
46
|
+
"""
|
|
47
|
+
history_key = self._get_history_key(context)
|
|
48
|
+
|
|
49
|
+
if self.file_history_front is None:
|
|
50
|
+
try:
|
|
51
|
+
record = self.read_storage(
|
|
52
|
+
context,
|
|
53
|
+
self.FILE_HISTORY_COLLECTION,
|
|
54
|
+
history_key,
|
|
55
|
+
)
|
|
56
|
+
if record and record.data:
|
|
57
|
+
return FileHistory.model_validate(record.data)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self.log_warning(context, f"Failed to load File history: {e}")
|
|
60
|
+
return self.file_history_front
|
|
61
|
+
|
|
62
|
+
def append_files_history(self, context: ModuleContext, files: list[FileModel]) -> None:
|
|
63
|
+
"""Append a message to File history.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
context: Module context containing storage strategy
|
|
67
|
+
files: list of files model
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
StorageServiceError: If history update fails
|
|
71
|
+
"""
|
|
72
|
+
history_key = self._get_history_key(context)
|
|
73
|
+
file_history = self.load_file_history(context)
|
|
74
|
+
|
|
75
|
+
file_history.files.extend(files)
|
|
76
|
+
if len(file_history.files) == len(files):
|
|
77
|
+
# Create new record
|
|
78
|
+
self.log_debug(context, f"Creating new file history for session: {history_key}")
|
|
79
|
+
self.store_storage(
|
|
80
|
+
context,
|
|
81
|
+
self.FILE_HISTORY_COLLECTION,
|
|
82
|
+
history_key,
|
|
83
|
+
file_history.model_dump(),
|
|
84
|
+
data_type="OUTPUT",
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
self.log_debug(context, f"Updating file history for session: {history_key}")
|
|
88
|
+
self.update_storage(
|
|
89
|
+
context,
|
|
90
|
+
self.FILE_HISTORY_COLLECTION,
|
|
91
|
+
history_key,
|
|
92
|
+
file_history.model_dump(),
|
|
93
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Filesystem Mixin to ease filesystem use."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
6
|
+
from digitalkin.services.filesystem.filesystem_strategy import FilesystemRecord
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilesystemMixin:
|
|
10
|
+
"""Mixin providing filesystem operations through the filesystem strategy.
|
|
11
|
+
|
|
12
|
+
This mixin wraps filesystem strategy calls to provide a cleaner API
|
|
13
|
+
for file operations in trigger handlers.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def upload_files(context: ModuleContext, files: list[Any]) -> tuple[list[FilesystemRecord], int, int]:
|
|
18
|
+
"""Upload files using the filesystem strategy.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
context: Module context containing the filesystem strategy
|
|
22
|
+
files: List of files to upload
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Tuple of (all_files, succeeded_files, failed_files)
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
FilesystemServiceError: If upload operation fails
|
|
29
|
+
"""
|
|
30
|
+
return context.filesystem.upload_files(files)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def get_file(context: ModuleContext, file_id: str) -> FilesystemRecord:
|
|
34
|
+
"""Retrieve a file by ID with the content.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
context: Module context containing the filesystem strategy
|
|
38
|
+
file_id: Unique identifier for the file
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
File object with metadata and optionally content
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
FilesystemServiceError: If file retrieval fails
|
|
45
|
+
"""
|
|
46
|
+
return context.filesystem.get_file(file_id, include_content=True)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Logger Mixin to ease and merge every logs."""
|
|
2
|
+
|
|
3
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LoggerMixin:
|
|
7
|
+
"""Mixin providing callback operations through the callbacks strategy.
|
|
8
|
+
|
|
9
|
+
This mixin wraps callback strategy calls to provide a cleaner API
|
|
10
|
+
for logging and messaging in trigger handlers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def log_debug(context: ModuleContext, message: str) -> None:
|
|
15
|
+
"""Log debug message using the callbacks strategy.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
context: Module context containing the callbacks strategy
|
|
19
|
+
message: Debug message to log
|
|
20
|
+
"""
|
|
21
|
+
return context.callbacks.logger.debug(message, extra=context.session.current_ids())
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def log_info(context: ModuleContext, message: str) -> None:
|
|
25
|
+
"""Log info message using the callbacks strategy.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
context: Module context containing the callbacks strategy
|
|
29
|
+
message: Info message to log
|
|
30
|
+
"""
|
|
31
|
+
return context.callbacks.logger.info(message, extra=context.session.current_ids())
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def log_warning(context: ModuleContext, message: str) -> None:
|
|
35
|
+
"""Log warning message using the callbacks strategy.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
context: Module context containing the callbacks strategy
|
|
39
|
+
message: Warning message to log
|
|
40
|
+
"""
|
|
41
|
+
return context.callbacks.logger.warning(message, extra=context.session.current_ids())
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def log_error(context: ModuleContext, message: str) -> None:
|
|
45
|
+
"""Log error message using the callbacks strategy.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
context: Module context containing the callbacks strategy
|
|
49
|
+
message: Error message to log
|
|
50
|
+
"""
|
|
51
|
+
return context.callbacks.logger.error(message, extra=context.session.current_ids())
|