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.
Files changed (45) hide show
  1. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/PKG-INFO +1 -1
  2. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/_agent.py +49 -41
  3. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/_agent_state.py +47 -5
  4. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_tool.py +5 -4
  5. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/PKG-INFO +1 -1
  6. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyproject.toml +1 -1
  7. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/LICENSE +0 -0
  8. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/README.md +0 -0
  9. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/__init__.py +0 -0
  10. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/__init__.py +0 -0
  11. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/__init__.py +0 -0
  12. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  13. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_exceptions.py +0 -0
  14. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_info.py +0 -0
  15. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_metaclasses.py +0 -0
  16. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_ref.py +0 -0
  17. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_spec.py +0 -0
  18. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_state.py +0 -0
  19. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_base/_validation.py +0 -0
  20. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_utils/_typing.py +0 -0
  21. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/_utils/_warnings.py +0 -0
  22. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/__init__.py +0 -0
  23. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_anthropic.py +0 -0
  24. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_gemini.py +0 -0
  25. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_mock.py +0 -0
  26. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_openai.py +0 -0
  27. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_openaiv1.py +0 -0
  28. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/llm/_provider.py +0 -0
  29. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/logging.py +0 -0
  30. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/models/llm.py +0 -0
  31. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/models/response.py +0 -0
  32. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/models/tracing.py +0 -0
  33. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/policies/__init__.py +0 -0
  34. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/policies/_events.py +0 -0
  35. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/policies/_policy.py +0 -0
  36. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/__init__.py +0 -0
  37. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/_basic.py +0 -0
  38. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/_langfuse.py +0 -0
  39. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/tracing/_tracer.py +0 -0
  40. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic/updates.py +0 -0
  41. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/SOURCES.txt +0 -0
  42. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  43. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/requires.txt +0 -0
  44. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/pyagentic_core.egg-info/top_level.txt +0 -0
  45. {pyagentic_core-2.2.0a1 → pyagentic_core-2.2.0a2}/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.0a2
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,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
- self.__machine__ = self._build_phase_machine()
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
- return LLMResponse(text=f"The LLM failed to generate a response: {e}", tool_calls=[])
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
- tool_defs.append(tool_def.resolve(self.agent_reference))
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
- # Execute all tool/agent calls from this response
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
- result = await self._process_tool_call(tool_call, call_depth=depth)
573
- tool_responses.append(result)
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
- result = await self._process_agent_call(tool_call)
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
- yield result
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, 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
@@ -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
- setattr(self, name, final_value)
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
- return self._instructions_template.render(**self.model_dump())
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
- content = self._input_template.render(**data)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.2.0a1
3
+ Version: 2.2.0a2
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.0-a.2"
4
4
  description = "Build LLM Agents in a Pythonic way"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"