mirascope 1.16.8__py3-none-any.whl → 1.17.0__py3-none-any.whl

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 (43) hide show
  1. mirascope/__init__.py +20 -1
  2. mirascope/core/anthropic/_utils/_convert_message_params.py +13 -0
  3. mirascope/core/anthropic/call_response.py +10 -2
  4. mirascope/core/azure/_utils/_convert_message_params.py +10 -0
  5. mirascope/core/azure/_utils/_message_param_converter.py +80 -19
  6. mirascope/core/azure/call_response.py +8 -2
  7. mirascope/core/base/__init__.py +4 -0
  8. mirascope/core/base/_create.py +1 -1
  9. mirascope/core/base/_utils/_convert_function_to_base_tool.py +2 -2
  10. mirascope/core/base/_utils/_convert_messages_to_message_params.py +36 -3
  11. mirascope/core/base/_utils/_parse_content_template.py +35 -9
  12. mirascope/core/base/call_response.py +7 -1
  13. mirascope/core/base/message_param.py +30 -2
  14. mirascope/core/base/messages.py +10 -0
  15. mirascope/core/bedrock/_utils/_convert_message_params.py +18 -1
  16. mirascope/core/bedrock/call_response.py +8 -2
  17. mirascope/core/cohere/_utils/_message_param_converter.py +3 -2
  18. mirascope/core/cohere/call_response.py +8 -2
  19. mirascope/core/gemini/_utils/_convert_message_params.py +48 -5
  20. mirascope/core/gemini/_utils/_message_param_converter.py +56 -6
  21. mirascope/core/gemini/_utils/_setup_call.py +12 -2
  22. mirascope/core/gemini/call_response.py +8 -2
  23. mirascope/core/groq/_utils/_convert_message_params.py +9 -0
  24. mirascope/core/groq/_utils/_message_param_converter.py +44 -15
  25. mirascope/core/groq/call_response.py +8 -2
  26. mirascope/core/mistral/_utils/_convert_message_params.py +7 -0
  27. mirascope/core/mistral/_utils/_message_param_converter.py +41 -35
  28. mirascope/core/mistral/call_response.py +8 -2
  29. mirascope/core/openai/_utils/_convert_message_params.py +39 -1
  30. mirascope/core/openai/_utils/_message_param_converter.py +36 -10
  31. mirascope/core/openai/call_response.py +8 -2
  32. mirascope/core/vertex/_utils/_convert_message_params.py +56 -6
  33. mirascope/core/vertex/_utils/_message_param_converter.py +17 -7
  34. mirascope/core/vertex/_utils/_setup_call.py +10 -1
  35. mirascope/core/vertex/call_response.py +8 -2
  36. mirascope/llm/call_response.py +11 -3
  37. mirascope/llm/stream.py +3 -3
  38. mirascope/retries/__init__.py +5 -0
  39. mirascope/retries/fallback.py +128 -0
  40. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/METADATA +1 -1
  41. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/RECORD +43 -42
  42. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/WHEEL +0 -0
  43. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/licenses/LICENSE +0 -0
mirascope/__init__.py CHANGED
@@ -6,6 +6,15 @@ from contextlib import suppress
6
6
  with suppress(ImportError):
7
7
  from . import core as core
8
8
 
9
+ from .core import (
10
+ BaseDynamicConfig,
11
+ BaseMessageParam,
12
+ BaseTool,
13
+ BaseToolKit,
14
+ Messages,
15
+ prompt_template,
16
+ )
17
+
9
18
  with suppress(ImportError):
10
19
  from . import integrations as integrations
11
20
 
@@ -14,4 +23,14 @@ with suppress(ImportError):
14
23
 
15
24
  __version__ = importlib.metadata.version("mirascope")
16
25
 
17
- __all__ = ["core", "integrations", "retries", "__version__"]
26
+ __all__ = [
27
+ "BaseDynamicConfig",
28
+ "BaseMessageParam",
29
+ "BaseTool",
30
+ "BaseToolKit",
31
+ "core",
32
+ "integrations",
33
+ "prompt_template",
34
+ "retries",
35
+ "__version__",
36
+ ]
@@ -5,6 +5,7 @@ import base64
5
5
  from anthropic.types import MessageParam
