-
Notifications
You must be signed in to change notification settings - Fork 572
fix(integrations): openai/openai-agents: convert input message format #5248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
1f32952
795bcea
a623e13
3d3ce5b
ce29e47
7074f0b
e8a1adc
c1a2239
bd46a6a
04b27f4
f8345d0
b74bdb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| from sentry_sdk.ai.utils import ( | ||
| set_data_normalized, | ||
| normalize_message_roles, | ||
| parse_data_uri, | ||
| truncate_and_annotate_messages, | ||
| ) | ||
| from sentry_sdk.consts import SPANDATA | ||
|
|
@@ -18,7 +19,7 @@ | |
| safe_serialize, | ||
| ) | ||
|
|
||
| from typing import TYPE_CHECKING | ||
| from typing import TYPE_CHECKING, Dict | ||
|
|
||
| if TYPE_CHECKING: | ||
| from typing import Any, Iterable, List, Optional, Callable, AsyncIterator, Iterator | ||
|
|
@@ -180,6 +181,84 @@ def _calculate_token_usage( | |
| ) | ||
|
|
||
|
|
||
| def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": | ||
| """ | ||
| Convert the message parts from OpenAI format to the `gen_ai.request.messages` format. | ||
| e.g: | ||
| { | ||
| "role": "user", | ||
| "content": [ | ||
| { | ||
| "text": "How many ponies do you see in the image?", | ||
| "type": "text" | ||
| }, | ||
| { | ||
| "type": "image_url", | ||
| "image_url": { | ||
| "url": "data:image/jpeg;base64,...", | ||
| "detail": "high" | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| becomes: | ||
| { | ||
| "role": "user", | ||
| "content": [ | ||
| { | ||
| "text": "How many ponies do you see in the image?", | ||
| "type": "text" | ||
| }, | ||
| { | ||
| "type": "blob", | ||
| "modality": "image", | ||
| "mime_type": "image/jpeg", | ||
| "content": "data:image/jpeg;base64,..." | ||
| } | ||
| ] | ||
| } | ||
| """ | ||
|
|
||
| def _map_item(item: "Dict[str, Any]") -> "Dict[str, Any]": | ||
| if not isinstance(item, dict): | ||
| return item | ||
|
|
||
| if item.get("type") == "image_url": | ||
| image_url = item.get("image_url") or {} | ||
| url = image_url.get("url", "") | ||
|
Comment on lines
+226
to
+228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The code will raise an Suggested FixAdd a check to ensure Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| if url.startswith("data:"): | ||
| try: | ||
| mime_type, content = parse_data_uri(url) | ||
| return { | ||
| "type": "blob", | ||
| "modality": "image", | ||
| "mime_type": mime_type, | ||
| "content": content, | ||
| } | ||
| except ValueError: | ||
| # If parsing fails, return as URI | ||
| return { | ||
| "type": "uri", | ||
| "modality": "image", | ||
| "uri": url, | ||
| } | ||
| else: | ||
| return { | ||
| "type": "uri", | ||
| "modality": "image", | ||
| "uri": url, | ||
constantinius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
constantinius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return item | ||
constantinius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| for message in messages: | ||
| if not isinstance(message, dict): | ||
| continue | ||
| content = message.get("content") | ||
| if isinstance(content, list): | ||
| message["content"] = [_map_item(item) for item in content] | ||
| return messages | ||
|
|
||
|
|
||
| def _set_input_data( | ||
| span: "Span", | ||
| kwargs: "dict[str, Any]", | ||
|
|
@@ -201,6 +280,8 @@ def _set_input_data( | |
| and integration.include_prompts | ||
| ): | ||
| normalized_messages = normalize_message_roles(messages) | ||
| normalized_messages = _convert_message_parts(normalized_messages) | ||
|
|
||
| scope = sentry_sdk.get_current_scope() | ||
| messages_data = truncate_and_annotate_messages(normalized_messages, span, scope) | ||
| if messages_data is not None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,14 +3,19 @@ | |
| get_start_span_function, | ||
| set_data_normalized, | ||
| normalize_message_roles, | ||
| normalize_message_role, | ||
| truncate_and_annotate_messages, | ||
| ) | ||
| from sentry_sdk.consts import OP, SPANDATA | ||
| from sentry_sdk.scope import should_send_default_pii | ||
| from sentry_sdk.utils import safe_serialize | ||
|
|
||
| from ..consts import SPAN_ORIGIN | ||
| from ..utils import _set_agent_data, _set_usage_data | ||
| from ..utils import ( | ||
| _set_agent_data, | ||
| _set_usage_data, | ||
| _transform_openai_agents_message_content, | ||
| ) | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
|
|
@@ -49,17 +54,40 @@ def invoke_agent_span( | |
|
|
||
| original_input = kwargs.get("original_input") | ||
| if original_input is not None: | ||
| message = ( | ||
| original_input | ||
| if isinstance(original_input, str) | ||
| else safe_serialize(original_input) | ||
| ) | ||
| messages.append( | ||
| { | ||
| "content": [{"text": message, "type": "text"}], | ||
| "role": "user", | ||
| } | ||
| ) | ||
| if isinstance(original_input, str): | ||
| # String input: wrap in text block | ||
| messages.append( | ||
| { | ||
| "content": [{"text": original_input, "type": "text"}], | ||
| "role": "user", | ||
| } | ||
| ) | ||
| elif isinstance(original_input, list) and len(original_input) > 0: | ||
| # Check if list contains message objects (with type="message") | ||
| # or content parts (input_text, input_image, etc.) | ||
| first_item = original_input[0] | ||
| if isinstance(first_item, dict) and first_item.get("type") == "message": | ||
| # List of message objects - process each individually | ||
| for msg in original_input: | ||
| if isinstance(msg, dict) and msg.get("type") == "message": | ||
| role = normalize_message_role(msg.get("role", "user")) | ||
| content = msg.get("content") | ||
| transformed = _transform_openai_agents_message_content( | ||
| content | ||
| ) | ||
| if isinstance(transformed, str): | ||
| transformed = [{"text": transformed, "type": "text"}] | ||
| elif not isinstance(transformed, list): | ||
| transformed = [ | ||
| {"text": str(transformed), "type": "text"} | ||
| ] | ||
| messages.append({"content": transformed, "role": role}) | ||
| else: | ||
| # List of content parts - transform and wrap as user message | ||
| content = _transform_openai_agents_message_content(original_input) | ||
| if not isinstance(content, list): | ||
| content = [{"text": str(content), "type": "text"}] | ||
| messages.append({"content": content, "role": "user"}) | ||
constantinius marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-dict content items produce invalid message structureLow Severity When Additional Locations (1) |
||
|
|
||
| if len(messages) > 0: | ||
| normalized_messages = normalize_message_roles(messages) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.