pyagentic-core 2.1.0a2__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.0a2 → pyagentic_core-2.2.0}/PKG-INFO +2 -1
  2. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent.py +115 -60
  3. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent_state.py +44 -4
  4. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_tool.py +5 -4
  5. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/PKG-INFO +2 -1
  6. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/requires.txt +1 -0
  7. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyproject.toml +3 -2
  8. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/LICENSE +0 -0
  9. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/README.md +0 -0
  10. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/__init__.py +0 -0
  11. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/__init__.py +0 -0
  12. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/__init__.py +0 -0
  13. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  14. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_exceptions.py +0 -0
  15. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_info.py +0 -0
  16. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_metaclasses.py +0 -0
  17. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_ref.py +0 -0
  18. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_spec.py +0 -0
  19. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_state.py +0 -0
  20. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_validation.py +0 -0
  21. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_utils/_typing.py +0 -0
  22. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_utils/_warnings.py +0 -0
  23. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/__init__.py +0 -0
  24. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_anthropic.py +0 -0
  25. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_gemini.py +0 -0
  26. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_mock.py +0 -0
  27. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_openai.py +0 -0
  28. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_openaiv1.py +0 -0
  29. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_provider.py +0 -0
  30. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/logging.py +0 -0
  31. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/models/llm.py +0 -0
  32. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/models/response.py +0 -0
  33. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/models/tracing.py +0 -0
  34. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/policies/__init__.py +0 -0
  35. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/policies/_events.py +0 -0
  36. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/policies/_policy.py +0 -0
  37. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/__init__.py +0 -0
  38. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/_basic.py +0 -0
  39. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/_langfuse.py +0 -0
  40. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/_tracer.py +0 -0
  41. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/updates.py +0 -0
  42. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/SOURCES.txt +0 -0
  43. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  44. {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/top_level.txt +0 -0
  45. {pyagentic_core-2.1.0a2 → 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.0a2
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
@@ -126,8 +127,6 @@ class BaseAgent(metaclass=AgentMeta):
126
127
  api_key (str, optional): API key matching the model provider
127
128
  provider (LLMProvider, optional): Pre-configured provider instance. Overrides
128
129
  `model` and `api_key` if provided.
129
- emitter (Callable, optional): Callback function to receive real-time updates
130
- about the agent's execution (useful for WebSocket streaming)
131
130
  tracer (AgentTracer, optional): Tracer instance for observability. Defaults
132
131
  to BasicTracer if not provided.
133
132
  max_call_depth (int): Maximum number of tool calling loops per run. Defaults to 1.
@@ -167,6 +166,7 @@ class BaseAgent(metaclass=AgentMeta):
167
166
  __description__: ClassVar[str] # Optional: description for linked agents
168
167
  __input_template__: ClassVar[str] = None # Optional: template for user input
169
168
  __response_format__: ClassVar[Type[BaseModel]] = None # Optional: structured output format
169
+ phases: ClassVar[list[tuple[str, str, Callable]]] = None
170
170
 
171
171
  # Generated Class Attributes (built by metaclass)
172
172
  __response_model__: ClassVar[Type[AgentResponse]] = None # Pydantic response model
@@ -177,7 +177,6 @@ class BaseAgent(metaclass=AgentMeta):
177
177
  model: str = None
178
178
  api_key: str = None
179
179
  provider: LLMProvider = None
180
- emitter: Callable[[Any], str] = None
181
180
  tracer: AgentTracer = None
182
181
  max_call_depth: int = 1
183
182
 
@@ -254,6 +253,9 @@ class BaseAgent(metaclass=AgentMeta):
254
253
  """
255
254
  self._check_llm_provider()
256
255
 
256
+ if self.phases:
257
+ self.state._build_phase_machine(self.phases)
258
+
257
259
  # Use BasicTracer as default if no tracer provided
258
260
  if not self.tracer:
259
261
  self.tracer = BasicTracer()
@@ -314,13 +316,18 @@ class BaseAgent(metaclass=AgentMeta):
314
316
  except Exception as e:
315
317
  # Handle inference errors gracefully
316
318
  logger.exception(e)
317
- if self.emitter:
318
- await _safe_run(self.emitter, EmitUpdate(status=Status.ERROR))
319
319
  # Add error message to conversation history
320
320
  self.state._messages.append(
321
321
  Message(role="assistant", content="Failed to generate a response")
322
322
  )
323
- 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
324
331
 
325
332
  @traced(SpanKind.AGENT)
326
333
  async def _process_agent_call(self, tool_call: ToolCall) -> AgentResponse:
@@ -367,6 +374,8 @@ class BaseAgent(metaclass=AgentMeta):
367
374
  self.state._messages.append(
368
375
  self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
369
376
  )
377
+ if self.phases:
378
+ self.state._update_state_machine(phases=self.phases)
370
379
  return response
371
380
 
372
381
  @traced(SpanKind.TOOL)
@@ -402,37 +411,15 @@ class BaseAgent(metaclass=AgentMeta):
402
411
  # Handle validation errors for tool arguments
403
412
  result = f"Function Args were invalid: {str(e)}"
404
413
  compiled_args = {}
405
- if self.emitter:
406
- self.tracer.record_exception(str(e))
407
- logger.exception(e)
408
- if self.emitter:
409
- await _safe_run(
410
- self.emitter,
411
- ToolUpdate(
412
- status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs
413
- ),
414
- )
415
-
416
- # Execute the tool, emitting status updates
414
+ self.tracer.record_exception(str(e))
415
+ logger.exception(e)
417
416
  try:
418
- if self.emitter:
419
- await _safe_run(
420
- self.emitter,
421
- ToolUpdate(
422
- status=Status.PROCESSING, tool_call=tool_call.name, tool_args=kwargs
423
- ),
424
- )
425
417
  if compiled_args:
426
418
  result = await _safe_run(handler, **compiled_args)
427
419
  self.tracer.set_attributes(result=result)
428
420
  except TypeError as e:
429
421
  self.tracer.record_exception(str(e))
430
422
  logger.exception(e)
431
- if self.emitter:
432
- await _safe_run(
433
- self.emitter,
434
- ToolUpdate(status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs),
435
- )
436
423
  raise InvalidToolDefinition(
437
424
  tool_name=tool_call.name,
438
425
  message=f"Tool must have a serializable return type; {tool_def.return_type} failed to be casted to a string.",
@@ -442,11 +429,6 @@ class BaseAgent(metaclass=AgentMeta):
442
429
  self.tracer.record_exception(str(e))
443
430
  logger.exception(e)
444
431
  result = f"Tool `{tool_call.name}` failed: {e}. Please kindly state to the user that is failed, provide state, and ask if they want to try again." # noqa E501
445
- if self.emitter:
446
- await _safe_run(
447
- self.emitter,
448
- ToolUpdate(status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs),
449
- )
450
432
 
451
433
  stringified_result = (
452
434
  result.model_dump_json(indent=2)
@@ -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(
@@ -479,7 +463,12 @@ class BaseAgent(metaclass=AgentMeta):
479
463
  # Add all @tool decorated methods
480
464
  for tool_def in self.__tool_defs__.values():
481
465
  # Resolve StateRefs in parameters (e.g., ref.self.user_name -> actual value)
482
- 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))
483
472
 
484
473
  # Add linked agents as tools
485
474
  for name, linked_def in self.__linked_agents__.items():
@@ -492,10 +481,12 @@ class BaseAgent(metaclass=AgentMeta):
492
481
  self, input_: str
493
482
  ) -> AsyncGenerator[Union[ToolResponse, AgentResponse, LLMResponse]]:
494
483
  """
495
- Main execution loop for the agent. Processes user input through multiple rounds
496
- of LLM inference and tool/agent calls until completion or max_call_depth reached.
484
+ Streams all intermediate responses as the agent executes. Yields LLMResponse for each
485
+ inference, ToolResponse for each tool execution, and finally AgentResponse with the
486
+ complete result.
497
487
 
498
- The agent follows an agentic loop pattern:
488
+ This is the core execution method that enables real-time streaming and fine-grained
489
+ control over agent execution. The agent follows an agentic loop pattern:
499
490
  1. Send user input and conversation history to the LLM
500
491
  2. LLM decides to either call tools or respond with final output
501
492
  3. If tools are called, execute them and feed results back to LLM
@@ -504,19 +495,23 @@ class BaseAgent(metaclass=AgentMeta):
504
495
  Args:
505
496
  input_ (str): The user input/query for the agent to process
506
497
 
507
- Returns:
508
- AgentResponse: Structured response containing:
509
- - final_output: The final text or structured output from the LLM
510
- - state: Current agent state after execution
511
- - tool_responses: List of all tool calls and their outputs
512
- - provider_info: Information about the LLM provider used
498
+ Yields:
499
+ Union[LLMResponse, ToolResponse, AgentResponse]: Responses in sequence:
500
+ - LLMResponse: Yielded each time the LLM is called (may happen multiple times)
501
+ - ToolResponse: Yielded for each tool execution
502
+ - AgentResponse: Final response with complete execution summary
513
503
 
514
504
  Example:
515
505
  ```python
516
506
  agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
517
- response = await agent.run("What's the weather in San Francisco?")
518
- print(response.final_output) # LLM's final answer
519
- print(response.tool_responses) # Tools that were called
507
+
508
+ async for response in agent.step("Analyze this data"):
509
+ if isinstance(response, LLMResponse):
510
+ print(f"LLM thinking: {response.text}")
511
+ elif isinstance(response, ToolResponse):
512
+ print(f"Tool executed: {response.output}")
513
+ elif isinstance(response, AgentResponse):
514
+ print(f"Final: {response.final_output}")
520
515
  ```
521
516
  """
522
517
  async with self.tracer.agent(
@@ -538,10 +533,6 @@ class BaseAgent(metaclass=AgentMeta):
538
533
  agent_responses: list = []
539
534
  processed_call_ids: set[str] = set()
540
535
 
541
- # Emit initial status
542
- if self.emitter:
543
- await _safe_run(self.emitter, EmitUpdate(status=Status.GENERATING))
544
-
545
536
  # Main agentic loop: LLM -> Tools -> LLM -> ...
546
537
  depth = 0
547
538
  final_ai_output: str | None = None
@@ -584,12 +575,6 @@ class BaseAgent(metaclass=AgentMeta):
584
575
  response = await self._process_llm_inference()
585
576
  final_ai_output = response.parsed if response.parsed else response.text
586
577
 
587
- # Emit final success status
588
- if self.emitter:
589
- await _safe_run(
590
- self.emitter, AiUpdate(status=Status.SUCCEDED, message=final_ai_output)
591
- )
592
-
593
578
  # Build the structured response
594
579
  response_fields = {
595
580
  "final_output": final_ai_output,
@@ -607,6 +592,32 @@ class BaseAgent(metaclass=AgentMeta):
607
592
  yield response
608
593
 
609
594
  async def run(self, input_: str) -> AgentResponse:
595
+ """
596
+ Executes the agent with a message string and returns the final result.
597
+
598
+ This method consumes the entire step() generator and returns only the final
599
+ AgentResponse. Use this when you don't need intermediate streaming responses
600
+ and just want the final output.
601
+
602
+ Args:
603
+ input_ (str): The user input/query for the agent to process
604
+
605
+ Returns:
606
+ AgentResponse: Structured response containing:
607
+ - final_output: The final text or structured output from the LLM
608
+ - state: Current agent state after execution
609
+ - tool_responses: List of all tool calls and their outputs
610
+ - agent_responses: List of linked agent calls (if any)
611
+ - provider_info: Information about the LLM provider used
612
+
613
+ Example:
614
+ ```python
615
+ agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
616
+ response = await agent.run("What's the weather in San Francisco?")
617
+ print(response.final_output) # LLM's final answer
618
+ print(response.tool_responses) # Tools that were called
619
+ ```
620
+ """
610
621
  final_response = None
611
622
  async for res in self.step(input_):
612
623
  final_response = res
@@ -614,13 +625,57 @@ class BaseAgent(metaclass=AgentMeta):
614
625
 
615
626
  async def __call__(self, user_input: str) -> BaseModel:
616
627
  """
617
- Allows the agent to be called directly as a function.
628
+ Customizable callable interface for the agent. Override this method to accept
629
+ structured, typed parameters that match your agent's purpose.
630
+
631
+ When this agent is linked to another agent, the parameters of this method become
632
+ the tool parameters that the LLM sees. This enables type-safe, structured agent
633
+ composition in multi-agent systems.
634
+
635
+ The default implementation accepts a single user_input string and forwards it to
636
+ run(). Override to provide a custom interface:
618
637
 
619
638
  Args:
620
- user_input (str): The user input to process
639
+ user_input (str): The user input to process (default implementation)
621
640
 
622
641
  Returns:
623
642
  AgentResponse: The agent's response
643
+
644
+ Example (Default Usage):
645
+ ```python
646
+ agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
647
+ response = await agent("What's the weather?")
648
+ ```
649
+
650
+ Example (Custom Implementation):
651
+ ```python
652
+ class CoursePlannerAgent(BaseAgent):
653
+ __system_message__ = "You design course curricula"
654
+ __description__ = "Creates structured course plans"
655
+
656
+ async def __call__(
657
+ self,
658
+ goal: str,
659
+ experience: str,
660
+ context: Optional[str] = None
661
+ ) -> CoursePlan:
662
+ # Build structured prompt from parameters
663
+ prompt = f"Goal: {goal}\\nExperience: {experience}"
664
+ if context:
665
+ prompt += f"\\nContext: {context}"
666
+ return await self.run(prompt)
667
+
668
+ # Call with structured parameters
669
+ planner = CoursePlannerAgent(model="openai::gpt-4o", api_key=API_KEY)
670
+ course = await planner(
671
+ goal="Learn ML",
672
+ experience="Python beginner",
673
+ context="Prefer hands-on projects"
674
+ )
675
+
676
+ # When linked to another agent, the LLM sees:
677
+ # Tool: planner(goal: str, experience: str, context: Optional[str])
678
+ ```
624
679
  """
625
680
  return await self.run(input_=user_input)
626
681
 
@@ -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.0a2
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.2"
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