pyagentic-core 2.2.0a1__tar.gz → 2.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 (45) hide show
  1. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/PKG-INFO +1 -1
  2. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_agent/_agent.py +35 -35
  3. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_agent/_agent_state.py +42 -3
  4. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_info.py +1 -0
  5. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_spec.py +9 -2
  6. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_tool.py +5 -4
  7. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic_core.egg-info/PKG-INFO +1 -1
  8. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyproject.toml +1 -1
  9. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/LICENSE +0 -0
  10. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/README.md +0 -0
  11. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/__init__.py +0 -0
  12. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/__init__.py +0 -0
  13. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_agent/__init__.py +0 -0
  14. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  15. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_exceptions.py +0 -0
  16. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_metaclasses.py +0 -0
  17. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_ref.py +0 -0
  18. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_state.py +0 -0
  19. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_base/_validation.py +0 -0
  20. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_utils/_typing.py +0 -0
  21. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/_utils/_warnings.py +0 -0
  22. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/llm/__init__.py +0 -0
  23. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/llm/_anthropic.py +0 -0
  24. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/llm/_gemini.py +0 -0
  25. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/llm/_mock.py +0 -0
  26. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/llm/_openai.py +0 -0
  27. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/llm/_openaiv1.py +0 -0
  28. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/llm/_provider.py +0 -0
  29. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/logging.py +0 -0
  30. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/models/llm.py +0 -0
  31. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/models/response.py +0 -0
  32. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/models/tracing.py +0 -0
  33. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/policies/__init__.py +0 -0
  34. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/policies/_events.py +0 -0
  35. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/policies/_policy.py +0 -0
  36. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/tracing/__init__.py +0 -0
  37. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/tracing/_basic.py +0 -0
  38. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/tracing/_langfuse.py +0 -0
  39. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/tracing/_tracer.py +0 -0
  40. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic/updates.py +0 -0
  41. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic_core.egg-info/SOURCES.txt +0 -0
  42. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  43. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic_core.egg-info/requires.txt +0 -0
  44. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/pyagentic_core.egg-info/top_level.txt +0 -0
  45. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.2.0a1
3
+ Version: 2.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
@@ -169,7 +169,6 @@ class BaseAgent(metaclass=AgentMeta):
169
169
  phases: ClassVar[list[tuple[str, str, Callable]]] = None
170
170
 
171
171
  # Generated Class Attributes (built by metaclass)
172
- __machine__: Machine | None = None
173
172
  __response_model__: ClassVar[Type[AgentResponse]] = None # Pydantic response model
174
173
  __state_class__: ClassVar[Type[_AgentState]] = None # Generated state class
175
174
  __tool_response_models__: ClassVar[dict[str, Type[ToolResponse]]] # Tool response models
@@ -232,34 +231,6 @@ class BaseAgent(metaclass=AgentMeta):
232
231
  if self.__tool_defs__ and not self.provider.__supports_tool_calls__:
233
232
  raise Exception("Tools are not supported with this provider")
234
233
 
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
234
  def __post_init__(self):
264
235
  """
265
236
  Post-initialization hook called after agent instance is created.
@@ -281,7 +252,9 @@ class BaseAgent(metaclass=AgentMeta):
281
252
  ```
282
253
  """
283
254
  self._check_llm_provider()
284
- self.__machine__ = self._build_phase_machine()
255
+
256
+ if self.phases:
257
+ self.state._build_phase_machine(self.phases)
285
258
 
286
259
  # Use BasicTracer as default if no tracer provided
287
260
  if not self.tracer:
@@ -347,7 +320,14 @@ class BaseAgent(metaclass=AgentMeta):
347
320
  self.state._messages.append(
348
321
  Message(role="assistant", content="Failed to generate a response")
349
322
  )
350
- return LLMResponse(text=f"The LLM failed to generate a response: {e}", tool_calls=[])
323
+ response = LLMResponse(
324
+ text=f"The LLM failed to generate a response: {e}", tool_calls=[]
325
+ )
326
+
327
+ if self.phases:
328
+ self.state._update_state_machine(phases=self.phases)
329
+
330
+ return response
351
331
 
352
332
  @traced(SpanKind.AGENT)
353
333
  async def _process_agent_call(self, tool_call: ToolCall) -> AgentResponse:
@@ -394,6 +374,8 @@ class BaseAgent(metaclass=AgentMeta):
394
374
  self.state._messages.append(
395
375
  self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
396
376
  )
377
+ if self.phases:
378
+ self.state._update_state_machine(phases=self.phases)
397
379
  return response
398
380
 
399
381
  @traced(SpanKind.TOOL)
@@ -458,6 +440,8 @@ class BaseAgent(metaclass=AgentMeta):
458
440
  self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
459
441
  )
460
442
 
443
+ if self.phases:
444
+ self.state._update_state_machine(phases=self.phases)
461
445
  # Build and return the structured tool response
462
446
  ToolResponseModel = self.__tool_response_models__[tool_call.name]
