pyagentic-core 2.2.0a1__tar.gz → 2.2.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-2.2.0a1 → pyagentic_core-2.2.0a2}/PKG-INFO +1 -1
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/_agent.py +49 -41
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/_agent_state.py +47 -5
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_tool.py +5 -4
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/PKG-INFO +1 -1
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyproject.toml +1 -1
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/LICENSE +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/README.md +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/__init__.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/__init__.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/_agent_linking.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_info.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_metaclasses.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_ref.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_spec.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_state.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_validation.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_utils/_typing.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_utils/_warnings.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/__init__.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_anthropic.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_gemini.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_mock.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_openai.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_openaiv1.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_provider.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/logging.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/models/llm.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/models/response.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/models/tracing.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/policies/__init__.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/policies/_events.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/policies/_policy.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/__init__.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/_basic.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/_langfuse.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/_tracer.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/updates.py +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/SOURCES.txt +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/requires.txt +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/setup.cfg +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import json
|
|
3
|
+
import asyncio
|
|
3
4
|
from functools import wraps
|
|
4
5
|
from typing import (
|
|
5
6
|
Callable,
|
|
@@ -169,7 +170,6 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
169
170
|
phases: ClassVar[list[tuple[str, str, Callable]]] = None
|
|
170
171
|
|
|
171
172
|
# Generated Class Attributes (built by metaclass)
|
|
172
|
-
__machine__: Machine | None = None
|
|
173
173
|
__response_model__: ClassVar[Type[AgentResponse]] = None # Pydantic response model
|
|
174
174
|
__state_class__: ClassVar[Type[_AgentState]] = None # Generated state class
|
|
175
175
|
__tool_response_models__: ClassVar[dict[str, Type[ToolResponse]]] # Tool response models
|
|
@@ -232,34 +232,6 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
232
232
|
if self.__tool_defs__ and not self.provider.__supports_tool_calls__:
|
|
233
233
|
raise Exception("Tools are not supported with this provider")
|
|
234
234
|
|
|
235
|
-
def _build_phase_machine(self) -> Machine:
|
|
236
|
-
if not self.phases:
|
|
237
|
-
return None
|
|
238
|
-
|
|
239
|
-
states = []
|
|
240
|
-
for to_, from_, _ in self.phases:
|
|
241
|
-
if to_ not in states:
|
|
242
|
-
states.append(to_)
|
|
243
|
-
if from_ not in states:
|
|
244
|
-
states.append(from_)
|
|
245
|
-
|
|
246
|
-
machine = Machine(states=states, initial=states[0])
|
|
247
|
-
|
|
248
|
-
for to_, from_, _ in self.phases:
|
|
249
|
-
machine.add_transition(
|
|
250
|
-
trigger=f"{to_}_to_{from_}",
|
|
251
|
-
source=to_,
|
|
252
|
-
dest=from_,
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
return machine
|
|
256
|
-
|
|
257
|
-
def _update_state_machine(self):
|
|
258
|
-
for to_, from_, condition in self.phases:
|
|
259
|
-
if condition(self.state):
|
|
260
|
-
trigger = f"{to_}_to_{from_}"
|
|
261
|
-
getattr(self.__machine__, trigger)()
|
|
262
|
-
|
|
263
235
|
def __post_init__(self):
|
|
264
236
|
"""
|
|
265
237
|
Post-initialization hook called after agent instance is created.
|
|
@@ -281,7 +253,9 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
281
253
|
```
|
|
282
254
|
"""
|
|
283
255
|
self._check_llm_provider()
|
|
284
|
-
|
|
256
|
+
|
|
257
|
+
if self.phases:
|
|
258
|
+
self.state._build_phase_machine(self.phases)
|
|
285
259
|
|
|
286
260
|
# Use BasicTracer as default if no tracer provided
|
|
287
261
|
if not self.tracer:
|
|
@@ -347,7 +321,14 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
347
321
|
self.state._messages.append(
|
|
348
322
|
Message(role="assistant", content="Failed to generate a response")
|
|
349
323
|
)
|
|
350
|
-
|
|
324
|
+
response = LLMResponse(
|
|
325
|
+
text=f"The LLM failed to generate a response: {e}", tool_calls=[]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if self.phases:
|
|
329
|
+
self.state._update_state_machine(phases=self.phases)
|
|
330
|
+
|
|
331
|
+
return response
|
|
351
332
|
|
|
352
333
|
@traced(SpanKind.AGENT)
|
|
353
334
|
async def _process_agent_call(self, tool_call: ToolCall) -> AgentResponse:
|
|
@@ -394,6 +375,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
394
375
|
self.state._messages.append(
|
|
395
376
|
self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
|
|
396
377
|
)
|
|
378
|
+
if self.phases:
|
|
379
|
+
self.state._update_state_machine(phases=self.phases)
|
|
397
380
|
return response
|
|
398
381
|
|
|
399
382
|
@traced(SpanKind.TOOL)
|
|
@@ -458,6 +441,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
458
441
|
self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
|
|
459
442
|
)
|
|
460
443
|
|
|
444
|
+
if self.phases:
|
|
445
|
+
self.state._update_state_machine(phases=self.phases)
|
|
461
446
|
# Build and return the structured tool response
|
|
462
447
|
ToolResponseModel = self.__tool_response_models__[tool_call.name]
|
|
463
448
|
return ToolResponseModel(
|
|
@@ -479,7 +464,12 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
479
464
|
# Add all @tool decorated methods
|
|
480
465
|
for tool_def in self.__tool_defs__.values():
|
|
481
466
|
# Resolve StateRefs in parameters (e.g., ref.self.user_name -> actual value)
|
|
482
|
-
|
|
467
|
+
|
|
468
|
+
if self.phases and tool_def.phases:
|
|
469
|
+
if self.state.phase in tool_def.phases:
|
|
470
|
+
tool_defs.append(tool_def.resolve(self.agent_reference))
|
|
471
|
+
else:
|
|
472
|
+
tool_defs.append(tool_def.resolve(self.agent_reference))
|
|
483
473
|
|
|
484
474
|
# Add linked agents as tools
|
|
485
475
|
for name, linked_def in self.__linked_agents__.items():
|
|
@@ -559,27 +549,45 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
559
549
|
self.state._messages.append(Message(role="assistant", content=response.text))
|
|
560
550
|
break
|
|
561
551
|
|
|
562
|
-
|
|
552
|
+
tasks = []
|
|
553
|
+
|
|
554
|
+
async def wrap(kind: str, tool_call, coro):
|
|
555
|
+
"""Run the coroutine and attach metadata."""
|
|
556
|
+
result = await coro
|
|
557
|
+
return kind, tool_call, result
|
|
558
|
+
|
|
563
559
|
for tool_call in response.tool_calls:
|
|
564
|
-
# Skip if we've already processed this call (prevents duplicates)
|
|
565
560
|
if tool_call.id and tool_call.id in processed_call_ids:
|
|
566
561
|
continue
|
|
567
562
|
|
|
568
563
|
processed_call_ids.add(tool_call.id)
|
|
569
564
|
|
|
570
|
-
# Route to either @tool methods or linked agents
|
|
571
565
|
if tool_call.name in self.__tool_defs__:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
yield result
|
|
566
|
+
coro = self._process_tool_call(tool_call, call_depth=depth)
|
|
567
|
+
kind = "tool"
|
|
575
568
|
|
|
576
569
|
elif tool_call.name in self.__linked_agents__:
|
|
577
|
-
|
|
570
|
+
coro = self._process_agent_call(tool_call)
|
|
571
|
+
kind = "agent"
|
|
572
|
+
|
|
573
|
+
else:
|
|
574
|
+
continue
|
|
575
|
+
|
|
576
|
+
task = asyncio.create_task(wrap(kind, tool_call, coro))
|
|
577
|
+
tasks.append(task)
|
|
578
|
+
|
|
579
|
+
# Process tasks as they *finish*, not in original order
|
|
580
|
+
for task in asyncio.as_completed(tasks):
|
|
581
|
+
kind, tool_call, result = await task
|
|
582
|
+
|
|
583
|
+
if kind == "tool":
|
|
584
|
+
tool_responses.append(result)
|
|
585
|
+
else:
|
|
578
586
|
agent_responses.append(result)
|
|
579
|
-
|
|
587
|
+
|
|
588
|
+
yield result
|
|
580
589
|
|
|
581
590
|
# Increment depth and continue loop (LLM will see tool results next iteration)
|
|
582
|
-
self._update_state_machine()
|
|
583
591
|
depth += 1
|
|
584
592
|
|
|
585
593
|
# If we exhausted max_call_depth without final text, get one more response
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import threading
|
|
3
3
|
from typing import Any, Type, Self, Optional, ClassVar
|
|
4
|
-
from pydantic import BaseModel, create_model, Field, PrivateAttr,
|
|
4
|
+
from pydantic import BaseModel, create_model, Field, PrivateAttr, computed_field
|
|
5
5
|
from jinja2 import Template
|
|
6
6
|
from typing import Optional, Literal, Callable
|
|
7
7
|
from transitions import Machine
|
|
@@ -26,21 +26,52 @@ class _AgentState(BaseModel):
|
|
|
26
26
|
("on", EventKind.SET): "on_set",
|
|
27
27
|
("background", EventKind.SET): "background_set",
|
|
28
28
|
}
|
|
29
|
-
_state_lock: ClassVar[threading.Lock] = PrivateAttr(default_factory=threading.Lock)
|
|
30
29
|
__policies__: ClassVar[dict[str, list[Policy]]]
|
|
31
30
|
|
|
32
31
|
instructions: str
|
|
33
32
|
input_template: Optional[str] = "{{ user_message }}"
|
|
33
|
+
_machine: Machine = PrivateAttr(default=None)
|
|
34
34
|
_messages: list[Message] = PrivateAttr(default_factory=list)
|
|
35
35
|
_instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
|
|
36
36
|
_input_template: Template = PrivateAttr(
|
|
37
37
|
default_factory=lambda: Template(source="{{ user_message }}")
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
+
def _build_phase_machine(self, phases: list[tuple[str, str, Callable]]) -> Machine:
|
|
41
|
+
if not phases:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
states = []
|
|
45
|
+
for source, dest, _ in phases:
|
|
46
|
+
if source not in states:
|
|
47
|
+
states.append(source)
|
|
48
|
+
if dest not in states:
|
|
49
|
+
states.append(dest)
|
|
50
|
+
|
|
51
|
+
machine = Machine(states=states, initial=states[0])
|
|
52
|
+
|
|
53
|
+
for source, dest, _ in phases:
|
|
54
|
+
machine.add_transition(
|
|
55
|
+
trigger=f"{source}_to_{dest}",
|
|
56
|
+
source=source,
|
|
57
|
+
dest=dest,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self._machine = machine
|
|
61
|
+
|
|
62
|
+
def _update_state_machine(self, phases):
|
|
63
|
+
for to_, from_, condition in phases:
|
|
64
|
+
with self._state_lock:
|
|
65
|
+
if condition(self) and self.phase == from_:
|
|
66
|
+
trigger = f"{to_}_to_{from_}"
|
|
67
|
+
getattr(self._machine, trigger)()
|
|
68
|
+
|
|
40
69
|
def model_post_init(self, state):
|
|
41
70
|
self._instructions_template = Template(source=self.instructions)
|
|
42
71
|
if self.input_template:
|
|
43
72
|
self._input_template = Template(source=self.input_template)
|
|
73
|
+
|
|
74
|
+
self._state_lock = threading.Lock()
|
|
44
75
|
return super().model_post_init(state)
|
|
45
76
|
|
|
46
77
|
def get_policies(self, state_name: str) -> list[Policy]:
|
|
@@ -179,7 +210,8 @@ class _AgentState(BaseModel):
|
|
|
179
210
|
event = SetEvent(name=name, previous=previous, value=value)
|
|
180
211
|
final_value = self._run_policies(event, "on")
|
|
181
212
|
|
|
182
|
-
|
|
213
|
+
with self._state_lock:
|
|
214
|
+
setattr(self, name, final_value)
|
|
183
215
|
asyncio.create_task(self._dispatch_policies(event, "background"))
|
|
184
216
|
|
|
185
217
|
@classmethod
|
|
@@ -222,6 +254,10 @@ class _AgentState(BaseModel):
|
|
|
222
254
|
# now build the dataclass
|
|
223
255
|
return create_model(f"AgentState[{name}]", __base__=cls, **pydantic_fields)
|
|
224
256
|
|
|
257
|
+
@property
|
|
258
|
+
def phase(self) -> str:
|
|
259
|
+
return self._machine.state if self._machine else None
|
|
260
|
+
|
|
225
261
|
@property
|
|
226
262
|
def recent_message(self) -> Message:
|
|
227
263
|
"""
|
|
@@ -243,7 +279,10 @@ class _AgentState(BaseModel):
|
|
|
243
279
|
# start with all the normal dataclass fields
|
|
244
280
|
|
|
245
281
|
# now format your instruction template
|
|
246
|
-
|
|
282
|
+
if self.phase:
|
|
283
|
+
return self._instructions_template.render(phase=self.phase, **self.model_dump())
|
|
284
|
+
else:
|
|
285
|
+
return self._instructions_template.render(**self.model_dump())
|
|
247
286
|
|
|
248
287
|
@property
|
|
249
288
|
def messages(self) -> list[Message]:
|
|
@@ -271,7 +310,10 @@ class _AgentState(BaseModel):
|
|
|
271
310
|
if self.input_template:
|
|
272
311
|
data = self.model_dump()
|
|
273
312
|
data["user_message"] = message
|
|
274
|
-
|
|
313
|
+
if self.phase:
|
|
314
|
+
content = self._input_template.render(phase=self.phase, **self.model_dump())
|
|
315
|
+
else:
|
|
316
|
+
content = self._input_template.render(**self.model_dump())
|
|
275
317
|
else:
|
|
276
318
|
content = message
|
|
277
319
|
self._messages.append(Message(role="user", content=content))
|
|
@@ -43,12 +43,14 @@ class _ToolDefinition:
|
|
|
43
43
|
parameters: dict[str, tuple[TypeVar, ParamInfo]],
|
|
44
44
|
return_type: Type[Any],
|
|
45
45
|
condition: Callable[[Any], bool] = None,
|
|
46
|
+
phases: list[str] = None,
|
|
46
47
|
):
|
|
47
48
|
self.name: str = name
|
|
48
49
|
self.description: str = description
|
|
49
50
|
self.parameters: dict[str, tuple[TypeVar, ParamInfo]] = parameters
|
|
50
51
|
self.condition = condition
|
|
51
52
|
self.return_type = return_type
|
|
53
|
+
self.phases = phases if phases else []
|
|
52
54
|
|
|
53
55
|
def resolve(self, agent_reference: dict) -> Self:
|
|
54
56
|
new_parameters = {}
|
|
@@ -67,6 +69,7 @@ class _ToolDefinition:
|
|
|
67
69
|
parameters=new_parameters,
|
|
68
70
|
condition=self.condition,
|
|
69
71
|
return_type=self.return_type,
|
|
72
|
+
phases=self.phases,
|
|
70
73
|
)
|
|
71
74
|
|
|
72
75
|
def to_openai_spec(self) -> dict:
|
|
@@ -210,10 +213,7 @@ class _ToolDefinition:
|
|
|
210
213
|
return compiled_args
|
|
211
214
|
|
|
212
215
|
|
|
213
|
-
def tool(
|
|
214
|
-
description: str,
|
|
215
|
-
condition: Callable[[Any], bool] = None,
|
|
216
|
-
):
|
|
216
|
+
def tool(description: str, condition: Callable[[Any], bool] = None, phases: list[str] = None):
|
|
217
217
|
"""
|
|
218
218
|
Decorator to mark an agent method as a tool that the LLM can call.
|
|
219
219
|
|
|
@@ -281,6 +281,7 @@ def tool(
|
|
|
281
281
|
parameters=params,
|
|
282
282
|
condition=condition,
|
|
283
283
|
return_type=return_type,
|
|
284
|
+
phases=phases,
|
|
284
285
|
)
|
|
285
286
|
return fn
|
|
286
287
|
|
|
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
|
|
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
|
|
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
|
{pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|