krons 0.1.0__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 +127 -0
- krons/core/base/__init__.py +121 -0
- {kronos/core → krons/core/base}/broadcaster.py +7 -3
- {kronos/core → krons/core/base}/element.py +15 -7
- {kronos/core → krons/core/base}/event.py +41 -8
- {kronos/core → krons/core/base}/eventbus.py +4 -2
- {kronos/core → krons/core/base}/flow.py +14 -7
- {kronos/core → krons/core/base}/graph.py +27 -11
- {kronos/core → krons/core/base}/node.py +47 -22
- {kronos/core → krons/core/base}/pile.py +26 -12
- {kronos/core → krons/core/base}/processor.py +23 -9
- {kronos/core → krons/core/base}/progression.py +5 -3
- {kronos → krons/core}/specs/__init__.py +0 -5
- {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
- {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
- {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
- {kronos → krons/core}/specs/catalog/__init__.py +2 -2
- {kronos → krons/core}/specs/catalog/_audit.py +3 -3
- {kronos → krons/core}/specs/catalog/_common.py +2 -2
- {kronos → krons/core}/specs/catalog/_content.py +5 -5
- {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
- {kronos → krons/core}/specs/factory.py +7 -7
- {kronos → krons/core}/specs/operable.py +9 -3
- {kronos → krons/core}/specs/protocol.py +4 -2
- {kronos → krons/core}/specs/spec.py +25 -13
- {kronos → krons/core}/types/base.py +7 -5
- {kronos → krons/core}/types/db_types.py +2 -2
- {kronos → krons/core}/types/identity.py +1 -1
- {kronos → krons}/errors.py +13 -13
- {kronos → krons}/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- {kronos/services → krons/resource}/backend.py +50 -24
- {kronos/services → krons/resource}/endpoint.py +28 -14
- {kronos/services → krons/resource}/hook.py +22 -9
- {kronos/services → krons/resource}/imodel.py +50 -32
- {kronos/services → krons/resource}/registry.py +27 -25
- {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
- {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
- {kronos/services → krons/resource}/utilities/resilience.py +17 -7
- krons/resource/utilities/token_calculator.py +185 -0
- {kronos → krons}/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- {kronos → krons}/session/exchange.py +14 -6
- {kronos → krons}/session/message.py +4 -2
- krons/session/registry.py +35 -0
- {kronos → krons}/session/session.py +165 -174
- krons/utils/__init__.py +85 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- {kronos → krons}/utils/_to_list.py +9 -3
- {kronos → krons}/utils/_utils.py +9 -5
- {kronos → krons}/utils/concurrency/__init__.py +38 -38
- {kronos → krons}/utils/concurrency/_async_call.py +6 -4
- {kronos → krons}/utils/concurrency/_errors.py +3 -1
- {kronos → krons}/utils/concurrency/_patterns.py +3 -1
- {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- {kronos → krons}/utils/fuzzy/__init__.py +6 -1
- {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
- {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
- {kronos → 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
- {kronos → krons}/utils/sql/_sql_validation.py +1 -1
- 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
- {kronos → krons/work}/operations/__init__.py +7 -4
- {kronos → krons/work}/operations/builder.py +4 -4
- {kronos/enforcement → krons/work/operations}/context.py +37 -6
- {kronos → krons/work}/operations/flow.py +17 -9
- krons/work/operations/node.py +103 -0
- krons/work/operations/registry.py +103 -0
- {kronos/specs → krons/work}/phrase.py +131 -14
- {kronos/enforcement → krons/work}/policy.py +3 -3
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
- {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
- {kronos/enforcement → krons/work/rules}/rule.py +2 -2
- {kronos/enforcement → krons/work/rules}/validator.py +21 -6
- {kronos/enforcement → krons/work}/service.py +16 -7
- krons/work/worker.py +266 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
- krons-0.2.0.dist-info/RECORD +154 -0
- kronos/core/__init__.py +0 -145
- kronos/enforcement/__init__.py +0 -57
- kronos/operations/node.py +0 -101
- kronos/operations/registry.py +0 -92
- kronos/services/__init__.py +0 -81
- kronos/specs/adapters/__init__.py +0 -0
- kronos/utils/__init__.py +0 -40
- krons-0.1.0.dist-info/RECORD +0 -101
- {kronos → krons/core/specs/adapters}/__init__.py +0 -0
- {kronos → krons/core}/specs/adapters/_utils.py +0 -0
- {kronos → krons/core}/specs/adapters/factory.py +0 -0
- {kronos → krons/core}/types/__init__.py +0 -0
- {kronos → krons/core}/types/_sentinel.py +0 -0
- {kronos → krons}/py.typed +0 -0
- {kronos/services → krons/resource}/utilities/__init__.py +0 -0
- {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
- {kronos → krons}/utils/_hash.py +0 -0
- {kronos → krons}/utils/_json_dump.py +0 -0
- {kronos → krons}/utils/_lazy_init.py +0 -0
- {kronos → krons}/utils/_to_num.py +0 -0
- {kronos → krons}/utils/concurrency/_cancel.py +0 -0
- {kronos → krons}/utils/concurrency/_primitives.py +0 -0
- {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
- {kronos → krons}/utils/concurrency/_run_async.py +0 -0
- {kronos → krons}/utils/concurrency/_task.py +0 -0
- {kronos → krons}/utils/concurrency/_utils.py +0 -0
- {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
- {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
- {kronos → krons}/utils/sql/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
- {kronos/enforcement → krons/work/rules}/registry.py +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Act operation: execute tool calls from LLM action requests.
|
|
5
|
+
|
|
6
|
+
Handler signature: act(params, ctx) → list[ActionResponse]
|
|
7
|
+
|
|
8
|
+
Supports sequential and concurrent execution strategies with
|
|
9
|
+
rate-limiting via alcall (delay, throttle, max_concurrent).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from functools import partial
|
|
15
|
+
from typing import TYPE_CHECKING, Literal
|
|
16
|
+
|
|
17
|
+
from krons.agent.message.action import ActionRequest, ActionResponse
|
|
18
|
+
from krons.core.types import Params
|
|
19
|
+
from krons.session.constraints import resource_must_be_accessible, resource_must_exist
|
|
20
|
+
from krons.utils import alcall
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from krons.resource.backend import Calling
|
|
24
|
+
from krons.session import Branch, Message, Session
|
|
25
|
+
from krons.work.operations import RequestContext
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ActParams(Params):
|
|
29
|
+
"""Parameters for tool execution.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
action_requests: Messages with ActionRequest content to execute.
|
|
33
|
+
delay_before_start: Initial delay before first execution (seconds).
|
|
34
|
+
throttle_period: Delay between starting tasks (seconds).
|
|
35
|
+
max_concurrent: Concurrency limit (None = unlimited).
|
|
36
|
+
strategy: "concurrent" (default) or "sequential".
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
action_requests: list[Message]
|
|
40
|
+
delay_before_start: float = 0
|
|
41
|
+
throttle_period: float | None = None
|
|
42
|
+
max_concurrent: int | None = None
|
|
43
|
+
strategy: Literal["sequential", "concurrent"] = "concurrent"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def act(params: ActParams, ctx: RequestContext) -> list[ActionResponse]:
|
|
47
|
+
"""Execute tool calls from action_requests."""
|
|
48
|
+
session = await ctx.get_session()
|
|
49
|
+
return await _act(session=session, branch=ctx.branch, **params.to_dict())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _act(
|
|
53
|
+
action_requests: list[Message],
|
|
54
|
+
session: Session,
|
|
55
|
+
branch: Branch,
|
|
56
|
+
delay_before_start: float = 0,
|
|
57
|
+
throttle_period: float | None = None,
|
|
58
|
+
max_concurrent: int | None = None,
|
|
59
|
+
strategy: Literal["sequential", "concurrent"] = "concurrent",
|
|
60
|
+
) -> list[ActionResponse]:
|
|
61
|
+
"""Execute action requests against session-registered tools.
|
|
62
|
+
|
|
63
|
+
Validates resource existence and branch access before execution.
|
|
64
|
+
Returns ActionResponse per request (with result or error).
|
|
65
|
+
"""
|
|
66
|
+
if not action_requests:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
# Validate all resources exist and are accessible before execution
|
|
70
|
+
for req in action_requests:
|
|
71
|
+
content: ActionRequest = req.content
|
|
72
|
+
resource_must_exist(session, content.function)
|
|
73
|
+
resource_must_be_accessible(branch, content.function)
|
|
74
|
+
|
|
75
|
+
async def _execute_one(req_msg: Message) -> ActionResponse:
|
|
76
|
+
"""Execute a single action request and normalize result."""
|
|
77
|
+
action_request: ActionRequest = req_msg.content
|
|
78
|
+
calling: Calling = await session.request(
|
|
79
|
+
action_request.function, branch=branch, **action_request.arguments
|
|
80
|
+
)
|
|
81
|
+
try:
|
|
82
|
+
calling.assert_is_normalized()
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return ActionResponse(
|
|
85
|
+
request_id=str(req_msg.id), error=f"ExecutionError: {e}"
|
|
86
|
+
)
|
|
87
|
+
return ActionResponse(request_id=str(req_msg.id), result=calling.response.data)
|
|
88
|
+
|
|
89
|
+
if strategy == "sequential":
|
|
90
|
+
results: list[ActionResponse] = []
|
|
91
|
+
for req_msg in action_requests:
|
|
92
|
+
results.append(await _execute_one(req_msg))
|
|
93
|
+
return results
|
|
94
|
+
|
|
95
|
+
return await partial(
|
|
96
|
+
alcall,
|
|
97
|
+
delay_before_start=delay_before_start,
|
|
98
|
+
throttle_period=throttle_period,
|
|
99
|
+
max_concurrent=max_concurrent,
|
|
100
|
+
)(action_requests, _execute_one)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Generate operation: stateless LLM call with message preparation.
|
|
5
|
+
|
|
6
|
+
Handler signature: generate(params, ctx) → Calling | text | raw | Message
|
|
7
|
+
Lowest-level operation — no message persistence, no validation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, JsonValue
|
|
17
|
+
|
|
18
|
+
from krons.agent.message import Instruction, prepare_messages_for_chat
|
|
19
|
+
from krons.core.types import ID, MaybeUnset, ModelConfig, Params, Unset
|
|
20
|
+
from krons.errors import ConfigurationError
|
|
21
|
+
from krons.session import Message, resource_must_be_accessible
|
|
22
|
+
|
|
23
|
+
from ..message.common import CustomRenderer
|
|
24
|
+
from .utils import ReturnAs, handle_return
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from krons.resource import iModel
|
|
28
|
+
from krons.session import Branch, Session
|
|
29
|
+
from krons.work.operations import RequestContext
|
|
30
|
+
|
|
31
|
+
__all__ = ("GenerateParams", "generate", "handle_return")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class GenerateParams(Params):
|
|
36
|
+
"""Parameters for generate operation.
|
|
37
|
+
|
|
38
|
+
Provide either `instruction` (pre-built Message/Instruction) or
|
|
39
|
+
`primary` (string) to auto-build an Instruction via Instruction.create().
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
instruction: Pre-built Instruction or Message (takes priority).
|
|
43
|
+
primary: Instruction text (used when instruction is Unset).
|
|
44
|
+
context: Additional context merged into instruction.
|
|
45
|
+
imodel: Model name or iModel instance (Unset = session default).
|
|
46
|
+
return_as: How to unwrap the Calling result.
|
|
47
|
+
imodel_kwargs: Extra kwargs forwarded to imodel.invoke().
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_config = ModelConfig(
|
|
51
|
+
sentinel_additions=frozenset({"none", "empty", "dataclass", "pydantic"})
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
instruction: MaybeUnset[Instruction | Message] = Unset
|
|
55
|
+
primary: MaybeUnset[str] = Unset
|
|
56
|
+
context: MaybeUnset[JsonValue] = Unset
|
|
57
|
+
imodel: MaybeUnset[iModel | str] = Unset
|
|
58
|
+
images: MaybeUnset[list[str]] = Unset
|
|
59
|
+
image_detail: MaybeUnset[Literal["low", "high", "auto"]] = Unset
|
|
60
|
+
tool_schemas: MaybeUnset[list[str]] = Unset
|
|
61
|
+
request_model: MaybeUnset[type[BaseModel]] = Unset
|
|
62
|
+
structure_format: Literal["json", "custom"] = "json"
|
|
63
|
+
custom_renderer: MaybeUnset[CustomRenderer] = Unset
|
|
64
|
+
return_as: ReturnAs = ReturnAs.CALLING
|
|
65
|
+
imodel_kwargs: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def instruction_message(self) -> Message:
|
|
69
|
+
"""Resolve to a Message, building from primary if needed."""
|
|
70
|
+
if not self.is_sentinel_field("instruction"):
|
|
71
|
+
if isinstance(self.instruction, Message):
|
|
72
|
+
return self.instruction
|
|
73
|
+
if isinstance(self.instruction, Instruction):
|
|
74
|
+
return Message(content=self.instruction)
|
|
75
|
+
|
|
76
|
+
content = Instruction.create(
|
|
77
|
+
primary=self.primary,
|
|
78
|
+
context=self.context,
|
|
79
|
+
images=self.images,
|
|
80
|
+
image_detail=self.image_detail,
|
|
81
|
+
tool_schemas=self.tool_schemas,
|
|
82
|
+
request_model=self.request_model,
|
|
83
|
+
structure_format=self.structure_format,
|
|
84
|
+
custom_renderer=self.custom_renderer,
|
|
85
|
+
)
|
|
86
|
+
return Message(content=content)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def generate(params: GenerateParams, ctx: RequestContext) -> Any:
|
|
90
|
+
"""Generate operation handler: resolve context and delegate to _generate."""
|
|
91
|
+
session = await ctx.get_session()
|
|
92
|
+
imodel = params.imodel if not params.is_sentinel_field("imodel") else None
|
|
93
|
+
|
|
94
|
+
# Propagate verbose from ctx to imodel_kwargs for streaming pretty-print
|
|
95
|
+
imodel_kwargs = dict(params.imodel_kwargs)
|
|
96
|
+
if ctx.metadata.get("_verbose"):
|
|
97
|
+
imodel_kwargs.setdefault("verbose", True)
|
|
98
|
+
|
|
99
|
+
return await _generate(
|
|
100
|
+
session=session,
|
|
101
|
+
branch=ctx.branch,
|
|
102
|
+
instruction=params.instruction_message,
|
|
103
|
+
imodel=imodel,
|
|
104
|
+
return_as=params.return_as,
|
|
105
|
+
**imodel_kwargs,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _generate(
|
|
110
|
+
session: Session,
|
|
111
|
+
branch: Branch | str,
|
|
112
|
+
instruction: Message | Instruction | ID[Message],
|
|
113
|
+
imodel: iModel | str | None = None,
|
|
114
|
+
return_as: ReturnAs = ReturnAs.CALLING,
|
|
115
|
+
**imodel_kwargs: Any,
|
|
116
|
+
) -> Any:
|
|
117
|
+
"""Core generate: resolve model/branch/instruction → invoke → handle_return.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
instruction: Message, Instruction, or message UUID to look up.
|
|
121
|
+
imodel: Model name (resolved from session.resources) or iModel instance.
|
|
122
|
+
return_as: Controls output unwrapping (see ReturnAs).
|
|
123
|
+
**imodel_kwargs: Forwarded to imodel.invoke().
|
|
124
|
+
"""
|
|
125
|
+
if imodel is None:
|
|
126
|
+
imodel = session.default_gen_model
|
|
127
|
+
elif isinstance(imodel, str):
|
|
128
|
+
imodel = session.resources.get(imodel, None)
|
|
129
|
+
# else: already an iModel instance
|
|
130
|
+
if imodel is None:
|
|
131
|
+
raise ConfigurationError(
|
|
132
|
+
"Provided imodel could not be resolved, or no default model is set."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
branch = session.get_branch(branch)
|
|
136
|
+
resource_must_be_accessible(branch, imodel.name)
|
|
137
|
+
|
|
138
|
+
if isinstance(instruction, UUID):
|
|
139
|
+
instruction = session.messages[instruction]
|
|
140
|
+
elif isinstance(instruction, Instruction):
|
|
141
|
+
instruction = Message(content=instruction)
|
|
142
|
+
|
|
143
|
+
prepared_msgs = prepare_messages_for_chat(session.messages, branch, instruction)
|
|
144
|
+
calling = await imodel.invoke(messages=prepared_msgs, **imodel_kwargs)
|
|
145
|
+
return handle_return(calling, return_as)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""LLM-assisted reparse: use a model to reformat malformed text into valid JSON.
|
|
5
|
+
|
|
6
|
+
Called as a fallback when direct parse (regex/fuzzy) fails.
|
|
7
|
+
Constructs an Instruction asking the model to extract structured data,
|
|
8
|
+
then fuzzy-validates the result against target keys.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from krons.agent.message import Instruction
|
|
18
|
+
from krons.core.types import MaybeUnset, Unset, is_sentinel
|
|
19
|
+
from krons.utils.fuzzy import HandleUnmatched, fuzzy_validate_mapping
|
|
20
|
+
|
|
21
|
+
from ..message.common import CustomParser, CustomRenderer
|
|
22
|
+
from .utils import ReturnAs
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from krons.resource import iModel
|
|
26
|
+
from krons.session import Branch, Session
|
|
27
|
+
|
|
28
|
+
__all__ = ("_llm_reparse",)
|
|
29
|
+
|
|
30
|
+
PARSE_PROMPT = (
|
|
31
|
+
"Reformat text into specified model or structure, "
|
|
32
|
+
"using the provided schema format as a guide"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _llm_reparse(
|
|
37
|
+
session: Session,
|
|
38
|
+
branch: Branch,
|
|
39
|
+
text: str,
|
|
40
|
+
imodel: iModel | str,
|
|
41
|
+
tool_schemas: MaybeUnset[list[str]] = Unset,
|
|
42
|
+
request_model: MaybeUnset[type[BaseModel]] = Unset,
|
|
43
|
+
structure_format: MaybeUnset[Literal["json", "custom"]] = Unset,
|
|
44
|
+
custom_renderer: MaybeUnset[CustomRenderer] = Unset,
|
|
45
|
+
custom_parser: CustomParser | None = None,
|
|
46
|
+
fill_mapping: dict[str, Any] | None = None,
|
|
47
|
+
**imodel_kwargs: Any,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
"""Ask LLM to reformat text into structured JSON.
|
|
50
|
+
|
|
51
|
+
Builds an Instruction with the text as context and the target
|
|
52
|
+
schema from request_model, then generates and parses the result.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dict mapping target keys to extracted values.
|
|
56
|
+
"""
|
|
57
|
+
instruction = Instruction.create(
|
|
58
|
+
primary=PARSE_PROMPT,
|
|
59
|
+
context=[{"text_to_format": text}],
|
|
60
|
+
request_model=request_model,
|
|
61
|
+
tool_schemas=tool_schemas,
|
|
62
|
+
structure_format=structure_format,
|
|
63
|
+
custom_renderer=custom_renderer,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
from .generate import _generate
|
|
67
|
+
|
|
68
|
+
res = await _generate(
|
|
69
|
+
session=session,
|
|
70
|
+
branch=branch,
|
|
71
|
+
instruction=instruction,
|
|
72
|
+
imodel=imodel,
|
|
73
|
+
return_as=ReturnAs.TEXT,
|
|
74
|
+
**imodel_kwargs,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if is_sentinel(request_model, {"none", "empty"}):
|
|
78
|
+
raise ValueError("request_model is required for LLM reparse")
|
|
79
|
+
target_keys = list(request_model.model_fields.keys())
|
|
80
|
+
|
|
81
|
+
if custom_parser is not None:
|
|
82
|
+
return custom_parser(res, target_keys)
|
|
83
|
+
|
|
84
|
+
return fuzzy_validate_mapping(
|
|
85
|
+
res,
|
|
86
|
+
target_keys,
|
|
87
|
+
handle_unmatched=HandleUnmatched.FORCE,
|
|
88
|
+
fill_mapping=fill_mapping,
|
|
89
|
+
)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Operate: top-level agent operation chain.
|
|
5
|
+
|
|
6
|
+
Handler signature: operate(params, ctx) -> validated model instance
|
|
7
|
+
|
|
8
|
+
Full pipeline:
|
|
9
|
+
1. Compose request structure from operable (inject action spec if needed)
|
|
10
|
+
2. Structure: generate -> parse -> validate (produces typed model)
|
|
11
|
+
3. Act: extract and execute action_requests, persist messages
|
|
12
|
+
4. Compose response structure, merge action_results, validate
|
|
13
|
+
|
|
14
|
+
Action spec injection:
|
|
15
|
+
If invoke_actions=True AND tool_schemas are present AND the branch
|
|
16
|
+
has "action" in its capabilities, the action_requests spec is injected
|
|
17
|
+
into the operable before composing the request structure. This lets
|
|
18
|
+
the LLM produce structured tool calls alongside regular output fields.
|
|
19
|
+
|
|
20
|
+
Unlike lionagi's operate, krons does NOT allow runtime spec injection
|
|
21
|
+
for arbitrary fields. Only the action spec is injected automatically
|
|
22
|
+
based on explicit capability declarations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
29
|
+
|
|
30
|
+
from krons.core.types import MaybeUnset, ModelConfig, Params, Unset
|
|
31
|
+
from krons.session import Message
|
|
32
|
+
|
|
33
|
+
from .act import ActParams, act
|
|
34
|
+
from .generate import GenerateParams
|
|
35
|
+
from .specs import Action, ActionResult, get_action_result_spec, get_action_spec
|
|
36
|
+
from .structure import StructureParams, structure
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from krons.agent.message.common import CustomParser
|
|
40
|
+
from krons.core.specs import Operable
|
|
41
|
+
from krons.resource import iModel
|
|
42
|
+
from krons.work.operations import RequestContext
|
|
43
|
+
from krons.work.rules.validator import Validator
|
|
44
|
+
|
|
45
|
+
__all__ = ("OperateParams", "operate")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class OperateParams(Params):
|
|
50
|
+
"""Parameters for operate (structure + act pipeline).
|
|
51
|
+
|
|
52
|
+
Flat parameter set — no nested StructureParams. The operate handler
|
|
53
|
+
builds StructureParams internally after runtime composition.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
operable: Spec definition (required). Used to compose structures.
|
|
57
|
+
validator: Rule-based validator (required).
|
|
58
|
+
generate_params: LLM generation config.
|
|
59
|
+
request_model: Base model type for structured output.
|
|
60
|
+
If None, composed entirely from operable specs.
|
|
61
|
+
capabilities: Field subset for runtime composition.
|
|
62
|
+
invoke_actions: Enable action spec injection and execution.
|
|
63
|
+
action_strategy: "concurrent" (default) or "sequential".
|
|
64
|
+
max_concurrent: Concurrency limit for tool execution.
|
|
65
|
+
throttle_period: Delay between starting tool calls (seconds).
|
|
66
|
+
persist: Persist assistant/action messages to branch.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_config = ModelConfig(sentinel_additions=frozenset({"none", "empty"}))
|
|
70
|
+
|
|
71
|
+
# Required
|
|
72
|
+
operable: Operable
|
|
73
|
+
validator: Validator
|
|
74
|
+
generate_params: GenerateParams
|
|
75
|
+
|
|
76
|
+
# Structure composition
|
|
77
|
+
capabilities: set[str] | None = None
|
|
78
|
+
persist: bool = True
|
|
79
|
+
|
|
80
|
+
# Action stage
|
|
81
|
+
invoke_actions: bool = False
|
|
82
|
+
action_strategy: Literal["sequential", "concurrent"] = "concurrent"
|
|
83
|
+
max_concurrent: int | None = None
|
|
84
|
+
throttle_period: float | None = None
|
|
85
|
+
|
|
86
|
+
# Validation
|
|
87
|
+
auto_fix: bool = True
|
|
88
|
+
strict: bool = True
|
|
89
|
+
|
|
90
|
+
# Parse overrides
|
|
91
|
+
parse_imodel: MaybeUnset[iModel | str] = Unset
|
|
92
|
+
parse_imodel_kwargs: dict[str, Any] = field(default_factory=dict)
|
|
93
|
+
custom_parser: CustomParser | None = None
|
|
94
|
+
similarity_threshold: float = 0.85
|
|
95
|
+
max_retries: int = 3
|
|
96
|
+
fill_mapping: dict[str, Any] | None = None
|
|
97
|
+
fill_value: Any = Unset
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def operate(params: OperateParams, ctx: RequestContext) -> Any:
|
|
101
|
+
"""Operate handler: compose -> structure -> act -> merge.
|
|
102
|
+
|
|
103
|
+
Returns a validated model instance. If actions were invoked,
|
|
104
|
+
the response structure includes action_results.
|
|
105
|
+
"""
|
|
106
|
+
session = await ctx.get_session()
|
|
107
|
+
branch = await ctx.get_branch()
|
|
108
|
+
|
|
109
|
+
operable = params.operable
|
|
110
|
+
gen_params = params.generate_params
|
|
111
|
+
|
|
112
|
+
# Determine if action spec should be injected
|
|
113
|
+
has_tools = not gen_params.is_sentinel_field("tool_schemas")
|
|
114
|
+
branch_caps = getattr(branch, "capabilities", set())
|
|
115
|
+
inject_actions = params.invoke_actions and has_tools and "action" in branch_caps
|
|
116
|
+
|
|
117
|
+
# --- Stage 1: Compose request structure ---
|
|
118
|
+
if inject_actions:
|
|
119
|
+
request_operable = operable.extend([get_action_spec()])
|
|
120
|
+
else:
|
|
121
|
+
request_operable = operable
|
|
122
|
+
|
|
123
|
+
request_structure = request_operable.compose_structure()
|
|
124
|
+
|
|
125
|
+
# Update generate params with the composed request model
|
|
126
|
+
use_gen_params = gen_params.with_updates(
|
|
127
|
+
copy_containers="deep", request_model=request_structure
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# --- Stage 2: Structure (generate -> parse -> validate) ---
|
|
131
|
+
structure_params = StructureParams(
|
|
132
|
+
generate_params=use_gen_params,
|
|
133
|
+
validator=params.validator,
|
|
134
|
+
operable=request_operable,
|
|
135
|
+
structure=request_structure,
|
|
136
|
+
persist=params.persist,
|
|
137
|
+
capabilities=params.capabilities,
|
|
138
|
+
auto_fix=params.auto_fix,
|
|
139
|
+
strict=params.strict,
|
|
140
|
+
parse_imodel=params.parse_imodel,
|
|
141
|
+
parse_imodel_kwargs=params.parse_imodel_kwargs,
|
|
142
|
+
custom_parser=params.custom_parser,
|
|
143
|
+
similarity_threshold=params.similarity_threshold,
|
|
144
|
+
max_retries=params.max_retries,
|
|
145
|
+
fill_mapping=params.fill_mapping,
|
|
146
|
+
fill_value=params.fill_value,
|
|
147
|
+
)
|
|
148
|
+
structured = await structure(structure_params, ctx)
|
|
149
|
+
|
|
150
|
+
# --- Stage 3: Extract and execute actions ---
|
|
151
|
+
if not inject_actions:
|
|
152
|
+
return structured
|
|
153
|
+
|
|
154
|
+
act_requests = getattr(structured, "action_requests", None)
|
|
155
|
+
if not act_requests:
|
|
156
|
+
return structured
|
|
157
|
+
|
|
158
|
+
# Convert Action models to ActionRequest messages, persist to branch
|
|
159
|
+
action_messages = _actions_to_messages(act_requests)
|
|
160
|
+
if not action_messages:
|
|
161
|
+
return structured
|
|
162
|
+
|
|
163
|
+
for msg in action_messages:
|
|
164
|
+
session.add_message(msg, branches=branch)
|
|
165
|
+
|
|
166
|
+
act_params = ActParams(
|
|
167
|
+
action_requests=action_messages,
|
|
168
|
+
strategy=params.action_strategy,
|
|
169
|
+
max_concurrent=params.max_concurrent,
|
|
170
|
+
throttle_period=params.throttle_period,
|
|
171
|
+
)
|
|
172
|
+
action_responses = await act(act_params, ctx)
|
|
173
|
+
|
|
174
|
+
# Persist action response messages to branch
|
|
175
|
+
for resp in action_responses:
|
|
176
|
+
resp_msg = Message(content=resp)
|
|
177
|
+
session.add_message(resp_msg, branches=branch)
|
|
178
|
+
|
|
179
|
+
# Build ActionResult models from ActionResponse messages
|
|
180
|
+
action_results = _responses_to_results(action_responses, action_messages)
|
|
181
|
+
|
|
182
|
+
# --- Stage 4: Compose response structure, merge, return ---
|
|
183
|
+
# No re-validation needed: structured output was validated in stage 2,
|
|
184
|
+
# action_results are execution artifacts (not LLM output).
|
|
185
|
+
response_operable = request_operable.extend([get_action_result_spec()])
|
|
186
|
+
response_structure = response_operable.compose_structure()
|
|
187
|
+
|
|
188
|
+
data = request_operable.dump_instance(structured)
|
|
189
|
+
data["action_results"] = action_results
|
|
190
|
+
|
|
191
|
+
return response_structure(**data)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _actions_to_messages(act_requests: list) -> list[Message]:
|
|
195
|
+
"""Convert Action models from structured output to ActionRequest Messages."""
|
|
196
|
+
from krons.agent.message.action import ActionRequest
|
|
197
|
+
|
|
198
|
+
messages: list[Message] = []
|
|
199
|
+
for req in act_requests:
|
|
200
|
+
if isinstance(req, Action):
|
|
201
|
+
content = ActionRequest.create(
|
|
202
|
+
function=req.function, arguments=req.arguments
|
|
203
|
+
)
|
|
204
|
+
messages.append(Message(content=content))
|
|
205
|
+
elif isinstance(req, dict):
|
|
206
|
+
content = ActionRequest.create(
|
|
207
|
+
function=req.get("function", ""),
|
|
208
|
+
arguments=req.get("arguments", {}),
|
|
209
|
+
)
|
|
210
|
+
messages.append(Message(content=content))
|
|
211
|
+
return messages
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _responses_to_results(
|
|
215
|
+
action_responses: list,
|
|
216
|
+
action_messages: list[Message],
|
|
217
|
+
) -> list[ActionResult]:
|
|
218
|
+
"""Convert ActionResponse messages to ActionResult spec models.
|
|
219
|
+
|
|
220
|
+
Maps request_id back to function name via action_messages.
|
|
221
|
+
"""
|
|
222
|
+
from krons.agent.message.action import ActionResponse
|
|
223
|
+
|
|
224
|
+
# Build request_id → function lookup from action messages
|
|
225
|
+
id_to_func: dict[str, str] = {}
|
|
226
|
+
for msg in action_messages:
|
|
227
|
+
content = msg.content
|
|
228
|
+
if hasattr(content, "function"):
|
|
229
|
+
id_to_func[str(msg.id)] = content.function
|
|
230
|
+
|
|
231
|
+
results: list[ActionResult] = []
|
|
232
|
+
for resp in action_responses:
|
|
233
|
+
if isinstance(resp, ActionResponse):
|
|
234
|
+
func = id_to_func.get(
|
|
235
|
+
resp.request_id if not resp._is_sentinel(resp.request_id) else "",
|
|
236
|
+
"",
|
|
237
|
+
)
|
|
238
|
+
results.append(
|
|
239
|
+
ActionResult(
|
|
240
|
+
function=func,
|
|
241
|
+
result=resp.result if resp.success else None,
|
|
242
|
+
error=resp.error if not resp.success else None,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
elif isinstance(resp, dict):
|
|
246
|
+
results.append(ActionResult.model_validate(resp))
|
|
247
|
+
return results
|