krons 0.1.1__py3-none-any.whl → 0.2.1__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 +115 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +242 -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/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/work/worker.py +266 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/METADATA +15 -1
- krons-0.2.1.dist-info/RECORD +151 -0
- krons/enforcement/__init__.py +0 -57
- krons/enforcement/policy.py +0 -80
- krons/enforcement/service.py +0 -370
- krons/operations/registry.py +0 -92
- krons/services/__init__.py +0 -81
- krons/specs/phrase.py +0 -405
- 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.1.dist-info}/WHEEL +0 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/licenses/LICENSE +0 -0
krons/session/session.py
CHANGED
|
@@ -10,50 +10,27 @@ Branch is a named message progression with capability/resource access control.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import contextlib
|
|
13
|
-
from collections.abc import Iterable
|
|
14
|
-
from typing import
|
|
13
|
+
from collections.abc import AsyncGenerator, Iterable
|
|
14
|
+
from typing import Any, Literal
|
|
15
15
|
from uuid import UUID
|
|
16
16
|
|
|
17
|
-
from pydantic import Field
|
|
17
|
+
from pydantic import Field, model_validator
|
|
18
18
|
|
|
19
|
-
from krons.core import Element, Flow, Progression
|
|
20
|
-
from krons.
|
|
21
|
-
from krons.
|
|
22
|
-
from krons.
|
|
23
|
-
from krons.
|
|
24
|
-
from krons.types import HashableModel, Unset, UnsetType, not_sentinel
|
|
19
|
+
from krons.core import Element, Flow, Pile, Progression
|
|
20
|
+
from krons.core.types import HashableModel, Unset, UnsetType, not_sentinel
|
|
21
|
+
from krons.errors import NotFoundError
|
|
22
|
+
from krons.resource import Calling, ResourceRegistry, iModel
|
|
23
|
+
from krons.work.operations import Operation, OperationRegistry, RequestContext
|
|
25
24
|
|
|
26
25
|
from .message import Message
|
|
27
26
|
|
|
28
|
-
if TYPE_CHECKING:
|
|
29
|
-
from krons.services.backend import Calling
|
|
30
|
-
|
|
31
27
|
__all__ = (
|
|
32
28
|
"Branch",
|
|
33
29
|
"Session",
|
|
34
30
|
"SessionConfig",
|
|
35
|
-
"capabilities_must_be_subset_of_branch",
|
|
36
|
-
"resource_must_be_accessible_by_branch",
|
|
37
|
-
"resource_must_exist_in_session",
|
|
38
31
|
)
|
|
39
32
|
|
|
40
33
|
|
|
41
|
-
class SessionConfig(HashableModel):
|
|
42
|
-
"""Session initialization configuration.
|
|
43
|
-
|
|
44
|
-
Attributes:
|
|
45
|
-
default_branch_name: Name for auto-created default branch.
|
|
46
|
-
default_capabilities: Capabilities granted to default branch.
|
|
47
|
-
default_resources: Resources accessible by default branch.
|
|
48
|
-
auto_create_default_branch: Create "main" branch on init.
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
default_branch_name: str | None = None
|
|
52
|
-
default_capabilities: set[str] = Field(default_factory=set)
|
|
53
|
-
default_resources: set[str] = Field(default_factory=set)
|
|
54
|
-
auto_create_default_branch: bool = True
|
|
55
|
-
|
|
56
|
-
|
|
57
34
|
class Branch(Progression):
|
|
58
35
|
"""Message progression with capability and resource access control.
|
|
59
36
|
|
|
@@ -67,95 +44,85 @@ class Branch(Progression):
|
|
|
67
44
|
"""
|
|
68
45
|
|
|
69
46
|
session_id: UUID = Field(..., frozen=True)
|
|
70
|
-
capabilities: set[str] = Field(default_factory=set)
|
|
71
|
-
resources: set[str] = Field(default_factory=set)
|
|
47
|
+
capabilities: set[str] = Field(default_factory=set, frozen=True)
|
|
48
|
+
resources: set[str] = Field(default_factory=set, frozen=True)
|
|
72
49
|
|
|
73
50
|
def __repr__(self) -> str:
|
|
74
51
|
name_str = f" name='{self.name}'" if self.name else ""
|
|
75
52
|
return f"Branch(messages={len(self)}, session={str(self.session_id)[:8]}{name_str})"
|
|
76
53
|
|
|
77
54
|
|
|
78
|
-
class
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
4. Add messages, conduct operations, make service requests
|
|
55
|
+
class SessionConfig(HashableModel):
|
|
56
|
+
default_branch_name: str | None = None
|
|
57
|
+
shared_capabilities: set[str] = Field(default_factory=set)
|
|
58
|
+
shared_resources: set[str] = Field(default_factory=set)
|
|
59
|
+
default_gen_model: str | None = None
|
|
60
|
+
default_parse_model: str | None = None
|
|
61
|
+
auto_create_default_branch: bool = True
|
|
86
62
|
|
|
87
|
-
Attributes:
|
|
88
|
-
user: Optional user identifier.
|
|
89
|
-
communications: Flow containing messages and branches.
|
|
90
|
-
services: Service registry for backend access.
|
|
91
|
-
operations: Operation factory registry.
|
|
92
|
-
config: Session configuration.
|
|
93
|
-
default_branch_id: Branch used when none specified.
|
|
94
|
-
|
|
95
|
-
Example:
|
|
96
|
-
session = Session(user="agent-1")
|
|
97
|
-
session.services.register("openai", openai_service)
|
|
98
|
-
session.operations.register("chat", chat_factory)
|
|
99
|
-
result = await session.conduct("chat", params={"prompt": "hello"})
|
|
100
|
-
"""
|
|
101
63
|
|
|
64
|
+
class Session(Element):
|
|
102
65
|
user: str | None = None
|
|
103
|
-
communications: Flow[Message, Branch] =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
66
|
+
communications: Flow[Message, Branch] = Field(
|
|
67
|
+
default_factory=lambda: Flow(item_type=Message)
|
|
68
|
+
)
|
|
69
|
+
resources: ResourceRegistry = Field(default_factory=ResourceRegistry)
|
|
70
|
+
operations: OperationRegistry = Field(default_factory=OperationRegistry)
|
|
71
|
+
config: SessionConfig = Field(default_factory=SessionConfig)
|
|
107
72
|
default_branch_id: UUID | None = None
|
|
108
73
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
74
|
+
@model_validator(mode="after")
|
|
75
|
+
def _validate_default_branch(self) -> Session:
|
|
76
|
+
"""Auto-create default branch and register built-in operations."""
|
|
77
|
+
if self.config.auto_create_default_branch and self.default_branch is None:
|
|
78
|
+
default_branch_name = self.config.default_branch_name or "main"
|
|
79
|
+
self.create_branch(
|
|
80
|
+
name=default_branch_name,
|
|
81
|
+
capabilities=self.config.shared_capabilities,
|
|
82
|
+
resources=self.config.shared_resources,
|
|
83
|
+
)
|
|
84
|
+
self.set_default_branch(default_branch_name)
|
|
85
|
+
|
|
86
|
+
# Register built-in operations (lazy import avoids circular)
|
|
87
|
+
from krons.agent.operations import (
|
|
88
|
+
generate,
|
|
89
|
+
operate,
|
|
90
|
+
react,
|
|
91
|
+
react_stream,
|
|
92
|
+
structure,
|
|
93
|
+
)
|
|
120
94
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
super().__init__(**data)
|
|
131
|
-
self.user = user
|
|
132
|
-
self.communications = communications or Flow(item_type=Message)
|
|
133
|
-
self.services = services or ServiceRegistry()
|
|
134
|
-
self.operations = operations or OperationRegistry()
|
|
135
|
-
self.default_branch_id = default_branch_id
|
|
136
|
-
|
|
137
|
-
if config is None:
|
|
138
|
-
self.config = SessionConfig()
|
|
139
|
-
elif isinstance(config, dict):
|
|
140
|
-
self.config = SessionConfig(**config)
|
|
141
|
-
else:
|
|
142
|
-
self.config = config
|
|
95
|
+
for name, handler in (
|
|
96
|
+
("generate", generate),
|
|
97
|
+
("structure", structure),
|
|
98
|
+
("operate", operate),
|
|
99
|
+
("react", react),
|
|
100
|
+
("react_stream", react_stream),
|
|
101
|
+
):
|
|
102
|
+
if not self.operations.has(name):
|
|
103
|
+
self.operations.register(name, handler)
|
|
143
104
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def default_gen_model(self) -> iModel | None:
|
|
109
|
+
if self.config.default_gen_model is None:
|
|
110
|
+
return None
|
|
111
|
+
return self.resources.get(self.config.default_gen_model)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def default_parse_model(self) -> iModel | None:
|
|
115
|
+
if self.config.default_parse_model is None:
|
|
116
|
+
return None
|
|
117
|
+
return self.resources.get(self.config.default_parse_model)
|
|
151
118
|
|
|
152
119
|
@property
|
|
153
|
-
def messages(self):
|
|
120
|
+
def messages(self) -> Pile[Message]:
|
|
154
121
|
"""All messages in session (Pile[Message])."""
|
|
155
122
|
return self.communications.items
|
|
156
123
|
|
|
157
124
|
@property
|
|
158
|
-
def branches(self):
|
|
125
|
+
def branches(self) -> Pile[Branch]:
|
|
159
126
|
"""All branches in session (Pile[Branch])."""
|
|
160
127
|
return self.communications.progressions
|
|
161
128
|
|
|
@@ -187,10 +154,14 @@ class Session(Element):
|
|
|
187
154
|
Returns:
|
|
188
155
|
Created Branch added to session.
|
|
189
156
|
"""
|
|
157
|
+
if name:
|
|
158
|
+
from .constraints import branch_name_must_be_unique
|
|
159
|
+
|
|
160
|
+
branch_name_must_be_unique(self, name)
|
|
161
|
+
|
|
190
162
|
order: list[UUID] = []
|
|
191
163
|
if messages:
|
|
192
|
-
for msg in messages
|
|
193
|
-
order.append(msg.id if isinstance(msg, Message) else msg)
|
|
164
|
+
order.extend([self._coerce_id(msg) for msg in messages])
|
|
194
165
|
|
|
195
166
|
branch = Branch(
|
|
196
167
|
session_id=self.id,
|
|
@@ -265,9 +236,13 @@ class Session(Element):
|
|
|
265
236
|
name=name or f"{source.name}_fork",
|
|
266
237
|
messages=source.order,
|
|
267
238
|
capabilities=(
|
|
268
|
-
{*source.capabilities}
|
|
239
|
+
{*source.capabilities}
|
|
240
|
+
if capabilities is True
|
|
241
|
+
else (capabilities or set())
|
|
242
|
+
),
|
|
243
|
+
resources=(
|
|
244
|
+
{*source.resources} if resources is True else (resources or set())
|
|
269
245
|
),
|
|
270
|
-
resources=({*source.resources} if resources is True else (resources or set())),
|
|
271
246
|
)
|
|
272
247
|
|
|
273
248
|
forked.metadata["forked_from"] = {
|
|
@@ -288,38 +263,25 @@ class Session(Element):
|
|
|
288
263
|
|
|
289
264
|
async def request(
|
|
290
265
|
self,
|
|
291
|
-
|
|
292
|
-
|
|
266
|
+
name: str,
|
|
267
|
+
/,
|
|
293
268
|
branch: Branch | UUID | str | None = None,
|
|
294
269
|
poll_timeout: float | None = None,
|
|
295
270
|
poll_interval: float | None = None,
|
|
296
|
-
**
|
|
271
|
+
**options,
|
|
297
272
|
) -> Calling:
|
|
298
|
-
"""Direct service invocation with optional access control.
|
|
299
|
-
|
|
300
|
-
Args:
|
|
301
|
-
service_name: Registered service name.
|
|
302
|
-
branch: If provided, checks service in branch.resources.
|
|
303
|
-
poll_timeout: Max wait seconds.
|
|
304
|
-
poll_interval: Poll interval seconds.
|
|
305
|
-
**kwargs: Service-specific arguments.
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
Calling with execution results.
|
|
309
|
-
|
|
310
|
-
Raises:
|
|
311
|
-
AccessError: If branch lacks access to service.
|
|
312
|
-
NotFoundError: If service not registered.
|
|
313
|
-
"""
|
|
314
273
|
if branch is not None:
|
|
315
274
|
resolved_branch = self.get_branch(branch)
|
|
316
|
-
resource_must_be_accessible_by_branch(resolved_branch, service_name)
|
|
317
275
|
|
|
318
|
-
|
|
319
|
-
|
|
276
|
+
from .constraints import resource_must_be_accessible
|
|
277
|
+
|
|
278
|
+
resource_must_be_accessible(resolved_branch, name)
|
|
279
|
+
|
|
280
|
+
resource = self.resources.get(name)
|
|
281
|
+
return await resource.invoke(
|
|
320
282
|
poll_timeout=poll_timeout,
|
|
321
283
|
poll_interval=poll_interval,
|
|
322
|
-
**
|
|
284
|
+
**options,
|
|
323
285
|
)
|
|
324
286
|
|
|
325
287
|
async def conduct(
|
|
@@ -327,6 +289,7 @@ class Session(Element):
|
|
|
327
289
|
operation_type: str,
|
|
328
290
|
branch: Branch | UUID | str | None = None,
|
|
329
291
|
params: Any | None = None,
|
|
292
|
+
verbose: bool = False,
|
|
330
293
|
) -> Operation:
|
|
331
294
|
"""Execute operation via registry.
|
|
332
295
|
|
|
@@ -334,6 +297,7 @@ class Session(Element):
|
|
|
334
297
|
operation_type: Registry key.
|
|
335
298
|
branch: Target branch (default if None).
|
|
336
299
|
params: Operation parameters.
|
|
300
|
+
verbose: Print real-time status updates.
|
|
337
301
|
|
|
338
302
|
Returns:
|
|
339
303
|
Invoked Operation (result in op.execution.response).
|
|
@@ -343,16 +307,85 @@ class Session(Element):
|
|
|
343
307
|
KeyError: Operation not registered.
|
|
344
308
|
"""
|
|
345
309
|
resolved = self._resolve_branch(branch)
|
|
310
|
+
|
|
311
|
+
if verbose:
|
|
312
|
+
from krons.utils.display import Timer, status
|
|
313
|
+
|
|
314
|
+
branch_name = resolved.name or str(resolved.id)[:8]
|
|
315
|
+
status(
|
|
316
|
+
f"conduct({operation_type}) on branch={branch_name}",
|
|
317
|
+
style="info",
|
|
318
|
+
)
|
|
319
|
+
|
|
346
320
|
op = Operation(
|
|
347
321
|
operation_type=operation_type,
|
|
348
322
|
parameters=params,
|
|
349
|
-
timeout=None,
|
|
350
|
-
streaming=False,
|
|
351
323
|
)
|
|
324
|
+
op._verbose = verbose
|
|
352
325
|
op.bind(self, resolved)
|
|
353
|
-
|
|
326
|
+
|
|
327
|
+
if verbose:
|
|
328
|
+
with Timer(f"{operation_type} completed"):
|
|
329
|
+
await op.invoke()
|
|
330
|
+
|
|
331
|
+
resp = op.execution.response
|
|
332
|
+
if op.execution.error:
|
|
333
|
+
status(f"ERROR: {op.execution.error}", style="error")
|
|
334
|
+
elif isinstance(resp, str):
|
|
335
|
+
status(f"response: {len(resp)} chars", style="success")
|
|
336
|
+
else:
|
|
337
|
+
status(f"response: {type(resp).__name__}", style="success")
|
|
338
|
+
else:
|
|
339
|
+
await op.invoke()
|
|
340
|
+
|
|
354
341
|
return op
|
|
355
342
|
|
|
343
|
+
async def stream_conduct(
|
|
344
|
+
self,
|
|
345
|
+
operation_type: str,
|
|
346
|
+
branch: Branch | UUID | str | None = None,
|
|
347
|
+
params: Any | None = None,
|
|
348
|
+
verbose: bool = False,
|
|
349
|
+
) -> AsyncGenerator[Any, None]:
|
|
350
|
+
"""Stream operation results via async generator.
|
|
351
|
+
|
|
352
|
+
For streaming handlers like react_stream that yield intermediate
|
|
353
|
+
results per round. Bypasses the Operation wrapper since streaming
|
|
354
|
+
handlers produce multiple values, not a single response.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
operation_type: Registry key (e.g. "react_stream").
|
|
358
|
+
branch: Target branch (default if None).
|
|
359
|
+
params: Operation parameters.
|
|
360
|
+
verbose: Enable real-time streaming output.
|
|
361
|
+
|
|
362
|
+
Yields:
|
|
363
|
+
Handler results (e.g., ReActAnalysis per round).
|
|
364
|
+
"""
|
|
365
|
+
resolved = self._resolve_branch(branch)
|
|
366
|
+
handler = self.operations.get(operation_type)
|
|
367
|
+
|
|
368
|
+
ctx = RequestContext(
|
|
369
|
+
name=operation_type,
|
|
370
|
+
session_id=self.id,
|
|
371
|
+
branch=resolved.name or str(resolved.id),
|
|
372
|
+
_bound_session=self,
|
|
373
|
+
_bound_branch=resolved,
|
|
374
|
+
_verbose=verbose,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
if verbose:
|
|
378
|
+
from krons.utils.display import status
|
|
379
|
+
|
|
380
|
+
branch_name = resolved.name or str(resolved.id)[:8]
|
|
381
|
+
status(
|
|
382
|
+
f"stream_conduct({operation_type}) on branch={branch_name}",
|
|
383
|
+
style="info",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
async for result in handler(params, ctx):
|
|
387
|
+
yield result
|
|
388
|
+
|
|
356
389
|
def _resolve_branch(self, branch: Branch | UUID | str | None) -> Branch:
|
|
357
390
|
"""Resolve to Branch, falling back to default. Raises if neither available."""
|
|
358
391
|
if branch is not None:
|
|
@@ -365,47 +398,5 @@ class Session(Element):
|
|
|
365
398
|
return (
|
|
366
399
|
f"Session(messages={len(self.messages)}, "
|
|
367
400
|
f"branches={len(self.branches)}, "
|
|
368
|
-
f"services={len(self.
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
def resource_must_exist_in_session(session: Session, name: str) -> None:
|
|
373
|
-
"""Validate service exists. Raise NotFoundError with available names if not."""
|
|
374
|
-
if not session.services.has(name):
|
|
375
|
-
raise NotFoundError(
|
|
376
|
-
f"Service '{name}' not found in session services",
|
|
377
|
-
details={"available": session.services.list_names()},
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def resource_must_be_accessible_by_branch(branch: Branch, name: str) -> None:
|
|
382
|
-
"""Validate branch has resource access. Raise AccessError if not."""
|
|
383
|
-
if name not in branch.resources:
|
|
384
|
-
raise AccessError(
|
|
385
|
-
f"Branch '{branch.name}' has no access to resource '{name}'",
|
|
386
|
-
details={
|
|
387
|
-
"branch": branch.name,
|
|
388
|
-
"resource": name,
|
|
389
|
-
"available": list(branch.resources),
|
|
390
|
-
},
|
|
401
|
+
f"services={len(self.resources)})"
|
|
391
402
|
)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def capabilities_must_be_subset_of_branch(branch: Branch, capabilities: set[str]) -> None:
|
|
395
|
-
"""Validate branch has all capabilities. Raise AccessError listing missing."""
|
|
396
|
-
if not capabilities.issubset(branch.capabilities):
|
|
397
|
-
missing = capabilities - branch.capabilities
|
|
398
|
-
raise AccessError(
|
|
399
|
-
f"Branch '{branch.name}' missing capabilities: {missing}",
|
|
400
|
-
details={
|
|
401
|
-
"requested": sorted(capabilities),
|
|
402
|
-
"available": sorted(branch.capabilities),
|
|
403
|
-
},
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def resolve_branch_exists_in_session(session: Session, branch: Branch | str) -> Branch:
|
|
408
|
-
"""Get branch from session. Raise NotFoundError if not found."""
|
|
409
|
-
if (b_ := session.get_branch(branch, None)) is None:
|
|
410
|
-
raise NotFoundError(f"Branch '{branch}' does not exist in session")
|
|
411
|
-
return b_
|
krons/utils/__init__.py
CHANGED
|
@@ -38,3 +38,48 @@ from .sql._sql_validation import (
|
|
|
38
38
|
sanitize_order_by,
|
|
39
39
|
validate_identifier,
|
|
40
40
|
)
|
|
41
|
+
|
|
42
|
+
__all__ = (
|
|
43
|
+
# _hash
|
|
44
|
+
"GENESIS_HASH",
|
|
45
|
+
"MAX_HASH_INPUT_BYTES",
|
|
46
|
+
"HashAlgorithm",
|
|
47
|
+
"compute_chain_hash",
|
|
48
|
+
"compute_hash",
|
|
49
|
+
"hash_obj",
|
|
50
|
+
# _json_dump
|
|
51
|
+
"json_dump",
|
|
52
|
+
"json_dumpb",
|
|
53
|
+
"json_lines_iter",
|
|
54
|
+
# _to_list, _to_num
|
|
55
|
+
"to_list",
|
|
56
|
+
"to_num",
|
|
57
|
+
# _utils
|
|
58
|
+
"async_synchronized",
|
|
59
|
+
"coerce_created_at",
|
|
60
|
+
"create_path",
|
|
61
|
+
"extract_types",
|
|
62
|
+
"get_bins",
|
|
63
|
+
"import_module",
|
|
64
|
+
"is_import_installed",
|
|
65
|
+
"load_type_from_string",
|
|
66
|
+
"now_utc",
|
|
67
|
+
"register_type_prefix",
|
|
68
|
+
"synchronized",
|
|
69
|
+
"to_uuid",
|
|
70
|
+
# concurrency
|
|
71
|
+
"alcall",
|
|
72
|
+
"is_coro_func",
|
|
73
|
+
# fuzzy
|
|
74
|
+
"SimilarityAlgo",
|
|
75
|
+
"extract_json",
|
|
76
|
+
"fuzzy_json",
|
|
77
|
+
"fuzzy_match_keys",
|
|
78
|
+
"string_similarity",
|
|
79
|
+
"to_dict",
|
|
80
|
+
# sql
|
|
81
|
+
"MAX_IDENTIFIER_LENGTH",
|
|
82
|
+
"SAFE_IDENTIFIER_PATTERN",
|
|
83
|
+
"sanitize_order_by",
|
|
84
|
+
"validate_identifier",
|
|
85
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from typing import Any, Union, get_args, get_origin
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def map_positional_args(
|
|
8
|
+
arguments: dict[str, Any], param_names: list[str]
|
|
9
|
+
) -> dict[str, Any]:
|
|
10
|
+
"""Map positional arguments (_pos_0, _pos_1, ...) to actual parameter names."""
|
|
11
|
+
mapped = {}
|
|
12
|
+
pos_count = 0
|
|
13
|
+
|
|
14
|
+
for key, value in arguments.items():
|
|
15
|
+
if key.startswith("_pos_"):
|
|
16
|
+
if pos_count >= len(param_names):
|
|
17
|
+
raise ValueError(
|
|
18
|
+
f"Too many positional arguments (expected {len(param_names)})"
|
|
19
|
+
)
|
|
20
|
+
mapped[param_names[pos_count]] = value
|
|
21
|
+
pos_count += 1
|
|
22
|
+
else:
|
|
23
|
+
# Keep keyword arguments as-is
|
|
24
|
+
mapped[key] = value
|
|
25
|
+
|
|
26
|
+
return mapped
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_nested_fields_from_annotation(annotation) -> set[str]:
|
|
30
|
+
"""Extract all field names from an annotation that may be a Pydantic model or Union."""
|
|
31
|
+
origin = get_origin(annotation)
|
|
32
|
+
|
|
33
|
+
# Handle Union types (typing.Union or types.UnionType)
|
|
34
|
+
if origin is Union or isinstance(annotation, types.UnionType):
|
|
35
|
+
union_members = get_args(annotation)
|
|
36
|
+
all_fields = set()
|
|
37
|
+
for member in union_members:
|
|
38
|
+
if member is type(None):
|
|
39
|
+
continue
|
|
40
|
+
# Recursively check nested unions or models
|
|
41
|
+
nested = _get_nested_fields_from_annotation(member)
|
|
42
|
+
all_fields.update(nested)
|
|
43
|
+
return all_fields
|
|
44
|
+
|
|
45
|
+
# Handle direct Pydantic model
|
|
46
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
47
|
+
return set(annotation.model_fields.keys())
|
|
48
|
+
|
|
49
|
+
# Handle Pydantic model class (not instance)
|
|
50
|
+
if hasattr(annotation, "model_fields"):
|
|
51
|
+
return set(annotation.model_fields.keys())
|
|
52
|
+
|
|
53
|
+
return set()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def nest_arguments_by_schema(arguments: dict[str, Any], schema_cls) -> dict[str, Any]:
|
|
57
|
+
"""Restructure flat arguments into nested format based on schema structure."""
|
|
58
|
+
if not schema_cls or not hasattr(schema_cls, "model_fields"):
|
|
59
|
+
return arguments
|
|
60
|
+
|
|
61
|
+
# Get top-level field names
|
|
62
|
+
top_level_fields = set(schema_cls.model_fields.keys())
|
|
63
|
+
|
|
64
|
+
# Find fields that are nested objects (Pydantic models or unions)
|
|
65
|
+
nested_field_mappings = {}
|
|
66
|
+
for field_name, field_info in schema_cls.model_fields.items():
|
|
67
|
+
annotation = field_info.annotation
|
|
68
|
+
nested_fields = _get_nested_fields_from_annotation(annotation)
|
|
69
|
+
if nested_fields:
|
|
70
|
+
nested_field_mappings[field_name] = nested_fields
|
|
71
|
+
|
|
72
|
+
# If no nested fields detected, return as-is
|
|
73
|
+
if not nested_field_mappings:
|
|
74
|
+
return arguments
|
|
75
|
+
|
|
76
|
+
# Separate top-level args from nested args
|
|
77
|
+
result: dict[str, Any] = {}
|
|
78
|
+
nested_args: dict[str, dict[str, Any]] = {}
|
|
79
|
+
|
|
80
|
+
for key, value in arguments.items():
|
|
81
|
+
if key in top_level_fields:
|
|
82
|
+
# This is a top-level field
|
|
83
|
+
result[key] = value
|
|
84
|
+
else:
|
|
85
|
+
# Check if this belongs to a nested field
|
|
86
|
+
for nested_field, nested_keys in nested_field_mappings.items():
|
|
87
|
+
if key in nested_keys:
|
|
88
|
+
if nested_field not in nested_args:
|
|
89
|
+
nested_args[nested_field] = {}
|
|
90
|
+
nested_args[nested_field][key] = value
|
|
91
|
+
break
|
|
92
|
+
else:
|
|
93
|
+
# Unknown field - keep at top level (will fail validation later)
|
|
94
|
+
result[key] = value
|
|
95
|
+
|
|
96
|
+
# Add nested structures to result
|
|
97
|
+
result.update(nested_args)
|
|
98
|
+
|
|
99
|
+
return result
|