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,295 @@
|
|
|
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
|
+
import warnings
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Literal, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------- Roles & content parts ----------
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChatRole(str, Enum):
|
|
19
|
+
system = "system"
|
|
20
|
+
developer = "developer" # modern system-like role
|
|
21
|
+
user = "user"
|
|
22
|
+
assistant = "assistant"
|
|
23
|
+
tool = "tool" # for tool results sent back to the model
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TextPart(BaseModel):
|
|
27
|
+
"""Text content part for multimodal messages."""
|
|
28
|
+
|
|
29
|
+
type: Literal["text"] = "text"
|
|
30
|
+
text: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ImageURLObject(BaseModel):
|
|
34
|
+
"""Image URL object; 'detail' is optional and model-dependent."""
|
|
35
|
+
|
|
36
|
+
url: str
|
|
37
|
+
detail: Literal["auto", "low", "high"] | None = Field(
|
|
38
|
+
default=None,
|
|
39
|
+
description="Optional detail control for vision models (auto/low/high).",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ImageURLPart(BaseModel):
|
|
44
|
+
"""Image content part for multimodal messages."""
|
|
45
|
+
|
|
46
|
+
type: Literal["image_url"] = "image_url"
|
|
47
|
+
image_url: ImageURLObject
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
ContentPart = TextPart | ImageURLPart
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------- Tool-calling structures ----------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FunctionDef(BaseModel):
|
|
57
|
+
"""JSON Schema function definition for tool-calling."""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
description: str | None = None
|
|
61
|
+
parameters: dict[str, Any] = Field(
|
|
62
|
+
default_factory=dict,
|
|
63
|
+
description="JSON Schema describing function parameters.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FunctionTool(BaseModel):
|
|
68
|
+
type: Literal["function"] = "function"
|
|
69
|
+
function: FunctionDef
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FunctionCall(BaseModel):
|
|
73
|
+
"""Legacy function_call field on assistant messages."""
|
|
74
|
+
|
|
75
|
+
name: str
|
|
76
|
+
arguments: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ToolCallFunction(BaseModel):
|
|
80
|
+
name: str
|
|
81
|
+
arguments: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ToolCall(BaseModel):
|
|
85
|
+
"""Assistant's tool call (modern)."""
|
|
86
|
+
|
|
87
|
+
id: str
|
|
88
|
+
type: Literal["function"] = "function"
|
|
89
|
+
function: ToolCallFunction
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ToolChoiceFunction(BaseModel):
|
|
93
|
+
"""Explicit tool selection."""
|
|
94
|
+
|
|
95
|
+
type: Literal["function"] = "function"
|
|
96
|
+
function: dict[str, str] # {"name": "<function_name>"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
ToolChoice = Union[Literal["auto", "none"], ToolChoiceFunction]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------- Response format (structured outputs) ----------
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ResponseFormatText(BaseModel):
|
|
106
|
+
type: Literal["text"] = "text"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ResponseFormatJSONObject(BaseModel):
|
|
110
|
+
type: Literal["json_object"] = "json_object"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class JSONSchemaFormat(BaseModel):
|
|
114
|
+
name: str
|
|
115
|
+
schema_: dict[str, Any] = Field(
|
|
116
|
+
alias="schema", description="JSON Schema definition"
|
|
117
|
+
)
|
|
118
|
+
strict: bool | None = Field(
|
|
119
|
+
default=None,
|
|
120
|
+
description="If true, disallow unspecified properties (strict schema).",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
model_config = {"populate_by_name": True}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ResponseFormatJSONSchema(BaseModel):
|
|
127
|
+
type: Literal["json_schema"] = "json_schema"
|
|
128
|
+
json_schema: JSONSchemaFormat
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
ResponseFormat = Union[
|
|
132
|
+
ResponseFormatText,
|
|
133
|
+
ResponseFormatJSONObject,
|
|
134
|
+
ResponseFormatJSONSchema,
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------- Messages (discriminated by role) ----------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class SystemMessage(BaseModel):
|
|
142
|
+
role: Literal[ChatRole.system] = ChatRole.system
|
|
143
|
+
content: str | list[ContentPart]
|
|
144
|
+
name: str | None = None # optional per API
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class DeveloperMessage(BaseModel):
|
|
148
|
+
role: Literal[ChatRole.developer] = ChatRole.developer
|
|
149
|
+
content: str | list[ContentPart]
|
|
150
|
+
name: str | None = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class UserMessage(BaseModel):
|
|
154
|
+
role: Literal[ChatRole.user] = ChatRole.user
|
|
155
|
+
content: str | list[ContentPart]
|
|
156
|
+
name: str | None = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class AssistantMessage(BaseModel):
|
|
160
|
+
role: Literal[ChatRole.assistant] = ChatRole.assistant
|
|
161
|
+
# Either textual content, or only tool_calls (when asking you to call tools)
|
|
162
|
+
content: str | list[ContentPart] | None = None
|
|
163
|
+
name: str | None = None
|
|
164
|
+
tool_calls: list[ToolCall] | None = None # modern tool-calling result
|
|
165
|
+
function_call: FunctionCall | None = None # legacy function-calling result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ToolMessage(BaseModel):
|
|
169
|
+
role: Literal[ChatRole.tool] = ChatRole.tool
|
|
170
|
+
content: str # tool output returned to the model
|
|
171
|
+
tool_call_id: str # must reference the assistant's tool_calls[i].id
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
ChatMessage = (
|
|
175
|
+
SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | ToolMessage
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# ---------- Stream options ----------
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class StreamOptions(BaseModel):
|
|
182
|
+
include_usage: bool | None = Field(
|
|
183
|
+
default=None,
|
|
184
|
+
description="If true, a final streamed chunk includes token usage.",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------- Main request model ----------
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class OpenAIChatCompletionsRequest(BaseModel):
|
|
192
|
+
"""
|
|
193
|
+
Request body for OpenAI Chat Completions.
|
|
194
|
+
Endpoint: POST https://api.openai.com/v1/chat/completions
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
# Required
|
|
198
|
+
model: str = Field(..., description="Model name, e.g., 'gpt-4o', 'gpt-4o-mini'.") # type: ignore
|
|
199
|
+
messages: list[ChatMessage] = Field(
|
|
200
|
+
...,
|
|
201
|
+
description="Conversation so far, including system/developer context.",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Sampling & penalties
|
|
205
|
+
temperature: float | None = Field(
|
|
206
|
+
default=None, ge=0.0, le=2.0, description="Higher is more random."
|
|
207
|
+
)
|
|
208
|
+
top_p: float | None = Field(
|
|
209
|
+
default=None, ge=0.0, le=1.0, description="Nucleus sampling."
|
|
210
|
+
)
|
|
211
|
+
presence_penalty: float | None = Field(
|
|
212
|
+
default=None,
|
|
213
|
+
ge=-2.0,
|
|
214
|
+
le=2.0,
|
|
215
|
+
description="Encourages new topics; -2..2.",
|
|
216
|
+
)
|
|
217
|
+
frequency_penalty: float | None = Field(
|
|
218
|
+
default=None,
|
|
219
|
+
ge=-2.0,
|
|
220
|
+
le=2.0,
|
|
221
|
+
description="Penalizes repetition; -2..2.",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Token limits
|
|
225
|
+
max_completion_tokens: int | None = Field(
|
|
226
|
+
default=None,
|
|
227
|
+
description="Preferred cap on generated tokens (newer models).",
|
|
228
|
+
)
|
|
229
|
+
max_tokens: int | None = Field(
|
|
230
|
+
default=None,
|
|
231
|
+
description="Legacy completion cap (still accepted by many models).",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Count, stop, logits
|
|
235
|
+
n: int | None = Field(default=None, ge=1, description="# of choices to generate.")
|
|
236
|
+
stop: str | list[str] | None = Field(default=None, description="Stop sequence(s).")
|
|
237
|
+
logit_bias: dict[str, float] | None = Field(
|
|
238
|
+
default=None,
|
|
239
|
+
description="Map of token-id -> bias (-100..100).",
|
|
240
|
+
)
|
|
241
|
+
seed: int | None = Field(
|
|
242
|
+
default=None,
|
|
243
|
+
description="Optional reproducibility seed (model-dependent).",
|
|
244
|
+
)
|
|
245
|
+
logprobs: bool | None = None
|
|
246
|
+
top_logprobs: int | None = Field(
|
|
247
|
+
default=None,
|
|
248
|
+
ge=0,
|
|
249
|
+
description="When logprobs is true, how many top tokens to include.",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Tool calling (modern)
|
|
253
|
+
tools: list[FunctionTool] | None = None
|
|
254
|
+
tool_choice: ToolChoice | None = Field(
|
|
255
|
+
default=None,
|
|
256
|
+
description="'auto' (default), 'none', or a function selection.",
|
|
257
|
+
)
|
|
258
|
+
parallel_tool_calls: bool | None = Field(
|
|
259
|
+
default=None,
|
|
260
|
+
description="Allow multiple tool calls in a single assistant turn.",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Legacy function-calling (still supported)
|
|
264
|
+
functions: list[FunctionDef] | None = None
|
|
265
|
+
function_call: Literal["none", "auto"] | FunctionCall | None = None
|
|
266
|
+
|
|
267
|
+
# Structured outputs
|
|
268
|
+
response_format: ResponseFormat | None = None
|
|
269
|
+
|
|
270
|
+
# Streaming
|
|
271
|
+
stream: bool | None = None
|
|
272
|
+
stream_options: StreamOptions | None = None
|
|
273
|
+
|
|
274
|
+
# Routing / tiering
|
|
275
|
+
service_tier: Literal["auto", "default", "flex", "scale", "priority"] | None = (
|
|
276
|
+
Field(
|
|
277
|
+
default=None,
|
|
278
|
+
description="Processing tier; requires account eligibility.",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Misc
|
|
283
|
+
user: str | None = Field(
|
|
284
|
+
default=None,
|
|
285
|
+
description="End-user identifier for abuse monitoring & analytics.",
|
|
286
|
+
)
|
|
287
|
+
store: bool | None = Field(
|
|
288
|
+
default=None,
|
|
289
|
+
description="Whether to store the response server-side (model-dependent).",
|
|
290
|
+
)
|
|
291
|
+
metadata: dict[str, Any] | None = None
|
|
292
|
+
reasoning_effort: Literal["low", "medium", "high"] | None = Field(
|
|
293
|
+
default=None,
|
|
294
|
+
description="For reasoning models: trade-off between speed and accuracy.",
|
|
295
|
+
)
|
krons/agent/tool.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Tool - Callable function backend for agents.
|
|
5
|
+
|
|
6
|
+
A Tool wraps a callable function with:
|
|
7
|
+
- Parameter validation via Pydantic schema
|
|
8
|
+
- Normalized response format
|
|
9
|
+
- Integration with the ResourceBackend/Calling pattern
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
@tool(name="calculator", description="Perform calculations")
|
|
13
|
+
async def calculator(expression: str) -> float:
|
|
14
|
+
return eval(expression) # simplified example
|
|
15
|
+
|
|
16
|
+
# Or create manually:
|
|
17
|
+
tool = Tool(
|
|
18
|
+
config=ToolConfig(
|
|
19
|
+
provider="local",
|
|
20
|
+
name="calculator",
|
|
21
|
+
description="Perform calculations",
|
|
22
|
+
),
|
|
23
|
+
handler=calculator,
|
|
24
|
+
)
|
|
25
|
+
response = await tool.call(arguments={"expression": "2 + 2"})
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import inspect
|
|
31
|
+
import logging
|
|
32
|
+
from collections.abc import Awaitable, Callable
|
|
33
|
+
from typing import Any, get_type_hints
|
|
34
|
+
|
|
35
|
+
from pydantic import BaseModel, Field
|
|
36
|
+
|
|
37
|
+
from krons.resource.backend import (
|
|
38
|
+
Calling,
|
|
39
|
+
NormalizedResponseModel,
|
|
40
|
+
ResourceBackend,
|
|
41
|
+
ResourceConfig,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
__all__ = (
|
|
47
|
+
"Tool",
|
|
48
|
+
"ToolCalling",
|
|
49
|
+
"ToolConfig",
|
|
50
|
+
"tool",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ToolConfig(ResourceConfig):
|
|
55
|
+
"""Configuration for a Tool backend.
|
|
56
|
+
|
|
57
|
+
Extends ResourceConfig with tool-specific fields.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
description: str = Field(default="", description="Human-readable tool description")
|
|
61
|
+
parameters_schema: type[BaseModel] | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
exclude=True,
|
|
64
|
+
description="Pydantic model for parameter validation",
|
|
65
|
+
)
|
|
66
|
+
return_type: type | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
exclude=True,
|
|
69
|
+
description="Expected return type (for documentation)",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ToolCalling(Calling):
|
|
74
|
+
"""Calling event for Tool execution.
|
|
75
|
+
|
|
76
|
+
Wraps a tool invocation with the standard Calling lifecycle (hooks, etc).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
arguments: dict[str, Any] = Field(
|
|
80
|
+
default_factory=dict,
|
|
81
|
+
description="Arguments to pass to the tool handler",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def call_args(self) -> dict:
|
|
86
|
+
"""Get arguments for backend.call()."""
|
|
87
|
+
return {"arguments": self.arguments}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Tool(ResourceBackend):
|
|
91
|
+
"""Tool backend - wraps a callable function.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
config: ToolConfig with name, description, schema
|
|
95
|
+
handler: The callable function to execute
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
config: ToolConfig = Field(..., description="Tool configuration")
|
|
99
|
+
handler: Callable[..., Any | Awaitable[Any]] = Field(
|
|
100
|
+
...,
|
|
101
|
+
exclude=True,
|
|
102
|
+
description="The callable function to execute",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def event_type(self) -> type[ToolCalling]:
|
|
107
|
+
"""Return ToolCalling as the event type for this backend."""
|
|
108
|
+
return ToolCalling
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def description(self) -> str:
|
|
112
|
+
"""Tool description from config."""
|
|
113
|
+
return self.config.description
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def parameters_schema(self) -> type[BaseModel] | None:
|
|
117
|
+
"""Parameters schema from config."""
|
|
118
|
+
return self.config.parameters_schema
|
|
119
|
+
|
|
120
|
+
def _validate_arguments(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
121
|
+
"""Validate arguments against schema if defined.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
arguments: Arguments dict to validate
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Validated arguments dict
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If validation fails
|
|
131
|
+
"""
|
|
132
|
+
if self.parameters_schema is None:
|
|
133
|
+
return arguments
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
validated = self.parameters_schema.model_validate(arguments)
|
|
137
|
+
return validated.model_dump()
|
|
138
|
+
except Exception as e:
|
|
139
|
+
raise ValueError(f"Tool argument validation failed: {e}") from e
|
|
140
|
+
|
|
141
|
+
async def call(
|
|
142
|
+
self, arguments: dict[str, Any] | None = None
|
|
143
|
+
) -> NormalizedResponseModel:
|
|
144
|
+
"""Execute the tool handler with given arguments.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
arguments: Arguments to pass to handler
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
NormalizedResponseModel with result or error
|
|
151
|
+
"""
|
|
152
|
+
arguments = arguments or {}
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# Validate arguments
|
|
156
|
+
validated_args = self._validate_arguments(arguments)
|
|
157
|
+
|
|
158
|
+
# Execute handler (sync or async)
|
|
159
|
+
if inspect.iscoroutinefunction(self.handler):
|
|
160
|
+
result = await self.handler(**validated_args)
|
|
161
|
+
else:
|
|
162
|
+
result = self.handler(**validated_args)
|
|
163
|
+
|
|
164
|
+
return self.normalize_response(result)
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.exception(f"Tool '{self.name}' execution failed: {e}")
|
|
168
|
+
return NormalizedResponseModel(
|
|
169
|
+
status="error",
|
|
170
|
+
data=None,
|
|
171
|
+
error=str(e),
|
|
172
|
+
raw_response={"error": str(e), "arguments": arguments},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def normalize_response(self, raw_response: Any) -> NormalizedResponseModel:
|
|
176
|
+
"""Normalize tool result to standard format.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
raw_response: Raw result from handler
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
NormalizedResponseModel with status and data
|
|
183
|
+
"""
|
|
184
|
+
return NormalizedResponseModel(
|
|
185
|
+
status="success",
|
|
186
|
+
data=raw_response,
|
|
187
|
+
raw_response={"result": raw_response},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def to_openai_schema(self) -> dict[str, Any]:
|
|
191
|
+
"""Generate OpenAI-compatible function schema.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dict with name, description, parameters schema
|
|
195
|
+
"""
|
|
196
|
+
schema: dict[str, Any] = {
|
|
197
|
+
"name": self.name,
|
|
198
|
+
"description": self.description,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if self.parameters_schema:
|
|
202
|
+
# Get JSON schema from Pydantic model
|
|
203
|
+
json_schema = self.parameters_schema.model_json_schema()
|
|
204
|
+
# OpenAI expects parameters directly, not wrapped
|
|
205
|
+
schema["parameters"] = {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"properties": json_schema.get("properties", {}),
|
|
208
|
+
"required": json_schema.get("required", []),
|
|
209
|
+
}
|
|
210
|
+
else:
|
|
211
|
+
schema["parameters"] = {"type": "object", "properties": {}}
|
|
212
|
+
|
|
213
|
+
return schema
|
|
214
|
+
|
|
215
|
+
def to_anthropic_schema(self) -> dict[str, Any]:
|
|
216
|
+
"""Generate Anthropic-compatible tool schema.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Dict with name, description, input_schema
|
|
220
|
+
"""
|
|
221
|
+
schema: dict[str, Any] = {
|
|
222
|
+
"name": self.name,
|
|
223
|
+
"description": self.description,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if self.parameters_schema:
|
|
227
|
+
json_schema = self.parameters_schema.model_json_schema()
|
|
228
|
+
schema["input_schema"] = {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"properties": json_schema.get("properties", {}),
|
|
231
|
+
"required": json_schema.get("required", []),
|
|
232
|
+
}
|
|
233
|
+
else:
|
|
234
|
+
schema["input_schema"] = {"type": "object", "properties": {}}
|
|
235
|
+
|
|
236
|
+
return schema
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def tool(
|
|
240
|
+
name: str | None = None,
|
|
241
|
+
description: str = "",
|
|
242
|
+
provider: str = "local",
|
|
243
|
+
parameters_schema: type[BaseModel] | None = None,
|
|
244
|
+
) -> Callable[[Callable[..., Any]], Tool]:
|
|
245
|
+
"""Decorator to create a Tool from a function.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
name: Tool name (defaults to function name)
|
|
249
|
+
description: Human-readable description
|
|
250
|
+
provider: Provider name (default: "local")
|
|
251
|
+
parameters_schema: Optional Pydantic model for parameter validation
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Decorator that creates a Tool
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
@tool(name="greet", description="Greet a person")
|
|
258
|
+
async def greet(name: str) -> str:
|
|
259
|
+
return f"Hello, {name}!"
|
|
260
|
+
|
|
261
|
+
# Use directly:
|
|
262
|
+
response = await greet.call(arguments={"name": "World"})
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def decorator(func: Callable[..., Any]) -> Tool:
|
|
266
|
+
tool_name = name or func.__name__
|
|
267
|
+
tool_desc = description or func.__doc__ or ""
|
|
268
|
+
|
|
269
|
+
# Try to infer parameters schema from type hints
|
|
270
|
+
schema = parameters_schema
|
|
271
|
+
if schema is None:
|
|
272
|
+
hints = get_type_hints(func) if hasattr(func, "__annotations__") else {}
|
|
273
|
+
hints.pop("return", None)
|
|
274
|
+
if hints:
|
|
275
|
+
# Dynamically create a Pydantic model from type hints
|
|
276
|
+
schema = type(
|
|
277
|
+
f"{tool_name.title()}Params",
|
|
278
|
+
(BaseModel,),
|
|
279
|
+
{"__annotations__": hints},
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
config = ToolConfig(
|
|
283
|
+
provider=provider,
|
|
284
|
+
name=tool_name,
|
|
285
|
+
description=tool_desc,
|
|
286
|
+
parameters_schema=schema,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return Tool(config=config, handler=func)
|
|
290
|
+
|
|
291
|
+
return decorator
|