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.
Files changed (25) hide show
  1. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/PKG-INFO +1 -1
  2. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_context.py +0 -3
  3. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_metaclasses.py +26 -10
  4. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_params.py +34 -23
  5. pyagentic_core-1.2.1/pyagentic/_base/_resolver.py +57 -0
  6. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_tool.py +32 -23
  7. pyagentic_core-1.2.1/pyagentic/_base/_validation.py +115 -0
  8. pyagentic_core-1.2.1/pyagentic/_utils/_typing.py +73 -0
  9. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/models/response.py +49 -44
  10. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/PKG-INFO +1 -1
  11. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/SOURCES.txt +2 -0
  12. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyproject.toml +2 -1
  13. pyagentic_core-1.2.0/pyagentic/_base/_resolver.py +0 -51
  14. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/LICENSE +0 -0
  15. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/README.md +0 -0
  16. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/__init__.py +0 -0
  17. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/__init__.py +0 -0
  18. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_agent.py +0 -0
  19. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/_base/_exceptions.py +0 -0
  20. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/logging.py +0 -0
  21. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic/updates.py +0 -0
  22. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  23. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/requires.txt +0 -0
  24. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/pyagentic_core.egg-info/top_level.txt +0 -0
  25. {pyagentic_core-1.2.0 → pyagentic_core-1.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -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): # type: ignore
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
- if not check_type(val, attr_type):
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
- cls = type(self)
87
- hints = get_type_hints(cls)
88
-
89
- # Assign annotated fields
90
- for name, typ in hints.items():
91
- if name in kwargs:
92
- value = kwargs.pop(name)
93
- # simple type check
94
- if not isinstance(value, typ) and value is not None:
95
- raise TypeError(f"Field '{name}' expected {typ}, got {type(value)}")
96
- setattr(self, name, value)
97
- else:
98
- # use class‐level default if given, else None
99
- attr = getattr(cls, name, None)
100
- if isinstance(attr, ParamInfo):
101
- default = attr.default
102
- else:
103
- default = attr
104
- setattr(self, name, default)
105
-
106
- if kwargs:
107
- unexpected = ", ".join(kwargs)
108
- raise TypeError(f"Unexpected fields for {cls.__name__}: {unexpected}")
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, get_args, get_origin
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
- if get_origin(type_) == list:
53
- listed_type = get_args(type_)[0]
54
- if issubclass(listed_type, Param):
55
- params[name] = {"type": "array", "items": listed_type.to_openai(context)}
56
- else:
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(listed_type, "string")},
62
+ "items": {"type": _TYPE_MAP.get(type_info.inner_type, "string")},
60
63
  }
61
- elif issubclass(type_, Param):
62
- params[name] = type_.to_openai(context)
63
- else:
64
- params[name] = {"type": _TYPE_MAP.get(type_, "string")}
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
- if get_origin(type_) == list:
101
- listed_type = get_args(type_)[0]
102
- if issubclass(listed_type, Param):
103
- list_ = [type_(**param_args) for param_args in kwargs[name]]
104
- else:
105
- list_ = kwargs[name]
106
- compiled_args[name] = list_
107
- elif issubclass(type_, Param):
108
- param_args = kwargs[name]
109
- compiled_args[name] = type_(**param_args)
110
- else:
111
- compiled_args[name] = kwargs[name]
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 Any, Type, get_origin, get_args, Self, Union
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
- if get_origin(attr_type) == list:
34
- origin_type = get_args(attr_type)[0]
35
- if is_primitive(origin_type):
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
- elif issubclass(origin_type, Param):
41
- SubParamModel = param_to_pydantic(origin_type)
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
- else:
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
- if get_origin(param_type) == list:
86
- pass
87
- elif is_primitive(param_type):
88
- fields[param_name] = (
89
- param_type,
90
- Field(default=param_info.default, description=param_info.description),
91
- )
92
- elif issubclass(param_type, Param):
93
- fields[param_name] = (
94
- param_to_pydantic(param_type),
95
- Field(default=param_info.default, description=param_info.description),
96
- )
97
- else:
98
- raise Exception(f"Unsupported type: {param_type}")
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -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.0"
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