diff --git a/sentry_sdk/ai/_openai_completions_api.py b/sentry_sdk/ai/_openai_completions_api.py new file mode 100644 index 0000000000..8150925b21 --- /dev/null +++ b/sentry_sdk/ai/_openai_completions_api.py @@ -0,0 +1,48 @@ +from collections.abc import Iterable + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sentry_sdk._types import TextPart + + from openai.types.chat import ( + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ) + + +def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: + return isinstance(message, dict) and message.get("role") == "system" + + +def _get_system_instructions( + messages: "Iterable[ChatCompletionMessageParam]", +) -> "list[ChatCompletionMessageParam]": + if not isinstance(messages, Iterable): + return [] + + return [message for message in messages if _is_system_instruction(message)] + + +def _transform_system_instructions( + system_instructions: "list[ChatCompletionSystemMessageParam]", +) -> "list[TextPart]": + instruction_text_parts: "list[TextPart]" = [] + + for instruction in system_instructions: + if not isinstance(instruction, dict): + continue + + content = instruction.get("content") + + if isinstance(content, str): + instruction_text_parts.append({"type": "text", "content": content}) + + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + text = part.get("text", None) + if text is not None: + instruction_text_parts.append({"type": "text", "content": text}) + + return instruction_text_parts diff --git a/sentry_sdk/ai/_openai_responses_api.py b/sentry_sdk/ai/_openai_responses_api.py new file mode 100644 index 0000000000..50fddf1d2f --- /dev/null +++ b/sentry_sdk/ai/_openai_responses_api.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.responses import ResponseInputParam, ResponseInputItemParam + + +def _is_system_instruction(message: "ResponseInputItemParam") -> bool: + if not isinstance(message, dict) or not message.get("role") == "system": + return False + + return "type" not in message or message["type"] == "message" + + +def _get_system_instructions( + messages: "Union[str, ResponseInputParam]", +) -> "list[ResponseInputItemParam]": + if not isinstance(messages, list): + return [] + + return [message for message in messages if _is_system_instruction(message)] diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 2a8f75714e..dc14038d9c 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -11,6 +11,15 @@ normalize_message_roles, truncate_and_annotate_messages, ) +from sentry_sdk.ai._openai_completions_api import ( + _is_system_instruction as _is_system_instruction_completions, + _get_system_instructions as _get_system_instructions_completions, + _transform_system_instructions, +) +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction as _is_system_instruction_responses, + _get_system_instructions as _get_system_instructions_responses, +) from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii @@ -33,11 +42,12 @@ AsyncIterator, Iterator, Union, + Iterable, ) from sentry_sdk.tracing import Span from sentry_sdk._types import TextPart - from openai.types.responses import ResponseInputParam, ResponseInputItemParam + from openai.types.responses import ResponseInputParam from openai import Omit try: @@ -200,63 +210,6 @@ def _calculate_token_usage( ) -def _is_system_instruction_completions(message: "ChatCompletionMessageParam") -> bool: - return isinstance(message, dict) and message.get("role") == "system" - - -def _get_system_instructions_completions( - messages: "Iterable[ChatCompletionMessageParam]", -) -> "list[ChatCompletionMessageParam]": - if not isinstance(messages, Iterable): - return [] - - return [ - message for message in messages if _is_system_instruction_completions(message) - ] - - -def _is_system_instruction_responses(message: "ResponseInputItemParam") -> bool: - if not isinstance(message, dict) or not message.get("role") == "system": - return False - - return "type" not in message or message["type"] == "message" - - -def _get_system_instructions_responses( - messages: "Union[str, ResponseInputParam]", -) -> "list[ResponseInputItemParam]": - if not isinstance(messages, list): - return [] - - return [ - message for message in messages if _is_system_instruction_responses(message) - ] - - -def _transform_system_instructions( - system_instructions: "list[ChatCompletionSystemMessageParam]", -) -> "list[TextPart]": - instruction_text_parts: "list[TextPart]" = [] - - for instruction in system_instructions: - if not isinstance(instruction, dict): - continue - - content = instruction.get("content") - - if isinstance(content, str): - instruction_text_parts.append({"type": "text", "content": content}) - - elif isinstance(content, list): - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - text = part.get("text", "") - if text: - instruction_text_parts.append({"type": "text", "content": text}) - - return instruction_text_parts - - def _get_input_messages( kwargs: "dict[str, Any]", ) -> "Optional[Union[Iterable[Any], list[str]]]": diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index fc9c9d4b00..c3a3a04dc9 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -18,17 +18,6 @@ import agents from typing import Any, Optional - from sentry_sdk._types import TextPart - - -def _transform_system_instruction(system_instructions: "str") -> "list[TextPart]": - return [ - { - "type": "text", - "content": system_instructions, - } - ] - def invoke_agent_span( context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" @@ -46,16 +35,16 @@ def invoke_agent_span( if should_send_default_pii(): messages = [] if agent.instructions: - system_instruction = ( + message = ( agent.instructions if isinstance(agent.instructions, str) else safe_serialize(agent.instructions) ) - set_data_normalized( - span, - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - _transform_system_instruction(system_instruction), - unpack=False, + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "system", + } ) original_input = kwargs.get("original_input") diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index a77d9f46ea..4df76746a9 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -11,14 +11,20 @@ from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import event_from_exception, safe_serialize +from sentry_sdk.ai._openai_completions_api import _transform_system_instructions +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction, + _get_system_instructions, +) from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any - from agents import Usage + from agents import Usage, TResponseInputItem from sentry_sdk.tracing import Span + from sentry_sdk._types import TextPart try: import agents @@ -121,19 +127,39 @@ def _set_input_data( return request_messages = [] - system_instructions = get_response_kwargs.get("system_instructions") - if system_instructions: - request_messages.append( + messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) + + instructions_text_parts: "list[TextPart]" = [] + explicit_instructions = get_response_kwargs.get("system_instructions") + if explicit_instructions is not None: + instructions_text_parts.append( { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM, - "content": [{"type": "text", "text": system_instructions}], + "type": "text", + "content": explicit_instructions, } ) - for message in get_response_kwargs.get("input", []): + system_instructions = _get_system_instructions(messages) + + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + instructions_text_parts, + unpack=False, + ) + + non_system_messages = [ + message for message in messages if not _is_system_instruction(message) + ] + for message in non_system_messages: if "role" in message: - normalized_role = normalize_message_role(message.get("role")) - content = message.get("content") + normalized_role = normalize_message_role(message.get("role")) # type: ignore + content = message.get("content") # type: ignore request_messages.append( { "role": normalized_role, @@ -145,14 +171,14 @@ def _set_input_data( } ) else: - if message.get("type") == "function_call": + if message.get("type") == "function_call": # type: ignore request_messages.append( { "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, "content": [message], } ) - elif message.get("type") == "function_call_output": + elif message.get("type") == "function_call_output": # type: ignore request_messages.append( { "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 81f4b54e1d..34dc5abd7d 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -132,6 +132,26 @@ def test_agent(): ) +@pytest.fixture +def test_agent_with_instructions(): + def inner(instructions): + """Create a real Agent instance for testing.""" + return Agent( + name="test_agent", + instructions=instructions, + model="gpt-4", + model_settings=ModelSettings( + max_tokens=100, + temperature=0.7, + top_p=1.0, + presence_penalty=0.0, + frequency_penalty=0.0, + ), + ) + + return inner + + @pytest.fixture def test_agent_custom_model(): """Create a real Agent instance for testing.""" @@ -151,12 +171,145 @@ def test_agent_custom_model(): @pytest.mark.asyncio +async def test_agent_invocation_span_no_pii( + sentry_init, capture_events, test_agent, mock_model_response +): + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + mock_get_response.return_value = mock_model_response + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=False, + ) + + events = capture_events() + + result = await agents.Runner.run( + test_agent, "Test input", run_config=test_run_config + ) + + assert result is not None + assert result.final_output == "Hello, how can I help you?" + + (transaction,) = events + spans = transaction["spans"] + invoke_agent_span, ai_client_span = spans + + assert transaction["transaction"] == "test_agent workflow" + assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" + + assert invoke_agent_span["description"] == "invoke_agent test_agent" + + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in invoke_agent_span["data"] + assert "gen_ai.request.messages" not in invoke_agent_span["data"] + assert "gen_ai.response.text" not in invoke_agent_span["data"] + + assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" + assert invoke_agent_span["data"]["gen_ai.system"] == "openai" + assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" + assert invoke_agent_span["data"]["gen_ai.request.max_tokens"] == 100 + assert invoke_agent_span["data"]["gen_ai.request.model"] == "gpt-4" + assert invoke_agent_span["data"]["gen_ai.request.temperature"] == 0.7 + assert invoke_agent_span["data"]["gen_ai.request.top_p"] == 1.0 + + assert ai_client_span["description"] == "chat gpt-4" + assert ai_client_span["data"]["gen_ai.operation.name"] == "chat" + assert ai_client_span["data"]["gen_ai.system"] == "openai" + assert ai_client_span["data"]["gen_ai.agent.name"] == "test_agent" + assert ai_client_span["data"]["gen_ai.request.max_tokens"] == 100 + assert ai_client_span["data"]["gen_ai.request.model"] == "gpt-4" + assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 + assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "instructions", + ( + None, + "You are a coding assistant that talks like a pirate.", + ), +) @pytest.mark.parametrize( - "send_default_pii", - (True, False), + "input", + [ + pytest.param("Test input", id="string"), + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="blocks_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="parts_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="parts", + ), + ], ) async def test_agent_invocation_span( - sentry_init, capture_events, test_agent, mock_model_response, send_default_pii + sentry_init, + capture_events, + test_agent_with_instructions, + mock_model_response, + instructions, + input, + request, ): """ Test that the integration creates spans for agent invocations. @@ -171,13 +324,15 @@ async def test_agent_invocation_span( sentry_init( integrations=[OpenAIAgentsIntegration()], traces_sample_rate=1.0, - send_default_pii=send_default_pii, + send_default_pii=True, ) events = capture_events() result = await agents.Runner.run( - test_agent, "Test input", run_config=test_run_config + test_agent_with_instructions(instructions), + input, + run_config=test_run_config, ) assert result is not None @@ -192,25 +347,99 @@ async def test_agent_invocation_span( assert invoke_agent_span["description"] == "invoke_agent test_agent" - if send_default_pii: - assert invoke_agent_span["data"][ - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS - ] == safe_serialize( - [{"type": "text", "content": "You are a helpful test assistant."}] - ) + # Only first case checks "gen_ai.request.messages" until further input handling work. + param_id = request.node.callspec.id + if "string" in param_id and instructions is None: # type: ignore + assert "gen_ai.system_instructions" not in ai_client_span["data"] + assert invoke_agent_span["data"]["gen_ai.request.messages"] == safe_serialize( [ {"content": [{"text": "Test input", "type": "text"}], "role": "user"}, ] ) - assert ( - invoke_agent_span["data"]["gen_ai.response.text"] - == "Hello, how can I help you?" + + elif "string" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + ] + ) + elif "blocks_no_type" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "parts_no_type" in param_id and instructions is None: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif "parts_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] ) else: - assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in invoke_agent_span["data"] - assert "gen_ai.request.messages" not in invoke_agent_span["data"] - assert "gen_ai.response.text" not in invoke_agent_span["data"] + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + + assert ( + invoke_agent_span["data"]["gen_ai.response.text"] + == "Hello, how can I help you?" + ) assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" assert invoke_agent_span["data"]["gen_ai.system"] == "openai" @@ -266,8 +495,11 @@ async def test_client_span_custom_model( assert ai_client_span["data"]["gen_ai.request.model"] == "my-custom-model" -def test_agent_invocation_span_sync( - sentry_init, capture_events, test_agent, mock_model_response +def test_agent_invocation_span_sync_no_pii( + sentry_init, + capture_events, + test_agent, + mock_model_response, ): """ Test that the integration creates spans for agent invocations. @@ -282,6 +514,7 @@ def test_agent_invocation_span_sync( sentry_init( integrations=[OpenAIAgentsIntegration()], traces_sample_rate=1.0, + send_default_pii=False, ) events = capture_events() @@ -318,6 +551,226 @@ def test_agent_invocation_span_sync( assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in invoke_agent_span["data"] + + +@pytest.mark.parametrize( + "instructions", + ( + None, + "You are a coding assistant that talks like a pirate.", + ), +) +@pytest.mark.parametrize( + "input", + [ + pytest.param("Test input", id="string"), + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="blocks_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="parts_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="parts", + ), + ], +) +def test_agent_invocation_span_sync( + sentry_init, + capture_events, + test_agent_with_instructions, + mock_model_response, + instructions, + input, + request, +): + """ + Test that the integration creates spans for agent invocations. + """ + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + mock_get_response.return_value = mock_model_response + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + events = capture_events() + + result = agents.Runner.run_sync( + test_agent_with_instructions(instructions), + input, + run_config=test_run_config, + ) + + assert result is not None + assert result.final_output == "Hello, how can I help you?" + + (transaction,) = events + spans = transaction["spans"] + invoke_agent_span, ai_client_span = spans + + assert transaction["transaction"] == "test_agent workflow" + assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" + + assert invoke_agent_span["description"] == "invoke_agent test_agent" + assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" + assert invoke_agent_span["data"]["gen_ai.system"] == "openai" + assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" + assert invoke_agent_span["data"]["gen_ai.request.max_tokens"] == 100 + assert invoke_agent_span["data"]["gen_ai.request.model"] == "gpt-4" + assert invoke_agent_span["data"]["gen_ai.request.temperature"] == 0.7 + assert invoke_agent_span["data"]["gen_ai.request.top_p"] == 1.0 + + assert ai_client_span["description"] == "chat gpt-4" + assert ai_client_span["data"]["gen_ai.operation.name"] == "chat" + assert ai_client_span["data"]["gen_ai.system"] == "openai" + assert ai_client_span["data"]["gen_ai.agent.name"] == "test_agent" + assert ai_client_span["data"]["gen_ai.request.max_tokens"] == 100 + assert ai_client_span["data"]["gen_ai.request.model"] == "gpt-4" + assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 + assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 + + param_id = request.node.callspec.id + if "string" in param_id and instructions is None: # type: ignore + assert "gen_ai.system_instructions" not in ai_client_span["data"] + elif "string" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + ] + ) + elif "blocks_no_type" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "parts_no_type" in param_id and instructions is None: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif "parts_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + else: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + @pytest.mark.asyncio async def test_handoff_span(sentry_init, capture_events, mock_usage): @@ -627,12 +1080,6 @@ def simple_test_tool(message: str) -> str: assert ai_client_span1["data"]["gen_ai.request.max_tokens"] == 100 assert ai_client_span1["data"]["gen_ai.request.messages"] == safe_serialize( [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, { "role": "user", "content": [ @@ -698,12 +1145,6 @@ def simple_test_tool(message: str) -> str: assert ai_client_span2["data"]["gen_ai.request.max_tokens"] == 100 assert ai_client_span2["data"]["gen_ai.request.messages"] == safe_serialize( [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, { "role": "user", "content": [ @@ -992,12 +1433,6 @@ async def test_error_captures_input_data(sentry_init, capture_events, test_agent assert "gen_ai.request.messages" in ai_client_span["data"] request_messages = safe_serialize( [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, {"role": "user", "content": [{"type": "text", "text": "Test input"}]}, ] ) @@ -2105,7 +2540,6 @@ def test_openai_agents_message_truncation(sentry_init, capture_events): ) test_messages = [ - {"role": "system", "content": "small message 1"}, {"role": "user", "content": large_content}, {"role": "assistant", "content": large_content}, {"role": "user", "content": "small message 4"}, @@ -2119,8 +2553,8 @@ def test_openai_agents_message_truncation(sentry_init, capture_events): _set_input_data(span, get_response_kwargs) if hasattr(scope, "_gen_ai_original_message_count"): truncated_count = scope._gen_ai_original_message_count.get(span.span_id) - assert truncated_count == 5, ( - f"Expected 5 original messages, got {truncated_count}" + assert truncated_count == 4, ( + f"Expected 4 original messages, got {truncated_count}" ) assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span._data