6
6
 
7
7
  from ...base import BaseMessageParam
8
+ from ...base._utils._parse_content_template import _load_media, get_image_type
8
9
 
9
10
 
10
11
  def convert_message_params(
@@ -45,6 +46,18 @@ def convert_message_params(
45
46
  },
46
47
  }
47
48
  )
49
+ elif part.type == "image_url":
50
+ image = _load_media(part.url)
51
+ converted_content.append(
52
+ {
53
+ "type": "image",
54
+ "source": {
55
+ "data": base64.b64encode(image).decode("utf-8"),
56
+ "media_type": f"image/{get_image_type(image)}",
57
+ "type": "base64",
58
+ },
59
+ }
60
+ )
48
61
  elif part.type == "document":
49
62
  if part.media_type != "application/pdf":
50
63
  raise ValueError(
@@ -186,5 +186,13 @@ class AnthropicCallResponse(
186
186
  return _convert_finish_reasons_to_common_finish_reasons(self.finish_reasons)
187
187
 
188
188
  @property
189
- def common_message_param(self) -> list[BaseMessageParam]:
190
- return AnthropicMessageParamConverter.from_provider([(self.message_param)])
189
+ def common_message_param(self) -> BaseMessageParam:
190
+ return AnthropicMessageParamConverter.from_provider([(self.message_param)])[0]
191
+
192
+ @property
193
+ def common_user_message_param(self) -> BaseMessageParam | None:
194
+ if not self.user_message_param:
195
+ return None
196
+ return AnthropicMessageParamConverter.from_provider(
197
+ [(self.user_message_param)]
198
+ )[0]
@@ -51,6 +51,16 @@ def convert_message_params(
51
51
  },
52
52
  }
53
53
  )
54
+ elif part.type == "image_url":
55
+ converted_content.append(
56
+ {
57
+ "type": "image_url",
58
+ "image_url": {
59
+ "url": part.url,
60
+ "detail": part.detail if part.detail else "auto",
61
+ },
62
+ }
63
+ )
54
64
  elif part.type == "tool_call":
