pyagentic-core 1.2.1__tar.gz → 1.3.0a2__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 (24) hide show
  1. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/PKG-INFO +1 -1
  2. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_agent.py +52 -13
  3. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_metaclasses.py +46 -7
  4. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_params.py +1 -1
  5. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_tool.py +1 -1
  6. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_validation.py +1 -4
  7. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_utils/_typing.py +18 -15
  8. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/models/response.py +10 -12
  9. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/PKG-INFO +1 -1
  10. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyproject.toml +1 -1
  11. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/LICENSE +0 -0
  12. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/README.md +0 -0
  13. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/__init__.py +0 -0
  14. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/__init__.py +0 -0
  15. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_context.py +0 -0
  16. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_exceptions.py +0 -0
  17. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_resolver.py +0 -0
  18. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/logging.py +0 -0
  19. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/updates.py +0 -0
  20. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/SOURCES.txt +0 -0
  21. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  22. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/requires.txt +0 -0
  23. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/top_level.txt +0 -0
  24. {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 1.2.1
3
+ Version: 1.3.0a2
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -1,10 +1,11 @@
1
1
  import inspect
2
2
  import json
3
3
  import openai
4
- from typing import Callable, Any, TypeVar, ClassVar, Type
4
+ from typing import Callable, Any, TypeVar, ClassVar, Type, Self
5
5
 
6
6
  from pyagentic.logging import get_logger
7
- from pyagentic._base._tool import _ToolDefinition
7
+ from pyagentic._base._params import ParamInfo
8
+ from pyagentic._base._tool import _ToolDefinition, tool
8
9
  from pyagentic._base._context import ContextItem
9
10
  from pyagentic._base._metaclasses import AgentMeta
10
11
 
@@ -48,9 +49,12 @@ class Agent(metaclass=AgentMeta):
48
49
  __tool_defs__: ClassVar[dict[str, _ToolDefinition]]
49
50
  __context_attrs__: ClassVar[dict[str, tuple[TypeVar, ContextItem]]]
50
51
  __system_message__: ClassVar[str]
52
+ __description__: ClassVar[str]
51
53
  __input_template__: ClassVar[str] = None
52
54
  __response_model__: ClassVar[Type[AgentResponse]] = None
53
55
  __tool_response_models__: ClassVar[dict[str, Type[ToolResponse]]]
56
+ __linked_agents__: ClassVar[dict[str, Type[Self]]]
57
+ __call_params__: ClassVar[dict[str, tuple[TypeVar, ParamInfo]]]
54
58
 
55
59
  # Base Attributes
56
60
  model: str
@@ -60,9 +64,22 @@ class Agent(metaclass=AgentMeta):
60
64
  def __post_init__(self):
61
65
  self.client: openai.AsyncOpenAI = openai.AsyncOpenAI(api_key=self.api_key)
62
66
 
67
+ async def _process_agent_call(self, tool_call) -> AgentResponse:
68
+ logger.info(f"Calling {tool_call.name} with kwargs: {tool_call.arguments}")
69
+ self.context._messages.append(tool_call)
70
+ try:
71
+ agent = getattr(self, tool_call.name)
72
+ kwargs = json.loads(tool_call.arguments)
73
+ response = await agent(**kwargs)
74
+ result = f"Agent {tool_call.name}: {response.final_output}"
75
+ except Exception as e:
76
+ 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
77
+ self.context._messages.append(
78
+ {"type": "function_call_output", "call_id": tool_call.call_id, "output": result}
79
+ )
80
+ return response
81
+
63
82
  async def _process_tool_call(self, tool_call) -> ToolResponse:
64
- if tool_call.type != "function_call":
65
- return False
66
83
  self.context._messages.append(tool_call)
67
84
  logger.info(f"Calling {tool_call.name} with kwargs: {tool_call.arguments}")
68
85
  # Lookup the bound method
@@ -98,8 +115,8 @@ class Agent(metaclass=AgentMeta):
98
115
  self.context._messages.append(
99
116
  {"type": "function_call_output", "call_id": tool_call.call_id, "output": result}
100
117
  )
101
- ToolCalledModel = self.__tool_response_models__[tool_call.name]
102
- return ToolCalledModel(
118
+ ToolResponseModel = self.__tool_response_models__[tool_call.name]
119
+ return ToolResponseModel(
103
120
  raw_kwargs=tool_call.arguments, call_depth=0, output=result, **compiled_args
104
121
  )
105
122
 
@@ -110,6 +127,9 @@ class Agent(metaclass=AgentMeta):
110
127
  # Check if any of the tool params use a ContextRef
111
128
  # convert to openai schema
112
129
  tool_defs.append(tool_def.to_openai(self.context))
130
+ for name, agent in self.__linked_agents__.items():
131
+ tool_def = agent.get_tool_definition(name)
132
+ tool_defs.append(tool_def.to_openai(self.context))
113
133
  return tool_defs
114
134
 
115
135
  async def run(self, input_: str) -> str:
@@ -168,9 +188,16 @@ class Agent(metaclass=AgentMeta):
168
188
 
169
189
  # Dispatch any tool calls
170
190
  tool_responses = []
191
+ agent_responses = []
171
192
  for tool_call in tool_calls:
172
- tool_response = await self._process_tool_call(tool_call)
173
- tool_responses.append(tool_response)
193
+ if tool_call.type != "function_call":
194
+ continue
195
+ elif tool_call.name in self.__tool_defs__:
196
+ tool_response = await self._process_tool_call(tool_call)
197
+ tool_responses.append(tool_response)
198
+ elif tool_call.name in self.__linked_agents__:
199
+ agent_response = await self._process_agent_call(tool_call)
200
+ agent_responses.append(agent_response)
174
201
 
175
202
  # If tools ran, re-invoke LLM for natural reply
176
203
  if tool_responses:
@@ -198,8 +225,20 @@ class Agent(metaclass=AgentMeta):
198
225
  if self.emitter:
199
226
  await _safe_run(self.emitter, AiUpdate(status=Status.SUCCEDED, message=ai_message))
200
227
 
201
- return self.__response_model__(
202
- response=response,
203
- final_output=ai_message,
204
- tool_responses=tool_responses,
205
- )
228
+ response_fields = {"final_output": ai_message}
229
+ if self.__tool_defs__:
230
+ response_fields["tool_responses"] = tool_responses
231
+ if self.__linked_agents__:
232
+ response_fields["agent_responses"] = agent_responses
233
+
234
+ return self.__response_model__(**response_fields)
235
+
236
+ async def __call__(self, user_input: str):
237
+ return await self.run(input_=user_input)
238
+
239
+ @classmethod
240
+ def get_tool_definition(cls, name: str) -> _ToolDefinition:
241
+ tool_def = tool(cls.__description__)(cls.__call__).__tool_def__
242
+ # Override the name
243
+ tool_def.name = name
244
+ return tool_def
@@ -9,6 +9,12 @@ from pyagentic._base._tool import _ToolDefinition
9
9
 
10
10
  from pyagentic.models.response import AgentResponse, ToolResponse
11
11
 
12
+ from pyagentic._utils._typing import analyze_type
13
+
14
+
15
+ class Agent:
16
+ pass
17
+
12
18
 
13
19
  @dataclass_transform(field_specifiers=(ContextItem,))
14
20
  class AgentMeta(type):
@@ -69,6 +75,21 @@ class AgentMeta(type):
69
75
  context_attrs[name] = (computed_context, value)
70
76
  return context_attrs
71
77
 
78
+ @staticmethod
79
+ def _extract_linked_agents(annotations, Agent) -> dict[str, "Agent"]:
80
+ """
81
+ Extracts any class field from annotations and namespace where the value is that of
82
+ `ContextItem`, these will later be appeneded to the agents context. This will return
83
+ both the type and the user defined context item.
84
+ """
85
+ linked_agents = {}
86
+ for attr_name, attr_type in annotations.items():
87
+ type_info = analyze_type(attr_type, Agent)
88
+ if type_info.is_subclass:
89
+ linked_agents[attr_name] = attr_type
90
+
91
+ return linked_agents
92
+
72
93
  @staticmethod
73
94
  def _build_init_signature(cls) -> inspect.Signature:
74
95
  """
@@ -80,6 +101,8 @@ class AgentMeta(type):
80
101
  for field_name, field_type in cls.__annotations__.items():
81
102
  if field_name in cls.__context_attrs__:
82
103
  default_val = cls.__context_attrs__[field_name][1].get_default_value()
104
+ elif field_name in cls.__linked_agents__:
105
+ default_val = None
83
106
  else:
84
107
  default_val = getattr(cls, field_name, inspect._empty)
85
108
  param = inspect.Parameter(
@@ -106,7 +129,7 @@ class AgentMeta(type):
106
129
  name=self.__class__.__name__, ctx_map=self.__context_attrs__
107
130
  )
108
131
 
109
- context_kwargs = {}
132
+ compiled = {}
110
133
  for attr_name, (attr_type, attr_default) in self.__context_attrs__.items():
111
134
  # Skip compted contexts, this validaiton will happen with the validator
112
135
  # using a dry run with supplied default values
@@ -121,15 +144,25 @@ class AgentMeta(type):
121
144
  raise UnexpectedContextItemType(
122
145
  name=attr_name, expected=attr_type, recieved=type(val)
123
146
  )
124
- context_kwargs[attr_name] = val
147
+ compiled[attr_name] = val
125
148
  else:
126
- context_kwargs[attr_name] = attr_default.get_default_value()
149
+ compiled[attr_name] = attr_default.get_default_value()
127
150
 
128
151
  self.context = ContextClass(
129
152
  instructions=self.__system_message__,
130
153
  input_template=self.__input_template__,
131
- **context_kwargs,
154
+ **compiled,
132
155
  )
156
+ # ------------- Retrieve Linked Agents -------------------
157
+ for agent_name in self.__linked_agents__.keys():
158
+ agent_instance = kwargs.get(agent_name, None)
159
+
160
+ # Not sure if there is a better way, but setting model = "validation"
161
+ # bypasses checking agent in args. This is so the Validator is able to create
162
+ # a dummy agent wihout having to worry about creating a chain of linked agents
163
+ if not agent_instance and kwargs["model"] != "validation":
164
+ raise AttributeError(f"Linked Agent {agent_name} not found")
165
+ compiled[agent_name] = agent_instance
133
166
 
134
167
  bound = sig.bind(self, *args, **kwargs)
135
168
 
@@ -164,16 +197,22 @@ class AgentMeta(type):
164
197
  tool_name: ToolResponse.from_tool_def(tool_def)
165
198
  for tool_name, tool_def in cls.__tool_defs__.items()
166
199
  }
200
+ # Attach linked agents
201
+ cls.__linked_agents__ = mcs._extract_linked_agents(cls.__annotations__, cls.__bases__[0])
167
202
  # Create final Agent response model, using the tool response models
203
+ tool_response_models = list(cls.__tool_response_models__.values())
204
+ linked_agent_response_models = [
205
+ agent.__response_model__ for agent in cls.__linked_agents__.values()
206
+ ]
168
207
  cls.__response_model__ = AgentResponse.from_tool_defs(
169
- cls.__name__, list(cls.__tool_response_models__.values())
208
+ agent_name=cls.__name__,
209
+ tool_response_models=tool_response_models,
210
+ linked_agents_response_models=linked_agent_response_models,
170
211
  )
171
-
172
212
  # Build the new init
173
213
  sig = mcs._build_init_signature(cls)
174
214
  cls.__init__ = mcs._build_init(sig)
175
215
 
176
216
  # Validate agent
177
217
  _AgentConstructionValidator(cls).validate()
178
-
179
218
  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) == dict else 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
  """
@@ -54,20 +54,23 @@ def analyze_type(type_: type, base_class: type) -> TypeInfo:
54
54
  """
55
55
  origin = get_origin(type_)
56
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_)
57
+ try:
58
+ if origin == list:
59
+ inner_type = get_args(type_)[0]
60
+ if is_primitive(inner_type):
61
+ return TypeInfo(TypeCategory.LIST_PRIMITIVE, type_, inner_type)
62
+ elif issubclass(inner_type, base_class):
63
+ return TypeInfo(TypeCategory.LIST_SUBCLASS, type_, inner_type)
64
+ else:
65
+ return TypeInfo(TypeCategory.UNSUPPORTED, type_, inner_type)
66
+
67
+ elif is_primitive(type_):
68
+ return TypeInfo(TypeCategory.PRIMITIVE, type_)
69
+
70
+ elif issubclass(type_, base_class):
71
+ return TypeInfo(TypeCategory.SUBCLASS, type_)
68
72
 
69
- elif issubclass(type_, base_class):
70
- return TypeInfo(TypeCategory.SUBCLASS, type_)
71
-
72
- else:
73
+ else:
74
+ return TypeInfo(TypeCategory.UNSUPPORTED, type_)
75
+ except TypeError:
73
76
  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, agent_name: str, tool_response_models: list[Type[ToolResponse]]
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
- return create_model(
131
- f"{agent_name}Response", __base__=cls, tool_responses=(list[ToolResult], ...)
132
- )
133
- else:
134
- return create_model(
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.2.1
3
+ Version: 1.3.0a2
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyagentic-core"
3
- version = "1.2.1"
3
+ version = "1.3.0-a.2"
4
4
  description = "Build LLM Agents in a Pythonic way"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
File without changes