pyagentic-core 2.0.2__tar.gz → 2.1.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.0.2 → pyagentic_core-2.1.0}/PKG-INFO +1 -1
  2. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_agent/_agent.py +119 -65
  3. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_agent/_agent_state.py +3 -1
  4. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/models/response.py +3 -1
  5. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic_core.egg-info/PKG-INFO +1 -1
  6. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyproject.toml +1 -1
  7. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/LICENSE +0 -0
  8. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/README.md +0 -0
  9. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/__init__.py +0 -0
  10. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/__init__.py +0 -0
  11. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_agent/__init__.py +0 -0
  12. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  13. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_exceptions.py +0 -0
  14. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_info.py +0 -0
  15. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_metaclasses.py +0 -0
  16. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_ref.py +0 -0
  17. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_spec.py +0 -0
  18. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_state.py +0 -0
  19. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_tool.py +0 -0
  20. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_base/_validation.py +0 -0
  21. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_utils/_typing.py +0 -0
  22. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/_utils/_warnings.py +0 -0
  23. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/llm/__init__.py +0 -0
  24. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/llm/_anthropic.py +0 -0
  25. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/llm/_gemini.py +0 -0
  26. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/llm/_mock.py +0 -0
  27. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/llm/_openai.py +0 -0
  28. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/llm/_openaiv1.py +0 -0
  29. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/llm/_provider.py +0 -0
  30. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/logging.py +0 -0
  31. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/models/llm.py +0 -0
  32. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/models/tracing.py +0 -0
  33. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/policies/__init__.py +0 -0
  34. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/policies/_events.py +0 -0
  35. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/policies/_policy.py +0 -0
  36. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/tracing/__init__.py +0 -0
  37. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/tracing/_basic.py +0 -0
  38. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/tracing/_langfuse.py +0 -0
  39. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/tracing/_tracer.py +0 -0
  40. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic/updates.py +0 -0
  41. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic_core.egg-info/SOURCES.txt +0 -0
  42. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  43. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic_core.egg-info/requires.txt +0 -0
  44. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/pyagentic_core.egg-info/top_level.txt +0 -0
  45. {pyagentic_core-2.0.2 → pyagentic_core-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.0.2
3
+ Version: 2.1.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
@@ -4,13 +4,13 @@ from functools import wraps
4
4
  from typing import (
5
5
  Callable,
6
6
  Any,
7
- TypeVar,
7
+ AsyncGenerator,
8
8
  ClassVar,
9
9
  Type,
10
- Self,
11
10
  dataclass_transform,
12
11
  Optional,
13
12
  TYPE_CHECKING,
13
+ Union,
14
14
  )
15
15
 
16
16
  from pydantic import BaseModel, ValidationError
@@ -126,8 +126,6 @@ class BaseAgent(metaclass=AgentMeta):
126
126
  api_key (str, optional): API key matching the model provider
127
127
  provider (LLMProvider, optional): Pre-configured provider instance. Overrides
128
128
  `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
129
  tracer (AgentTracer, optional): Tracer instance for observability. Defaults
132
130
  to BasicTracer if not provided.
133
131
  max_call_depth (int): Maximum number of tool calling loops per run. Defaults to 1.
@@ -177,7 +175,6 @@ class BaseAgent(metaclass=AgentMeta):
177
175
  model: str = None
178
176
  api_key: str = None
179
177
  provider: LLMProvider = None
180
- emitter: Callable[[Any], str] = None
181
178
  tracer: AgentTracer = None
182
179
  max_call_depth: int = 1
183
180
 
@@ -314,8 +311,6 @@ class BaseAgent(metaclass=AgentMeta):
314
311
  except Exception as e:
315
312
  # Handle inference errors gracefully
316
313
  logger.exception(e)
317
- if self.emitter:
318
- await _safe_run(self.emitter, EmitUpdate(status=Status.ERROR))
319
314
  # Add error message to conversation history
320
315
  self.state._messages.append(
321
316
  Message(role="assistant", content="Failed to generate a response")
@@ -359,8 +354,13 @@ class BaseAgent(metaclass=AgentMeta):
359
354
  response = AgentResponse(final_output=result, provider_info=agent.provider._info)
360
355
 
361
356
  # Add agent result to conversation history
357
+ stringified_result = (
358
+ result.model_dump_json(indent=2)
359
+ if issubclass(result.__class__, BaseModel)
360
+ else str(result)
361
+ )
362
362
  self.state._messages.append(
363
- self.provider.to_tool_call_result_message(result=result, id_=tool_call.id)
363
+ self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
364
364
  )
365
365
  return response
366
366
 
@@ -397,38 +397,15 @@ class BaseAgent(metaclass=AgentMeta):
397
397
  # Handle validation errors for tool arguments
398
398
  result = f"Function Args were invalid: {str(e)}"
399
399
  compiled_args = {}
400
- if self.emitter:
401
- self.tracer.record_exception(str(e))
402
- logger.exception(e)
403
- if self.emitter:
404
- await _safe_run(
405
- self.emitter,
406
- ToolUpdate(
407
- status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs
408
- ),
409
- )
410
-
411
- # Execute the tool, emitting status updates
400
+ self.tracer.record_exception(str(e))
401
+ logger.exception(e)
412
402
  try:
413
- if self.emitter:
414
- await _safe_run(
415
- self.emitter,
416
- ToolUpdate(
417
- status=Status.PROCESSING, tool_call=tool_call.name, tool_args=kwargs
418
- ),
419
- )
420
403
  if compiled_args:
421
404
  result = await _safe_run(handler, **compiled_args)
422
- result = str(result)
423
405
  self.tracer.set_attributes(result=result)
424
406
  except TypeError as e:
425
407
  self.tracer.record_exception(str(e))
426
408
  logger.exception(e)
427
- if self.emitter:
428
- await _safe_run(
429
- self.emitter,
430
- ToolUpdate(status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs),
431
- )
432
409
  raise InvalidToolDefinition(
433
410
  tool_name=tool_call.name,
434
411
  message=f"Tool must have a serializable return type; {tool_def.return_type} failed to be casted to a string.",
@@ -438,15 +415,15 @@ class BaseAgent(metaclass=AgentMeta):
438
415
  self.tracer.record_exception(str(e))
439
416
  logger.exception(e)
440
417
  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
441
- if self.emitter:
442
- await _safe_run(
443
- self.emitter,
444
- ToolUpdate(status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs),
445
- )
446
418
 
419
+ stringified_result = (
420
+ result.model_dump_json(indent=2)
421
+ if issubclass(result.__class__, BaseModel)
422
+ else str(result)
423
+ )
447
424
  # Add tool result to conversation history for LLM
448
425
  self.state._messages.append(
449
- self.provider.to_tool_call_result_message(result=result, id_=tool_call.id)
426
+ self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
450
427
  )
451
428
 
452
429
  # Build and return the structured tool response
@@ -479,12 +456,16 @@ class BaseAgent(metaclass=AgentMeta):
479
456
 
480
457
  return tool_defs
481
458
 
482
- async def run(self, input_: str) -> BaseModel:
459
+ async def step(
460
+ self, input_: str
461
+ ) -> AsyncGenerator[Union[ToolResponse, AgentResponse, LLMResponse]]:
483
462
  """
484
- Main execution loop for the agent. Processes user input through multiple rounds
485
- of LLM inference and tool/agent calls until completion or max_call_depth reached.
463
+ Streams all intermediate responses as the agent executes. Yields LLMResponse for each
464
+ inference, ToolResponse for each tool execution, and finally AgentResponse with the
465
+ complete result.
486
466
 
487
- The agent follows an agentic loop pattern:
467
+ This is the core execution method that enables real-time streaming and fine-grained
468
+ control over agent execution. The agent follows an agentic loop pattern:
488
469
  1. Send user input and conversation history to the LLM
489
470
  2. LLM decides to either call tools or respond with final output
490
471
  3. If tools are called, execute them and feed results back to LLM
@@ -493,19 +474,23 @@ class BaseAgent(metaclass=AgentMeta):
493
474
  Args:
494
475
  input_ (str): The user input/query for the agent to process
495
476
 
496
- Returns:
497
- AgentResponse: Structured response containing:
498
- - final_output: The final text or structured output from the LLM
499
- - state: Current agent state after execution
500
- - tool_responses: List of all tool calls and their outputs
501
- - provider_info: Information about the LLM provider used
477
+ Yields:
478
+ Union[LLMResponse, ToolResponse, AgentResponse]: Responses in sequence:
479
+ - LLMResponse: Yielded each time the LLM is called (may happen multiple times)
480
+ - ToolResponse: Yielded for each tool execution
481
+ - AgentResponse: Final response with complete execution summary
502
482
 
503
483
  Example:
504
484
  ```python
505
485
  agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
506
- response = await agent.run("What's the weather in San Francisco?")
507
- print(response.final_output) # LLM's final answer
508
- print(response.tool_responses) # Tools that were called
486
+
487
+ async for response in agent.step("Analyze this data"):
488
+ if isinstance(response, LLMResponse):
489
+ print(f"LLM thinking: {response.text}")
490
+ elif isinstance(response, ToolResponse):
491
+ print(f"Tool executed: {response.output}")
492
+ elif isinstance(response, AgentResponse):
493
+ print(f"Final: {response.final_output}")
509
494
  ```
510
495
  """
511
496
  async with self.tracer.agent(
@@ -527,10 +512,6 @@ class BaseAgent(metaclass=AgentMeta):
527
512
  agent_responses: list = []
528
513
  processed_call_ids: set[str] = set()
529
514
 
530
- # Emit initial status
531
- if self.emitter:
532
- await _safe_run(self.emitter, EmitUpdate(status=Status.GENERATING))
533
-
534
515
  # Main agentic loop: LLM -> Tools -> LLM -> ...
535
516
  depth = 0
536
517
  final_ai_output: str | None = None
@@ -538,6 +519,7 @@ class BaseAgent(metaclass=AgentMeta):
538
519
  while depth < self.max_call_depth:
539
520
  # Ask the LLM what to do next (may return tool calls or final text)
540
521
  response = await self._process_llm_inference(tool_defs=tool_defs)
522
+ yield response
541
523
 
542
524
  # If the model produced final text without tool calls, we're done
543
525
  if not response.tool_calls:
@@ -557,10 +539,12 @@ class BaseAgent(metaclass=AgentMeta):
557
539
  if tool_call.name in self.__tool_defs__:
558
540
  result = await self._process_tool_call(tool_call, call_depth=depth)
559
541
  tool_responses.append(result)
542
+ yield result
560
543
 
561
544
  elif tool_call.name in self.__linked_agents__:
562
545
  result = await self._process_agent_call(tool_call)
563
546
  agent_responses.append(result)
547
+ yield result
564
548
 
565
549
  # Increment depth and continue loop (LLM will see tool results next iteration)
566
550
  depth += 1
@@ -570,12 +554,6 @@ class BaseAgent(metaclass=AgentMeta):
570
554
  response = await self._process_llm_inference()
571
555
  final_ai_output = response.parsed if response.parsed else response.text
572
556
 
573
- # Emit final success status
574
- if self.emitter:
575
- await _safe_run(
576
- self.emitter, AiUpdate(status=Status.SUCCEDED, message=final_ai_output)
577
- )
578
-
579
557
  # Build the structured response
580
558
  response_fields = {
581
559
  "final_output": final_ai_output,
@@ -590,17 +568,93 @@ class BaseAgent(metaclass=AgentMeta):
590
568
 
591
569
  response = self.__response_model__(**response_fields)
592
570
  self.tracer.set_attributes(output=response)
593
- return response
571
+ yield response
572
+
573
+ async def run(self, input_: str) -> AgentResponse:
574
+ """
575
+ Executes the agent with a message string and returns the final result.
576
+
577
+ This method consumes the entire step() generator and returns only the final
578
+ AgentResponse. Use this when you don't need intermediate streaming responses
579
+ and just want the final output.
580
+
581
+ Args:
582
+ input_ (str): The user input/query for the agent to process
583
+
584
+ Returns:
585
+ AgentResponse: Structured response containing:
586
+ - final_output: The final text or structured output from the LLM
587
+ - state: Current agent state after execution
588
+ - tool_responses: List of all tool calls and their outputs
589
+ - agent_responses: List of linked agent calls (if any)
590
+ - provider_info: Information about the LLM provider used
591
+
592
+ Example:
593
+ ```python
594
+ agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
595
+ response = await agent.run("What's the weather in San Francisco?")
596
+ print(response.final_output) # LLM's final answer
597
+ print(response.tool_responses) # Tools that were called
598
+ ```
599
+ """
600
+ final_response = None
601
+ async for res in self.step(input_):
602
+ final_response = res
603
+ return final_response
594
604
 
595
605
  async def __call__(self, user_input: str) -> BaseModel:
596
606
  """
597
- Allows the agent to be called directly as a function.
607
+ Customizable callable interface for the agent. Override this method to accept
608
+ structured, typed parameters that match your agent's purpose.
609
+
610
+ When this agent is linked to another agent, the parameters of this method become
611
+ the tool parameters that the LLM sees. This enables type-safe, structured agent
612
+ composition in multi-agent systems.
613
+
614
+ The default implementation accepts a single user_input string and forwards it to
615
+ run(). Override to provide a custom interface:
598
616
 
599
617
  Args:
600
- user_input (str): The user input to process
618
+ user_input (str): The user input to process (default implementation)
601
619
 
602
620
  Returns:
603
621
  AgentResponse: The agent's response
622
+
623
+ Example (Default Usage):
624
+ ```python
625
+ agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
626
+ response = await agent("What's the weather?")
627
+ ```
628
+
629
+ Example (Custom Implementation):
630
+ ```python
631
+ class CoursePlannerAgent(BaseAgent):
632
+ __system_message__ = "You design course curricula"
633
+ __description__ = "Creates structured course plans"
634
+
635
+ async def __call__(
636
+ self,
637
+ goal: str,
638
+ experience: str,
639
+ context: Optional[str] = None
640
+ ) -> CoursePlan:
641
+ # Build structured prompt from parameters
642
+ prompt = f"Goal: {goal}\\nExperience: {experience}"
643
+ if context:
644
+ prompt += f"\\nContext: {context}"
645
+ return await self.run(prompt)
646
+
647
+ # Call with structured parameters
648
+ planner = CoursePlannerAgent(model="openai::gpt-4o", api_key=API_KEY)
649
+ course = await planner(
650
+ goal="Learn ML",
651
+ experience="Python beginner",
652
+ context="Prefer hands-on projects"
653
+ )
654
+
655
+ # When linked to another agent, the LLM sees:
656
+ # Tool: planner(goal: str, experience: str, context: Optional[str])
657
+ ```
604
658
  """
605
659
  return await self.run(input_=user_input)
606
660
 
@@ -32,7 +32,9 @@ class _AgentState(BaseModel):
32
32
  input_template: Optional[str] = "{{ user_message }}"
33
33
  _messages: list[Message] = PrivateAttr(default_factory=list)
34
34
  _instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
35
- _input_template: Template = PrivateAttr(default_factory=lambda: Template(source="{{ user_message }}"))
35
+ _input_template: Template = PrivateAttr(
36
+ default_factory=lambda: Template(source="{{ user_message }}")
37
+ )
36
38
 
37
39
  def model_post_init(self, state):
38
40
  self._instructions_template = Template(source=self.instructions)
@@ -60,7 +60,9 @@ class ToolResponse(BaseModel):
60
60
  case _:
61
61
  raise Exception(f"Unsupported type: {param_type}")
62
62
 
63
- return create_model(f"ToolResponse[{tool_def.name}]", __base__=cls, **fields)
63
+ return create_model(
64
+ f"ToolResponse[{tool_def.name}]", __base__=cls, output=tool_def.return_type, **fields
65
+ )
64
66
 
65
67
 
66
68
  class AgentResponse(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.0.2
3
+ Version: 2.1.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyagentic-core"
3
- version = "2.0.2"
3
+ version = "2.1.0"
4
4
  description = "Build LLM Agents in a Pythonic way"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
File without changes
File without changes
File without changes