55
65
  converted_message_param = AssistantMessage(
56
66
  tool_calls=[
@@ -4,14 +4,41 @@ from typing import cast
4
4
  from azure.ai.inference.models import (
5
5
  AssistantMessage,
6
6
  ChatRequestMessage,
7
+ ContentItem,
8
+ ImageContentItem,
9
+ TextContentItem,
10
+ ToolMessage,
11
+ UserMessage,
7
12
  )
8
13
 
9
- from mirascope.core import BaseMessageParam
10
14
  from mirascope.core.azure._utils import convert_message_params
15
+ from mirascope.core.base import (
16
+ BaseMessageParam,
17
+ ImageURLPart,
18
+ TextPart,
19
+ ToolCallPart,
20
+ ToolResultPart,
21
+ )
11
22
  from mirascope.core.base._utils._base_message_param_converter import (
12
23
  BaseMessageParamConverter,
13
24
  )
14
- from mirascope.core.base.message_param import ToolCallPart
25
+
26
+
27
+ def _parse_content(content: list[ContentItem]) -> list[TextPart | ImageURLPart]:
28
+ converted_parts = []
29
+ for part in content:
30
+ if isinstance(part, TextContentItem):
31
+ converted_parts.append(TextPart(type="text", text=part.text))
32
+ elif isinstance(part, ImageContentItem):
33
+ converted_parts.append(
34
+ ImageURLPart(
35
+ type="image_url",
36
+ url=part.image_url.url,
37
+ detail=part.image_url.detail,
38
+ )
39
+ )
40
+
41
+ return converted_parts
15
42
 
16
43
 
17
44
  class AzureMessageParamConverter(BaseMessageParamConverter):
@@ -19,38 +46,72 @@ class AzureMessageParamConverter(BaseMessageParamConverter):
19
46
 
20
47
  @staticmethod
21
48
  def to_provider(message_params: list[BaseMessageParam]) -> list[ChatRequestMessage]:
22
- """
23
- Convert from Mirascope `BaseMessageParam` to Azure's `ChatRequestMessage`.
24
- """
49
+ """Convert from Mirascope `BaseMessageParam` to Azure's `ChatRequestMessage`."""
25
50
  return convert_message_params(
26
51
  cast(list[BaseMessageParam | ChatRequestMessage], message_params)
27
52
  )
28
53
 
29
54
  @staticmethod
30
- def from_provider(message_params: list[AssistantMessage]) -> list[BaseMessageParam]:
55
+ def from_provider(
56
+ message_params: list[ChatRequestMessage],
57
+ ) -> list[BaseMessageParam]:
31
58
  """
32
59
  Convert from Azure's `AssistantMessage` back to Mirascope `BaseMessageParam`.
33
60
  """
34
61
  converted: list[BaseMessageParam] = []
35
62
  for message_param in message_params:
36
- role: str = "assistant"
37
- if not message_param.tool_calls:
38
- converted.append(
39
- BaseMessageParam(role=role, content=message_param.content or "")
40
- )
41
- continue
42
-
43
- contents = []
44
- if tool_calls := message_param.tool_calls:
45
- for tool_call in tool_calls:
46
- contents.append(
63
+ if isinstance(message_param, UserMessage):
64
+ if isinstance(message_param.content, str):
65
+ converted.append(
66
+ BaseMessageParam(role="user", content=message_param.content)
67
+ )
68
+ elif isinstance(message_param.content, list):
69
+ converted_parts = _parse_content(message_param.content)
70
+ converted.append(
71
+ BaseMessageParam(role="user", content=converted_parts)
72
+ )
73
+ elif isinstance(message_param, AssistantMessage):
74
+ converted_parts = []
75
+ if tool_calls := message_param.tool_calls:
76
+ converted_parts.extend(
47
77
  ToolCallPart(
48
78
  type="tool_call",
49
79
  name=tool_call.function.name,
50
80
  id=tool_call.id,
51
81
  args=json.loads(tool_call.function.arguments),
52
82
  )
83
+ for tool_call in tool_calls
53
84
  )
54
-
55
- converted.append(BaseMessageParam(role="tool", content=contents))
85
+ if isinstance(message_param.content, str):
86
+ converted_parts.append(
87
+ TextPart(type="text", text=message_param.content)
88
+ )
89
+ elif isinstance(message_param.content, list):
90
+ converted_parts = _parse_content(message_param.content)
91
+ else:
92
+ converted_parts.append(TextPart(type="text", text=""))
93
+ converted.append(
94
+ BaseMessageParam(
95
+ role="assistant",
96
+ content=converted_parts[0].text
97
+ if len(converted_parts) == 1
98
+ and isinstance(converted_parts[0], TextPart)
99
+ else converted_parts,
100
+ )
101
+ )
102
+ elif isinstance(message_param, ToolMessage):
103
+ converted.append(
104
+ BaseMessageParam(
105
+ role="tool",
106
+ content=[
107
+ ToolResultPart(
108
+ type="tool_result",
109
+ name="",
110
+ content=message_param.content,
111
+ id=message_param.tool_call_id,
112
+ is_error=False,
113
+ )
114
+ ],
115
+ )
116
+ )
56
117
  return converted
@@ -199,5 +199,11 @@ class AzureCallResponse(
199
199
  )
200
200
 
201
201
  @property
202
- def common_message_param(self) -> list[BaseMessageParam]:
203
- return AzureMessageParamConverter.from_provider([self.message_param])
202
+ def common_message_param(self) -> BaseMessageParam:
203
+ return AzureMessageParamConverter.from_provider([self.message_param])[0]
204
+
205
+ @property
206
+ def common_user_message_param(self) -> BaseMessageParam | None:
207
+ if not self.user_message_param:
208
+ return None
209
+ return AzureMessageParamConverter.from_provider([self.user_message_param])[0]
@@ -12,10 +12,12 @@ from .from_call_args import FromCallArgs
12
12
  from .merge_decorators import merge_decorators
13
13
  from .message_param import (
14
14
  AudioPart,
15
+ AudioURLPart,
15
16
  BaseMessageParam,
16
17
  CacheControlPart,
17
18
  DocumentPart,
18
19
  ImagePart,
20
+ ImageURLPart,
19
21
  TextPart,
20
22
  ToolCallPart,
21
23
  ToolResultPart,
@@ -32,6 +34,7 @@ from .types import AudioSegment
32
34
 
33
35
  __all__ = [
34
36
  "AudioPart",
37
+ "AudioURLPart",
35
38
  "AudioSegment",
36
39
  "BaseCallKwargs",
37
40
  "BaseCallParams",
@@ -52,6 +55,7 @@ __all__ = [
52
55
  "FromCallArgs",
53
56
  "GenerateJsonSchemaNoTitles",
54
57
  "ImagePart",
58
+ "ImageURLPart",
55
59
  "merge_decorators",
56
60
  "metadata",
57
61
  "Messages",
@@ -166,7 +166,7 @@ def create_factory( # noqa: ANN202
166
166
  @wraps(fn)
167
167
  async def inner_async(
168
168
  *args: _P.args, **kwargs: _P.kwargs
169
- ) -> TCallResponse | _ParsedOutputT:
169
+ ) -> TCallResponse | _ParsedOutputT: # pyright: ignore [reportInvalidTypeForm]
170
170
  fn_args = get_fn_args(fn, args, kwargs)
171
171
  dynamic_config = await get_dynamic_configuration(fn, args, kwargs)
172
172
  nonlocal client
@@ -114,7 +114,7 @@ def convert_function_to_base_tool(
114
114
  if examples:
115
115
  model.model_config["json_schema_extra"] = {"examples": examples}
116
116
 
117
- def call(self: base) -> Any: # noqa: ANN401
117
+ def call(self: base) -> Any: # pyright: ignore [reportInvalidTypeForm] # noqa: ANN401
118
118
  return fn(
119
119
  **(
120
120
  ({"self": self} if has_self else {})
@@ -129,7 +129,7 @@ def convert_function_to_base_tool(
129
129
  )
130
130
  )
131
131
 
132
- async def call_async(self: base) -> Callable:
132
+ async def call_async(self: base) -> Callable: # pyright: ignore [reportInvalidTypeForm]
133
133
  return await call(self)
134
134
 
135
135
  if inspect.iscoroutinefunction(fn):
@@ -10,10 +10,12 @@ from typing_extensions import TypeIs
10
10
 
11
11
  from ..message_param import (
12
12
  AudioPart,
13
+ AudioURLPart,
13
14
  BaseMessageParam,
14
15
  CacheControlPart,
15
16
  DocumentPart,
16
17
  ImagePart,
18
+ ImageURLPart,
17
19
  TextPart,
18
20
  )
19
21
  from ..types import AudioSegment, Image, has_pil_module, has_pydub_module
@@ -29,17 +31,33 @@ def _convert_message_sequence_part_to_content_part(
29
31
  | TextPart
30
32
  | CacheControlPart
31
33
  | ImagePart
34
+ | ImageURLPart
32
35
  | Image.Image
33
36
  | AudioPart
37
+ | AudioURLPart
34
38
  | AudioSegment
35
39
  | Wave_read
36
40
  | DocumentPart,
37
- ) -> TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart:
41
+ ) -> (
42
+ TextPart
43
+ | ImagePart
44
+ | ImageURLPart
45
+ | AudioPart
46
+ | AudioURLPart
47
+ | CacheControlPart
48
+ | DocumentPart
49
+ ):
38
50
  if isinstance(message_sequence_part, str):
39
51
  return TextPart(text=message_sequence_part, type="text")
40
52
  elif isinstance(
41
53
  message_sequence_part,
42
- TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart,
54
+ TextPart
55
+ | ImagePart
56
+ | ImageURLPart
57
+ | AudioPart
58
+ | AudioURLPart
59
+ | CacheControlPart
60
+ | DocumentPart,
43
61
  ):
44
62
  return message_sequence_part
45
63
  elif has_pil_module and isinstance(message_sequence_part, Image.Image):
@@ -82,13 +100,26 @@ def convert_message_content_to_message_param_content(
82
100
  | TextPart
83
101
  | CacheControlPart
84
102
  | ImagePart
103
+ | ImageURLPart
85
104
  | Image.Image
86
105
  | AudioPart
106
+ | AudioURLPart
87
107
  | AudioSegment
88
108
  | Wave_read
89
109
  | DocumentPart
90
110
  ],
91
- ) -> list[TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart] | str:
111
+ ) -> (
112
+ list[
113
+ TextPart
114
+ | ImagePart
115
+ | ImageURLPart
116
+ | AudioPart
117
+ | AudioURLPart
118
+ | CacheControlPart
119
+ | DocumentPart
120
+ ]
121
+ | str
122
+ ):
92
123
  if isinstance(message_sequence, str):
93
124
  return message_sequence
94
125
  return [
@@ -113,8 +144,10 @@ def convert_messages_to_message_params(
113
144
  | TextPart
114
145
  | CacheControlPart
115
146
  | ImagePart
147
+ | ImageURLPart
116
148
  | Image.Image
117
149
  | AudioPart
150
+ | AudioURLPart
118
151
  | AudioSegment
119
152
  | Wave_read
120
153
  | DocumentPart
@@ -9,10 +9,12 @@ from typing_extensions import TypedDict
9
9
 
10
10
  from ..message_param import (
11
11
  AudioPart,
12
+ AudioURLPart,
12
13
  BaseMessageParam,
13
14
  CacheControlPart,
14
15
  DocumentPart,
15
16
  ImagePart,
17
+ ImageURLPart,
16
18
  TextPart,
17
19
  )
18
20
  from ..types import Image, has_pil_module
@@ -138,7 +140,12 @@ def _load_media(source: str | bytes) -> bytes:
138
140
 
139
141
  def _construct_image_part(
140
142
  source: str | bytes | Image.Image, options: dict[str, str] | None
141
- ) -> ImagePart:
143
+ ) -> ImagePart | ImageURLPart:
144
+ detail = None
145
+ if options:
146
+ detail = options.get("detail", None)
147
+ if isinstance(source, str) and source.startswith(("http://", "https://", "gs://")):
148
+ return ImageURLPart(type="image_url", url=source, detail=detail)
142
149
  if isinstance(source, Image.Image):
143
150
  image = pil_image_to_bytes(source)
144
151
  media_type = (
@@ -149,9 +156,6 @@ def _construct_image_part(
149
156
  else:
150
157
  image = _load_media(source)
151
158
  media_type = f"image/{get_image_type(image)}"
152
- detail = None
153
- if options:
154
- detail = options.get("detail", None)
155
159
  return ImagePart(
156
160
  type="image",
157
161
  media_type=media_type,
@@ -160,8 +164,10 @@ def _construct_image_part(
160
164
  )
161
165
 
162
166
 
163
- def _construct_audio_part(source: str | bytes) -> AudioPart:
167
+ def _construct_audio_part(source: str | bytes) -> AudioPart | AudioURLPart:
164
168
  # Note: audio does not currently support additional options, at least for now.
169
+ if isinstance(source, str) and source.startswith(("http://", "https://", "gs://")):
170
+ return AudioURLPart(type="audio_url", url=source)
165
171
  audio = _load_media(source)
166
172
  return AudioPart(
167
173
  type="audio", media_type=f"audio/{get_audio_type(audio)}", audio=audio
@@ -179,8 +185,16 @@ def _construct_document_part(source: str | bytes) -> DocumentPart:
179
185
 
180
186
  def _construct_parts(
181
187
  part: _Part, attrs: dict[str, Any]
182
- ) -> list[TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart]:
183
- if part["type"] == "image":
188
+ ) -> list[
189
+ TextPart
190
+ | ImagePart
191
+ | ImageURLPart
192
+ | AudioPart
193
+ | AudioURLPart
194
+ | CacheControlPart
195
+ | DocumentPart
196
+ ]:
197
+ if part["type"] in "image":
184
198
  source = attrs[part["template"]]
185
199
  return [_construct_image_part(source, part["options"])] if source else []
186
200
  elif part["type"] == "images":
@@ -229,7 +243,13 @@ def _construct_parts(
229
243
  source = attrs[part["template"]]
230
244
  if not isinstance(
231
245
  source,
232
- TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart,
246
+ TextPart
247
+ | ImagePart
248
+ | ImageURLPart
249
+ | AudioPart
250
+ | AudioURLPart
251
+ | CacheControlPart
252
+ | DocumentPart,
233
253
  ):
234
254
  raise ValueError(
235
255
  f"When using 'part' template, '{part['template']}' must be a valid content part."
@@ -246,7 +266,13 @@ def _construct_parts(
246
266
  for source in sources:
247
267
  if not isinstance(
248
268
  source,
249
- TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart,
269
+ TextPart
270
+ | ImagePart
271
+ | ImageURLPart
272
+ | AudioPart
273
+ | AudioURLPart
274
+ | CacheControlPart
275
+ | DocumentPart,
250
276
  ):
251
277
  raise ValueError(
252
278
  f"When using 'parts' template, '{part['template']}' must be a list of valid content parts."
@@ -275,10 +275,16 @@ class BaseCallResponse(
275
275
 
276
276
  @property
277
277
  @abstractmethod
278
- def common_message_param(self) -> list[BaseMessageParam]:
278
+ def common_message_param(self) -> BaseMessageParam:
279
279
  """Provider-agnostic assistant message param."""
280
280
  ...
281
281
 
282
+ @property
283
+ @abstractmethod
284
+ def common_user_message_param(self) -> BaseMessageParam | None:
285
+ """Provider-agnostic user message param."""
286
+ ...
287
+
282
288
  @property
283
289
  def common_tools(self) -> list[Tool] | None:
284
290
  """Provider-agnostic tools."""
@@ -49,18 +49,44 @@ class ImagePart(BaseModel):
49
49
  detail: str | None
50
50
 
51
51
 
52
+ class ImageURLPart(BaseModel):
53
+ """A content part for images with a URL or base64 encoded image data.
54
+
55
+ Attributes:
56
+ type: Always "image_url"
57
+ url: The URL to the image
58
+ detail: (Optional) The detail to use for the image (supported by OpenAI)
59
+ """
60
+
61
+ type: Literal["image_url"]
62
+ url: str
63
+ detail: str | None
64
+
65
+
52
66
  class AudioPart(BaseModel):
53
67
  """A content part for audio.
54
68
 
55
69
  Attributes:
56
70
  type: Always "audio"
57
71
  media_type: The media type (e.g. audio/wav)
58
- audio: The raw audio bytes
72
+ audio: The raw audio bytes or base64 encoded audio data
59
73
  """
60
74
 
61
75
  type: Literal["audio"]
62
76
  media_type: str
63
- audio: bytes
77
+ audio: bytes | str
78
+
79
+
80
+ class AudioURLPart(BaseModel):
81
+ """A content part for audio with a URL or base64 encoded audio data.
82
+
83
+ Attributes:
84
+ type: Always "audio_url"
85
+ url: The URL to the audio
86
+ """
87
+
88
+ type: Literal["audio_url"]
89
+ url: str
64
90
 
65
91
 
66
92
  class DocumentPart(BaseModel):
@@ -125,7 +151,9 @@ class BaseMessageParam(BaseModel):
125
151
  | Sequence[
126
152
  TextPart
127
153
  | ImagePart
154
+ | ImageURLPart
128
155
  | AudioPart
156
+ | AudioURLPart
129
157
  | CacheControlPart
130
158
  | DocumentPart
131
159
  | ToolCallPart
@@ -10,10 +10,12 @@ from ._utils._convert_messages_to_message_params import (
10
10
  )
11
11
  from .message_param import (
12
12
  AudioPart,
13
+ AudioURLPart,
13
14
  BaseMessageParam,
14
15
  CacheControlPart,
15
16
  DocumentPart,
16
17
  ImagePart,
18
+ ImageURLPart,
17
19
  TextPart,
18
20
  )
19
21
  from .types import AudioSegment
@@ -27,8 +29,10 @@ class Messages:
27
29
  | TextPart
28
30
  | CacheControlPart
29
31
  | ImagePart
32
+ | ImageURLPart
30
33
  | Image.Image
31
34
  | AudioPart
35
+ | AudioURLPart
32
36
  | AudioSegment
33
37
  | Wave_read
34
38
  | DocumentPart
@@ -46,8 +50,10 @@ class Messages:
46
50
  | TextPart
47
51
  | CacheControlPart
48
52
  | ImagePart
53
+ | ImageURLPart
49
54
  | Image.Image
50
55
  | AudioPart
56
+ | AudioURLPart
51
57
  | AudioSegment
52
58
  | Wave_read
53
59
  | DocumentPart
@@ -67,8 +73,10 @@ class Messages:
67
73
  | TextPart
68
74
  | CacheControlPart
69
75
  | ImagePart
76
+ | ImageURLPart
70
77
  | Image.Image
71
78
  | AudioPart
79
+ | AudioURLPart
72
80
  | AudioSegment
73
81
  | Wave_read
74
82
  | DocumentPart
@@ -88,8 +96,10 @@ class Messages:
88
96
  | TextPart
89
97
  | CacheControlPart
90
98
  | ImagePart
99
+ | ImageURLPart
91
100
  | Image.Image
92
101
  | AudioPart
102
+ | AudioURLPart
93
103
  | AudioSegment
94
104
  | Wave_read
95
105
  | DocumentPart
@@ -3,6 +3,8 @@
3
3
  from typing import cast
4
4
 
5
5
  from ...base import BaseMessageParam
6
+ from ...base._utils import get_image_type
7
+ from ...base._utils._parse_content_template import _load_media
6
8
  from .._types import ConversationRoleType, InternalBedrockMessageParam
7
9
 
8
10
 
@@ -37,7 +39,22 @@ def convert_message_params(
37
39
  " currently only supports JPEG, PNG, GIF, and WebP images."
38
40
  )
39
41
  converted_content.append(
40
- {"format": part.media_type, "bytes": part.image}
42
+ {
43
+ "image": {
44
+ "format": part.media_type.split("/")[-1],
45
+ "source": {"bytes": part.image},
46
+ }
47
+ }
48
+ )
49
+ elif part.type == "image_url":
50
+ image = _load_media(part.url)
51
+ converted_content.append(
52
+ {
53
+ "image": {
54
+ "format": get_image_type(image),
55
+ "source": {"bytes": image},
56
+ },
57
+ }
41
58
  )
42
59
  elif part.type == "tool_result":
43
60
  if converted_content:
@@ -232,5 +232,11 @@ class BedrockCallResponse(
232
232
  return _convert_finish_reasons_to_common_finish_reasons(self.finish_reasons)
233
233
 
234
234
  @property
235
- def common_message_param(self) -> list[BaseMessageParam]:
236
- return BedrockMessageParamConverter.from_provider([self.message_param])
235
+ def common_message_param(self) -> BaseMessageParam:
236
+ return BedrockMessageParamConverter.from_provider([self.message_param])[0]
237
+
238
+ @property
239
+ def common_user_message_param(self) -> BaseMessageParam | None:
240
+ if not self.user_message_param:
241
+ return None
242
+ return BedrockMessageParamConverter.from_provider([self.user_message_param])[0] # pyright: ignore [reportArgumentType]
@@ -29,9 +29,10 @@ class CohereMessageParamConverter(BaseMessageParamConverter):
29
29
  """
30
30
  converted = []
31
31
  for message_param in message_params:
32
+ role = getattr(message_param, "role", "assistant")
32
33
  if not message_param.tool_calls:
33
34
  converted.append(
34
- BaseMessageParam(role="assistant", content=message_param.message)
35
+ BaseMessageParam(role=role, content=message_param.message)
35
36
  )
36
37
  continue
37
38
 
@@ -49,5 +50,5 @@ class CohereMessageParamConverter(BaseMessageParamConverter):
49
50
  )
50
51
  )
51
52
 
52
- converted.append(BaseMessageParam(role="tool", content=converted_content))
53
+ converted.append(BaseMessageParam(role=role, content=converted_content))
53
54
  return converted