pyagentic-core 2.3.0a1__tar.gz → 2.3.0a3__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 (46) hide show
  1. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/PKG-INFO +1 -1
  2. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_agent/_agent.py +26 -9
  3. pyagentic_core-2.3.0a3/pyagentic/_utils/_image.py +20 -0
  4. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/llm/_openai.py +31 -25
  5. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/llm/_provider.py +3 -1
  6. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/models/llm.py +6 -1
  7. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/models/response.py +17 -1
  8. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic_core.egg-info/PKG-INFO +1 -1
  9. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic_core.egg-info/SOURCES.txt +1 -0
  10. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyproject.toml +1 -1
  11. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/LICENSE +0 -0
  12. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/README.md +0 -0
  13. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/__init__.py +0 -0
  14. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/__init__.py +0 -0
  15. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_agent/__init__.py +0 -0
  16. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  17. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_agent/_agent_state.py +0 -0
  18. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_exceptions.py +0 -0
  19. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_info.py +0 -0
  20. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_metaclasses.py +0 -0
  21. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_ref.py +0 -0
  22. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_spec.py +0 -0
  23. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_state.py +0 -0
  24. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_tool.py +0 -0
  25. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_base/_validation.py +0 -0
  26. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_utils/_typing.py +0 -0
  27. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/_utils/_warnings.py +0 -0
  28. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/llm/__init__.py +0 -0
  29. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/llm/_anthropic.py +0 -0
  30. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/llm/_gemini.py +0 -0
  31. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/llm/_mock.py +0 -0
  32. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/llm/_openaiv1.py +0 -0
  33. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/logging.py +0 -0
  34. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/models/tracing.py +0 -0
  35. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/policies/__init__.py +0 -0
  36. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/policies/_events.py +0 -0
  37. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/policies/_policy.py +0 -0
  38. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/tracing/__init__.py +0 -0
  39. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/tracing/_basic.py +0 -0
  40. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/tracing/_langfuse.py +0 -0
  41. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/tracing/_tracer.py +0 -0
  42. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic/updates.py +0 -0
  43. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  44. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic_core.egg-info/requires.txt +0 -0
  45. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/pyagentic_core.egg-info/top_level.txt +0 -0
  46. {pyagentic_core-2.3.0a1 → pyagentic_core-2.3.0a3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.3.0a1
3
+ Version: 2.3.0a3
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -16,6 +16,7 @@ from typing import (
16
16
 
17
17
  from transitions import Machine
18
18
  from pydantic import BaseModel, ValidationError
19
+ from PIL.Image import Image
19
20
 
20
21
  from pyagentic.logging import get_logger
21
22
  from pyagentic._base._tool import _ToolDefinition, tool
@@ -431,15 +432,31 @@ class BaseAgent(metaclass=AgentMeta):
431
432
  logger.exception(e)
432
433
  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
433
434
 
434
- stringified_result = (
435
- result.model_dump_json(indent=2)
436
- if issubclass(result.__class__, BaseModel)
437
- else str(result)
438
- )
439
- # Add tool result to conversation history for LLM
440
- self.state._messages.append(
441
- self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
442
- )
435
+ # Handle image results - create a message with the image attached
436
+ if isinstance(result, Image):
437
+ stringified_result = f"[Image returned by {tool_call.name}]"
438
+ # Add tool result message
439
+ self.state._messages.append(
440
+ self.provider.to_tool_call_result_message(
441
+ result=stringified_result, id_=tool_call.id
442
+ )
443
+ )
444
+ # Add the image as a user message so the LLM can see it
445
+ self.state._messages.append(
446
+ Message(role="user", content=f"Image from {tool_call.name}:", image=result)
447
+ )
448
+ else:
449
+ stringified_result = (
450
+ result.model_dump_json(indent=2)
451
+ if issubclass(result.__class__, BaseModel)
452
+ else str(result)
453
+ )
454
+ # Add tool result to conversation history for LLM
455
+ self.state._messages.append(
456
+ self.provider.to_tool_call_result_message(
457
+ result=stringified_result, id_=tool_call.id
458
+ )
459
+ )
443
460
 
444
461
  if self.phases:
445
462
  self.state._update_state_machine(phases=self.phases)
@@ -0,0 +1,20 @@
1
+ from PIL.Image import Image
2
+ import base64
3
+ import io
4
+
5
+
6
+ def _encode_image(image: Image) -> str:
7
+ """
8
+ Convert a PIL Image to a base64-encoded data URL.
9
+
10
+ Args:
11
+ image: PIL Image object to encode
12
+
13
+ Returns:
14
+ Base64-encoded data URL string
15
+ """
16
+ buffer = io.BytesIO()
17
+ image.save(buffer, format="PNG")
18
+ image_bytes = buffer.getvalue()
19
+ base64_image = base64.b64encode(image_bytes).decode("utf-8")
20
+ return f"data:image/png;base64,{base64_image}"
@@ -9,8 +9,6 @@ import openai
9
9
  from openai.types.responses import Response as OpenAIResponse
10
10
  from openai.types.responses import ParsedResponse as OpenAIParsedResponse
11
11
 
12
- import base64
13
- import io
14
12
  from typing import List, Optional, Type
15
13
  from pydantic import BaseModel
16
14
  from PIL.Image import Image
@@ -19,6 +17,7 @@ from pyagentic._base._agent._agent_state import _AgentState
19
17
  from pyagentic._base._tool import _ToolDefinition
20
18
  from pyagentic.llm._provider import LLMProvider
21
19
  from pyagentic.models.llm import ProviderInfo, LLMResponse, ToolCall, Message, UsageInfo
20
+ from pyagentic._utils._image import _encode_image
22
21
 
23
22
 
24
23
  class OpenAIMessage(Message):
@@ -88,27 +87,45 @@ class OpenAIProvider(LLMProvider):
88
87
  """
89
88
  return OpenAIMessage(type="function_call_output", call_id=id_, output=result)
90
89
 
91
- def _encode_image(self, image: Image) -> str:
90
+ def _convert_messages_to_openai_format(self, messages: List[Message]) -> List[dict]:
92
91
  """
93
- Convert a PIL Image to a base64-encoded data URL.
92
+ Convert pyagentic messages to OpenAI's API format.
93
+
94
+ Handles messages with images by converting them to OpenAI's content array format.
94
95
 
95
96
  Args:
96
- image: PIL Image object to encode
97
+ messages: List of Message objects from the agent state
97
98
 
98
99
  Returns:
99
- Base64-encoded data URL string
100
+ List of message dictionaries in OpenAI API format
100
101
  """
101
- buffer = io.BytesIO()
102
- image.save(buffer, format="PNG")
103
- image_bytes = buffer.getvalue()
104
- base64_image = base64.b64encode(image_bytes).decode("utf-8")
105
- return f"data:image/png;base64,{base64_image}"
102
+ openai_messages = []
103
+
104
+ for message in messages:
105
+ msg_dict = message.to_dict()
106
+
107
+ # If message has an image, convert to OpenAI's content array format
108
+ if message.image is not None:
109
+ image_url = _encode_image(message.image)
110
+
111
+ # Build content array with text and image
112
+ content = []
113
+ if message.content:
114
+ content.append({"type": "text", "text": message.content})
115
+ content.append({"type": "image_url", "image_url": {"url": image_url}})
116
+
117
+ msg_dict["content"] = content
118
+ # Remove the image field as it's now in content
119
+ msg_dict.pop("image", None)
120
+
121
+ openai_messages.append(msg_dict)
122
+
123
+ return openai_messages
106
124
 
107
125
  async def generate(
108
126
  self,
109
127
  state: _AgentState,
110
128
  *,
111
- images: Optional[Image] = None,
112
129
  tool_defs: Optional[List[_ToolDefinition]] = None,
113
130
  response_format: Optional[Type[BaseModel]] = None,
114
131
  **kwargs,
@@ -122,7 +139,6 @@ class OpenAIProvider(LLMProvider):
122
139
 
123
140
  Args:
124
141
  state: Agent state containing conversation history and system messages
125
- images: Optional PIL Image to include in the request
126
142
  tool_defs: List of available tools the model can call
127
143
  response_format: Optional Pydantic model for structured output
128
144
  **kwargs: Additional parameters for the OpenAI API call
@@ -134,18 +150,8 @@ class OpenAIProvider(LLMProvider):
134
150
  if tool_defs is None:
135
151
  tool_defs = []
136
152
 
137
- # Prepare input messages
138
- input_messages = [message.to_dict() for message in state._messages]
139
-
140
- # Add image if provided
141
- if images:
142
- image_url = self._encode_image(images)
143
- # Add image as a user message with image content
144
- image_message = {
145
- "role": "user",
146
- "content": [{"type": "image_url", "image_url": {"url": image_url}}],
147
- }
148
- input_messages.append(image_message)
153
+ # Convert messages to OpenAI format (handles images in messages)
154
+ input_messages = self._convert_messages_to_openai_format(state._messages)
149
155
 
150
156
  if response_format:
151
157
  response: OpenAIParsedResponse[Type[BaseModel]] = await self.client.responses.parse(
@@ -81,7 +81,6 @@ class LLMProvider(ABC):
81
81
  self,
82
82
  state: _AgentState,
83
83
  *,
84
- images: Optional[Image] = None,
85
84
  tool_defs: Optional[list[_ToolDefinition]] = None,
86
85
  response_format: Optional[Type[BaseModel]] = None,
87
86
  **kwargs,
@@ -89,6 +88,9 @@ class LLMProvider(ABC):
89
88
  """
90
89
  Generate a response from the language model.
91
90
 
91
+ Images can be included by adding them to messages in the agent state.
92
+ Each provider handles image conversion to their API format.
93
+
92
94
  Args:
93
95
  state: The agent state containing conversation history and system messages
94
96
  tool_defs: Optional list of tool definitions the model can use
@@ -1,5 +1,7 @@
1
1
  from typing import Any, List, Optional
2
- from pydantic import BaseModel
2
+ from pydantic import BaseModel, ConfigDict
3
+
4
+ from PIL.Image import Image
3
5
 
4
6
 
5
7
  class Message(BaseModel):
@@ -10,9 +12,12 @@ class Message(BaseModel):
10
12
  to a dictionary format for API communication.
11
13
  """
12
14
 
15
+ model_config = ConfigDict(arbitrary_types_allowed=True)
16
+
13
17
  type: Optional[str] = None
14
18
  role: Optional[str] = None
15
19
  content: Optional[str] = None
20
+ image: Optional[Image] = None
16
21
 
17
22
  def to_dict(self, exclude_none: bool = True):
18
23
  """
@@ -1,10 +1,12 @@
1
- from pydantic import BaseModel, Field, create_model
1
+ from pydantic import BaseModel, Field, create_model, field_serializer
2
2
  from typing import Type, Self, Union, Any
3
+ from PIL.Image import Image
3
4
 
4
5
  from pyagentic._base._tool import _ToolDefinition
5
6
  from pyagentic._base._agent._agent_state import _AgentState
6
7
 
7
8
  from pyagentic._utils._typing import TypeCategory, analyze_type
9
+ from pyagentic._utils._image import _encode_image
8
10
  from pyagentic.models.llm import ProviderInfo
9
11
 
10
12
 
@@ -20,6 +22,13 @@ class ToolResponse(BaseModel):
20
22
  call_depth: int
21
23
  output: Any
22
24
 
25
+ @field_serializer("output")
26
+ def serialize_output(self, value: Any, _info) -> Any:
27
+ """Serialize Pillow images to base64-encoded data URLs."""
28
+ if isinstance(value, Image):
29
+ return _encode_image(value)
30
+ return value
31
+
23
32
  @classmethod
24
33
  def from_tool_def(cls, tool_def: _ToolDefinition) -> Type[Self]:
25
34
  """
@@ -77,6 +86,13 @@ class AgentResponse(BaseModel):
77
86
  final_output: Union[str, Type[BaseModel]]
78
87
  provider_info: ProviderInfo
79
88
 
89
+ @field_serializer("final_output")
90
+ def serialize_final_output(self, value: Any, _info) -> Any:
91
+ """Serialize Pillow images to base64-encoded data URLs."""
92
+ if isinstance(value, Image):
93
+ return _encode_image(value)
94
+ return value
95
+
80
96
  @classmethod
81
97
  def from_agent_class(
82
98
  cls,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.3.0a1
3
+ Version: 2.3.0a3
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -17,6 +17,7 @@ pyagentic/_base/_agent/__init__.py
17
17
  pyagentic/_base/_agent/_agent.py
18
18
  pyagentic/_base/_agent/_agent_linking.py
19
19
  pyagentic/_base/_agent/_agent_state.py
20
+ pyagentic/_utils/_image.py
20
21
  pyagentic/_utils/_typing.py
21
22
  pyagentic/_utils/_warnings.py
22
23
  pyagentic/llm/__init__.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyagentic-core"
3
- version = "2.3.0-a.1"
3
+ version = "2.3.0-a.3"
4
4
  description = "Build LLM Agents in a Pythonic way"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"