463
447
  return ToolResponseModel(
@@ -478,13 +462,30 @@ class BaseAgent(metaclass=AgentMeta):
478
462
 
479
463
  # Add all @tool decorated methods
480
464
  for tool_def in self.__tool_defs__.values():
465
+ if tool_def.condition:
466
+ if not tool_def.condition(self.state):
467
+ continue
481
468
  # Resolve StateRefs in parameters (e.g., ref.self.user_name -> actual value)
482
- tool_defs.append(tool_def.resolve(self.agent_reference))
469
+ if self.phases and tool_def.phases:
470
+ if self.state.phase in tool_def.phases:
471
+ tool_defs.append(tool_def.resolve(self.agent_reference))
472
+ else:
473
+ tool_defs.append(tool_def.resolve(self.agent_reference))
483
474
 
484
475
  # Add linked agents as tools
485
476
  for name, linked_def in self.__linked_agents__.items():
486
- tool_def = linked_def.agent.get_tool_definition(name)
487
- tool_defs.append(tool_def.resolve(self.agent_reference))
477
+
478
+ if linked_def.info.condition:
479
+ if not linked_def.info.condition(self.state):
480
+ continue
481
+
482
+ if self.phases and linked_def.info.phases:
483
+ if self.state.phase in linked_def.info.phases:
484
+ tool_def = linked_def.agent.get_tool_definition(name)
485
+ tool_defs.append(tool_def.resolve(self.agent_reference))
486
+ else:
487
+ tool_def = linked_def.agent.get_tool_definition(name)
488
+ tool_defs.append(tool_def.resolve(self.agent_reference))
488
489
 
489
490
  return tool_defs
490
491
 
@@ -579,7 +580,6 @@ class BaseAgent(metaclass=AgentMeta):
579
580
  yield result
580
581
 
581
582
  # Increment depth and continue loop (LLM will see tool results next iteration)
582
- self._update_state_machine()
583
583
  depth += 1
584
584
 
585
585
  # 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, ConfigDict
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
@@ -31,12 +31,41 @@ class _AgentState(BaseModel):
31
31
 
32
32
  instructions: str
33
33
  input_template: Optional[str] = "{{ user_message }}"
34
+ _machine: Machine = PrivateAttr(default=None)
34
35
  _messages: list[Message] = PrivateAttr(default_factory=list)
35
36
  _instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
36
37
  _input_template: Template = PrivateAttr(
37
38
  default_factory=lambda: Template(source="{{ user_message }}")
38
39
  )
39
40
 
41
+ def _build_phase_machine(self, phases: list[tuple[str, str, Callable]]) -> Machine:
42
+ if not phases:
43
+ return None
44
+
45
+ states = []
46
+ for source, dest, _ in phases:
47
+ if source not in states:
48
+ states.append(source)
49
+ if dest not in states:
50
+ states.append(dest)
51
+
52
+ machine = Machine(states=states, initial=states[0])
53
+
54
+ for source, dest, _ in phases:
55
+ machine.add_transition(
56
+ trigger=f"{source}_to_{dest}",
57
+ source=source,
58
+ dest=dest,
59
+ )
60
+
61
+ self._machine = machine
62
+
63
+ def _update_state_machine(self, phases):
64
+ for to_, from_, condition in phases:
65
+ if condition(self):
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:
@@ -222,6 +251,10 @@ class _AgentState(BaseModel):
222
251
  # now build the dataclass
223
252
  return create_model(f"AgentState[{name}]", __base__=cls, **pydantic_fields)
224
253
 
254
+ @property
255
+ def phase(self) -> str:
256
+ return self._machine.state if self._machine else None
257
+
225
258
  @property
226
259
  def recent_message(self) -> Message:
227
260
  """
@@ -243,7 +276,10 @@ class _AgentState(BaseModel):
243
276
  # start with all the normal dataclass fields
244
277
 
245
278
  # now format your instruction template
246
- return self._instructions_template.render(**self.model_dump())
279
+ if self.phase:
280
+ return self._instructions_template.render(phase=self.phase, **self.model_dump())
281
+ else:
282
+ return self._instructions_template.render(**self.model_dump())
247
283
 
248
284
  @property
249
285
  def messages(self) -> list[Message]:
@@ -271,7 +307,10 @@ class _AgentState(BaseModel):
271
307
  if self.input_template:
272
308
  data = self.model_dump()
273
309
  data["user_message"] = message
274
- content = self._input_template.render(**data)
310
+ if self.phase:
311
+ content = self._input_template.render(phase=self.phase, **self.model_dump())
312
+ else:
313
+ content = self._input_template.render(**self.model_dump())
275
314
  else:
276
315
  content = message
277
316
  self._messages.append(Message(role="user", content=content))
@@ -45,6 +45,7 @@ class AgentInfo(_SpecInfo):
45
45
  """
46
46
 
47
47
  condition: MaybeRef[Callable] | None = None
48
+ phases: list[str] | None = None
48
49
 
49
50
 
50
51
  @dataclass
@@ -117,7 +117,10 @@ class spec:
117
117
 
118
118
  @staticmethod
119
119
  def AgentLink(
120
- default: Any = None, default_factory: Callable = None, condition: Callable = None
120
+ default: Any = None,
121
+ default_factory: Callable = None,
122
+ condition: Callable = None,
123
+ phases: list[str] | None = None,
121
124
  ) -> AgentInfo:
122
125
  """
123
126
  Creates an AgentInfo descriptor for configuring linked agent fields.
@@ -126,8 +129,12 @@ class spec:
126
129
  default (Any, optional): The default agent instance
127
130
  default_factory (Callable, optional): A factory function to generate the default agent
128
131
  condition (Callable, optional): A callable determining when this agent link is active
132
+ phases (list[str], optional): A list of phases of when this agent will be available.
133
+ When None, will show for all phases. Defaults to None.
129
134
 
130
135
  Returns:
131
136
  AgentInfo: A configured AgentInfo descriptor
132
137
  """
133
- return AgentInfo(default=default, default_factory=default_factory, condition=condition)
138
+ return AgentInfo(
139
+ default=default, default_factory=default_factory, condition=condition, phases=phases
140
+ )
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.2.0a1
3
+ Version: 2.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyagentic-core"
3
- version = "2.2.0-a.1"
3
+ version = "2.2.1"
4
4
  description = "Build LLM Agents in a Pythonic way"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
File without changes