pyagentic-core 2.1.0a3__tar.gz → 2.2.0__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.1.0a3 → pyagentic_core-2.2.0}/PKG-INFO +2 -1
  2. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent.py +23 -2
  3. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent_state.py +44 -4
  4. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_tool.py +5 -4
  5. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/PKG-INFO +2 -1
  6. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/requires.txt +1 -0
  7. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyproject.toml +3 -2
  8. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/LICENSE +0 -0
  9. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/README.md +0 -0
  10. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/__init__.py +0 -0
  11. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/__init__.py +0 -0
  12. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/__init__.py +0 -0
  13. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  14. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_exceptions.py +0 -0
  15. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_info.py +0 -0
  16. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_metaclasses.py +0 -0
  17. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_ref.py +0 -0
  18. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_spec.py +0 -0
  19. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_state.py +0 -0
  20. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_base/_validation.py +0 -0
  21. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_utils/_typing.py +0 -0
  22. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/_utils/_warnings.py +0 -0
  23. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/llm/__init__.py +0 -0
  24. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/llm/_anthropic.py +0 -0
  25. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/llm/_gemini.py +0 -0
  26. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/llm/_mock.py +0 -0
  27. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/llm/_openai.py +0 -0
  28. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/llm/_openaiv1.py +0 -0
  29. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/llm/_provider.py +0 -0
  30. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/logging.py +0 -0
  31. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/models/llm.py +0 -0
  32. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/models/response.py +0 -0
  33. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/models/tracing.py +0 -0
  34. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/policies/__init__.py +0 -0
  35. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/policies/_events.py +0 -0
  36. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/policies/_policy.py +0 -0
  37. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/tracing/__init__.py +0 -0
  38. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/tracing/_basic.py +0 -0
  39. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/tracing/_langfuse.py +0 -0
  40. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/tracing/_tracer.py +0 -0
  41. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic/updates.py +0 -0
  42. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/SOURCES.txt +0 -0
  43. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  44. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/top_level.txt +0 -0
  45. {pyagentic_core-2.1.0a3 → pyagentic_core-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.1.0a3
3
+ Version: 2.2.0
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Requires-Dist: typeguard>=4.4.4
20
20
  Requires-Dist: c3linearize>=0.1.0
21
21
  Requires-Dist: anthropic>=0.62.0
22
22
  Requires-Dist: google-generativeai>=0.8.0
23
+ Requires-Dist: transitions>=0.9.3
23
24
  Dynamic: license-file
24
25
 
25
26
  # PyAgentic
@@ -13,6 +13,7 @@ from typing import (
13
13
  Union,
14
14
  )
15
15
 
16
+ from transitions import Machine
16
17
  from pydantic import BaseModel, ValidationError
17
18
 
18
19
  from pyagentic.logging import get_logger
@@ -165,6 +166,7 @@ class BaseAgent(metaclass=AgentMeta):
165
166
  __description__: ClassVar[str] # Optional: description for linked agents
166
167
  __input_template__: ClassVar[str] = None # Optional: template for user input
167
168
  __response_format__: ClassVar[Type[BaseModel]] = None # Optional: structured output format
169
+ phases: ClassVar[list[tuple[str, str, Callable]]] = None
168
170
 
169
171
  # Generated Class Attributes (built by metaclass)
170
172
  __response_model__: ClassVar[Type[AgentResponse]] = None # Pydantic response model
@@ -251,6 +253,9 @@ class BaseAgent(metaclass=AgentMeta):
251
253
  """
252
254
  self._check_llm_provider()
253
255
 
256
+ if self.phases:
257
+ self.state._build_phase_machine(self.phases)
258
+
254
259
  # Use BasicTracer as default if no tracer provided
255
260
  if not self.tracer:
256
261
  self.tracer = BasicTracer()
@@ -315,7 +320,14 @@ class BaseAgent(metaclass=AgentMeta):
315
320
  self.state._messages.append(
316
321
  Message(role="assistant", content="Failed to generate a response")
317
322
  )
318
- 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
319
331
 
320
332
  @traced(SpanKind.AGENT)
321
333
  async def _process_agent_call(self, tool_call: ToolCall) -> AgentResponse:
@@ -362,6 +374,8 @@ class BaseAgent(metaclass=AgentMeta):
362
374
  self.state._messages.append(
363
375
  self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
364
376
  )
377
+ if self.phases:
378
+ self.state._update_state_machine(phases=self.phases)
365
379
  return response
366
380
 
367
381
  @traced(SpanKind.TOOL)
@@ -426,6 +440,8 @@ class BaseAgent(metaclass=AgentMeta):
426
440
  self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
427
441
  )
428
442
 
443
+ if self.phases:
444
+ self.state._update_state_machine(phases=self.phases)
429
445
  # Build and return the structured tool response
430
446
  ToolResponseModel = self.__tool_response_models__[tool_call.name]
431
447
  return ToolResponseModel(
@@ -447,7 +463,12 @@ class BaseAgent(metaclass=AgentMeta):
447
463
  # Add all @tool decorated methods
448
464
  for tool_def in self.__tool_defs__.values():
449
465
  # Resolve StateRefs in parameters (e.g., ref.self.user_name -> actual value)
450
- tool_defs.append(tool_def.resolve(self.agent_reference))
466
+
467
+ if self.phases and tool_def.phases:
468
+ if self.state.phase in tool_def.phases:
469
+ tool_defs.append(tool_def.resolve(self.agent_reference))
470
+ else:
471
+ tool_defs.append(tool_def.resolve(self.agent_reference))
451
472
 
452
473
  # Add linked agents as tools
453
474
  for name, linked_def in self.__linked_agents__.items():
@@ -1,9 +1,10 @@
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
- from typing import Optional, Literal
6
+ from typing import Optional, Literal, Callable
7
+ from transitions import Machine
7
8
 
8
9
  from pyagentic._base._exceptions import InvalidStateRefNotFoundInState
9
10
  from pyagentic._base._state import _StateDefinition
@@ -30,12 +31,41 @@ class _AgentState(BaseModel):
30
31
 
31
32
  instructions: str
32
33
  input_template: Optional[str] = "{{ user_message }}"
34
+ _machine: Machine = PrivateAttr(default=None)
33
35
  _messages: list[Message] = PrivateAttr(default_factory=list)
34
36
  _instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
35
37
  _input_template: Template = PrivateAttr(
36
38
  default_factory=lambda: Template(source="{{ user_message }}")
37
39
  )
38
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
+
39
69
  def model_post_init(self, state):
40
70
  self._instructions_template = Template(source=self.instructions)
41
71
  if self.input_template:
@@ -221,6 +251,10 @@ class _AgentState(BaseModel):
221
251
  # now build the dataclass
222
252
  return create_model(f"AgentState[{name}]", __base__=cls, **pydantic_fields)
223
253
 
254
+ @property
255
+ def phase(self) -> str:
256
+ return self._machine.state if self._machine else None
257
+
224
258
  @property
225
259
  def recent_message(self) -> Message:
226
260
  """
@@ -242,7 +276,10 @@ class _AgentState(BaseModel):
242
276
  # start with all the normal dataclass fields
243
277
 
244
278
  # now format your instruction template
245
- 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())
246
283
 
