agentframe-py 0.1.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.
- agentframe/__init__.py +170 -0
- agentframe/authoring/__init__.py +1 -0
- agentframe/authoring/agent.py +75 -0
- agentframe/authoring/context.py +123 -0
- agentframe/authoring/decorators.py +202 -0
- agentframe/authoring/definitions.py +372 -0
- agentframe/authoring/services.py +176 -0
- agentframe/cli.py +293 -0
- agentframe/errors.py +194 -0
- agentframe/hosting/__init__.py +1 -0
- agentframe/hosting/a2a_executor.py +569 -0
- agentframe/hosting/admission.py +69 -0
- agentframe/hosting/app.py +617 -0
- agentframe/hosting/card.py +138 -0
- agentframe/hosting/dedup.py +193 -0
- agentframe/hosting/draining_server.py +47 -0
- agentframe/hosting/host.py +143 -0
- agentframe/hosting/lifecycle.py +81 -0
- agentframe/hosting/model_router.py +407 -0
- agentframe/hosting/request_size.py +52 -0
- agentframe/hosting/routing.py +349 -0
- agentframe/models/__init__.py +1 -0
- agentframe/models/base.py +264 -0
- agentframe/models/foundry.py +570 -0
- agentframe/models/ollama.py +320 -0
- agentframe/models/strict_schema.py +211 -0
- agentframe/py.typed +0 -0
- agentframe/runtime/__init__.py +1 -0
- agentframe/runtime/generation.py +725 -0
- agentframe/runtime/limits.py +124 -0
- agentframe/runtime/options.py +50 -0
- agentframe/runtime/runtime.py +449 -0
- agentframe/runtime/tools.py +216 -0
- agentframe/runtime/usage.py +40 -0
- agentframe/security/__init__.py +1 -0
- agentframe/security/authentication.py +265 -0
- agentframe/security/authorization.py +66 -0
- agentframe/security/entra.py +260 -0
- agentframe/security/execution_policy.py +241 -0
- agentframe/security/principal.py +16 -0
- agentframe/testing.py +135 -0
- agentframe_py-0.1.0.dist-info/METADATA +570 -0
- agentframe_py-0.1.0.dist-info/RECORD +46 -0
- agentframe_py-0.1.0.dist-info/WHEEL +4 -0
- agentframe_py-0.1.0.dist-info/entry_points.txt +2 -0
- agentframe_py-0.1.0.dist-info/licenses/LICENSE +201 -0
agentframe/__init__.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""AgentFrame: define agents with Python, communicate through A2A."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from agentframe.authoring.agent import Agent
|
|
6
|
+
from agentframe.authoring.context import RunContext
|
|
7
|
+
from agentframe.authoring.decorators import agent, skill, tool
|
|
8
|
+
from agentframe.authoring.definitions import SkillDefinition, ToolDefinition
|
|
9
|
+
from agentframe.authoring.services import Inject, Services
|
|
10
|
+
from agentframe.errors import (
|
|
11
|
+
AgentFrameError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
CancelledError,
|
|
14
|
+
DefinitionError,
|
|
15
|
+
IncompleteOutputError,
|
|
16
|
+
InputValidationError,
|
|
17
|
+
LimitExceededError,
|
|
18
|
+
ModelCallLimitExceededError,
|
|
19
|
+
ModelError,
|
|
20
|
+
ModelRefusalError,
|
|
21
|
+
OutputValidationError,
|
|
22
|
+
PermissionDeniedError,
|
|
23
|
+
PolicyUnavailableError,
|
|
24
|
+
ProviderError,
|
|
25
|
+
TimeoutLimitExceededError,
|
|
26
|
+
TokenLimitExceededError,
|
|
27
|
+
ToolCallLimitExceededError,
|
|
28
|
+
UnsupportedCapabilityError,
|
|
29
|
+
)
|
|
30
|
+
from agentframe.hosting.app import AgentApp
|
|
31
|
+
from agentframe.hosting.dedup import MessageDedupStore
|
|
32
|
+
from agentframe.hosting.host import A2AConfig, HostConfig
|
|
33
|
+
from agentframe.hosting.model_router import ModelRouter
|
|
34
|
+
from agentframe.hosting.routing import (
|
|
35
|
+
Clarification,
|
|
36
|
+
RoutingContext,
|
|
37
|
+
SkillCall,
|
|
38
|
+
SkillRouter,
|
|
39
|
+
)
|
|
40
|
+
from agentframe.models.base import (
|
|
41
|
+
Message,
|
|
42
|
+
Model,
|
|
43
|
+
ModelCapabilities,
|
|
44
|
+
ModelRequest,
|
|
45
|
+
ModelResponse,
|
|
46
|
+
ModelSettings,
|
|
47
|
+
OutputSchema,
|
|
48
|
+
Role,
|
|
49
|
+
ToolCall,
|
|
50
|
+
ToolSpec,
|
|
51
|
+
)
|
|
52
|
+
from agentframe.runtime.limits import RunLimits
|
|
53
|
+
from agentframe.runtime.options import RunOptions
|
|
54
|
+
from agentframe.runtime.runtime import RunResult, Runtime
|
|
55
|
+
from agentframe.runtime.usage import FinishReason, Usage
|
|
56
|
+
from agentframe.security.authentication import (
|
|
57
|
+
AllowAnonymous,
|
|
58
|
+
APIKey,
|
|
59
|
+
APIKeyAuth,
|
|
60
|
+
Authenticator,
|
|
61
|
+
DeclaresCardSecurity,
|
|
62
|
+
VerifiedCaller,
|
|
63
|
+
)
|
|
64
|
+
from agentframe.security.authorization import (
|
|
65
|
+
AuthorizationPolicy,
|
|
66
|
+
ToolAuthorizationPolicy,
|
|
67
|
+
)
|
|
68
|
+
from agentframe.security.execution_policy import (
|
|
69
|
+
ExecutionPolicy,
|
|
70
|
+
InputCheck,
|
|
71
|
+
ModelRequestCheck,
|
|
72
|
+
OutputCheck,
|
|
73
|
+
ToolCheck,
|
|
74
|
+
)
|
|
75
|
+
from agentframe.security.principal import Principal
|
|
76
|
+
|
|
77
|
+
if TYPE_CHECKING:
|
|
78
|
+
from agentframe.models.foundry import FoundryModel as FoundryModel
|
|
79
|
+
from agentframe.models.ollama import OllamaModel as OllamaModel
|
|
80
|
+
from agentframe.security.entra import EntraIDAuth as EntraIDAuth
|
|
81
|
+
|
|
82
|
+
__version__ = "0.1.0"
|
|
83
|
+
|
|
84
|
+
__all__ = [
|
|
85
|
+
"A2AConfig",
|
|
86
|
+
"APIKey",
|
|
87
|
+
"APIKeyAuth",
|
|
88
|
+
"Agent",
|
|
89
|
+
"AgentApp",
|
|
90
|
+
"AgentFrameError",
|
|
91
|
+
"AllowAnonymous",
|
|
92
|
+
"AuthenticationError",
|
|
93
|
+
"Authenticator",
|
|
94
|
+
"AuthorizationPolicy",
|
|
95
|
+
"CancelledError",
|
|
96
|
+
"Clarification",
|
|
97
|
+
"DeclaresCardSecurity",
|
|
98
|
+
"DefinitionError",
|
|
99
|
+
"ExecutionPolicy",
|
|
100
|
+
"FinishReason",
|
|
101
|
+
"HostConfig",
|
|
102
|
+
"IncompleteOutputError",
|
|
103
|
+
"Inject",
|
|
104
|
+
"InputCheck",
|
|
105
|
+
"InputValidationError",
|
|
106
|
+
"LimitExceededError",
|
|
107
|
+
"Message",
|
|
108
|
+
"MessageDedupStore",
|
|
109
|
+
"Model",
|
|
110
|
+
"ModelCallLimitExceededError",
|
|
111
|
+
"ModelCapabilities",
|
|
112
|
+
"ModelError",
|
|
113
|
+
"ModelRefusalError",
|
|
114
|
+
"ModelRequest",
|
|
115
|
+
"ModelRequestCheck",
|
|
116
|
+
"ModelResponse",
|
|
117
|
+
"ModelRouter",
|
|
118
|
+
"ModelSettings",
|
|
119
|
+
"OutputCheck",
|
|
120
|
+
"OutputSchema",
|
|
121
|
+
"OutputValidationError",
|
|
122
|
+
"PermissionDeniedError",
|
|
123
|
+
"PolicyUnavailableError",
|
|
124
|
+
"Principal",
|
|
125
|
+
"ProviderError",
|
|
126
|
+
"Role",
|
|
127
|
+
"RoutingContext",
|
|
128
|
+
"RunContext",
|
|
129
|
+
"RunLimits",
|
|
130
|
+
"RunOptions",
|
|
131
|
+
"RunResult",
|
|
132
|
+
"Runtime",
|
|
133
|
+
"Services",
|
|
134
|
+
"SkillCall",
|
|
135
|
+
"SkillDefinition",
|
|
136
|
+
"SkillRouter",
|
|
137
|
+
"TimeoutLimitExceededError",
|
|
138
|
+
"TokenLimitExceededError",
|
|
139
|
+
"ToolAuthorizationPolicy",
|
|
140
|
+
"ToolCall",
|
|
141
|
+
"ToolCallLimitExceededError",
|
|
142
|
+
"ToolCheck",
|
|
143
|
+
"ToolDefinition",
|
|
144
|
+
"ToolSpec",
|
|
145
|
+
"UnsupportedCapabilityError",
|
|
146
|
+
"Usage",
|
|
147
|
+
"VerifiedCaller",
|
|
148
|
+
"__version__",
|
|
149
|
+
"agent",
|
|
150
|
+
"skill",
|
|
151
|
+
"tool",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def __getattr__(name: str) -> object:
|
|
156
|
+
# Provider adapters and EntraIDAuth are imported on first use so that
|
|
157
|
+
# ``import agentframe`` works without their optional extras installed.
|
|
158
|
+
if name == "FoundryModel":
|
|
159
|
+
from agentframe.models.foundry import FoundryModel as FoundryModel
|
|
160
|
+
|
|
161
|
+
return FoundryModel
|
|
162
|
+
if name == "OllamaModel":
|
|
163
|
+
from agentframe.models.ollama import OllamaModel as OllamaModel
|
|
164
|
+
|
|
165
|
+
return OllamaModel
|
|
166
|
+
if name == "EntraIDAuth":
|
|
167
|
+
from agentframe.security.entra import EntraIDAuth as EntraIDAuth
|
|
168
|
+
|
|
169
|
+
return EntraIDAuth
|
|
170
|
+
raise AttributeError(f"module 'agentframe' has no attribute {name!r}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Authoring: the Agent base class, decorators, definitions, context and services."""
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The Agent base class."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
|
+
|
|
7
|
+
from agentframe.authoring.definitions import (
|
|
8
|
+
AgentDefinition,
|
|
9
|
+
ToolDefinition,
|
|
10
|
+
tools_by_name,
|
|
11
|
+
)
|
|
12
|
+
from agentframe.errors import DefinitionError
|
|
13
|
+
from agentframe.models.base import Model, ModelSettings
|
|
14
|
+
from agentframe.runtime.limits import RunLimits
|
|
15
|
+
|
|
16
|
+
DEFINITION_ATTRIBUTE = "__agentframe_definition__"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Agent:
|
|
20
|
+
"""Base class for agents: declare skills with @skill, describe it with @agent.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
model: The model ``ctx.generate`` calls. Model-free agents omit it.
|
|
24
|
+
The application owns the model and closes it.
|
|
25
|
+
tools: ``@tool`` functions this agent's ``ctx.generate`` calls may
|
|
26
|
+
select. Registering grants nothing by itself: each call names the
|
|
27
|
+
tools it offers the model. Tools are never advertised as skills.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
DefinitionError: A tool is not decorated with ``@tool``, or two tools
|
|
31
|
+
share a name.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
limits: Default execution limits for this agent's skills. Fields it
|
|
35
|
+
sets override the library defaults; fields a skill or run sets
|
|
36
|
+
override it in turn.
|
|
37
|
+
instructions: Trusted instructions sent as the first ``system`` message
|
|
38
|
+
of every ``ctx.generate`` call. Never put untrusted content here.
|
|
39
|
+
model_settings: Default settings for this agent's model calls, merged
|
|
40
|
+
field by field like ``limits``.
|
|
41
|
+
model: The model passed to the constructor, or ``None``.
|
|
42
|
+
tools: The registered tools by name, read-only.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
limits: ClassVar[RunLimits] = RunLimits()
|
|
46
|
+
instructions: ClassVar[str | None] = None
|
|
47
|
+
model_settings: ClassVar[ModelSettings] = ModelSettings()
|
|
48
|
+
# Class defaults keep subclasses that do not call super().__init__() working.
|
|
49
|
+
model: Model | None = None
|
|
50
|
+
tools: Mapping[str, ToolDefinition] = MappingProxyType({})
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
model: Model | None = None,
|
|
56
|
+
tools: Iterable[Callable[..., Any]] = (),
|
|
57
|
+
) -> None:
|
|
58
|
+
self.model = model
|
|
59
|
+
self.tools = tools_by_name(tools)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def definition(cls) -> AgentDefinition:
|
|
63
|
+
"""Return the validated definition created by ``@agent``.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
DefinitionError: If this exact class was not decorated with ``@agent``.
|
|
67
|
+
"""
|
|
68
|
+
# Look only at this class so an undecorated subclass cannot silently
|
|
69
|
+
# reuse its parent's definition and skills.
|
|
70
|
+
definition = cls.__dict__.get(DEFINITION_ATTRIBUTE)
|
|
71
|
+
if not isinstance(definition, AgentDefinition):
|
|
72
|
+
raise DefinitionError(
|
|
73
|
+
f"Agent '{cls.__name__}' is not decorated with @agent"
|
|
74
|
+
)
|
|
75
|
+
return definition
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Per-run context passed to skills."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, overload
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from agentframe.runtime.generation import GenerationState
|
|
10
|
+
from agentframe.security.principal import Principal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class RunContext:
|
|
15
|
+
"""Per-run information passed to every skill. Never shared between runs.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
invocation_id: Library-generated identifier of this run.
|
|
19
|
+
principal: Verified caller identity, or ``None`` when none was supplied.
|
|
20
|
+
deadline: Event-loop time (``loop.time()``) when the run times out.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
invocation_id: str
|
|
24
|
+
principal: Principal | None
|
|
25
|
+
deadline: float
|
|
26
|
+
# Private per-run model state; the context is frozen but its accounting
|
|
27
|
+
# must change as the run makes model calls.
|
|
28
|
+
_generation: GenerationState = field(repr=False, compare=False)
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
async def generate(
|
|
32
|
+
self,
|
|
33
|
+
text: str,
|
|
34
|
+
*,
|
|
35
|
+
instructions: str | None = None,
|
|
36
|
+
tools: Iterable[Callable[..., Any]] = (),
|
|
37
|
+
) -> str: ...
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
async def generate[T: BaseModel](
|
|
41
|
+
self,
|
|
42
|
+
text: str,
|
|
43
|
+
*,
|
|
44
|
+
instructions: str | None = None,
|
|
45
|
+
output: type[T],
|
|
46
|
+
tools: Iterable[Callable[..., Any]] = (),
|
|
47
|
+
) -> T: ...
|
|
48
|
+
|
|
49
|
+
async def generate(
|
|
50
|
+
self,
|
|
51
|
+
text: str,
|
|
52
|
+
*,
|
|
53
|
+
instructions: str | None = None,
|
|
54
|
+
output: type[BaseModel] | None = None,
|
|
55
|
+
tools: Iterable[Callable[..., Any]] = (),
|
|
56
|
+
) -> str | BaseModel:
|
|
57
|
+
"""Generate text, or a validated ``output`` instance, with the agent's model.
|
|
58
|
+
|
|
59
|
+
The agent's ``instructions`` and then ``instructions`` are sent as
|
|
60
|
+
trusted ``system`` messages; ``text`` is sent as a separate ``user``
|
|
61
|
+
message and never merged into them. Keeping them separate does not
|
|
62
|
+
guarantee resistance to prompt injection.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
text: Untrusted content for the model, such as caller input.
|
|
66
|
+
instructions: Trusted instructions for this call only. Never put
|
|
67
|
+
untrusted content here.
|
|
68
|
+
output: A Pydantic ``BaseModel`` subclass. When given, the model
|
|
69
|
+
is asked for JSON matching its schema and the reply is
|
|
70
|
+
validated into an instance. Invalid output is never repaired
|
|
71
|
+
or re-requested.
|
|
72
|
+
tools: ``@tool`` functions to offer the model, each registered on
|
|
73
|
+
the agent and selected once. Omitted or empty offers no
|
|
74
|
+
tools; registered tools are never offered unless selected
|
|
75
|
+
here. The model's tool calls run one at a time, in its
|
|
76
|
+
order, and their results are sent back until it answers.
|
|
77
|
+
A call naming a tool that was not offered, or with invalid
|
|
78
|
+
arguments, runs nothing; the model is told what was wrong.
|
|
79
|
+
Tools whose ``requires_permissions`` the caller lacks are
|
|
80
|
+
not offered, and every call is authorized before it runs.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The model's complete answer, or an ``output`` instance when
|
|
84
|
+
``output`` is given. With tools, the answer after the tool calls.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
DefinitionError: ``output`` is not a ``BaseModel`` subclass, or is
|
|
88
|
+
a ``RootModel``, or a selected tool is not registered on the
|
|
89
|
+
agent or is selected twice. Raised before anything is sent.
|
|
90
|
+
UnsupportedCapabilityError: The agent has no model configured, or
|
|
91
|
+
the model lacks something the request needs, such as
|
|
92
|
+
structured output or tool calling. Raised before anything is
|
|
93
|
+
sent.
|
|
94
|
+
OutputValidationError: The structured reply is not valid JSON or
|
|
95
|
+
does not match ``output``, or a tool returned a value that
|
|
96
|
+
does not match its return annotation.
|
|
97
|
+
ModelRefusalError: The model refused, or a content filter blocked
|
|
98
|
+
its output.
|
|
99
|
+
IncompleteOutputError: The output was cut short at the token
|
|
100
|
+
limit or was empty. Partial text or objects are never returned;
|
|
101
|
+
for text cut short at the limit, the error's ``partial_text``
|
|
102
|
+
holds what arrived, and a skill may catch it and use it.
|
|
103
|
+
ProviderError: The model provider call failed.
|
|
104
|
+
PermissionDeniedError: The model called a tool the caller lacks
|
|
105
|
+
a required permission for, or the runtime's tool
|
|
106
|
+
authorization policy denied a call. The model is never told.
|
|
107
|
+
Also raised, before anything is sent, when an execution
|
|
108
|
+
policy model-request check blocks a request.
|
|
109
|
+
PolicyUnavailableError: The tool authorization policy or an
|
|
110
|
+
execution policy model-request check raised an error or did
|
|
111
|
+
not answer before the run's deadline.
|
|
112
|
+
LimitExceededError: A model call does not fit the run's limits,
|
|
113
|
+
the run has made ``max_tool_calls`` tool calls, or the run's
|
|
114
|
+
timeout expired during a model or tool call.
|
|
115
|
+
|
|
116
|
+
Exceptions raised by a tool propagate unchanged and fail the run;
|
|
117
|
+
their text is never sent to the model.
|
|
118
|
+
"""
|
|
119
|
+
if output is None:
|
|
120
|
+
return await self._generation.generate_text(self, text, instructions, tools)
|
|
121
|
+
return await self._generation.generate_output(
|
|
122
|
+
self, text, instructions, output, tools
|
|
123
|
+
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""The ``@agent``, ``@skill`` and ``@tool`` decorators."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from agentframe.authoring.agent import DEFINITION_ATTRIBUTE, Agent
|
|
7
|
+
from agentframe.authoring.definitions import (
|
|
8
|
+
TOOL_ATTRIBUTE,
|
|
9
|
+
AgentDefinition,
|
|
10
|
+
SkillDefinition,
|
|
11
|
+
SkillMetadata,
|
|
12
|
+
build_skill_definition,
|
|
13
|
+
build_tool_definition,
|
|
14
|
+
class_docstring,
|
|
15
|
+
)
|
|
16
|
+
from agentframe.errors import DefinitionError
|
|
17
|
+
from agentframe.models.base import ModelSettings
|
|
18
|
+
from agentframe.runtime.limits import RunLimits
|
|
19
|
+
|
|
20
|
+
_SKILL_ATTRIBUTE = "_skill_metadata"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# noinspection PyShadowingBuiltins
|
|
24
|
+
def skill[F: Callable[..., Any]](
|
|
25
|
+
id: str,
|
|
26
|
+
*,
|
|
27
|
+
name: str | None = None,
|
|
28
|
+
description: str | None = None,
|
|
29
|
+
tags: Iterable[str] = (),
|
|
30
|
+
requires_permissions: Iterable[str] = (),
|
|
31
|
+
limits: RunLimits | None = None,
|
|
32
|
+
model_settings: ModelSettings | None = None,
|
|
33
|
+
) -> Callable[[F], F]:
|
|
34
|
+
"""Mark an async ``Agent`` method as an advertised skill.
|
|
35
|
+
|
|
36
|
+
The method is inspected later by ``@agent``, once its class exists.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
id: Stable skill identifier that callers select.
|
|
40
|
+
name: Display name. Defaults to the title-cased method name.
|
|
41
|
+
description: What the skill does. Defaults to the method docstring.
|
|
42
|
+
tags: Descriptive discovery tags. At least one non-blank tag is
|
|
43
|
+
required (A2A 1.0); ``@agent`` rejects a skill without one.
|
|
44
|
+
requires_permissions: Permissions the caller must all hold.
|
|
45
|
+
limits: Execution limits for this skill. Each field it sets overrides
|
|
46
|
+
the agent's value; unset fields are inherited.
|
|
47
|
+
model_settings: Model settings for this skill. Each field it sets
|
|
48
|
+
overrides the agent's value; unset fields are inherited.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A decorator that records the metadata and returns the same function.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
DefinitionError: If ``id`` is empty, or ``tags`` or
|
|
55
|
+
``requires_permissions`` is a single string.
|
|
56
|
+
"""
|
|
57
|
+
if not id:
|
|
58
|
+
raise DefinitionError("Skill id must not be empty")
|
|
59
|
+
if isinstance(tags, str) or isinstance(requires_permissions, str):
|
|
60
|
+
raise DefinitionError(
|
|
61
|
+
f"Skill '{id}': tags and requires_permissions must be lists of strings"
|
|
62
|
+
)
|
|
63
|
+
metadata = SkillMetadata(
|
|
64
|
+
id=id,
|
|
65
|
+
name=name,
|
|
66
|
+
description=description,
|
|
67
|
+
tags=tuple(tags),
|
|
68
|
+
requires_permissions=frozenset(requires_permissions),
|
|
69
|
+
limits=limits,
|
|
70
|
+
model_settings=model_settings,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def decorator(func: F) -> F:
|
|
74
|
+
setattr(func, _SKILL_ATTRIBUTE, metadata)
|
|
75
|
+
return func
|
|
76
|
+
|
|
77
|
+
return decorator
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def agent[A: type[Agent]](
|
|
81
|
+
version: str, *, name: str | None = None, description: str | None = None
|
|
82
|
+
) -> Callable[[A], A]:
|
|
83
|
+
"""Describe an ``Agent`` subclass and validate its skills.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
version: Agent version string, for example ``"0.1.0"``.
|
|
87
|
+
name: Agent name. Defaults to the class name.
|
|
88
|
+
description: What the agent does. Defaults to the class docstring.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A decorator that stores the definition and returns the same class.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
DefinitionError: If the class or any of its skills is invalid.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def decorator(cls: A) -> A:
|
|
98
|
+
if not (isinstance(cls, type) and issubclass(cls, Agent)):
|
|
99
|
+
raise DefinitionError("@agent can only decorate Agent subclasses")
|
|
100
|
+
agent_name = name or cls.__name__
|
|
101
|
+
agent_description = description or class_docstring(cls)
|
|
102
|
+
if not agent_description:
|
|
103
|
+
raise DefinitionError(
|
|
104
|
+
f"Agent '{agent_name}' needs a description or docstring"
|
|
105
|
+
)
|
|
106
|
+
definition = AgentDefinition(
|
|
107
|
+
name=agent_name,
|
|
108
|
+
description=agent_description,
|
|
109
|
+
version=version,
|
|
110
|
+
skills=_collect_skills(agent_name, cls),
|
|
111
|
+
)
|
|
112
|
+
setattr(cls, DEFINITION_ATTRIBUTE, definition)
|
|
113
|
+
return cls
|
|
114
|
+
|
|
115
|
+
return decorator
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _collect_skills(agent_name: str, cls: type) -> dict[str, SkillDefinition]:
|
|
119
|
+
skill_methods = _skill_methods(agent_name, cls)
|
|
120
|
+
if not skill_methods:
|
|
121
|
+
raise DefinitionError(f"Agent '{agent_name}' has no skills")
|
|
122
|
+
|
|
123
|
+
skills: dict[str, SkillDefinition] = {}
|
|
124
|
+
for method in skill_methods.values():
|
|
125
|
+
metadata: SkillMetadata = getattr(method, _SKILL_ATTRIBUTE)
|
|
126
|
+
if metadata.id in skills:
|
|
127
|
+
raise DefinitionError(
|
|
128
|
+
f"Agent '{agent_name}': skill id '{metadata.id}' is used by both "
|
|
129
|
+
f"'{skills[metadata.id].method_name}' and '{method.__name__}'"
|
|
130
|
+
)
|
|
131
|
+
skills[metadata.id] = build_skill_definition(agent_name, method, metadata)
|
|
132
|
+
return skills
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _skill_methods(agent_name: str, cls: type) -> dict[str, Callable[..., Any]]:
|
|
136
|
+
"""Return skill methods by name, walking from the base class to ``cls``."""
|
|
137
|
+
methods: dict[str, Callable[..., Any]] = {}
|
|
138
|
+
for klass in reversed(cls.__mro__):
|
|
139
|
+
for attribute_name, value in vars(klass).items():
|
|
140
|
+
if hasattr(value, _SKILL_ATTRIBUTE):
|
|
141
|
+
methods[attribute_name] = value
|
|
142
|
+
elif attribute_name in methods:
|
|
143
|
+
raise DefinitionError(
|
|
144
|
+
f"Agent '{agent_name}': '{klass.__name__}.{attribute_name}' "
|
|
145
|
+
"overrides a skill without @skill"
|
|
146
|
+
)
|
|
147
|
+
return methods
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def tool[F: Callable[..., Any]](
|
|
151
|
+
*,
|
|
152
|
+
description: str | None = None,
|
|
153
|
+
side_effects: bool = False,
|
|
154
|
+
idempotent: bool = False,
|
|
155
|
+
requires_permissions: Iterable[str] = (),
|
|
156
|
+
) -> Callable[[F], F]:
|
|
157
|
+
"""Mark a typed function as a tool a model may call.
|
|
158
|
+
|
|
159
|
+
Always call it with parentheses: ``@tool()`` or ``@tool(description=...)``.
|
|
160
|
+
The function is validated immediately and stays directly callable. Sync
|
|
161
|
+
functions run inline on the event loop, so use ``async def`` for any I/O.
|
|
162
|
+
Decorating grants nothing: the tool must still be registered on an agent
|
|
163
|
+
(``Agent(tools=[...])``) and selected by ``ctx.generate(..., tools=[...])``.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
description: What the tool does, shown to the model. Defaults to the
|
|
167
|
+
function docstring; one of them is required.
|
|
168
|
+
side_effects: Whether a call has consequential effects, such as
|
|
169
|
+
cancelling an order.
|
|
170
|
+
idempotent: Whether repeating a call with the same arguments has no
|
|
171
|
+
further effect. Not a retry grant: tools are never auto-retried.
|
|
172
|
+
requires_permissions: Permissions the caller must all hold. A tool
|
|
173
|
+
the caller lacks any of is not offered to the model and fails the
|
|
174
|
+
run if the model calls it anyway.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A decorator that attaches a ``ToolDefinition`` and returns the same
|
|
178
|
+
function (or ``staticmethod``).
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
DefinitionError: If the function breaks a tool rule: it is a lambda or
|
|
182
|
+
method, is a generator, has an invalid name, unannotated,
|
|
183
|
+
``*args``/``**kwargs`` or positional-only parameters, an
|
|
184
|
+
unsupported type, or no description; or if
|
|
185
|
+
``requires_permissions`` is a single string.
|
|
186
|
+
"""
|
|
187
|
+
if isinstance(requires_permissions, str):
|
|
188
|
+
raise DefinitionError("Tool requires_permissions must be a list of strings")
|
|
189
|
+
permissions = frozenset(requires_permissions)
|
|
190
|
+
|
|
191
|
+
def decorator(func: F) -> F:
|
|
192
|
+
definition = build_tool_definition(
|
|
193
|
+
func,
|
|
194
|
+
description=description,
|
|
195
|
+
side_effects=side_effects,
|
|
196
|
+
idempotent=idempotent,
|
|
197
|
+
requires_permissions=permissions,
|
|
198
|
+
)
|
|
199
|
+
setattr(definition.function, TOOL_ATTRIBUTE, definition)
|
|
200
|
+
return func
|
|
201
|
+
|
|
202
|
+
return decorator
|