pyagentic-core 1.2.1__tar.gz → 1.3.0__tar.gz
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.
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/PKG-INFO +2 -1
- pyagentic_core-1.3.0/pyagentic/__init__.py +15 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/_agent.py +82 -13
- pyagentic_core-1.3.0/pyagentic/_base/_metaclasses.py +378 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/_params.py +1 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/_tool.py +1 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/_validation.py +1 -4
- pyagentic_core-1.3.0/pyagentic/_utils/_typing.py +97 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/models/response.py +10 -12
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic_core.egg-info/PKG-INFO +2 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic_core.egg-info/requires.txt +1 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyproject.toml +3 -1
- pyagentic_core-1.2.1/pyagentic/__init__.py +0 -6
- pyagentic_core-1.2.1/pyagentic/_base/_metaclasses.py +0 -179
- pyagentic_core-1.2.1/pyagentic/_utils/_typing.py +0 -73
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/LICENSE +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/README.md +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/_context.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/_base/_resolver.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/logging.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic/updates.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic_core.egg-info/SOURCES.txt +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyagentic-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Build LLM Agents in a Pythonic way
|
|
5
5
|
Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,7 @@ Requires-Dist: taskipy>=1.14.1
|
|
|
17
17
|
Requires-Dist: openai>=1.99.3
|
|
18
18
|
Requires-Dist: pydantic>=2.11.7
|
|
19
19
|
Requires-Dist: typeguard>=4.4.4
|
|
20
|
+
Requires-Dist: c3linearize>=0.1.0
|
|
20
21
|
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# PyAgentic
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pyagentic._base._agent import Agent, AgentExtension
|
|
2
|
+
from pyagentic._base._params import Param, ParamInfo
|
|
3
|
+
from pyagentic._base._tool import tool
|
|
4
|
+
from pyagentic._base._context import computed_context, ContextRef, ContextItem
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Agent",
|
|
8
|
+
"AgentExtension",
|
|
9
|
+
"Param",
|
|
10
|
+
"ParamInfo",
|
|
11
|
+
"tool",
|
|
12
|
+
"computed_context",
|
|
13
|
+
"ContextRef",
|
|
14
|
+
"ContextItem",
|
|
15
|
+
]
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import json
|
|
3
3
|
import openai
|
|
4
|
-
from
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Callable, Any, TypeVar, ClassVar, Type, Self, dataclass_transform
|
|
5
6
|
|
|
6
7
|
from pyagentic.logging import get_logger
|
|
7
|
-
from pyagentic._base.
|
|
8
|
+
from pyagentic._base._params import ParamInfo
|
|
9
|
+
from pyagentic._base._tool import _ToolDefinition, tool
|
|
8
10
|
from pyagentic._base._context import ContextItem
|
|
9
11
|
from pyagentic._base._metaclasses import AgentMeta
|
|
10
12
|
|
|
@@ -25,6 +27,29 @@ async def _safe_run(fn, *args, **kwargs):
|
|
|
25
27
|
return result
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
@dataclass_transform(field_specifiers=(ContextItem,))
|
|
31
|
+
class AgentExtension:
|
|
32
|
+
"""Inherit this in any mixin that contributes fields to the Agent __init__."""
|
|
33
|
+
|
|
34
|
+
__annotations__: dict[str, Any] = {}
|
|
35
|
+
|
|
36
|
+
def __init_subclass__(cls, **kwargs):
|
|
37
|
+
super().__init_subclass__(**kwargs)
|
|
38
|
+
|
|
39
|
+
# Merge annotations from all AgentExtension bases (oldest first),
|
|
40
|
+
# then let the subclass' own annotations win on key conflicts.
|
|
41
|
+
merged: dict[str, Any] = {}
|
|
42
|
+
for base in reversed(cls.__mro__[1:]): # skip cls, walk up towards object
|
|
43
|
+
if issubclass(base, AgentExtension):
|
|
44
|
+
ann = getattr(base, "__annotations__", None)
|
|
45
|
+
if ann:
|
|
46
|
+
merged.update(ann)
|
|
47
|
+
|
|
48
|
+
merged.update(getattr(cls, "__annotations__", {}))
|
|
49
|
+
# Assign a fresh dict so we don't mutate a base class' annotations
|
|
50
|
+
cls.__annotations__ = dict(merged)
|
|
51
|
+
|
|
52
|
+
|
|
28
53
|
class Agent(metaclass=AgentMeta):
|
|
29
54
|
__abstract_base__ = ClassVar[True]
|
|
30
55
|
"""
|
|
@@ -48,9 +73,12 @@ class Agent(metaclass=AgentMeta):
|
|
|
48
73
|
__tool_defs__: ClassVar[dict[str, _ToolDefinition]]
|
|
49
74
|
__context_attrs__: ClassVar[dict[str, tuple[TypeVar, ContextItem]]]
|
|
50
75
|
__system_message__: ClassVar[str]
|
|
76
|
+
__description__: ClassVar[str]
|
|
51
77
|
__input_template__: ClassVar[str] = None
|
|
52
78
|
__response_model__: ClassVar[Type[AgentResponse]] = None
|
|
53
79
|
__tool_response_models__: ClassVar[dict[str, Type[ToolResponse]]]
|
|
80
|
+
__linked_agents__: ClassVar[dict[str, Type[Self]]]
|
|
81
|
+
__call_params__: ClassVar[dict[str, tuple[TypeVar, ParamInfo]]]
|
|
54
82
|
|
|
55
83
|
# Base Attributes
|
|
56
84
|
model: str
|
|
@@ -60,9 +88,22 @@ class Agent(metaclass=AgentMeta):
|
|
|
60
88
|
def __post_init__(self):
|
|
61
89
|
self.client: openai.AsyncOpenAI = openai.AsyncOpenAI(api_key=self.api_key)
|
|
62
90
|
|
|
91
|
+
async def _process_agent_call(self, tool_call) -> AgentResponse:
|
|
92
|
+
logger.info(f"Calling {tool_call.name} with kwargs: {tool_call.arguments}")
|
|
93
|
+
self.context._messages.append(tool_call)
|
|
94
|
+
try:
|
|
95
|
+
agent = getattr(self, tool_call.name)
|
|
96
|
+
kwargs = json.loads(tool_call.arguments)
|
|
97
|
+
response = await agent(**kwargs)
|
|
98
|
+
result = f"Agent {tool_call.name}: {response.final_output}"
|
|
99
|
+
except Exception as e:
|
|
100
|
+
result = f"Agent `{tool_call.name}` failed: {e}. Please kindly state to the user that is failed, provide context, and ask if they want to try again." # noqa E501
|
|
101
|
+
self.context._messages.append(
|
|
102
|
+
{"type": "function_call_output", "call_id": tool_call.call_id, "output": result}
|
|
103
|
+
)
|
|
104
|
+
return response
|
|
105
|
+
|
|
63
106
|
async def _process_tool_call(self, tool_call) -> ToolResponse:
|
|
64
|
-
if tool_call.type != "function_call":
|
|
65
|
-
return False
|
|
66
107
|
self.context._messages.append(tool_call)
|
|
67
108
|
logger.info(f"Calling {tool_call.name} with kwargs: {tool_call.arguments}")
|
|
68
109
|
# Lookup the bound method
|
|
@@ -98,8 +139,8 @@ class Agent(metaclass=AgentMeta):
|
|
|
98
139
|
self.context._messages.append(
|
|
99
140
|
{"type": "function_call_output", "call_id": tool_call.call_id, "output": result}
|
|
100
141
|
)
|
|
101
|
-
|
|
102
|
-
return
|
|
142
|
+
ToolResponseModel = self.__tool_response_models__[tool_call.name]
|
|
143
|
+
return ToolResponseModel(
|
|
103
144
|
raw_kwargs=tool_call.arguments, call_depth=0, output=result, **compiled_args
|
|
104
145
|
)
|
|
105
146
|
|
|
@@ -110,6 +151,9 @@ class Agent(metaclass=AgentMeta):
|
|
|
110
151
|
# Check if any of the tool params use a ContextRef
|
|
111
152
|
# convert to openai schema
|
|
112
153
|
tool_defs.append(tool_def.to_openai(self.context))
|
|
154
|
+
for name, agent in self.__linked_agents__.items():
|
|
155
|
+
tool_def = agent.get_tool_definition(name)
|
|
156
|
+
tool_defs.append(tool_def.to_openai(self.context))
|
|
113
157
|
return tool_defs
|
|
114
158
|
|
|
115
159
|
async def run(self, input_: str) -> str:
|
|
@@ -168,9 +212,16 @@ class Agent(metaclass=AgentMeta):
|
|
|
168
212
|
|
|
169
213
|
# Dispatch any tool calls
|
|
170
214
|
tool_responses = []
|
|
215
|
+
agent_responses = []
|
|
171
216
|
for tool_call in tool_calls:
|
|
172
|
-
|
|
173
|
-
|
|
217
|
+
if tool_call.type != "function_call":
|
|
218
|
+
continue
|
|
219
|
+
elif tool_call.name in self.__tool_defs__:
|
|
220
|
+
tool_response = await self._process_tool_call(tool_call)
|
|
221
|
+
tool_responses.append(tool_response)
|
|
222
|
+
elif tool_call.name in self.__linked_agents__:
|
|
223
|
+
agent_response = await self._process_agent_call(tool_call)
|
|
224
|
+
agent_responses.append(agent_response)
|
|
174
225
|
|
|
175
226
|
# If tools ran, re-invoke LLM for natural reply
|
|
176
227
|
if tool_responses:
|
|
@@ -198,8 +249,26 @@ class Agent(metaclass=AgentMeta):
|
|
|
198
249
|
if self.emitter:
|
|
199
250
|
await _safe_run(self.emitter, AiUpdate(status=Status.SUCCEDED, message=ai_message))
|
|
200
251
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
252
|
+
response_fields = {"final_output": ai_message}
|
|
253
|
+
if self.__tool_defs__:
|
|
254
|
+
response_fields["tool_responses"] = tool_responses
|
|
255
|
+
if self.__linked_agents__:
|
|
256
|
+
response_fields["agent_responses"] = agent_responses
|
|
257
|
+
|
|
258
|
+
return self.__response_model__(**response_fields)
|
|
259
|
+
|
|
260
|
+
async def __call__(self, user_input: str):
|
|
261
|
+
return await self.run(input_=user_input)
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def get_tool_definition(cls, name: str) -> _ToolDefinition:
|
|
265
|
+
desc = getattr(cls, "__description__", "") or ""
|
|
266
|
+
|
|
267
|
+
# fresh async wrapper so each class gets its own function object
|
|
268
|
+
@wraps(cls.__call__)
|
|
269
|
+
async def _invoke(self, *args, **kwargs):
|
|
270
|
+
return await cls.__call__(self, *args, **kwargs)
|
|
271
|
+
|
|
272
|
+
td = tool(desc)(_invoke).__tool_def__ # decorator attaches metadata to the wrapper
|
|
273
|
+
td.name = name
|
|
274
|
+
return td
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import threading
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import dataclass_transform, TypeVar, Mapping
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from collections import ChainMap
|
|
7
|
+
from c3linearize import linearize
|
|
8
|
+
from typeguard import check_type, TypeCheckError
|
|
9
|
+
|
|
10
|
+
from pyagentic._base._validation import _AgentConstructionValidator
|
|
11
|
+
from pyagentic._base._exceptions import SystemMessageNotDeclared, UnexpectedContextItemType
|
|
12
|
+
from pyagentic._base._context import _AgentContext, ContextItem, computed_context
|
|
13
|
+
from pyagentic._base._tool import _ToolDefinition
|
|
14
|
+
|
|
15
|
+
from pyagentic.models.response import AgentResponse, ToolResponse
|
|
16
|
+
|
|
17
|
+
from pyagentic._utils._typing import analyze_type
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Placeholder class for Agent type annotation
|
|
21
|
+
# Can't import actual agent as it would cause a circular import error
|
|
22
|
+
class Agent:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass_transform(field_specifiers=(ContextItem,))
|
|
27
|
+
class AgentMeta(type):
|
|
28
|
+
"""
|
|
29
|
+
Metaclass that applies only to Agent subclasses:
|
|
30
|
+
- Ensures @system_message was declared
|
|
31
|
+
- Collects @tool definitions and ContextItem attributes
|
|
32
|
+
- Initializes class __tool_defs__ and __context_items__
|
|
33
|
+
- Dynamically injects an __init__ signature based on class __annotations__
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__BaseAgent__ = None
|
|
37
|
+
_lock = threading.RLock()
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _inherited_namespace_from_bases(bases: tuple[type, ...]) -> dict[str, object]:
|
|
41
|
+
"""
|
|
42
|
+
Build the inherited (raw) namespace you'd see via MRO lookup for a class with `bases`.
|
|
43
|
+
Returns a dict where earlier bases in the MRO win.
|
|
44
|
+
"""
|
|
45
|
+
# Build a graph: any hashable node -> list of parents.
|
|
46
|
+
# We'll use a sentinel NEW for the (not-yet-created) class.
|
|
47
|
+
NEW = object()
|
|
48
|
+
graph = {NEW: list(bases)}
|
|
49
|
+
|
|
50
|
+
# Add all reachable base classes and their parents.
|
|
51
|
+
stack = list(bases)
|
|
52
|
+
seen = set()
|
|
53
|
+
while stack:
|
|
54
|
+
cls = stack.pop()
|
|
55
|
+
if cls in seen or cls is object:
|
|
56
|
+
continue
|
|
57
|
+
seen.add(cls)
|
|
58
|
+
parents = [b for b in cls.__bases__ if b is not object]
|
|
59
|
+
graph[cls] = parents
|
|
60
|
+
stack.extend(parents)
|
|
61
|
+
|
|
62
|
+
# C3 linearize starting from NEW
|
|
63
|
+
order = linearize(graph)[NEW]
|
|
64
|
+
mro_bases = [c for c in order if isinstance(c, type) and c is not object]
|
|
65
|
+
|
|
66
|
+
# Chain the raw class dicts in MRO precedence (leftmost wins)
|
|
67
|
+
return dict(ChainMap(*(vars(c) for c in mro_bases)))
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _extract_tool_defs(namespace) -> Mapping[str, _ToolDefinition]:
|
|
71
|
+
"""
|
|
72
|
+
Extracts tool definitions from a given namespace
|
|
73
|
+
|
|
74
|
+
Any method with the `@tool` descriptor will be attached to the `__tool_defs__` class
|
|
75
|
+
attribute
|
|
76
|
+
"""
|
|
77
|
+
tools: dict[str, _ToolDefinition] = {}
|
|
78
|
+
for attr_name, attr_value in namespace.items():
|
|
79
|
+
if hasattr(attr_value, "__tool_def__"):
|
|
80
|
+
tools[attr_name] = attr_value.__tool_def__
|
|
81
|
+
return MappingProxyType(tools)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _extract_annotations(namespace, bases) -> dict[str, TypeVar]:
|
|
85
|
+
"""
|
|
86
|
+
Extracts all annotations from current class and all its subclasses. Combines them into
|
|
87
|
+
one dictionary, with class order respected (subclasses overide parent classes.)
|
|
88
|
+
"""
|
|
89
|
+
annotations = {}
|
|
90
|
+
for base in reversed(bases):
|
|
91
|
+
if hasattr(base, "__annotations__"):
|
|
92
|
+
for name, type_ in base.__annotations__.items():
|
|
93
|
+
if not name.startswith("__"):
|
|
94
|
+
annotations[name] = type_
|
|
95
|
+
for name, type_ in namespace.get("__annotations__", {}).items():
|
|
96
|
+
if not name.startswith("__"):
|
|
97
|
+
annotations[name] = type_
|
|
98
|
+
return annotations
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _extract_context_attrs(
|
|
102
|
+
annotations, namespace
|
|
103
|
+
) -> Mapping[str, tuple[TypeVar, ContextItem]]:
|
|
104
|
+
"""
|
|
105
|
+
Extracts any class field from annotations and namespace where the value is that of
|
|
106
|
+
`ContextItem`, these will later be appeneded to the agents context. This will return
|
|
107
|
+
both the type and the user defined context item.
|
|
108
|
+
"""
|
|
109
|
+
context_attrs: dict[str, tuple[TypeVar, ContextItem]] = {}
|
|
110
|
+
for attr_name, attr_type in annotations.items():
|
|
111
|
+
default = namespace.get(attr_name, None)
|
|
112
|
+
if isinstance(default, ContextItem):
|
|
113
|
+
context_attrs[attr_name] = (attr_type, default)
|
|
114
|
+
|
|
115
|
+
for name, value in namespace.items():
|
|
116
|
+
if getattr(value, "_is_context", False):
|
|
117
|
+
context_attrs[name] = (computed_context, value)
|
|
118
|
+
return MappingProxyType(context_attrs)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _extract_linked_agents(annotations, Agent) -> Mapping[str, "Agent"]:
|
|
122
|
+
"""
|
|
123
|
+
Extracts any class field from annotations and namespace where the value is that of
|
|
124
|
+
`ContextItem`, these will later be appeneded to the agents context. This will return
|
|
125
|
+
both the type and the user defined context item.
|
|
126
|
+
"""
|
|
127
|
+
linked_agents: dict[str, "Agent"] = {}
|
|
128
|
+
for attr_name, attr_type in annotations.items():
|
|
129
|
+
type_info = analyze_type(attr_type, Agent)
|
|
130
|
+
if type_info.has_forward_ref:
|
|
131
|
+
msg = (
|
|
132
|
+
f"Forward reference for agents are unsupported: '{attr_name}': {attr_type!r}. "
|
|
133
|
+
"Make sure the forward ref was not used for an agent, or a TypeError may occur"
|
|
134
|
+
)
|
|
135
|
+
warnings.warn(msg, RuntimeWarning, stacklevel=2)
|
|
136
|
+
elif type_info.is_subclass:
|
|
137
|
+
linked_agents[attr_name] = attr_type
|
|
138
|
+
|
|
139
|
+
return MappingProxyType(linked_agents)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _build_init_signature(cls) -> inspect.Signature:
|
|
143
|
+
"""
|
|
144
|
+
Build __init__ signature with all non-default (required) params
|
|
145
|
+
before any defaulted (optional) params.
|
|
146
|
+
"""
|
|
147
|
+
self_param = inspect.Parameter("self", inspect.Parameter.POSITIONAL_ONLY)
|
|
148
|
+
|
|
149
|
+
required: list[inspect.Parameter] = []
|
|
150
|
+
optional: list[inspect.Parameter] = []
|
|
151
|
+
agents: list[inspect.Parameter] = [] # Agents go last in signature for better order
|
|
152
|
+
|
|
153
|
+
for field_name, field_type in cls.__annotations__.items():
|
|
154
|
+
if field_name in cls.__context_attrs__:
|
|
155
|
+
param = inspect.Parameter(
|
|
156
|
+
field_name,
|
|
157
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
158
|
+
default=cls.__context_attrs__[field_name][1].get_default_value(),
|
|
159
|
+
annotation=field_type,
|
|
160
|
+
)
|
|
161
|
+
optional.append(param)
|
|
162
|
+
elif field_name in cls.__linked_agents__:
|
|
163
|
+
# Treat linked agents as optional by default
|
|
164
|
+
param = inspect.Parameter(
|
|
165
|
+
field_name,
|
|
166
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
167
|
+
default=None,
|
|
168
|
+
annotation=field_type,
|
|
169
|
+
)
|
|
170
|
+
agents.append(param)
|
|
171
|
+
else:
|
|
172
|
+
default = getattr(cls, field_name, inspect._empty)
|
|
173
|
+
param = inspect.Parameter(
|
|
174
|
+
field_name,
|
|
175
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
176
|
+
default=default,
|
|
177
|
+
annotation=field_type,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if default is inspect._empty:
|
|
181
|
+
required.append(param)
|
|
182
|
+
else:
|
|
183
|
+
optional.append(param)
|
|
184
|
+
|
|
185
|
+
return inspect.Signature([self_param, *required, *optional, *agents])
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _build_init(sig):
|
|
189
|
+
"""
|
|
190
|
+
Builds the init function for the class. This init will automatically have user-defined
|
|
191
|
+
context items as arguements, allow for easy initialization of agents for a variety
|
|
192
|
+
of different tasks.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(self, *args, **kwargs):
|
|
196
|
+
|
|
197
|
+
# -------- ContextClass Construction --------------------
|
|
198
|
+
ContextClass = _AgentContext.make_ctx_class(
|
|
199
|
+
name=self.__class__.__name__, ctx_map=self.__context_attrs__
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
compiled = {}
|
|
203
|
+
for attr_name, (attr_type, attr_default) in self.__context_attrs__.items():
|
|
204
|
+
# Skip compted contexts, this validaiton will happen with the validator
|
|
205
|
+
# using a dry run with supplied default values
|
|
206
|
+
if attr_type == computed_context:
|
|
207
|
+
continue
|
|
208
|
+
# Add all ContextItems to the kwargs, checking type as it goes
|
|
209
|
+
if attr_name in kwargs:
|
|
210
|
+
val = kwargs[attr_name]
|
|
211
|
+
try:
|
|
212
|
+
check_type(val, attr_type)
|
|
213
|
+
except TypeCheckError:
|
|
214
|
+
raise UnexpectedContextItemType(
|
|
215
|
+
name=attr_name, expected=attr_type, recieved=type(val)
|
|
216
|
+
)
|
|
217
|
+
compiled[attr_name] = val
|
|
218
|
+
else:
|
|
219
|
+
compiled[attr_name] = attr_default.get_default_value()
|
|
220
|
+
|
|
221
|
+
self.context = ContextClass(
|
|
222
|
+
instructions=self.__system_message__,
|
|
223
|
+
input_template=self.__input_template__,
|
|
224
|
+
**compiled,
|
|
225
|
+
)
|
|
226
|
+
# ------------- Retrieve Linked Agents -------------------
|
|
227
|
+
for agent_name in self.__linked_agents__.keys():
|
|
228
|
+
agent_instance = kwargs.get(agent_name, None)
|
|
229
|
+
compiled[agent_name] = agent_instance
|
|
230
|
+
|
|
231
|
+
bound = sig.bind(self, *args, **(kwargs | compiled))
|
|
232
|
+
# Add all other arguements to instance
|
|
233
|
+
for name, val in list(bound.arguments.items())[1:]: # skip 'self'
|
|
234
|
+
if name in self.__context_attrs__:
|
|
235
|
+
continue
|
|
236
|
+
setattr(self, name, val)
|
|
237
|
+
|
|
238
|
+
self.__post_init__()
|
|
239
|
+
|
|
240
|
+
__init__.__signature__ = sig
|
|
241
|
+
__init__.__annotations__ = {
|
|
242
|
+
p.name: p.annotation for p in sig.parameters.values() if p.name != "self"
|
|
243
|
+
}
|
|
244
|
+
return __init__
|
|
245
|
+
|
|
246
|
+
def __new__(mcs, name, bases, namespace, **kwargs):
|
|
247
|
+
"""
|
|
248
|
+
This metaclass is attached to the Agent base class, so that when a new subclass of Agent
|
|
249
|
+
is created, then this class will automatically set up class variables that define
|
|
250
|
+
the functionality of the agent.
|
|
251
|
+
|
|
252
|
+
- __tool_defs__: dictionary holding all tool defintions registered by @tool
|
|
253
|
+
- __context_attrs__: dictionary holding tuple of type and item for all attributes
|
|
254
|
+
that are either have a default of ContextItem or use @computed_context
|
|
255
|
+
- __tool_response_models__: dictionary holding pydantic response models for each tool
|
|
256
|
+
- __response_model__: The response model of the current agent that is being built
|
|
257
|
+
|
|
258
|
+
Inhertance is repected in MRO order. Tools, context attributes, computed contexts and
|
|
259
|
+
linked agents can all be inherited, from other agents or mixins.
|
|
260
|
+
__system_message__ and __input_template__ are *not* inherited
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
Create an inherited namespace by combining all bases in MRO order.
|
|
265
|
+
This uses c3linearize to determine the order, allowing uses to extend other Agents
|
|
266
|
+
and / or any mixins.
|
|
267
|
+
Mixins are classes that do not extend Agent, but can offer Agent attributes, like
|
|
268
|
+
tools, context items, and/or linked agents
|
|
269
|
+
"""
|
|
270
|
+
inherited_namespace = mcs._inherited_namespace_from_bases(bases)
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
Declare the new Agent subclass.
|
|
274
|
+
If this is the base agent being declared (usually on import), then the initializtion of
|
|
275
|
+
tools, context items, etc.. will be skipped, and this class will be stored in the meta
|
|
276
|
+
for future use.
|
|
277
|
+
All other Agent subclasses will have __abstract_base__ marked as False, so that future
|
|
278
|
+
implementions "know" it is not the base.
|
|
279
|
+
Since system message is not inherited, an exception is raised if the user does not
|
|
280
|
+
supply one
|
|
281
|
+
"""
|
|
282
|
+
with mcs._lock:
|
|
283
|
+
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
284
|
+
# If it is a base Agent, then return
|
|
285
|
+
if namespace.get("__abstract_base__", False):
|
|
286
|
+
mcs.__BaseAgent__ = cls
|
|
287
|
+
return cls
|
|
288
|
+
cls.__abstract_base__ = False
|
|
289
|
+
# Verify system message is set
|
|
290
|
+
if "__system_message__" not in namespace:
|
|
291
|
+
raise SystemMessageNotDeclared()
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
Extract and attach Agent attributes
|
|
295
|
+
|
|
296
|
+
__tool_defs__: Tool definitions (from any method marked with an @tool decorator) are
|
|
297
|
+
extracted from both the current namespace and the MRO ordered inherited namespace
|
|
298
|
+
|
|
299
|
+
__annotations__: Python annotations are extracted using the namespace and the inherited
|
|
300
|
+
namespace
|
|
301
|
+
|
|
302
|
+
__context_attrs__: Context attributes (from any class attribute with a ContextItem in
|
|
303
|
+
the namespace, or any method marked with a @computed_context decorator) are extracted
|
|
304
|
+
from both the current namespace and the inherited namespace. This also needs the
|
|
305
|
+
classes annotations, in order to attach the annotation to the context attribute for
|
|
306
|
+
later validation
|
|
307
|
+
|
|
308
|
+
__linked_agents__: Linked agents work a bit differently, since they cannot have default
|
|
309
|
+
values, the namespaces are not used. Instead, it relies on the MRO ordered annotations
|
|
310
|
+
to build a dict of any agents are are linked.
|
|
311
|
+
"""
|
|
312
|
+
tool_defs = mcs._extract_tool_defs(inherited_namespace | namespace)
|
|
313
|
+
annotations = mcs._extract_annotations(inherited_namespace | namespace, bases)
|
|
314
|
+
context_attrs = mcs._extract_context_attrs(annotations, inherited_namespace | namespace)
|
|
315
|
+
linked_agents = mcs._extract_linked_agents(annotations, mcs.__BaseAgent__)
|
|
316
|
+
with mcs._lock:
|
|
317
|
+
cls.__tool_defs__ = tool_defs
|
|
318
|
+
cls.__annotations__ = annotations
|
|
319
|
+
cls.__context_attrs__ = context_attrs
|
|
320
|
+
cls.__linked_agents__ = linked_agents
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
Create response models. Response models are created on class declaration to give the agent
|
|
324
|
+
a predetermined output. This allows developers to know exactly what the output of the
|
|
325
|
+
agent will be, before even creating an instance of the agent.
|
|
326
|
+
|
|
327
|
+
__tool_response_models__: All tools have their own pydantic response model, these need
|
|
328
|
+
to be build using their Tool Definition. This needs to be stored on the agent, so that
|
|
329
|
+
it can create instances of the tool response after calling the tool.
|
|
330
|
+
|
|
331
|
+
__response_model__: The final pydantic response model of the agent. This is constructed
|
|
332
|
+
using the tool definition models, and any response model of linked agents.
|
|
333
|
+
"""
|
|
334
|
+
tool_response_models = {
|
|
335
|
+
tool_name: ToolResponse.from_tool_def(tool_def)
|
|
336
|
+
for tool_name, tool_def in cls.__tool_defs__.items()
|
|
337
|
+
}
|
|
338
|
+
tool_response_model_list = list(tool_response_models.values())
|
|
339
|
+
linked_agent_response_model_list = [
|
|
340
|
+
agent.__response_model__ for agent in cls.__linked_agents__.values()
|
|
341
|
+
]
|
|
342
|
+
ResponseModel = AgentResponse.from_tool_defs(
|
|
343
|
+
agent_name=cls.__name__,
|
|
344
|
+
tool_response_models=tool_response_model_list,
|
|
345
|
+
linked_agents_response_models=linked_agent_response_model_list,
|
|
346
|
+
)
|
|
347
|
+
with mcs._lock:
|
|
348
|
+
cls.__tool_response_models__ = MappingProxyType(tool_response_models)
|
|
349
|
+
cls.__response_model__ = ResponseModel
|
|
350
|
+
|
|
351
|
+
"""
|
|
352
|
+
Build the new init
|
|
353
|
+
|
|
354
|
+
The base init just accepts *args and **kwargs, this is changed by building a new init
|
|
355
|
+
signature. The new signature combines any context attributes, agents, and any other
|
|
356
|
+
dataclass field in the following order:
|
|
357
|
+
1. required: any dataclass field with no value in the namespace
|
|
358
|
+
2. optional: mostly context attributes, can include dataclass fields with values in
|
|
359
|
+
the namespace
|
|
360
|
+
3. linked agents: These come last in order to keep a clear order in the init. They
|
|
361
|
+
all default to None. So if the user does not supply a linked agent, then the
|
|
362
|
+
parent agent will ignore it.
|
|
363
|
+
|
|
364
|
+
The new init function then creates a new AgentContext class, using the context attributes
|
|
365
|
+
as its attributes. It loads in all the context items in it using the specified defaults
|
|
366
|
+
and attaches it to the agent.
|
|
367
|
+
After that it attaches any linked agents to the parent agent.
|
|
368
|
+
"""
|
|
369
|
+
sig = mcs._build_init_signature(cls)
|
|
370
|
+
__init__ = mcs._build_init(sig)
|
|
371
|
+
with mcs._lock:
|
|
372
|
+
cls.__init__ = __init__
|
|
373
|
+
|
|
374
|
+
"""
|
|
375
|
+
Validate and return
|
|
376
|
+
"""
|
|
377
|
+
_AgentConstructionValidator(cls).validate()
|
|
378
|
+
return cls
|
|
@@ -103,7 +103,7 @@ class Param:
|
|
|
103
103
|
case TypeCategory.LIST_PRIMITIVE:
|
|
104
104
|
setattr(self, field_name, value)
|
|
105
105
|
case TypeCategory.SUBCLASS:
|
|
106
|
-
value = field_type(**value) if type(value)
|
|
106
|
+
value = field_type(**value) if type(value) is dict else value
|
|
107
107
|
setattr(self, field_name, value)
|
|
108
108
|
case TypeCategory.LIST_SUBCLASS:
|
|
109
109
|
listed_value = (
|
|
@@ -142,7 +142,7 @@ def tool(
|
|
|
142
142
|
# Check return type
|
|
143
143
|
types = get_type_hints(fn)
|
|
144
144
|
return_type = types.pop("return", None)
|
|
145
|
-
if return_type != str:
|
|
145
|
+
if return_type != str and fn.__name__ != "__call__":
|
|
146
146
|
raise ToolDeclarationFailed(
|
|
147
147
|
tool_name=fn.__name__, message="Method must have a return type of `str`"
|
|
148
148
|
)
|
|
@@ -27,10 +27,7 @@ class _AgentConstructionValidator:
|
|
|
27
27
|
def __init__(self, AgentClass: Type["Agent"]):
|
|
28
28
|
self.problems = []
|
|
29
29
|
self.AgentClass = AgentClass
|
|
30
|
-
self.sample_agent = self.AgentClass(
|
|
31
|
-
model="testing",
|
|
32
|
-
api_key="validation",
|
|
33
|
-
)
|
|
30
|
+
self.sample_agent = self.AgentClass(model="validation", api_key="validation")
|
|
34
31
|
|
|
35
32
|
def validate(self):
|
|
36
33
|
"""
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import get_origin, get_args, Any, Optional, ForwardRef
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PRIMITIVES = (bool, str, int, float, type(None))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_primitive(type_: Any) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Helper function to check if a type is a python primitive
|
|
12
|
+
"""
|
|
13
|
+
return type_ in PRIMITIVES
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TypeCategory(Enum):
|
|
17
|
+
PRIMITIVE = "primitive"
|
|
18
|
+
LIST_PRIMITIVE = "list_primitive"
|
|
19
|
+
SUBCLASS = "subclass"
|
|
20
|
+
LIST_SUBCLASS = "list_subclass"
|
|
21
|
+
UNSUPPORTED = "unsupported"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class TypeInfo:
|
|
26
|
+
"""Normalized information about a type"""
|
|
27
|
+
|
|
28
|
+
category: TypeCategory
|
|
29
|
+
base_type: type
|
|
30
|
+
inner_type: Optional[type] = None # For list types
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_list(self) -> bool:
|
|
34
|
+
return self.category in [TypeCategory.LIST_PRIMITIVE, TypeCategory.LIST_SUBCLASS]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_subclass(self) -> bool:
|
|
38
|
+
return self.category in [TypeCategory.SUBCLASS, TypeCategory.LIST_SUBCLASS]
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def effective_type(self) -> type:
|
|
42
|
+
"""Returns the type to work with (inner type for lists, base type otherwise)"""
|
|
43
|
+
return self.inner_type if self.is_list else self.base_type
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def has_forward_ref(self) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
True if either base_type or inner_type is a forward reference
|
|
49
|
+
(e.g., a string annotation or typing.ForwardRef). Safe across Python versions.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def _is_forward_ref(t) -> bool:
|
|
53
|
+
if t is None:
|
|
54
|
+
return False
|
|
55
|
+
# Strings from deferred annotations / forward refs
|
|
56
|
+
if isinstance(t, str):
|
|
57
|
+
return True
|
|
58
|
+
# typing.ForwardRef in 3.8+ (internal shape has varied, so duck-type too)
|
|
59
|
+
if isinstance(t, ForwardRef):
|
|
60
|
+
return True
|
|
61
|
+
# Fallback: anything that looks like a ForwardRef (duck-typing)
|
|
62
|
+
return hasattr(t, "__forward_arg__") # covers older/private ForwardRef variants
|
|
63
|
+
|
|
64
|
+
return _is_forward_ref(self.base_type) or _is_forward_ref(self.inner_type)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def analyze_type(type_: type, base_class: type) -> TypeInfo:
|
|
68
|
+
"""
|
|
69
|
+
Analyze a type and return normalized information about it.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
type_: The type to analyze
|
|
73
|
+
is_primitive_func: Function to check if a type is primitive
|
|
74
|
+
param_base_class: Base class for Param types (e.g., Param)
|
|
75
|
+
"""
|
|
76
|
+
origin = get_origin(type_)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
if origin == list:
|
|
80
|
+
inner_type = get_args(type_)[0]
|
|
81
|
+
if is_primitive(inner_type):
|
|
82
|
+
return TypeInfo(TypeCategory.LIST_PRIMITIVE, type_, inner_type)
|
|
83
|
+
elif issubclass(inner_type, base_class):
|
|
84
|
+
return TypeInfo(TypeCategory.LIST_SUBCLASS, type_, inner_type)
|
|
85
|
+
else:
|
|
86
|
+
return TypeInfo(TypeCategory.UNSUPPORTED, type_, inner_type)
|
|
87
|
+
|
|
88
|
+
elif is_primitive(type_):
|
|
89
|
+
return TypeInfo(TypeCategory.PRIMITIVE, type_)
|
|
90
|
+
|
|
91
|
+
elif issubclass(type_, base_class):
|
|
92
|
+
return TypeInfo(TypeCategory.SUBCLASS, type_)
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
return TypeInfo(TypeCategory.UNSUPPORTED, type_)
|
|
96
|
+
except TypeError:
|
|
97
|
+
return TypeInfo(TypeCategory.UNSUPPORTED, type_)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, create_model
|
|
2
2
|
from typing import Type, Self, Union
|
|
3
3
|
|
|
4
|
-
from openai.types.responses import Response
|
|
5
|
-
|
|
6
4
|
from pyagentic._base._tool import _ToolDefinition
|
|
7
5
|
from pyagentic._base._params import Param
|
|
8
6
|
|
|
@@ -114,24 +112,24 @@ class AgentResponse(BaseModel):
|
|
|
114
112
|
a fastapi app. This is done by calling `from_tool_defs`.
|
|
115
113
|
"""
|
|
116
114
|
|
|
117
|
-
response: Response
|
|
118
115
|
final_output: str
|
|
119
116
|
|
|
120
117
|
@classmethod
|
|
121
118
|
def from_tool_defs(
|
|
122
|
-
cls,
|
|
119
|
+
cls,
|
|
120
|
+
agent_name: str,
|
|
121
|
+
tool_response_models: list[Type[ToolResponse]],
|
|
122
|
+
linked_agents_response_models: list[Type[Self]],
|
|
123
123
|
) -> Type[Self]:
|
|
124
124
|
"""
|
|
125
125
|
Creates a subclass of `AgentResponse`, using Tool Definitions to create a predetermined
|
|
126
126
|
schema of what the response will look like.
|
|
127
127
|
"""
|
|
128
|
+
fields = {}
|
|
128
129
|
if tool_response_models:
|
|
129
130
|
ToolResult = Union[tuple(tool_response_models)]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
f"{agent_name}Response",
|
|
136
|
-
__base__=cls,
|
|
137
|
-
)
|
|
131
|
+
fields["tool_responses"] = (list[ToolResult], ...)
|
|
132
|
+
if linked_agents_response_models:
|
|
133
|
+
AgentResult = Union[tuple(linked_agents_response_models)]
|
|
134
|
+
fields["agent_responses"] = (list[AgentResult], ...)
|
|
135
|
+
return create_model(f"{agent_name}Response", __base__=cls, **fields)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyagentic-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Build LLM Agents in a Pythonic way
|
|
5
5
|
Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,7 @@ Requires-Dist: taskipy>=1.14.1
|
|
|
17
17
|
Requires-Dist: openai>=1.99.3
|
|
18
18
|
Requires-Dist: pydantic>=2.11.7
|
|
19
19
|
Requires-Dist: typeguard>=4.4.4
|
|
20
|
+
Requires-Dist: c3linearize>=0.1.0
|
|
20
21
|
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# PyAgentic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyagentic-core"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.0"
|
|
4
4
|
description = "Build LLM Agents in a Pythonic way"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -20,6 +20,7 @@ dependencies = [
|
|
|
20
20
|
"openai>=1.99.3",
|
|
21
21
|
"pydantic>=2.11.7",
|
|
22
22
|
"typeguard>=4.4.4",
|
|
23
|
+
"c3linearize>=0.1.0",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[dependency-groups]
|
|
@@ -28,6 +29,7 @@ dev = [
|
|
|
28
29
|
"coverage>=7.10.2",
|
|
29
30
|
"deepdiff>=8.5.0",
|
|
30
31
|
"flake8>=7.3.0",
|
|
32
|
+
"pytest-asyncio>=1.1.0",
|
|
31
33
|
"pytest>=8.4.1",
|
|
32
34
|
"python-semantic-release>=10.3.1",
|
|
33
35
|
]
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
from pyagentic._base._agent import Agent
|
|
2
|
-
from pyagentic._base._params import Param, ParamInfo
|
|
3
|
-
from pyagentic._base._tool import tool
|
|
4
|
-
from pyagentic._base._context import computed_context, ContextRef, ContextItem
|
|
5
|
-
|
|
6
|
-
__all__ = ["Agent", "Param", "ParamInfo", "tool", "computed_context", "ContextRef", "ContextItem"]
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
from typing import dataclass_transform, TypeVar
|
|
3
|
-
from typeguard import check_type, TypeCheckError
|
|
4
|
-
|
|
5
|
-
from pyagentic._base._validation import _AgentConstructionValidator
|
|
6
|
-
from pyagentic._base._exceptions import SystemMessageNotDeclared, UnexpectedContextItemType
|
|
7
|
-
from pyagentic._base._context import _AgentContext, ContextItem, computed_context
|
|
8
|
-
from pyagentic._base._tool import _ToolDefinition
|
|
9
|
-
|
|
10
|
-
from pyagentic.models.response import AgentResponse, ToolResponse
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass_transform(field_specifiers=(ContextItem,))
|
|
14
|
-
class AgentMeta(type):
|
|
15
|
-
"""
|
|
16
|
-
Metaclass that applies only to Agent subclasses:
|
|
17
|
-
- Ensures @system_message was declared
|
|
18
|
-
- Collects @tool definitions and ContextItem attributes
|
|
19
|
-
- Initializes class __tool_defs__ and __context_items__
|
|
20
|
-
- Dynamically injects an __init__ signature based on class __annotations__
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
@staticmethod
|
|
24
|
-
def _extract_tool_defs(namespace) -> dict[str, _ToolDefinition]:
|
|
25
|
-
"""
|
|
26
|
-
Extracts tool definitions from a given namespace
|
|
27
|
-
|
|
28
|
-
Any method with the `@tool` descriptor will be attached to the `__tool_defs__` class
|
|
29
|
-
attribute
|
|
30
|
-
"""
|
|
31
|
-
tools = {}
|
|
32
|
-
for attr_name, attr_value in namespace.items():
|
|
33
|
-
if hasattr(attr_value, "__tool_def__"):
|
|
34
|
-
tools[attr_name] = attr_value.__tool_def__
|
|
35
|
-
return tools
|
|
36
|
-
|
|
37
|
-
@staticmethod
|
|
38
|
-
def _extract_annotations(bases, namespace) -> dict[str, TypeVar]:
|
|
39
|
-
"""
|
|
40
|
-
Extracts all annotations from current class and all its subclasses. Combines them into
|
|
41
|
-
one dictionary, with class order respected (subclasses overide parent classes.)
|
|
42
|
-
"""
|
|
43
|
-
annotations = {}
|
|
44
|
-
for base in reversed(bases):
|
|
45
|
-
if hasattr(base, "__annotations__"):
|
|
46
|
-
for name, type_ in base.__annotations__.items():
|
|
47
|
-
if not name.startswith("__"):
|
|
48
|
-
annotations[name] = type_
|
|
49
|
-
for name, type_ in namespace.get("__annotations__", {}).items():
|
|
50
|
-
if not name.startswith("__"):
|
|
51
|
-
annotations[name] = type_
|
|
52
|
-
return annotations
|
|
53
|
-
|
|
54
|
-
@staticmethod
|
|
55
|
-
def _extract_context_attrs(annotations, namespace) -> dict[str, tuple[TypeVar, ContextItem]]:
|
|
56
|
-
"""
|
|
57
|
-
Extracts any class field from annotations and namespace where the value is that of
|
|
58
|
-
`ContextItem`, these will later be appeneded to the agents context. This will return
|
|
59
|
-
both the type and the user defined context item.
|
|
60
|
-
"""
|
|
61
|
-
context_attrs = {}
|
|
62
|
-
for attr_name, attr_type in annotations.items():
|
|
63
|
-
default = namespace.get(attr_name, None)
|
|
64
|
-
if isinstance(default, ContextItem):
|
|
65
|
-
context_attrs[attr_name] = (attr_type, default)
|
|
66
|
-
|
|
67
|
-
for name, value in namespace.items():
|
|
68
|
-
if getattr(value, "_is_context", False):
|
|
69
|
-
context_attrs[name] = (computed_context, value)
|
|
70
|
-
return context_attrs
|
|
71
|
-
|
|
72
|
-
@staticmethod
|
|
73
|
-
def _build_init_signature(cls) -> inspect.Signature:
|
|
74
|
-
"""
|
|
75
|
-
Builds the signature for the classes __init__, injecting any context item attributes
|
|
76
|
-
defined by the user properly into the inits signature. This allows IDE's to be able
|
|
77
|
-
to recognize user-defined class attributes when initializing a class.
|
|
78
|
-
"""
|
|
79
|
-
params = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_ONLY)]
|
|
80
|
-
for field_name, field_type in cls.__annotations__.items():
|
|
81
|
-
if field_name in cls.__context_attrs__:
|
|
82
|
-
default_val = cls.__context_attrs__[field_name][1].get_default_value()
|
|
83
|
-
else:
|
|
84
|
-
default_val = getattr(cls, field_name, inspect._empty)
|
|
85
|
-
param = inspect.Parameter(
|
|
86
|
-
field_name,
|
|
87
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
88
|
-
default=default_val,
|
|
89
|
-
annotation=field_type,
|
|
90
|
-
)
|
|
91
|
-
params.append(param)
|
|
92
|
-
return inspect.Signature(params)
|
|
93
|
-
|
|
94
|
-
@staticmethod
|
|
95
|
-
def _build_init(sig):
|
|
96
|
-
"""
|
|
97
|
-
Builds the init function for the class. This init will automatically have user-defined
|
|
98
|
-
context items as arguements, allow for easy initialization of agents for a variety
|
|
99
|
-
of different tasks.
|
|
100
|
-
"""
|
|
101
|
-
|
|
102
|
-
def __init__(self, *args, **kwargs):
|
|
103
|
-
|
|
104
|
-
# -------- ContextClass Construction --------------------
|
|
105
|
-
ContextClass = _AgentContext.make_ctx_class(
|
|
106
|
-
name=self.__class__.__name__, ctx_map=self.__context_attrs__
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
context_kwargs = {}
|
|
110
|
-
for attr_name, (attr_type, attr_default) in self.__context_attrs__.items():
|
|
111
|
-
# Skip compted contexts, this validaiton will happen with the validator
|
|
112
|
-
# using a dry run with supplied default values
|
|
113
|
-
if attr_type == computed_context:
|
|
114
|
-
continue
|
|
115
|
-
# Add all ContextItems to the kwargs, checking type as it goes
|
|
116
|
-
if attr_name in kwargs:
|
|
117
|
-
val = kwargs[attr_name]
|
|
118
|
-
try:
|
|
119
|
-
check_type(val, attr_type)
|
|
120
|
-
except TypeCheckError:
|
|
121
|
-
raise UnexpectedContextItemType(
|
|
122
|
-
name=attr_name, expected=attr_type, recieved=type(val)
|
|
123
|
-
)
|
|
124
|
-
context_kwargs[attr_name] = val
|
|
125
|
-
else:
|
|
126
|
-
context_kwargs[attr_name] = attr_default.get_default_value()
|
|
127
|
-
|
|
128
|
-
self.context = ContextClass(
|
|
129
|
-
instructions=self.__system_message__,
|
|
130
|
-
input_template=self.__input_template__,
|
|
131
|
-
**context_kwargs,
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
bound = sig.bind(self, *args, **kwargs)
|
|
135
|
-
|
|
136
|
-
# Add all other arguements to instance
|
|
137
|
-
for name, val in list(bound.arguments.items())[1:]: # skip 'self'
|
|
138
|
-
if name in self.__context_attrs__:
|
|
139
|
-
pass
|
|
140
|
-
setattr(self, name, val)
|
|
141
|
-
|
|
142
|
-
self.__post_init__()
|
|
143
|
-
|
|
144
|
-
__init__.__signature__ = sig # type: ignore
|
|
145
|
-
return __init__
|
|
146
|
-
|
|
147
|
-
def __new__(mcs, name, bases, namespace, **kwargs):
|
|
148
|
-
# Create a new Agent class
|
|
149
|
-
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
150
|
-
# If it is a base Agent, then return
|
|
151
|
-
if namespace.get("__abstract_base__", False):
|
|
152
|
-
return cls
|
|
153
|
-
# Verify system message is set
|
|
154
|
-
if "__system_message__" not in namespace:
|
|
155
|
-
raise SystemMessageNotDeclared()
|
|
156
|
-
# Attach tool definitions
|
|
157
|
-
cls.__tool_defs__ = mcs._extract_tool_defs(namespace)
|
|
158
|
-
# Attach new annotations
|
|
159
|
-
cls.__annotations__ = mcs._extract_annotations(bases, namespace)
|
|
160
|
-
# Attach context attributes (ContextItems and computed_context)
|
|
161
|
-
cls.__context_attrs__ = mcs._extract_context_attrs(cls.__annotations__, namespace)
|
|
162
|
-
# Create tool response models
|
|
163
|
-
cls.__tool_response_models__ = {
|
|
164
|
-
tool_name: ToolResponse.from_tool_def(tool_def)
|
|
165
|
-
for tool_name, tool_def in cls.__tool_defs__.items()
|
|
166
|
-
}
|
|
167
|
-
# Create final Agent response model, using the tool response models
|
|
168
|
-
cls.__response_model__ = AgentResponse.from_tool_defs(
|
|
169
|
-
cls.__name__, list(cls.__tool_response_models__.values())
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
# Build the new init
|
|
173
|
-
sig = mcs._build_init_signature(cls)
|
|
174
|
-
cls.__init__ = mcs._build_init(sig)
|
|
175
|
-
|
|
176
|
-
# Validate agent
|
|
177
|
-
_AgentConstructionValidator(cls).validate()
|
|
178
|
-
|
|
179
|
-
return cls
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
from typing import get_origin, get_args, Any, Optional
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
PRIMITIVES = (bool, str, int, float, type(None))
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def is_primitive(type_: Any) -> bool:
|
|
10
|
-
"""
|
|
11
|
-
Helper function to check if a type is a python primitive
|
|
12
|
-
"""
|
|
13
|
-
return type_ in PRIMITIVES
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class TypeCategory(Enum):
|
|
17
|
-
PRIMITIVE = "primitive"
|
|
18
|
-
LIST_PRIMITIVE = "list_primitive"
|
|
19
|
-
SUBCLASS = "subclass"
|
|
20
|
-
LIST_SUBCLASS = "list_subclass"
|
|
21
|
-
UNSUPPORTED = "unsupported"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class TypeInfo:
|
|
26
|
-
"""Normalized information about a type"""
|
|
27
|
-
|
|
28
|
-
category: TypeCategory
|
|
29
|
-
base_type: type
|
|
30
|
-
inner_type: Optional[type] = None # For list types
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def is_list(self) -> bool:
|
|
34
|
-
return self.category in [TypeCategory.LIST_PRIMITIVE, TypeCategory.LIST_SUBCLASS]
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def is_subclass(self) -> bool:
|
|
38
|
-
return self.category in [TypeCategory.SUBCLASS, TypeCategory.LIST_SUBCLASS]
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def effective_type(self) -> type:
|
|
42
|
-
"""Returns the type to work with (inner type for lists, base type otherwise)"""
|
|
43
|
-
return self.inner_type if self.is_list else self.base_type
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def analyze_type(type_: type, base_class: type) -> TypeInfo:
|
|
47
|
-
"""
|
|
48
|
-
Analyze a type and return normalized information about it.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
type_: The type to analyze
|
|
52
|
-
is_primitive_func: Function to check if a type is primitive
|
|
53
|
-
param_base_class: Base class for Param types (e.g., Param)
|
|
54
|
-
"""
|
|
55
|
-
origin = get_origin(type_)
|
|
56
|
-
|
|
57
|
-
if origin == list:
|
|
58
|
-
inner_type = get_args(type_)[0]
|
|
59
|
-
if is_primitive(inner_type):
|
|
60
|
-
return TypeInfo(TypeCategory.LIST_PRIMITIVE, type_, inner_type)
|
|
61
|
-
elif issubclass(inner_type, base_class):
|
|
62
|
-
return TypeInfo(TypeCategory.LIST_SUBCLASS, type_, inner_type)
|
|
63
|
-
else:
|
|
64
|
-
return TypeInfo(TypeCategory.UNSUPPORTED, type_, inner_type)
|
|
65
|
-
|
|
66
|
-
elif is_primitive(type_):
|
|
67
|
-
return TypeInfo(TypeCategory.PRIMITIVE, type_)
|
|
68
|
-
|
|
69
|
-
elif issubclass(type_, base_class):
|
|
70
|
-
return TypeInfo(TypeCategory.SUBCLASS, type_)
|
|
71
|
-
|
|
72
|
-
else:
|
|
73
|
-
return TypeInfo(TypeCategory.UNSUPPORTED, type_)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|