247
284
  @property
248
285
  def messages(self) -> list[Message]:
@@ -270,7 +307,10 @@ class _AgentState(BaseModel):
270
307
  if self.input_template:
271
308
  data = self.model_dump()
272
309
  data["user_message"] = message
273
- 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())
274
314
  else:
275
315
  content = message
276
316
  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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.1.0a3
3
+ Version: 2.2.0
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Requires-Dist: typeguard>=4.4.4
20
20
  Requires-Dist: c3linearize>=0.1.0
21
21
  Requires-Dist: anthropic>=0.62.0
22
22
  Requires-Dist: google-generativeai>=0.8.0
23
+ Requires-Dist: transitions>=0.9.3
23
24
  Dynamic: license-file
24
25
 
25
26
  # PyAgentic
@@ -8,3 +8,4 @@ typeguard>=4.4.4
8
8
  c3linearize>=0.1.0
9
9
  anthropic>=0.62.0
10
10
  google-generativeai>=0.8.0
11
+ transitions>=0.9.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyagentic-core"
3
- version = "2.1.0-a.3"
3
+ version = "2.2.0"
4
4
  description = "Build LLM Agents in a Pythonic way"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "c3linearize>=0.1.0",
24
24
  "anthropic>=0.62.0",
25
25
  "google-generativeai>=0.8.0",
26
+ "transitions>=0.9.3",
26
27
  ]
27
28
 
28
29
  [dependency-groups]
@@ -105,4 +106,4 @@ include = ["pyagentic*"]
105
106
  compile-diagrams = "for file in docs/diagrams/source/*.d2; do d2 --layout elk \"$file\" \"docs/diagrams/$(basename \"$file\" .d2).svg\"; done"
106
107
  build-docs = "task compile-diagrams && mkdocs build --clean"
107
108
  serve-docs = "task compile-diagrams && mkdocs serve"
108
- deploy-docs = "task compile-diagrams && mkdocs gh-deploy --force"
109
+ deploy-docs = "task compile-diagrams && mkdocs gh-deploy --force"
File without changes