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.
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/PKG-INFO +1 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_agent.py +52 -13
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_metaclasses.py +46 -7
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_params.py +1 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_tool.py +1 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_validation.py +1 -4
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_utils/_typing.py +18 -15
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/models/response.py +10 -12
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/PKG-INFO +1 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyproject.toml +1 -1
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/LICENSE +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/README.md +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/__init__.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_context.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/_base/_resolver.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/logging.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic/updates.py +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/SOURCES.txt +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/requires.txt +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/setup.cfg +0 -0
|
@@ -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.
|
|
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
|
-
|
|
102
|
-
return
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
compiled[attr_name] = val
|
|
125
148
|
else:
|
|
126
|
-
|
|
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
|
-
**
|
|
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__,
|
|
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)
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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,
|
|
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)
|
|
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
|
{pyagentic_core-1.2.1 → pyagentic_core-1.3.0a2}/pyagentic_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|