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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Specs for agent operations.
|
|
5
|
+
|
|
6
|
+
Action: validated tool-call request model (function + arguments).
|
|
7
|
+
ActionResult: validated tool-call result model (function, result, error).
|
|
8
|
+
get_action_spec / get_action_result_spec: Spec factories for
|
|
9
|
+
runtime operable composition in operate.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from functools import cache
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field, JsonValue, field_validator
|
|
19
|
+
|
|
20
|
+
from krons.core.types import HashableModel
|
|
21
|
+
from krons.utils import extract_json, to_dict, to_list
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
"Action",
|
|
25
|
+
"ActionResult",
|
|
26
|
+
"Instruct",
|
|
27
|
+
"get_action_spec",
|
|
28
|
+
"get_action_result_spec",
|
|
29
|
+
"get_instruct_spec",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Action(HashableModel):
|
|
34
|
+
"""Validated tool/action request: (function, arguments) pair.
|
|
35
|
+
|
|
36
|
+
Parsed from LLM output via fuzzy JSON extraction.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
function: str = Field(
|
|
40
|
+
description=(
|
|
41
|
+
"Function name from tool_schemas. "
|
|
42
|
+
"Never invent names outside provided schemas."
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
arguments: dict[str, Any] = Field(
|
|
46
|
+
default_factory=dict,
|
|
47
|
+
description=(
|
|
48
|
+
"Argument dict for the function. Use only names/types from tool_schemas."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@field_validator("arguments", mode="before")
|
|
53
|
+
@classmethod
|
|
54
|
+
def _coerce_arguments(cls, value: Any) -> dict[str, Any]:
|
|
55
|
+
"""Coerce arguments into a dict, handling JSON strings."""
|
|
56
|
+
if isinstance(value, dict):
|
|
57
|
+
return value
|
|
58
|
+
return to_dict(
|
|
59
|
+
value,
|
|
60
|
+
fuzzy_parse=True,
|
|
61
|
+
recursive=True,
|
|
62
|
+
recursive_python_only=False,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def create(cls, content: str | dict | BaseModel) -> list[Action]:
|
|
67
|
+
"""Parse raw LLM output into Action instances. Returns [] on failure."""
|
|
68
|
+
try:
|
|
69
|
+
parsed = _parse_action_blocks(content)
|
|
70
|
+
return [cls.model_validate(item) for item in parsed] if parsed else []
|
|
71
|
+
except Exception:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ActionResult(HashableModel):
|
|
76
|
+
"""Validated tool-call result: (function, result, error) triple.
|
|
77
|
+
|
|
78
|
+
Produced by act stage after executing Action requests.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
function: str = Field(description="Function name that was called.")
|
|
82
|
+
result: Any = Field(default=None, description="Return value on success.")
|
|
83
|
+
error: str | None = Field(default=None, description="Error message on failure.")
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def success(self) -> bool:
|
|
87
|
+
return self.error is None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Instruct(HashableModel):
|
|
91
|
+
"""Instruction bundle for orchestrated task handoff.
|
|
92
|
+
|
|
93
|
+
Encapsulates everything needed to hand off a task: what to do,
|
|
94
|
+
strategic guidance, background context, and execution preferences.
|
|
95
|
+
Used as a structured handoff between orchestrator and workers.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
instruction: str | None = Field(
|
|
99
|
+
default=None,
|
|
100
|
+
description=(
|
|
101
|
+
"Clear, actionable task definition. Specify the primary goal, "
|
|
102
|
+
"key constraints, and success criteria."
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
guidance: JsonValue | None = Field(
|
|
106
|
+
default=None,
|
|
107
|
+
description=(
|
|
108
|
+
"Strategic direction: preferred methods, quality benchmarks, "
|
|
109
|
+
"resource constraints, or compliance requirements."
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
context: JsonValue | None = Field(
|
|
113
|
+
default=None,
|
|
114
|
+
description=(
|
|
115
|
+
"Background information directly relevant to the task: "
|
|
116
|
+
"environment, prior outcomes, system states, or dependencies."
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
reason: bool = Field(
|
|
120
|
+
default=False,
|
|
121
|
+
description=(
|
|
122
|
+
"Include reasoning: explanations of decisions, trade-offs, "
|
|
123
|
+
"alternatives considered, and confidence assessment."
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
actions: bool = Field(
|
|
127
|
+
default=False,
|
|
128
|
+
description=(
|
|
129
|
+
"Enable action execution via tool_schemas. "
|
|
130
|
+
"True: execute tool calls. False: analysis only."
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
action_strategy: Literal["sequential", "concurrent"] = Field(
|
|
134
|
+
default="concurrent",
|
|
135
|
+
description="How to execute actions: sequential or concurrent.",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@field_validator("action_strategy", mode="before")
|
|
139
|
+
@classmethod
|
|
140
|
+
def _validate_action_strategy(cls, v: Any) -> str:
|
|
141
|
+
if v not in ("sequential", "concurrent"):
|
|
142
|
+
return "concurrent"
|
|
143
|
+
return v
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@cache
|
|
147
|
+
def get_instruct_spec():
|
|
148
|
+
"""Spec for instruct_model: Instruct | None."""
|
|
149
|
+
from krons.core.specs import Spec
|
|
150
|
+
|
|
151
|
+
return Spec(Instruct, name="instruct_model").as_nullable()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@cache
|
|
155
|
+
def get_action_spec():
|
|
156
|
+
"""Spec for action_requests: list[Action] | None."""
|
|
157
|
+
from krons.core.specs import Spec
|
|
158
|
+
|
|
159
|
+
return Spec(Action, name="action_requests").as_listable().as_nullable()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@cache
|
|
163
|
+
def get_action_result_spec():
|
|
164
|
+
"""Spec for action_results: list[ActionResult] | None."""
|
|
165
|
+
from krons.core.specs import Spec
|
|
166
|
+
|
|
167
|
+
return Spec(ActionResult, name="action_results").as_listable().as_nullable()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Parsing helpers
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse_action_blocks(content: str | dict | BaseModel) -> list[dict]:
|
|
176
|
+
"""Extract action request dicts from raw content.
|
|
177
|
+
|
|
178
|
+
Normalizes provider-specific key names to {function, arguments}.
|
|
179
|
+
"""
|
|
180
|
+
json_blocks: list = []
|
|
181
|
+
|
|
182
|
+
if isinstance(content, BaseModel):
|
|
183
|
+
json_blocks = [content.model_dump()]
|
|
184
|
+
elif isinstance(content, str):
|
|
185
|
+
json_blocks = extract_json(content, fuzzy_parse=True)
|
|
186
|
+
if not json_blocks:
|
|
187
|
+
# Fallback: try extracting from ```python ... ``` blocks
|
|
188
|
+
matches = re.findall(r"```python\s*(.*?)\s*```", content, re.DOTALL)
|
|
189
|
+
json_blocks = to_list(
|
|
190
|
+
[extract_json(m, fuzzy_parse=True) for m in matches],
|
|
191
|
+
dropna=True,
|
|
192
|
+
)
|
|
193
|
+
elif isinstance(content, dict):
|
|
194
|
+
json_blocks = [content]
|
|
195
|
+
|
|
196
|
+
if json_blocks and not isinstance(json_blocks, list):
|
|
197
|
+
json_blocks = [json_blocks]
|
|
198
|
+
|
|
199
|
+
out: list[dict] = []
|
|
200
|
+
for block in json_blocks:
|
|
201
|
+
if not isinstance(block, dict):
|
|
202
|
+
continue
|
|
203
|
+
normalized = _normalize_action_keys(block)
|
|
204
|
+
if normalized:
|
|
205
|
+
out.append(normalized)
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _normalize_action_keys(d: dict) -> dict | None:
|
|
210
|
+
"""Map provider-specific key names to canonical {function, arguments}.
|
|
211
|
+
|
|
212
|
+
Returns None if required keys are missing.
|
|
213
|
+
"""
|
|
214
|
+
result: dict[str, Any] = {}
|
|
215
|
+
|
|
216
|
+
# Handle nested function.name pattern
|
|
217
|
+
if "function" in d and isinstance(d["function"], dict) and "name" in d["function"]:
|
|
218
|
+
d = {**d, "function": d["function"]["name"]}
|
|
219
|
+
|
|
220
|
+
for k, v in d.items():
|
|
221
|
+
# Strip common prefixes: action_name → name, recipient_name → name
|
|
222
|
+
normalized = (
|
|
223
|
+
k.replace("action_", "").replace("recipient_", "").removesuffix("s")
|
|
224
|
+
)
|
|
225
|
+
if normalized in ("name", "function", "recipient"):
|
|
226
|
+
result["function"] = v
|
|
227
|
+
elif normalized in ("parameter", "argument", "arg", "param"):
|
|
228
|
+
result["arguments"] = to_dict(
|
|
229
|
+
v, str_type="json", fuzzy_parse=True, suppress=True
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if "function" in result:
|
|
233
|
+
result.setdefault("arguments", {})
|
|
234
|
+
return result
|
|
235
|
+
return None
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Structure operation: generate -> parse -> validate pipeline.
|
|
5
|
+
|
|
6
|
+
Handler signature: structure(params, ctx) -> validated dict or model instance
|
|
7
|
+
|
|
8
|
+
Three-stage pipeline:
|
|
9
|
+
1. generate: LLM call (TEXT or MESSAGE depending on persist)
|
|
10
|
+
2. parse: extract JSON from text (direct + LLM reparse fallback)
|
|
11
|
+
3. validate: enforce operable specs + cast to composed structure
|
|
12
|
+
|
|
13
|
+
When persist=True, the assistant message is stored in the branch
|
|
14
|
+
before parsing. The text is extracted from message.content.response.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from krons.core.types import MaybeUnset, ModelConfig, Params, Unset, is_unset
|
|
25
|
+
|
|
26
|
+
from .generate import GenerateParams, generate
|
|
27
|
+
from .parse import ParseParams, parse
|
|
28
|
+
from .utils import ReturnAs
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from krons.agent.message.common import CustomParser
|
|
32
|
+
from krons.core.specs import Operable
|
|
33
|
+
from krons.resource import iModel
|
|
34
|
+
from krons.work.operations import RequestContext
|
|
35
|
+
from krons.work.rules.validator import Validator
|
|
36
|
+
|
|
37
|
+
__all__ = ("StructureParams", "structure")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class StructureParams(Params):
|
|
42
|
+
"""Parameters for structure operation (generate -> parse -> validate).
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
generate_params: LLM generation config.
|
|
46
|
+
validator: Rule-based validator for operable spec enforcement.
|
|
47
|
+
operable: Spec definition for field validation (required).
|
|
48
|
+
structure: Pre-composed Pydantic model to cast into.
|
|
49
|
+
If None, auto-composed from operable via compose_structure().
|
|
50
|
+
persist: Persist assistant message to branch before parsing.
|
|
51
|
+
capabilities: Allowed field subset (None = all operable fields).
|
|
52
|
+
auto_fix: Auto-coerce validation issues (e.g., wrap scalar -> list).
|
|
53
|
+
strict: Raise on validation failure vs. skip.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
_config = ModelConfig(sentinel_additions=frozenset({"none", "empty"}))
|
|
57
|
+
|
|
58
|
+
# Generate stage
|
|
59
|
+
generate_params: GenerateParams
|
|
60
|
+
|
|
61
|
+
# Validate stage
|
|
62
|
+
validator: Validator
|
|
63
|
+
operable: Operable
|
|
64
|
+
structure: type[BaseModel] | None = None
|
|
65
|
+
capabilities: set[str] | None = None
|
|
66
|
+
auto_fix: bool = True
|
|
67
|
+
strict: bool = True
|
|
68
|
+
|
|
69
|
+
# Message persistence
|
|
70
|
+
persist: bool = False
|
|
71
|
+
|
|
72
|
+
# Parse stage overrides
|
|
73
|
+
parse_imodel: MaybeUnset[iModel | str] = Unset
|
|
74
|
+
parse_imodel_kwargs: dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
custom_parser: CustomParser | None = None
|
|
76
|
+
similarity_threshold: float = 0.85
|
|
77
|
+
max_retries: int = 3
|
|
78
|
+
fill_mapping: dict[str, Any] | None = None
|
|
79
|
+
fill_value: Any = Unset
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def structure(params: StructureParams, ctx: RequestContext) -> Any:
|
|
83
|
+
"""Structure operation handler: generate -> parse -> validate.
|
|
84
|
+
|
|
85
|
+
When persist=True, generates as MESSAGE, persists to branch,
|
|
86
|
+
then extracts text from message.content.response for parsing.
|
|
87
|
+
"""
|
|
88
|
+
# Resolve structure type: explicit or auto-composed from operable
|
|
89
|
+
structure_type = params.structure
|
|
90
|
+
if structure_type is None:
|
|
91
|
+
structure_type = params.operable.compose_structure()
|
|
92
|
+
|
|
93
|
+
# Stage 1: Generate
|
|
94
|
+
if params.persist:
|
|
95
|
+
text = await _generate_and_persist(params.generate_params, ctx)
|
|
96
|
+
else:
|
|
97
|
+
gen_params = params.generate_params.with_updates(
|
|
98
|
+
copy_containers="deep", return_as=ReturnAs.TEXT
|
|
99
|
+
)
|
|
100
|
+
text = await generate(gen_params, ctx)
|
|
101
|
+
|
|
102
|
+
# Stage 2: Parse (inherit schema config from generate params)
|
|
103
|
+
gen = params.generate_params
|
|
104
|
+
parse_params = ParseParams(
|
|
105
|
+
text=text,
|
|
106
|
+
imodel=params.parse_imodel if not is_unset(params.parse_imodel) else None,
|
|
107
|
+
imodel_kwargs=params.parse_imodel_kwargs or {},
|
|
108
|
+
custom_parser=params.custom_parser,
|
|
109
|
+
similarity_threshold=params.similarity_threshold,
|
|
110
|
+
max_retries=params.max_retries,
|
|
111
|
+
fill_mapping=params.fill_mapping,
|
|
112
|
+
fill_value=params.fill_value,
|
|
113
|
+
request_model=gen.request_model,
|
|
114
|
+
tool_schemas=gen.tool_schemas,
|
|
115
|
+
structure_format=gen.structure_format,
|
|
116
|
+
custom_renderer=gen.custom_renderer,
|
|
117
|
+
)
|
|
118
|
+
parsed = await parse(parse_params, ctx)
|
|
119
|
+
|
|
120
|
+
# Stage 3: Validate against operable specs + structure type
|
|
121
|
+
return await params.validator.validate(
|
|
122
|
+
parsed,
|
|
123
|
+
params.operable,
|
|
124
|
+
capabilities=params.capabilities,
|
|
125
|
+
auto_fix=params.auto_fix,
|
|
126
|
+
strict=params.strict,
|
|
127
|
+
structure=structure_type,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def _generate_and_persist(
|
|
132
|
+
generate_params: GenerateParams,
|
|
133
|
+
ctx: RequestContext,
|
|
134
|
+
) -> str:
|
|
135
|
+
"""Generate as MESSAGE, persist to branch, return text.
|
|
136
|
+
|
|
137
|
+
The assistant message is added to both session messages and
|
|
138
|
+
the branch progression for conversation continuity.
|
|
139
|
+
"""
|
|
140
|
+
gen_params = generate_params.with_updates(
|
|
141
|
+
copy_containers="deep", return_as=ReturnAs.MESSAGE
|
|
142
|
+
)
|
|
143
|
+
message = await generate(gen_params, ctx)
|
|
144
|
+
|
|
145
|
+
# Persist to branch
|
|
146
|
+
session = await ctx.get_session()
|
|
147
|
+
branch = await ctx.get_branch()
|
|
148
|
+
session.add_message(message, branches=branch)
|
|
149
|
+
|
|
150
|
+
# Extract text from assistant content
|
|
151
|
+
return message.content.response
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Shared utilities for agent operations.
|
|
5
|
+
|
|
6
|
+
ReturnAs: Enum controlling how Calling results are unwrapped.
|
|
7
|
+
handle_return: Dispatch Calling → desired output form.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from krons.core.types import Enum, MaybeUnset, Unset, is_sentinel
|
|
15
|
+
from krons.errors import ValidationError
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
from krons.resource.backend import Calling
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReturnAs(Enum):
|
|
24
|
+
"""How to unwrap a Calling result.
|
|
25
|
+
|
|
26
|
+
TEXT - response.data (typically the LLM text)
|
|
27
|
+
RAW - response.raw_response (provider-specific dict)
|
|
28
|
+
RESPONSE - the NormalizedResponse object
|
|
29
|
+
MESSAGE - wrapped as a Message with Assistant content
|
|
30
|
+
CALLING - the raw Calling event (no unwrap, no validation)
|
|
31
|
+
CUSTOM - apply caller-supplied return_parser
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
TEXT = "text"
|
|
35
|
+
RAW = "raw"
|
|
36
|
+
RESPONSE = "response"
|
|
37
|
+
MESSAGE = "message"
|
|
38
|
+
CALLING = "calling"
|
|
39
|
+
CUSTOM = "custom"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def handle_return(
|
|
43
|
+
calling: Calling,
|
|
44
|
+
return_as: ReturnAs,
|
|
45
|
+
/,
|
|
46
|
+
*,
|
|
47
|
+
return_parser: MaybeUnset[Callable] = Unset,
|
|
48
|
+
):
|
|
49
|
+
"""Unwrap a Calling into the form requested by return_as.
|
|
50
|
+
|
|
51
|
+
CALLING and CUSTOM bypass normalization checks.
|
|
52
|
+
All other modes call calling.assert_is_normalized() first.
|
|
53
|
+
"""
|
|
54
|
+
if return_as == ReturnAs.CALLING:
|
|
55
|
+
return calling
|
|
56
|
+
|
|
57
|
+
if return_as == ReturnAs.CUSTOM:
|
|
58
|
+
if is_sentinel(return_parser, {"none", "empty"}) or not callable(return_parser):
|
|
59
|
+
raise ValidationError(
|
|
60
|
+
"return_parser must be provided as a callable when return_as is 'custom'"
|
|
61
|
+
)
|
|
62
|
+
return return_parser(calling)
|
|
63
|
+
|
|
64
|
+
calling.assert_is_normalized()
|
|
65
|
+
response = calling.response
|
|
66
|
+
|
|
67
|
+
match return_as:
|
|
68
|
+
case ReturnAs.TEXT:
|
|
69
|
+
return response.data
|
|
70
|
+
case ReturnAs.RAW:
|
|
71
|
+
return response.raw_response
|
|
72
|
+
case ReturnAs.RESPONSE:
|
|
73
|
+
return response
|
|
74
|
+
case ReturnAs.MESSAGE:
|
|
75
|
+
from krons.agent.message.assistant import parse_to_assistant_message
|
|
76
|
+
|
|
77
|
+
return parse_to_assistant_message(response)
|
|
78
|
+
case _:
|
|
79
|
+
raise ValidationError(f"Unsupported return_as: {return_as.value}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from .anthropic_messages import AnthropicMessagesEndpoint, create_anthropic_config
|
|
5
|
+
from .gemini import GeminiCodeEndpoint, create_gemini_code_config
|
|
6
|
+
from .match import match_endpoint
|
|
7
|
+
from .oai_chat import OAIChatEndpoint, create_oai_chat
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"AnthropicMessagesEndpoint",
|
|
11
|
+
"GeminiCodeEndpoint",
|
|
12
|
+
"OAIChatEndpoint",
|
|
13
|
+
"create_anthropic_config",
|
|
14
|
+
"create_gemini_code_config",
|
|
15
|
+
"create_oai_chat",
|
|
16
|
+
"match_endpoint",
|
|
17
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from krons.resource.backend import NormalizedResponseModel
|
|
9
|
+
from krons.resource.endpoint import Endpoint, EndpointConfig
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"AnthropicMessagesEndpoint",
|
|
13
|
+
"create_anthropic_config",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_anthropic_config(
|
|
18
|
+
name: str = "anthropic_messages",
|
|
19
|
+
api_key: str | None = None,
|
|
20
|
+
base_url: str = "https://api.anthropic.com/v1",
|
|
21
|
+
endpoint: str = "messages",
|
|
22
|
+
anthropic_version: str = "2023-06-01",
|
|
23
|
+
) -> dict:
|
|
24
|
+
"""Factory for Anthropic Messages API config.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
api_key: API key or env var name (default: "ANTHROPIC_API_KEY")
|
|
28
|
+
base_url: Base API URL
|
|
29
|
+
endpoint: Endpoint path
|
|
30
|
+
anthropic_version: API version header
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Config dict
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> config = create_anthropic_config()
|
|
37
|
+
>>> endpoint = AnthropicMessagesEndpoint(config=config)
|
|
38
|
+
>>> response = await endpoint.call(
|
|
39
|
+
... {"model": "claude-sonnet-4-5-20250929", "messages": [...]}
|
|
40
|
+
... )
|
|
41
|
+
"""
|
|
42
|
+
if api_key is None:
|
|
43
|
+
api_key = "ANTHROPIC_API_KEY"
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"name": name,
|
|
47
|
+
"provider": "anthropic",
|
|
48
|
+
"base_url": base_url,
|
|
49
|
+
"endpoint": endpoint,
|
|
50
|
+
"api_key": api_key,
|
|
51
|
+
"auth_type": "x-api-key",
|
|
52
|
+
"default_headers": {"anthropic-version": anthropic_version},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AnthropicMessagesEndpoint(Endpoint):
|
|
57
|
+
"""Anthropic Messages API endpoint.
|
|
58
|
+
|
|
59
|
+
Supports Anthropic-specific response normalization.
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
endpoint = AnthropicMessagesEndpoint()
|
|
63
|
+
response = await endpoint.call({
|
|
64
|
+
"model": "claude-sonnet-4-5-20250929",
|
|
65
|
+
"messages": [{"role": "user", "content": "Hello"}],
|
|
66
|
+
"max_tokens": 1024
|
|
67
|
+
})
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
config: dict | EndpointConfig | None = None,
|
|
73
|
+
circuit_breaker: Any | None = None,
|
|
74
|
+
**kwargs,
|
|
75
|
+
):
|
|
76
|
+
"""Initialize with Anthropic config."""
|
|
77
|
+
if config is None:
|
|
78
|
+
config = create_anthropic_config(**kwargs)
|
|
79
|
+
elif isinstance(config, EndpointConfig):
|
|
80
|
+
config = config.model_dump()
|
|
81
|
+
if not isinstance(config, dict):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
"Provided config must be a dict or EndpointConfig instance"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Ensure request_options is set
|
|
87
|
+
if config.get("request_options") is None:
|
|
88
|
+
from ..third_party.anthropic_models import CreateMessageRequest
|
|
89
|
+
|
|
90
|
+
config["request_options"] = CreateMessageRequest
|
|
91
|
+
|
|
92
|
+
super().__init__(config=config, circuit_breaker=circuit_breaker, **kwargs)
|
|
93
|
+
|
|
94
|
+
def normalize_response(self, response: dict[str, Any]) -> NormalizedResponseModel:
|
|
95
|
+
"""Normalize Anthropic response to standard format.
|
|
96
|
+
|
|
97
|
+
Extracts:
|
|
98
|
+
- Text from content blocks
|
|
99
|
+
- Thinking blocks (if extended thinking enabled)
|
|
100
|
+
- Usage stats
|
|
101
|
+
- Stop reason
|
|
102
|
+
- Model info
|
|
103
|
+
- Tool uses
|
|
104
|
+
"""
|
|
105
|
+
# Extract text and thinking from content blocks
|
|
106
|
+
text_parts = []
|
|
107
|
+
thinking_parts = []
|
|
108
|
+
|
|
109
|
+
content = response.get("content")
|
|
110
|
+
if content:
|
|
111
|
+
for block in content:
|
|
112
|
+
block_type = block.get("type")
|
|
113
|
+
if block_type == "text":
|
|
114
|
+
text_parts.append(block.get("text", ""))
|
|
115
|
+
elif block_type == "thinking":
|
|
116
|
+
thinking_parts.append(block.get("thinking", ""))
|
|
117
|
+
|
|
118
|
+
# Combine text
|
|
119
|
+
text = "\n\n".join(text_parts)
|
|
120
|
+
|
|
121
|
+
# Extract metadata
|
|
122
|
+
metadata: dict[str, Any] = {
|
|
123
|
+
k: response[k]
|
|
124
|
+
for k in ("model", "usage", "stop_reason", "stop_sequence", "id")
|
|
125
|
+
if k in response
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Add thinking if present
|
|
129
|
+
if thinking_parts:
|
|
130
|
+
metadata["thinking"] = "\n\n".join(thinking_parts)
|
|
131
|
+
|
|
132
|
+
# Add tool use blocks if present
|
|
133
|
+
tool_uses = [
|
|
134
|
+
block
|
|
135
|
+
for block in response.get("content", [])
|
|
136
|
+
if block.get("type") == "tool_use"
|
|
137
|
+
]
|
|
138
|
+
if tool_uses:
|
|
139
|
+
metadata["tool_uses"] = tool_uses
|
|
140
|
+
|
|
141
|
+
return NormalizedResponseModel(
|
|
142
|
+
status="success",
|
|
143
|
+
data=text,
|
|
144
|
+
raw_response=response,
|
|
145
|
+
metadata=metadata,
|
|
146
|
+
)
|