krons 0.1.1__py3-none-any.whl → 0.2.0__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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +56 -74
- krons/core/base/__init__.py +121 -0
- krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
- krons/core/{element.py → base/element.py} +13 -5
- krons/core/{event.py → base/event.py} +39 -6
- krons/core/{eventbus.py → base/eventbus.py} +3 -1
- krons/core/{flow.py → base/flow.py} +11 -4
- krons/core/{graph.py → base/graph.py} +24 -8
- krons/core/{node.py → base/node.py} +44 -19
- krons/core/{pile.py → base/pile.py} +22 -8
- krons/core/{processor.py → base/processor.py} +21 -7
- krons/core/{progression.py → base/progression.py} +3 -1
- krons/{specs → core/specs}/__init__.py +0 -5
- krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
- krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
- krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
- krons/{specs → core/specs}/catalog/__init__.py +2 -2
- krons/{specs → core/specs}/catalog/_audit.py +2 -2
- krons/{specs → core/specs}/catalog/_common.py +2 -2
- krons/{specs → core/specs}/catalog/_content.py +4 -4
- krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
- krons/{specs → core/specs}/factory.py +5 -5
- krons/{specs → core/specs}/operable.py +8 -2
- krons/{specs → core/specs}/protocol.py +4 -2
- krons/{specs → core/specs}/spec.py +23 -11
- krons/{types → core/types}/base.py +4 -2
- krons/{types → core/types}/db_types.py +2 -2
- krons/errors.py +13 -13
- krons/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- krons/{services → resource}/backend.py +48 -22
- krons/{services → resource}/endpoint.py +28 -14
- krons/{services → resource}/hook.py +20 -7
- krons/{services → resource}/imodel.py +46 -28
- krons/{services → resource}/registry.py +26 -24
- krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
- krons/{services → resource}/utilities/rate_limiter.py +3 -1
- krons/{services → resource}/utilities/resilience.py +15 -5
- krons/resource/utilities/token_calculator.py +185 -0
- krons/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- krons/session/exchange.py +11 -3
- krons/session/message.py +3 -1
- krons/session/registry.py +35 -0
- krons/session/session.py +165 -174
- krons/utils/__init__.py +45 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- krons/utils/_to_list.py +9 -3
- krons/utils/_utils.py +6 -2
- krons/utils/concurrency/_async_call.py +4 -2
- krons/utils/concurrency/_errors.py +3 -1
- krons/utils/concurrency/_patterns.py +3 -1
- krons/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- krons/utils/fuzzy/__init__.py +6 -1
- krons/utils/fuzzy/_fuzzy_match.py +14 -8
- krons/utils/fuzzy/_string_similarity.py +3 -1
- krons/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +126 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +305 -0
- krons/{operations → work/operations}/__init__.py +7 -4
- krons/{operations → work/operations}/builder.py +1 -1
- krons/{enforcement → work/operations}/context.py +36 -5
- krons/{operations → work/operations}/flow.py +13 -5
- krons/{operations → work/operations}/node.py +45 -43
- krons/work/operations/registry.py +103 -0
- krons/{specs → work}/phrase.py +130 -13
- krons/{enforcement → work}/policy.py +3 -3
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- krons/{enforcement → work/rules}/common/boolean.py +3 -1
- krons/{enforcement → work/rules}/common/choice.py +9 -3
- krons/{enforcement → work/rules}/common/number.py +3 -1
- krons/{enforcement → work/rules}/common/string.py +9 -3
- krons/{enforcement → work/rules}/rule.py +1 -1
- krons/{enforcement → work/rules}/validator.py +20 -5
- krons/{enforcement → work}/service.py +16 -7
- krons/work/worker.py +266 -0
- {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/METADATA +15 -1
- krons-0.2.0.dist-info/RECORD +154 -0
- krons/enforcement/__init__.py +0 -57
- krons/operations/registry.py +0 -92
- krons/services/__init__.py +0 -81
- krons-0.1.1.dist-info/RECORD +0 -101
- /krons/{specs → core/specs}/adapters/__init__.py +0 -0
- /krons/{specs → core/specs}/adapters/_utils.py +0 -0
- /krons/{specs → core/specs}/adapters/factory.py +0 -0
- /krons/{types → core/types}/__init__.py +0 -0
- /krons/{types → core/types}/_sentinel.py +0 -0
- /krons/{types → core/types}/identity.py +0 -0
- /krons/{services → resource}/utilities/__init__.py +0 -0
- /krons/{services → resource}/utilities/header_factory.py +0 -0
- /krons/{enforcement → work/rules}/common/__init__.py +0 -0
- /krons/{enforcement → work/rules}/common/mapping.py +0 -0
- /krons/{enforcement → work/rules}/common/model.py +0 -0
- /krons/{enforcement → work/rules}/registry.py +0 -0
- {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
- {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
import tiktoken
|
|
9
|
+
|
|
10
|
+
from krons.errors import KronsError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TokenCalculationError(KronsError):
|
|
16
|
+
"""Raised when token calculation fails due to encoding/model errors."""
|
|
17
|
+
|
|
18
|
+
default_message = "Token calculation failed"
|
|
19
|
+
default_retryable = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_encoding_name(value: str | None) -> str:
|
|
23
|
+
"""Get encoding name for model, with fallback chain.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Encoding name (defaults to o200k_base if model/encoding not found or None)
|
|
27
|
+
"""
|
|
28
|
+
if value is None:
|
|
29
|
+
return "o200k_base"
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
enc = tiktoken.encoding_for_model(value)
|
|
33
|
+
return enc.name
|
|
34
|
+
except KeyError:
|
|
35
|
+
# Not a known model name, try as encoding name
|
|
36
|
+
try:
|
|
37
|
+
tiktoken.get_encoding(value)
|
|
38
|
+
return value
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.warning(
|
|
41
|
+
f"Unknown model/encoding '{value}', falling back to o200k_base: {e}"
|
|
42
|
+
)
|
|
43
|
+
return "o200k_base"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TokenCalculator:
|
|
47
|
+
@staticmethod
|
|
48
|
+
def calculate_message_tokens(messages: list[dict], /, **kwargs) -> int:
|
|
49
|
+
model = kwargs.get("model", "gpt-4o")
|
|
50
|
+
image_token_cost = kwargs.get("image_token_cost", 500)
|
|
51
|
+
tokenizer = tiktoken.get_encoding(get_encoding_name(model)).encode
|
|
52
|
+
|
|
53
|
+
num_tokens = 0
|
|
54
|
+
for msg in messages:
|
|
55
|
+
num_tokens += 4
|
|
56
|
+
_c = msg.get("content")
|
|
57
|
+
num_tokens += TokenCalculator._calculate_chatitem(
|
|
58
|
+
_c,
|
|
59
|
+
tokenizer=tokenizer,
|
|
60
|
+
model_name=model,
|
|
61
|
+
image_token_cost=image_token_cost,
|
|
62
|
+
)
|
|
63
|
+
return num_tokens # buffer for chat
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def calculate_embed_token(inputs: list[str], /, **kwargs) -> int:
|
|
67
|
+
if not inputs:
|
|
68
|
+
raise ValueError("inputs must be a non-empty list of strings")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
tokenizer = tiktoken.get_encoding(
|
|
72
|
+
get_encoding_name(kwargs.get("model", "text-embedding-3-small"))
|
|
73
|
+
).encode
|
|
74
|
+
|
|
75
|
+
return sum(
|
|
76
|
+
TokenCalculator._calculate_embed_item(i, tokenizer=tokenizer)
|
|
77
|
+
for i in inputs
|
|
78
|
+
)
|
|
79
|
+
except TokenCalculationError:
|
|
80
|
+
# Re-raise from nested calls
|
|
81
|
+
raise
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Failed to calculate embed tokens: {e}", exc_info=True)
|
|
84
|
+
raise TokenCalculationError(f"Embed token calculation failed: {e}") from e
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def tokenize(
|
|
88
|
+
s_: str | None = None,
|
|
89
|
+
/,
|
|
90
|
+
encoding_name: str | None = None,
|
|
91
|
+
tokenizer: Callable | None = None,
|
|
92
|
+
decoder: Callable | None = None,
|
|
93
|
+
return_tokens: bool = False,
|
|
94
|
+
return_decoded: bool = False,
|
|
95
|
+
) -> int | list[int] | tuple[int, str]:
|
|
96
|
+
if not s_:
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
if not callable(tokenizer):
|
|
100
|
+
encoding_name = get_encoding_name(encoding_name)
|
|
101
|
+
tokenizer = tiktoken.get_encoding(encoding_name).encode
|
|
102
|
+
if not callable(decoder):
|
|
103
|
+
# Use encoding_name if available, otherwise fallback to default
|
|
104
|
+
decoder_encoding = encoding_name if encoding_name else "o200k_base"
|
|
105
|
+
decoder = tiktoken.get_encoding(decoder_encoding).decode
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
if return_tokens:
|
|
109
|
+
if return_decoded:
|
|
110
|
+
a = tokenizer(s_)
|
|
111
|
+
return len(a), decoder(a)
|
|
112
|
+
return tokenizer(s_)
|
|
113
|
+
return len(tokenizer(s_))
|
|
114
|
+
except Exception as e:
|
|
115
|
+
# Actual encoding failure during tokenization - this is an error
|
|
116
|
+
logger.error(
|
|
117
|
+
f"Tokenization failed for input (len={len(s_) if s_ else 0}): {e}",
|
|
118
|
+
exc_info=True,
|
|
119
|
+
)
|
|
120
|
+
raise TokenCalculationError(f"Tokenization failed: {e}") from e
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _calculate_chatitem(
|
|
124
|
+
i_, tokenizer: Callable, model_name: str, image_token_cost: int = 500
|
|
125
|
+
) -> int:
|
|
126
|
+
try:
|
|
127
|
+
if isinstance(i_, str):
|
|
128
|
+
# tokenize returns int when return_tokens=False (default)
|
|
129
|
+
return cast(int, TokenCalculator.tokenize(i_, tokenizer=tokenizer))
|
|
130
|
+
|
|
131
|
+
if isinstance(i_, dict):
|
|
132
|
+
if "text" in i_:
|
|
133
|
+
return TokenCalculator._calculate_chatitem(
|
|
134
|
+
str(i_["text"]), tokenizer, model_name, image_token_cost
|
|
135
|
+
)
|
|
136
|
+
elif "image_url" in i_:
|
|
137
|
+
return image_token_cost
|
|
138
|
+
|
|
139
|
+
if isinstance(i_, list):
|
|
140
|
+
return sum(
|
|
141
|
+
TokenCalculator._calculate_chatitem(
|
|
142
|
+
x, tokenizer, model_name, image_token_cost
|
|
143
|
+
)
|
|
144
|
+
for x in i_
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Unknown type - return 0 is valid (no text content)
|
|
148
|
+
return 0
|
|
149
|
+
except TokenCalculationError:
|
|
150
|
+
# Re-raise tokenization errors from nested calls
|
|
151
|
+
raise
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(
|
|
154
|
+
f"Failed to calculate chat item tokens (type={type(i_).__name__}): {e}",
|
|
155
|
+
exc_info=True,
|
|
156
|
+
)
|
|
157
|
+
raise TokenCalculationError(
|
|
158
|
+
f"Chat item token calculation failed: {e}"
|
|
159
|
+
) from e
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _calculate_embed_item(s_, tokenizer: Callable) -> int:
|
|
163
|
+
try:
|
|
164
|
+
if isinstance(s_, str):
|
|
165
|
+
# tokenize returns int when return_tokens=False (default)
|
|
166
|
+
return cast(int, TokenCalculator.tokenize(s_, tokenizer=tokenizer))
|
|
167
|
+
|
|
168
|
+
if isinstance(s_, list):
|
|
169
|
+
return sum(
|
|
170
|
+
TokenCalculator._calculate_embed_item(x, tokenizer) for x in s_
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Unknown type - return 0 is valid (no text content)
|
|
174
|
+
return 0
|
|
175
|
+
except TokenCalculationError:
|
|
176
|
+
# Re-raise tokenization errors from nested calls
|
|
177
|
+
raise
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(
|
|
180
|
+
f"Failed to calculate embed item tokens (type={type(s_).__name__}): {e}",
|
|
181
|
+
exc_info=True,
|
|
182
|
+
)
|
|
183
|
+
raise TokenCalculationError(
|
|
184
|
+
f"Embed item token calculation failed: {e}"
|
|
185
|
+
) from e
|
krons/session/__init__.py
CHANGED
|
@@ -10,23 +10,19 @@ Core types:
|
|
|
10
10
|
Exchange: Async message router between entity mailboxes.
|
|
11
11
|
|
|
12
12
|
Validators (raise on failure):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
resolve_branch_exists_in_session
|
|
13
|
+
resource_must_exist
|
|
14
|
+
resource_must_be_accessible
|
|
15
|
+
capabilities_must_be_granted
|
|
17
16
|
"""
|
|
18
17
|
|
|
18
|
+
from .constraints import (
|
|
19
|
+
capabilities_must_be_granted,
|
|
20
|
+
resource_must_be_accessible,
|
|
21
|
+
resource_must_exist,
|
|
22
|
+
)
|
|
19
23
|
from .exchange import Exchange
|
|
20
24
|
from .message import Message
|
|
21
|
-
from .session import
|
|
22
|
-
Branch,
|
|
23
|
-
Session,
|
|
24
|
-
SessionConfig,
|
|
25
|
-
capabilities_must_be_subset_of_branch,
|
|
26
|
-
resolve_branch_exists_in_session,
|
|
27
|
-
resource_must_be_accessible_by_branch,
|
|
28
|
-
resource_must_exist_in_session,
|
|
29
|
-
)
|
|
25
|
+
from .session import Branch, Session, SessionConfig
|
|
30
26
|
|
|
31
27
|
__all__ = (
|
|
32
28
|
"Branch",
|
|
@@ -34,8 +30,7 @@ __all__ = (
|
|
|
34
30
|
"Message",
|
|
35
31
|
"Session",
|
|
36
32
|
"SessionConfig",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"resource_must_exist_in_session",
|
|
33
|
+
"capabilities_must_be_granted",
|
|
34
|
+
"resource_must_be_accessible",
|
|
35
|
+
"resource_must_exist",
|
|
41
36
|
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from krons.errors import AccessError, ConfigurationError, ExistsError, NotFoundError
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
"resource_must_exist",
|
|
5
|
+
"resource_must_be_accessible",
|
|
6
|
+
"capabilities_must_be_granted",
|
|
7
|
+
"branch_name_must_be_unique",
|
|
8
|
+
"genai_model_must_be_configured",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resource_must_exist(session, name: str):
|
|
13
|
+
"""Validate resource exists in session. Raise NotFoundError if not."""
|
|
14
|
+
if not session.resources.has(name):
|
|
15
|
+
raise NotFoundError(
|
|
16
|
+
f"Service '{name}' not found in session services",
|
|
17
|
+
details={"available": session.resources.list_names()},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resource_must_be_accessible(branch, name: str) -> None:
|
|
22
|
+
"""Validate branch has resource access. Raise AccessError if not."""
|
|
23
|
+
if name not in branch.resources:
|
|
24
|
+
raise AccessError(
|
|
25
|
+
f"Branch '{branch.name}' has no access to resource '{name}'",
|
|
26
|
+
details={
|
|
27
|
+
"branch": branch.name,
|
|
28
|
+
"resource": name,
|
|
29
|
+
"available": list(branch.resources),
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def capabilities_must_be_granted(branch, capabilities: set[str]) -> None:
|
|
35
|
+
"""Validate branch has all capabilities. Raise AccessError listing missing."""
|
|
36
|
+
if not capabilities.issubset(branch.capabilities):
|
|
37
|
+
missing = capabilities - branch.capabilities
|
|
38
|
+
raise AccessError(
|
|
39
|
+
f"Branch '{branch.name}' missing capabilities: {missing}",
|
|
40
|
+
details={
|
|
41
|
+
"requested": sorted(capabilities),
|
|
42
|
+
"available": sorted(branch.capabilities),
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def branch_name_must_be_unique(session, name: str) -> None:
|
|
48
|
+
try:
|
|
49
|
+
session.communications.get_progression(name)
|
|
50
|
+
# If we get here, the name exists - not unique
|
|
51
|
+
raise ExistsError(f"Branch with name '{name}' already exists")
|
|
52
|
+
except KeyError:
|
|
53
|
+
# KeyError means name not found - it's unique, which is good
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def genai_model_must_be_configured(session) -> None:
|
|
58
|
+
"""Validate session has a default GenAI model configured.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
session: Session to check
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ConfigurationError: If no default model configured
|
|
65
|
+
"""
|
|
66
|
+
if session.default_gen_model is None:
|
|
67
|
+
raise ConfigurationError(
|
|
68
|
+
"Session has no default_gen_model configured",
|
|
69
|
+
details={"session_id": str(session.id)},
|
|
70
|
+
)
|
krons/session/exchange.py
CHANGED
|
@@ -134,11 +134,17 @@ class Exchange(Element):
|
|
|
134
134
|
except Exception:
|
|
135
135
|
message_copy = message.model_copy()
|
|
136
136
|
deliveries.append((other_id, message_copy))
|
|
137
|
-
elif
|
|
137
|
+
elif (
|
|
138
|
+
message.recipient is not None
|
|
139
|
+
and message.recipient in self._owner_index
|
|
140
|
+
):
|
|
138
141
|
deliveries.append((message.recipient, message))
|
|
139
142
|
if deliveries:
|
|
140
143
|
await concurrency.gather(
|
|
141
|
-
*[
|
|
144
|
+
*[
|
|
145
|
+
self._deliver_to(recipient_id, message)
|
|
146
|
+
for recipient_id, message in deliveries
|
|
147
|
+
],
|
|
142
148
|
return_exceptions=True,
|
|
143
149
|
)
|
|
144
150
|
|
|
@@ -196,7 +202,9 @@ class Exchange(Element):
|
|
|
196
202
|
if flow is None:
|
|
197
203
|
raise ValueError(f"Sender {sender} not registered")
|
|
198
204
|
|
|
199
|
-
message = Message(
|
|
205
|
+
message = Message(
|
|
206
|
+
sender=sender, recipient=recipient, content=content, channel=channel
|
|
207
|
+
)
|
|
200
208
|
flow.add_item(message, progressions=OUTBOX)
|
|
201
209
|
return message
|
|
202
210
|
|
krons/session/message.py
CHANGED
|
@@ -27,7 +27,9 @@ class Message(Node):
|
|
|
27
27
|
content: Any
|
|
28
28
|
sender: UUID | None = None
|
|
29
29
|
recipient: UUID | None = None
|
|
30
|
-
channel: str | None = Field(
|
|
30
|
+
channel: str | None = Field(
|
|
31
|
+
None, description="Optional namespace for message grouping"
|
|
32
|
+
)
|
|
31
33
|
|
|
32
34
|
@property
|
|
33
35
|
def is_broadcast(self) -> bool:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from krons.core import Pile
|
|
2
|
+
from krons.core.types import ID
|
|
3
|
+
|
|
4
|
+
from .session import Session
|
|
5
|
+
|
|
6
|
+
SESSION_REGISTRY: Pile[Session] = Pile(item_type=Session, strict_type=True)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def get_session(session_id: ID[Session]) -> Session:
|
|
10
|
+
if session_id not in SESSION_REGISTRY:
|
|
11
|
+
raise ValueError(f"Session with id {session_id} not found in registry.")
|
|
12
|
+
async with SESSION_REGISTRY:
|
|
13
|
+
return SESSION_REGISTRY[session_id]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def create_session():
|
|
17
|
+
session = Session()
|
|
18
|
+
async with SESSION_REGISTRY:
|
|
19
|
+
SESSION_REGISTRY.add(session)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def delete_session(session_id: ID[Session]):
|
|
23
|
+
if session_id not in SESSION_REGISTRY:
|
|
24
|
+
raise ValueError(f"Session with id {session_id} not found in registry.")
|
|
25
|
+
async with SESSION_REGISTRY:
|
|
26
|
+
SESSION_REGISTRY.remove(session_id)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def list_sessions_ids() -> list[ID[Session]]:
|
|
30
|
+
return list(SESSION_REGISTRY.keys())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def clear_sessions():
|
|
34
|
+
async with SESSION_REGISTRY:
|
|
35
|
+
SESSION_REGISTRY.clear()
|