pyagentic-core 1.2.0__tar.gz → 1.2.1__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.0 → pyagentic_core-1.2.1}/PKG-INFO +1 -1
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_context.py +0 -3
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_metaclasses.py +26 -10
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_params.py +34 -23
- pyagentic_core-1.2.1/pyagentic/_base/_resolver.py +57 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_tool.py +32 -23
- pyagentic_core-1.2.1/pyagentic/_base/_validation.py +115 -0
- pyagentic_core-1.2.1/pyagentic/_utils/_typing.py +73 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/models/response.py +49 -44
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/PKG-INFO +1 -1
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/SOURCES.txt +2 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyproject.toml +2 -1
- pyagentic_core-1.2.0/pyagentic/_base/_resolver.py +0 -51
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/LICENSE +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/README.md +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/__init__.py +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_agent.py +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/logging.py +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/updates.py +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/requires.txt +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/setup.cfg +0 -0
|
@@ -210,7 +210,4 @@ class ContextRef:
|
|
|
210
210
|
val = context
|
|
211
211
|
for part in self.path.split("."):
|
|
212
212
|
val = getattr(val, part)
|
|
213
|
-
# if it’s wrapped in our Context helper, drill into .value
|
|
214
|
-
if hasattr(val, "value"):
|
|
215
|
-
val = val.value
|
|
216
213
|
return val() if callable(val) else val
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from typing import dataclass_transform, TypeVar
|
|
3
|
-
from typeguard import check_type
|
|
3
|
+
from typeguard import check_type, TypeCheckError
|
|
4
4
|
|
|
5
|
+
from pyagentic._base._validation import _AgentConstructionValidator
|
|
5
6
|
from pyagentic._base._exceptions import SystemMessageNotDeclared, UnexpectedContextItemType
|
|
6
7
|
from pyagentic._base._context import _AgentContext, ContextItem, computed_context
|
|
7
8
|
from pyagentic._base._tool import _ToolDefinition
|
|
@@ -98,18 +99,25 @@ class AgentMeta(type):
|
|
|
98
99
|
of different tasks.
|
|
99
100
|
"""
|
|
100
101
|
|
|
101
|
-
def __init__(self, *args, **kwargs):
|
|
102
|
+
def __init__(self, *args, **kwargs):
|
|
103
|
+
|
|
104
|
+
# -------- ContextClass Construction --------------------
|
|
102
105
|
ContextClass = _AgentContext.make_ctx_class(
|
|
103
106
|
name=self.__class__.__name__, ctx_map=self.__context_attrs__
|
|
104
107
|
)
|
|
105
108
|
|
|
106
109
|
context_kwargs = {}
|
|
107
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
|
|
108
113
|
if attr_type == computed_context:
|
|
109
114
|
continue
|
|
115
|
+
# Add all ContextItems to the kwargs, checking type as it goes
|
|
110
116
|
if attr_name in kwargs:
|
|
111
117
|
val = kwargs[attr_name]
|
|
112
|
-
|
|
118
|
+
try:
|
|
119
|
+
check_type(val, attr_type)
|
|
120
|
+
except TypeCheckError:
|
|
113
121
|
raise UnexpectedContextItemType(
|
|
114
122
|
name=attr_name, expected=attr_type, recieved=type(val)
|
|
115
123
|
)
|
|
@@ -124,6 +132,8 @@ class AgentMeta(type):
|
|
|
124
132
|
)
|
|
125
133
|
|
|
126
134
|
bound = sig.bind(self, *args, **kwargs)
|
|
135
|
+
|
|
136
|
+
# Add all other arguements to instance
|
|
127
137
|
for name, val in list(bound.arguments.items())[1:]: # skip 'self'
|
|
128
138
|
if name in self.__context_attrs__:
|
|
129
139
|
pass
|
|
@@ -135,29 +145,35 @@ class AgentMeta(type):
|
|
|
135
145
|
return __init__
|
|
136
146
|
|
|
137
147
|
def __new__(mcs, name, bases, namespace, **kwargs):
|
|
148
|
+
# Create a new Agent class
|
|
138
149
|
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
139
|
-
|
|
150
|
+
# If it is a base Agent, then return
|
|
140
151
|
if namespace.get("__abstract_base__", False):
|
|
141
152
|
return cls
|
|
142
|
-
|
|
153
|
+
# Verify system message is set
|
|
143
154
|
if "__system_message__" not in namespace:
|
|
144
155
|
raise SystemMessageNotDeclared()
|
|
145
|
-
|
|
156
|
+
# Attach tool definitions
|
|
146
157
|
cls.__tool_defs__ = mcs._extract_tool_defs(namespace)
|
|
147
|
-
|
|
158
|
+
# Attach new annotations
|
|
148
159
|
cls.__annotations__ = mcs._extract_annotations(bases, namespace)
|
|
149
|
-
|
|
160
|
+
# Attach context attributes (ContextItems and computed_context)
|
|
150
161
|
cls.__context_attrs__ = mcs._extract_context_attrs(cls.__annotations__, namespace)
|
|
151
|
-
|
|
162
|
+
# Create tool response models
|
|
152
163
|
cls.__tool_response_models__ = {
|
|
153
164
|
tool_name: ToolResponse.from_tool_def(tool_def)
|
|
154
165
|
for tool_name, tool_def in cls.__tool_defs__.items()
|
|
155
166
|
}
|
|
167
|
+
# Create final Agent response model, using the tool response models
|
|
156
168
|
cls.__response_model__ = AgentResponse.from_tool_defs(
|
|
157
169
|
cls.__name__, list(cls.__tool_response_models__.values())
|
|
158
170
|
)
|
|
159
171
|
|
|
172
|
+
# Build the new init
|
|
160
173
|
sig = mcs._build_init_signature(cls)
|
|
161
|
-
|
|
162
174
|
cls.__init__ = mcs._build_init(sig)
|
|
175
|
+
|
|
176
|
+
# Validate agent
|
|
177
|
+
_AgentConstructionValidator(cls).validate()
|
|
178
|
+
|
|
163
179
|
return cls
|
|
@@ -2,8 +2,11 @@ from dataclasses import dataclass
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from typing import get_type_hints, Any, List, Dict, Type
|
|
4
4
|
|
|
5
|
+
from typeguard import check_type, TypeCheckError
|
|
6
|
+
|
|
5
7
|
from pyagentic._base._resolver import ContextualMixin, MaybeContext
|
|
6
8
|
from pyagentic._base._context import _AgentContext
|
|
9
|
+
from pyagentic._utils._typing import analyze_type, TypeCategory
|
|
7
10
|
|
|
8
11
|
# simple mapping from Python types to JSON Schema/OpenAI types
|
|
9
12
|
_TYPE_MAP: Dict[Type[Any], str] = {
|
|
@@ -83,29 +86,37 @@ class Param:
|
|
|
83
86
|
TypeError: if a provided value does not match the annotated type,
|
|
84
87
|
or if unexpected fields are passed.
|
|
85
88
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
for field_name, (field_type, field_info) in self.__attributes__.items():
|
|
90
|
+
type_info = analyze_type(field_type, self.__class__.__bases__[0])
|
|
91
|
+
value = kwargs.get(field_name, field_info.default)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
if not type_info.is_subclass:
|
|
95
|
+
check_type(value, field_type)
|
|
96
|
+
except TypeCheckError:
|
|
97
|
+
raise TypeError(f"Field '{field_name}' expected {field_type}, got {type(value)}")
|
|
98
|
+
|
|
99
|
+
match type_info.category:
|
|
100
|
+
|
|
101
|
+
case TypeCategory.PRIMITIVE:
|
|
102
|
+
setattr(self, field_name, value)
|
|
103
|
+
case TypeCategory.LIST_PRIMITIVE:
|
|
104
|
+
setattr(self, field_name, value)
|
|
105
|
+
case TypeCategory.SUBCLASS:
|
|
106
|
+
value = field_type(**value) if type(value) == dict else value
|
|
107
|
+
setattr(self, field_name, value)
|
|
108
|
+
case TypeCategory.LIST_SUBCLASS:
|
|
109
|
+
listed_value = (
|
|
110
|
+
[type_info.inner_type(**param_kwargs) for param_kwargs in value]
|
|
111
|
+
if isinstance(value, list) and all(isinstance(v, dict) for v in value)
|
|
112
|
+
else value
|
|
113
|
+
)
|
|
114
|
+
setattr(self, field_name, listed_value)
|
|
115
|
+
|
|
116
|
+
unexpected_args = [kwarg for kwarg in kwargs if kwarg not in self.__attributes__]
|
|
117
|
+
if unexpected_args:
|
|
118
|
+
unexpected = ", ".join(unexpected_args)
|
|
119
|
+
raise TypeError(f"Unexpected fields for {self.__class__.__name__}: {unexpected}")
|
|
109
120
|
|
|
110
121
|
def __repr__(self):
|
|
111
122
|
vals = ", ".join(f"{k}={v!r}" for k, v in self.dict().items())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass, fields, replace
|
|
2
|
+
from typing import Annotated, TypeVar, get_origin, get_args, Any, Self
|
|
3
|
+
|
|
4
|
+
from typeguard import check_type, TypeCheckError
|
|
5
|
+
|
|
6
|
+
from pyagentic._base._context import _AgentContext, ContextRef
|
|
7
|
+
from pyagentic._base._exceptions import InvalidContextRefMismatchTyping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _CtxMarker:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
MaybeContext = Annotated[T, _CtxMarker()]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ContextualMixin:
|
|
20
|
+
"""
|
|
21
|
+
Class to be extended if any of the properties in the class may use a `ContextRef`. Gives the
|
|
22
|
+
subclass access to `resolve`, which allows a context to be passed in to backfill any
|
|
23
|
+
context-ready properties
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _get_maybe_context(cls):
|
|
28
|
+
maybe_contexts = []
|
|
29
|
+
for field in fields(cls):
|
|
30
|
+
type_ = field.type
|
|
31
|
+
# only look at Annotated[...] with our marker
|
|
32
|
+
if get_origin(type_) is Annotated and any(
|
|
33
|
+
isinstance(m, _CtxMarker) for m in get_args(type_)[1:]
|
|
34
|
+
):
|
|
35
|
+
maybe_contexts.append(field)
|
|
36
|
+
return maybe_contexts
|
|
37
|
+
|
|
38
|
+
def resolve(self, ctx: _AgentContext) -> Self:
|
|
39
|
+
updates: dict[str, Any] = {}
|
|
40
|
+
for field in self._get_maybe_context():
|
|
41
|
+
raw = getattr(self, field.name)
|
|
42
|
+
if isinstance(raw, ContextRef):
|
|
43
|
+
value = ctx.get(raw.path)
|
|
44
|
+
# expected type is the first Annotated arg
|
|
45
|
+
expected_type = get_args(field.type)[0]
|
|
46
|
+
try:
|
|
47
|
+
check_type(value, expected_type)
|
|
48
|
+
except TypeCheckError:
|
|
49
|
+
raise InvalidContextRefMismatchTyping(
|
|
50
|
+
ref_path=raw.path,
|
|
51
|
+
field_name=field.name,
|
|
52
|
+
recieved_type=type(value),
|
|
53
|
+
expected_type=expected_type,
|
|
54
|
+
)
|
|
55
|
+
updates[field.name] = value
|
|
56
|
+
# return a new instance with those fields replaced
|
|
57
|
+
return replace(self, **updates)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from typing import Callable, Any, TypeVar, get_type_hints
|
|
2
|
+
from typing import Callable, Any, TypeVar, get_type_hints
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
|
|
5
5
|
from pyagentic._base._params import Param, ParamInfo, _TYPE_MAP
|
|
6
6
|
from pyagentic._base._context import _AgentContext
|
|
7
7
|
from pyagentic._base._exceptions import ToolDeclarationFailed
|
|
8
8
|
|
|
9
|
+
from pyagentic._utils._typing import TypeCategory, analyze_type
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class _ToolDefinition:
|
|
11
13
|
"""
|
|
@@ -49,19 +51,24 @@ class _ToolDefinition:
|
|
|
49
51
|
required = []
|
|
50
52
|
|
|
51
53
|
for name, (type_, default) in self.parameters.items():
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
type_info = analyze_type(type_, Param)
|
|
55
|
+
|
|
56
|
+
match type_info.category:
|
|
57
|
+
case TypeCategory.PRIMITIVE:
|
|
58
|
+
params[name] = {"type": _TYPE_MAP.get(type_, "string")}
|
|
59
|
+
case TypeCategory.LIST_PRIMITIVE:
|
|
57
60
|
params[name] = {
|
|
58
61
|
"type": "array",
|
|
59
|
-
"items": {"type": _TYPE_MAP.get(
|
|
62
|
+
"items": {"type": _TYPE_MAP.get(type_info.inner_type, "string")},
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
case TypeCategory.SUBCLASS:
|
|
65
|
+
params[name] = type_.to_openai(context)
|
|
66
|
+
case TypeCategory.LIST_SUBCLASS:
|
|
67
|
+
params[name] = {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": type_info.inner_type.to_openai(context),
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
if isinstance(default, ParamInfo):
|
|
66
73
|
resolved_default = default.resolve(context)
|
|
67
74
|
if resolved_default.description:
|
|
@@ -97,18 +104,20 @@ class _ToolDefinition:
|
|
|
97
104
|
|
|
98
105
|
for name, (type_, info) in self.parameters.items():
|
|
99
106
|
if name in kwargs:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
type_info = analyze_type(type_, Param)
|
|
108
|
+
|
|
109
|
+
match type_info.category:
|
|
110
|
+
case TypeCategory.PRIMITIVE:
|
|
111
|
+
compiled_args[name] = kwargs[name]
|
|
112
|
+
case TypeCategory.LIST_PRIMITIVE:
|
|
113
|
+
compiled_args[name] = kwargs[name]
|
|
114
|
+
case TypeCategory.SUBCLASS:
|
|
115
|
+
param_args = kwargs[name]
|
|
116
|
+
compiled_args[name] = type_(**param_args)
|
|
117
|
+
case TypeCategory.LIST_SUBCLASS:
|
|
118
|
+
compiled_args[name] = [
|
|
119
|
+
type_info.inner_type(**param_args) for param_args in kwargs[name]
|
|
120
|
+
]
|
|
112
121
|
else:
|
|
113
122
|
compiled_args[name] = info.default
|
|
114
123
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import Type, get_args
|
|
2
|
+
from typeguard import check_type, TypeCheckError
|
|
3
|
+
|
|
4
|
+
from pyagentic._base._context import ContextItem, ContextRef
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Temp class for typing
|
|
8
|
+
class Agent:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentValidationError(Exception):
|
|
13
|
+
def __init__(self, problems):
|
|
14
|
+
message = "Agent failed to be validated: \n"
|
|
15
|
+
message += "\n".join(problems)
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _AgentConstructionValidator:
|
|
20
|
+
"""
|
|
21
|
+
Class to hold validation logic that needs to be checked at runtime
|
|
22
|
+
|
|
23
|
+
Class works by using default values to construct a sample agent, then runs additional checks
|
|
24
|
+
that could not be run on creation of the class
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, AgentClass: Type["Agent"]):
|
|
28
|
+
self.problems = []
|
|
29
|
+
self.AgentClass = AgentClass
|
|
30
|
+
self.sample_agent = self.AgentClass(
|
|
31
|
+
model="testing",
|
|
32
|
+
api_key="validation",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def validate(self):
|
|
36
|
+
"""
|
|
37
|
+
Validate an Agent class
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
AgentValidationError: A custom exception that includes all problems found in the
|
|
41
|
+
validation pipelines
|
|
42
|
+
"""
|
|
43
|
+
self._verify_default_values(self.AgentClass)
|
|
44
|
+
self._verify_context_items_can_be_strings(self.AgentClass)
|
|
45
|
+
self._verify_tool_context_refs(self.AgentClass)
|
|
46
|
+
|
|
47
|
+
if self.problems:
|
|
48
|
+
raise AgentValidationError(self.problems)
|
|
49
|
+
|
|
50
|
+
def _verify_tool_context_refs(self, AgentClass: Type["Agent"]):
|
|
51
|
+
"""
|
|
52
|
+
Verifies that all context refs used:
|
|
53
|
+
- links to an item in the context
|
|
54
|
+
- The linked context item has the same type as the field it is being used in
|
|
55
|
+
"""
|
|
56
|
+
for tool_name, tool_def in AgentClass.__tool_defs__.items():
|
|
57
|
+
for param_name, (param_type, param_info) in tool_def.parameters.items():
|
|
58
|
+
for info_field in param_info._get_maybe_context():
|
|
59
|
+
attr = getattr(param_info, info_field.name)
|
|
60
|
+
expected_type = get_args(info_field.type)[0]
|
|
61
|
+
if isinstance(attr, ContextRef):
|
|
62
|
+
if attr.path not in AgentClass.__context_attrs__:
|
|
63
|
+
self.problems.append(
|
|
64
|
+
f"tool.{tool_name}.param.{param_name}.{info_field.name}: Ref not found in context: {attr.path}" # noqa E501
|
|
65
|
+
)
|
|
66
|
+
sample_value = self.sample_agent.context.get(attr.path)
|
|
67
|
+
try:
|
|
68
|
+
check_type(sample_value, expected_type)
|
|
69
|
+
except TypeCheckError:
|
|
70
|
+
self.problems.append(
|
|
71
|
+
(
|
|
72
|
+
f"tool.{tool_name}.param.{param_name}.{info_field.name}: Ref typing does not match param info field:\n" # noqa E501
|
|
73
|
+
f" Expected: {expected_type}\n"
|
|
74
|
+
f" Recieved: {type(sample_value).__name__}\n"
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _verify_context_items_can_be_strings(self, AgentClass: Type["Agent"]):
|
|
79
|
+
"""
|
|
80
|
+
Verifies that all items in the context can be injected / used in the system message or
|
|
81
|
+
input template
|
|
82
|
+
"""
|
|
83
|
+
for context_name in AgentClass.__context_attrs__.keys():
|
|
84
|
+
sample_value = self.sample_agent.context.get(context_name)
|
|
85
|
+
try:
|
|
86
|
+
str(sample_value)
|
|
87
|
+
except Exception:
|
|
88
|
+
self.problems.append(
|
|
89
|
+
(
|
|
90
|
+
f"context.{context_name}: Value cannot be stringified"
|
|
91
|
+
f" Value type: {type(sample_value)}"
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _verify_default_values(self, AgentClass: Type["Agent"]):
|
|
96
|
+
"""
|
|
97
|
+
Verifies that default values and default factories have the same type as to that
|
|
98
|
+
specified by the user
|
|
99
|
+
"""
|
|
100
|
+
defaults = {}
|
|
101
|
+
for context_name, (context_type, context_item) in AgentClass.__context_attrs__.items():
|
|
102
|
+
if isinstance(context_item, ContextItem):
|
|
103
|
+
default = context_item.get_default_value()
|
|
104
|
+
try:
|
|
105
|
+
check_type(default, context_type)
|
|
106
|
+
except TypeCheckError:
|
|
107
|
+
self.problems.append(
|
|
108
|
+
(
|
|
109
|
+
f"context.{context_name}: Default value does not match context item typing:\n" # noqa E501
|
|
110
|
+
f" Expected: {context_type}\n"
|
|
111
|
+
f" Recieved: {type(default)}\n"
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
defaults[context_name] = default
|
|
115
|
+
return defaults
|
|
@@ -0,0 +1,73 @@
|
|
|
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_)
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, create_model
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Type, Self, Union
|
|
3
3
|
|
|
4
4
|
from openai.types.responses import Response
|
|
5
5
|
|
|
6
6
|
from pyagentic._base._tool import _ToolDefinition
|
|
7
7
|
from pyagentic._base._params import Param
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
PRIMITIVES = (bool, str, int, float, type(None))
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def is_primitive(type_: Any) -> bool:
|
|
14
|
-
"""
|
|
15
|
-
Helper function to check if a type is a python primitive
|
|
16
|
-
"""
|
|
17
|
-
return type_ in PRIMITIVES
|
|
9
|
+
from pyagentic._utils._typing import TypeCategory, analyze_type
|
|
18
10
|
|
|
19
11
|
|
|
20
12
|
def param_to_pydantic(ParamClass: Type[Param]) -> Type[BaseModel]:
|
|
@@ -30,36 +22,36 @@ def param_to_pydantic(ParamClass: Type[Param]) -> Type[BaseModel]:
|
|
|
30
22
|
fields = {}
|
|
31
23
|
|
|
32
24
|
for attr_name, (attr_type, attr_info) in ParamClass.__attributes__.items():
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
type_info = analyze_type(attr_type, Param)
|
|
26
|
+
|
|
27
|
+
match type_info.category:
|
|
28
|
+
|
|
29
|
+
case TypeCategory.PRIMITIVE:
|
|
30
|
+
fields[attr_name] = (
|
|
31
|
+
attr_type,
|
|
32
|
+
Field(default=attr_info.default, description=attr_info.description),
|
|
33
|
+
)
|
|
34
|
+
case TypeCategory.LIST_PRIMITIVE:
|
|
36
35
|
fields[attr_name] = (
|
|
37
36
|
list[attr_type],
|
|
38
37
|
Field(default=attr_info.default, description=attr_info.description),
|
|
39
38
|
)
|
|
40
|
-
|
|
41
|
-
SubParamModel = param_to_pydantic(
|
|
39
|
+
case TypeCategory.SUBCLASS:
|
|
40
|
+
SubParamModel = param_to_pydantic(attr_type)
|
|
41
|
+
fields[attr_name] = (
|
|
42
|
+
SubParamModel,
|
|
43
|
+
Field(default=attr_info.default, description=attr_info.description),
|
|
44
|
+
)
|
|
45
|
+
case TypeCategory.LIST_SUBCLASS:
|
|
46
|
+
SubParamModel = param_to_pydantic(type_info.inner_type)
|
|
42
47
|
fields[attr_name] = (
|
|
43
48
|
list[SubParamModel],
|
|
44
49
|
Field(default=attr_info.default, description=attr_info.description),
|
|
45
50
|
)
|
|
46
|
-
|
|
51
|
+
case _:
|
|
47
52
|
raise Exception(f"Unsupported type: {attr_type}")
|
|
48
|
-
elif is_primitive(attr_type):
|
|
49
|
-
fields[attr_name] = (
|
|
50
|
-
attr_type,
|
|
51
|
-
Field(default=attr_info.default, description=attr_info.description),
|
|
52
|
-
)
|
|
53
|
-
elif issubclass(attr_type, Param):
|
|
54
|
-
SubParamModel = param_to_pydantic(attr_type)
|
|
55
|
-
fields[attr_name] = (
|
|
56
|
-
SubParamModel,
|
|
57
|
-
Field(default=attr_info.default, description=attr_info.description),
|
|
58
|
-
)
|
|
59
|
-
else:
|
|
60
|
-
raise Exception(f"Unsupported type: {attr_type}")
|
|
61
53
|
|
|
62
|
-
return create_model(f"{ParamClass.__name__}", **fields)
|
|
54
|
+
return create_model(f"{ParamClass.__name__}Model", **fields)
|
|
63
55
|
|
|
64
56
|
|
|
65
57
|
class ToolResponse(BaseModel):
|
|
@@ -82,20 +74,33 @@ class ToolResponse(BaseModel):
|
|
|
82
74
|
"""
|
|
83
75
|
fields = {}
|
|
84
76
|
for param_name, (param_type, param_info) in tool_def.parameters.items():
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
77
|
+
type_info = analyze_type(param_type, Param)
|
|
78
|
+
match type_info.category:
|
|
79
|
+
|
|
80
|
+
case TypeCategory.PRIMITIVE:
|
|
81
|
+
fields[param_name] = (
|
|
82
|
+
param_type,
|
|
83
|
+
Field(default=param_info.default, description=param_info.description),
|
|
84
|
+
)
|
|
85
|
+
case TypeCategory.LIST_PRIMITIVE:
|
|
86
|
+
fields[param_name] = (
|
|
87
|
+
param_type,
|
|
88
|
+
Field(default=param_info.default, description=param_info.description),
|
|
89
|
+
)
|
|
90
|
+
case TypeCategory.SUBCLASS:
|
|
91
|
+
ParamSubModel = param_to_pydantic(param_type)
|
|
92
|
+
fields[param_name] = (
|
|
93
|
+
ParamSubModel,
|
|
94
|
+
Field(default=param_info.default, description=param_info.description),
|
|
95
|
+
)
|
|
96
|
+
case TypeCategory.LIST_SUBCLASS:
|
|
97
|
+
ParamSubModel = param_to_pydantic(type_info.inner_type)
|
|
98
|
+
fields[param_name] = (
|
|
99
|
+
list[ParamSubModel],
|
|
100
|
+
Field(default=param_info.default, description=param_info.description),
|
|
101
|
+
)
|
|
102
|
+
case _:
|
|
103
|
+
raise Exception(f"Unsupported type: {param_type}")
|
|
99
104
|
|
|
100
105
|
return create_model(f"ToolResponse[{tool_def.name}]", __base__=cls, **fields)
|
|
101
106
|
|
|
@@ -12,6 +12,8 @@ pyagentic/_base/_metaclasses.py
|
|
|
12
12
|
pyagentic/_base/_params.py
|
|
13
13
|
pyagentic/_base/_resolver.py
|
|
14
14
|
pyagentic/_base/_tool.py
|
|
15
|
+
pyagentic/_base/_validation.py
|
|
16
|
+
pyagentic/_utils/_typing.py
|
|
15
17
|
pyagentic/models/response.py
|
|
16
18
|
pyagentic_core.egg-info/PKG-INFO
|
|
17
19
|
pyagentic_core.egg-info/SOURCES.txt
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyagentic-core"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.1"
|
|
4
4
|
description = "Build LLM Agents in a Pythonic way"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
[dependency-groups]
|
|
26
26
|
dev = [
|
|
27
27
|
"black>=25.1.0",
|
|
28
|
+
"coverage>=7.10.2",
|
|
28
29
|
"deepdiff>=8.5.0",
|
|
29
30
|
"flake8>=7.3.0",
|
|
30
31
|
"pytest>=8.4.1",
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, fields, replace
|
|
2
|
-
from typing import Annotated, TypeVar, get_origin, get_args, Any, Self
|
|
3
|
-
|
|
4
|
-
from typeguard import check_type, TypeCheckError
|
|
5
|
-
|
|
6
|
-
from pyagentic._base._context import _AgentContext, ContextRef
|
|
7
|
-
from pyagentic._base._exceptions import InvalidContextRefMismatchTyping
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class _CtxMarker:
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
T = TypeVar("T")
|
|
15
|
-
MaybeContext = Annotated[T, _CtxMarker()]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass
|
|
19
|
-
class ContextualMixin:
|
|
20
|
-
"""
|
|
21
|
-
Class to be extended if any of the properties in the class may use a `ContextRef`. Gives the
|
|
22
|
-
subclass access to `resolve`, which allows a context to be passed in to backfill any
|
|
23
|
-
context-ready properties
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def resolve(self, ctx: _AgentContext) -> Self:
|
|
27
|
-
updates: dict[str, Any] = {}
|
|
28
|
-
for f in fields(self):
|
|
29
|
-
tp = f.type
|
|
30
|
-
# only look at Annotated[...] with our marker
|
|
31
|
-
if get_origin(tp) is Annotated and any(
|
|
32
|
-
isinstance(m, _CtxMarker) for m in get_args(tp)[1:]
|
|
33
|
-
):
|
|
34
|
-
|
|
35
|
-
raw = getattr(self, f.name)
|
|
36
|
-
if isinstance(raw, ContextRef):
|
|
37
|
-
value = ctx.get(raw.path)
|
|
38
|
-
# expected type is the first Annotated arg
|
|
39
|
-
expected_type = get_args(tp)[0]
|
|
40
|
-
try:
|
|
41
|
-
check_type(value, expected_type)
|
|
42
|
-
except TypeCheckError:
|
|
43
|
-
raise InvalidContextRefMismatchTyping(
|
|
44
|
-
ref_path=raw.path,
|
|
45
|
-
field_name=f.name,
|
|
46
|
-
recieved_type=type(value),
|
|
47
|
-
expected_type=expected_type,
|
|
48
|
-
)
|
|
49
|
-
updates[f.name] = value
|
|
50
|
-
# return a new instance with those fields replaced
|
|
51
|
-
return replace(self, **updates)
|
|
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
|