pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/modelcontextprotocol/python-sdk/pull/2347.patch

ype='web_search_call' + # ) + MessageAttributes.COMPLETION_TOOL_CALL_ID: "id", + MessageAttributes.COMPLETION_TOOL_CALL_TYPE: "type", + MessageAttributes.COMPLETION_TOOL_CALL_STATUS: "status", +} + +RESPONSE_OUTPUT_TOOL_WEB_SEARCH_URL_ANNOTATIONS: IndexedAttributeMap = { + # AnnotationURLCitation( + # end_index=747, + # start_index=553, + # title="You can now play a real-time AI-rendered Quake II in your browser", + # type='url_citation', + # url='https://www.tomshardware.com/video-games/you-can-now-play-a-real-time-ai-rendered-quake-ii-in-your-browser-microsofts-whamm-offers-generative-ai-for-games?utm_source=openai' + # ) + MessageAttributes.COMPLETION_ANNOTATION_END_INDEX: "end_index", + MessageAttributes.COMPLETION_ANNOTATION_START_INDEX: "start_index", + MessageAttributes.COMPLETION_ANNOTATION_TITLE: "title", + MessageAttributes.COMPLETION_ANNOTATION_TYPE: "type", + MessageAttributes.COMPLETION_ANNOTATION_URL: "url", +} + + +RESPONSE_OUTPUT_TOOL_COMPUTER_ATTRIBUTES: IndexedAttributeMap = { + # ResponseComputerToolCall( + # id='comp_12345', + # action=Action( + # type='click', + # target='button_submit' + # ), + # call_id='call_67890', + # pending_safety_checks=[ + # PendingSafetyCheck( + # type='check_type', + # status='pending' + # ) + # ], + # status='completed', + # type='computer_call' + # ) + # TODO semantic conventions for `ResponseComputerToolCall` are not defined yet +} + + +RESPONSE_OUTPUT_TOOL_FILE_SEARCH_ATTRIBUTES: IndexedAttributeMap = { + # ResponseFileSearchToolCall( + # id='fsc_12345', + # queries=['example query'], + # status='completed', + # type='file_search_call', + # results=[ + # Result( + # attributes={'key1': 'value1', 'key2': 42}, + # file_id='file_67890', + # filename='example.txt', + # score=0.95, + # text='Example text retrieved from the file.' + # ), + # ... + # ] + # ) + # TODO semantic conventions for `ResponseFileSearchToolCall` are not defined yet +} + + +RESPONSE_USAGE_ATTRIBUTES: AttributeMap = { + SpanAttributes.LLM_USAGE_COMPLETION_TOKENS: "output_tokens", + SpanAttributes.LLM_USAGE_PROMPT_TOKENS: "input_tokens", + SpanAttributes.LLM_USAGE_TOTAL_TOKENS: "total_tokens", +} + + +# usage attributes are shared with `input_details_tokens` and `output_details_tokens` +RESPONSE_USAGE_DETAILS_ATTRIBUTES: AttributeMap = { + SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS: "cached_tokens", + SpanAttributes.LLM_USAGE_REASONING_TOKENS: "reasoning_tokens", +} + + +RESPONSE_REASONING_ATTRIBUTES: AttributeMap = { + # Reasoning( + # effort='medium', + # generate_summary=None, + # ) + # TODO `effort` and `generate_summary` need semantic conventions +} + + +def get_response_kwarg_attributes(kwargs: dict) -> AttributeMap: + """Handles interpretation of openai Responses.create method keyword arguments.""" + + # Just gather the attributes that are not present in the Response object + # TODO We could gather more here and have more context available in the + # event of an error during the request execution. + + # Method signature for `Responses.create`: + # input: Union[str, ResponseInputParam], + # model: Union[str, ChatModel], + # include: Optional[List[ResponseIncludable]] | NotGiven = NOT_GIVEN, + # instructions: Optional[str] | NotGiven = NOT_GIVEN, + # max_output_tokens: Optional[int] | NotGiven = NOT_GIVEN, + # metadata: Optional[Metadata] | NotGiven = NOT_GIVEN, + # parallel_tool_calls: Optional[bool] | NotGiven = NOT_GIVEN, + # previous_response_id: Optional[str] | NotGiven = NOT_GIVEN, + # reasoning: Optional[Reasoning] | NotGiven = NOT_GIVEN, + # store: Optional[bool] | NotGiven = NOT_GIVEN, + # stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + # temperature: Optional[float] | NotGiven = NOT_GIVEN, + # text: ResponseTextConfigParam | NotGiven = NOT_GIVEN, + # tool_choice: response_create_params.ToolChoice | NotGiven = NOT_GIVEN, + # tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + # top_p: Optional[float] | NotGiven = NOT_GIVEN, + # truncation: Optional[Literal["auto", "disabled"]] | NotGiven = NOT_GIVEN, + # user: str | NotGiven = NOT_GIVEN, + # extra_headers: Headers | None = None, + # extra_query: Query | None = None, + # extra_body: Body | None = None, + # timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + attributes = {} + + # `input` can either be a `str` or a list of many internal types, so we duck + # type our way into some usable common attributes + _input: Union[str, list, None] = kwargs.get("input") + if isinstance(_input, str): + attributes[MessageAttributes.PROMPT_ROLE.format(i=0)] = "user" + attributes[MessageAttributes.PROMPT_CONTENT.format(i=0)] = _input + + elif isinstance(_input, list): + for i, prompt in enumerate(_input): + # Object type is pretty diverse, so we handle common attributes, but do so + # conditionally because not all attributes are guaranteed to exist + if hasattr(prompt, "type"): + attributes[MessageAttributes.PROMPT_TYPE.format(i=i)] = prompt.type + if hasattr(prompt, "role"): + attributes[MessageAttributes.PROMPT_ROLE.format(i=i)] = prompt.role + if hasattr(prompt, "content"): + attributes[MessageAttributes.PROMPT_CONTENT.format(i=i)] = prompt.content + + else: + logger.debug(f"[agentops.instrumentation.openai.response] '{type(_input)}' is not a recognized input type.") + + # `model` is always `str` (`ChatModel` type is just a string literal) + attributes[SpanAttributes.LLM_REQUEST_MODEL] = str(kwargs.get("model")) + + return attributes + + +# We call this `response_response` because in OpenAI Agents the `Response` is +# a return type from the `responses` module +def get_response_response_attributes(response: "Response") -> AttributeMap: + """Handles interpretation of an openai Response object.""" + attributes = _extract_attributes_from_mapping(response.__dict__, RESPONSE_ATTRIBUTES) + + if response.output: + attributes.update(get_response_output_attributes(response.output)) + + if response.tools: + attributes.update(get_response_tools_attributes(response.tools)) + + if response.reasoning: + attributes.update(_extract_attributes_from_mapping(response.reasoning.__dict__, RESPONSE_REASONING_ATTRIBUTES)) + + if response.usage: + attributes.update(get_response_usage_attributes(response.usage)) + + return attributes + + +def get_response_output_attributes(output: List["ResponseOutputTypes"]) -> AttributeMap: + """Handles interpretation of an openai Response `output` list.""" + attributes = {} + + for i, output_item in enumerate(output): + if isinstance(output_item, ResponseOutputMessage): + attributes.update(get_response_output_message_attributes(i, output_item)) + + elif isinstance(output_item, ResponseReasoningItem): + attributes.update( + _extract_attributes_from_mapping_with_index(output_item, RESPONSE_OUTPUT_REASONING_ATTRIBUTES, i) + ) + + elif isinstance(output_item, ResponseFunctionToolCall): + attributes.update( + _extract_attributes_from_mapping_with_index(output_item, RESPONSE_OUTPUT_TOOL_ATTRIBUTES, i=i, j=0) + ) + + elif isinstance(output_item, ResponseFunctionWebSearch): + attributes.update( + _extract_attributes_from_mapping_with_index( + output_item, RESPONSE_OUTPUT_TOOL_WEB_SEARCH_ATTRIBUTES, i=i, j=0 + ) + ) + + elif isinstance(output_item, ResponseComputerToolCall): + attributes.update( + _extract_attributes_from_mapping_with_index( + output_item, RESPONSE_OUTPUT_TOOL_COMPUTER_ATTRIBUTES, i=i, j=0 + ) + ) + + elif isinstance(output_item, ResponseFileSearchToolCall): + attributes.update( + _extract_attributes_from_mapping_with_index( + output_item, RESPONSE_OUTPUT_TOOL_FILE_SEARCH_ATTRIBUTES, i=i, j=0 + ) + ) + + else: + logger.debug(f"[agentops.instrumentation.openai.response] '{output_item}' is not a recognized output type.") + + return attributes + + +def get_response_output_text_attributes(output_text: "ResponseOutputText", index: int) -> AttributeMap: + """Handles interpretation of an openai ResponseOutputText object.""" + # This function is a helper to handle the ResponseOutputText type specifically + attributes = _extract_attributes_from_mapping_with_index(output_text, RESPONSE_OUTPUT_TEXT_ATTRIBUTES, index) + + if hasattr(output_text, "annotations"): + for j, output_text_annotation in enumerate(output_text.annotations): + attributes.update( + _extract_attributes_from_mapping_with_index( + output_text_annotation, RESPONSE_OUTPUT_TOOL_WEB_SEARCH_URL_ANNOTATIONS, i=index, j=j + ) + ) + + return attributes + + +def get_response_output_message_attributes(index: int, message: "ResponseOutputMessage") -> AttributeMap: + """Handles interpretation of an openai ResponseOutputMessage object.""" + attributes = _extract_attributes_from_mapping_with_index(message, RESPONSE_OUTPUT_MESSAGE_ATTRIBUTES, index) + + if message.content: + for i, content in enumerate(message.content): + if isinstance(content, ResponseOutputText): + attributes.update(get_response_output_text_attributes(content, i)) + + else: + logger.debug( + f"[agentops.instrumentation.openai.response] '{content}' is not a recognized content type." + ) + + return attributes + + +def get_response_tools_attributes(tools: List["ToolTypes"]) -> AttributeMap: + """Handles interpretation of openai Response `tools` list.""" + attributes = {} + + for i, tool in enumerate(tools): + if isinstance(tool, FunctionTool): + attributes.update(_extract_attributes_from_mapping_with_index(tool, RESPONSE_TOOL_ATTRIBUTES, i)) + + elif isinstance(tool, WebSearchTool): + attributes.update(get_response_tool_web_search_attributes(tool, i)) + + elif isinstance(tool, FileSearchTool): + attributes.update(get_response_tool_file_search_attributes(tool, i)) + + elif isinstance(tool, ComputerTool): + attributes.update(get_response_tool_computer_attributes(tool, i)) + + else: + logger.debug(f"[agentops.instrumentation.openai.response] '{tool}' is not a recognized tool type.") + + return attributes + + +def get_response_tool_web_search_attributes(tool: "WebSearchTool", index: int) -> AttributeMap: + """Handles interpretation of an openai WebSearchTool object.""" + parameters = {} + if hasattr(tool, "search_context_size"): + parameters["search_context_size"] = tool.search_context_size + + if hasattr(tool, "user_location") and tool.user_location is not None: + parameters["user_location"] = tool.user_location.__dict__ + + tool_data = tool.__dict__ + if parameters: + # add parameters to the tool_data dict so we can format them with the other attributes + tool_data["parameters"] = parameters + + return _extract_attributes_from_mapping_with_index(tool_data, RESPONSE_TOOL_WEB_SEARCH_ATTRIBUTES, index) + + +def get_response_tool_file_search_attributes(tool: "FileSearchTool", index: int) -> AttributeMap: + """Handles interpretation of an openai FileSearchTool object.""" + parameters = {} + + if hasattr(tool, "vector_store_ids"): + parameters["vector_store_ids"] = tool.vector_store_ids + + if hasattr(tool, "filters") and tool.filters is not None: + parameters["filters"] = tool.filters.__dict__ + + if hasattr(tool, "max_num_results"): + parameters["max_num_results"] = tool.max_num_results + + if hasattr(tool, "ranking_options") and tool.ranking_options is not None: + parameters["ranking_options"] = tool.ranking_options.__dict__ + + tool_data = tool.__dict__ + if parameters: + # add parameters to the tool_data dict so we can format them with the other attributes + tool_data["parameters"] = parameters + + return _extract_attributes_from_mapping_with_index(tool_data, RESPONSE_TOOL_FILE_SEARCH_ATTRIBUTES, index) + + +def get_response_tool_computer_attributes(tool: "ComputerTool", index: int) -> AttributeMap: + """Handles interpretation of an openai ComputerTool object.""" + parameters = {} + + if hasattr(tool, "display_height"): + parameters["display_height"] = tool.display_height + + if hasattr(tool, "display_width"): + parameters["display_width"] = tool.display_width + + if hasattr(tool, "environment"): + parameters["environment"] = tool.environment + + tool_data = tool.__dict__ + if parameters: + # add parameters to the tool_data dict so we can format them with the other attributes + tool_data["parameters"] = parameters + + return _extract_attributes_from_mapping_with_index(tool_data, RESPONSE_TOOL_COMPUTER_ATTRIBUTES, index) + + +def get_response_usage_attributes(usage: "ResponseUsage") -> AttributeMap: + """Handles interpretation of an openai ResponseUsage object.""" + # ResponseUsage( + # input_tokens=0, + # output_tokens=0, + # output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + # total_tokens=0, + # input_tokens_details={'cached_tokens': 0} + # ) + attributes = {} + + attributes.update(_extract_attributes_from_mapping(usage.__dict__, RESPONSE_USAGE_ATTRIBUTES)) + + # input_tokens_details is an `InputTokensDetails` object or `dict` if it exists + if hasattr(usage, "input_tokens_details"): + input_details = usage.input_tokens_details + if input_details is None: + pass + + elif isinstance(input_details, InputTokensDetails): + attributes.update( + _extract_attributes_from_mapping(input_details.__dict__, RESPONSE_USAGE_DETAILS_ATTRIBUTES) + ) + + elif isinstance(input_details, dict): # openai-agents often returns a dict for some reason. + attributes.update(_extract_attributes_from_mapping(input_details, RESPONSE_USAGE_DETAILS_ATTRIBUTES)) + + else: + logger.debug( + f"[agentops.instrumentation.openai.response] '{input_details}' is not a recognized input details type." + ) + + # output_tokens_details is an `OutputTokensDetails` object + output_details = usage.output_tokens_details + if output_details is None: + pass + + elif isinstance(output_details, OutputTokensDetails): + attributes.update(_extract_attributes_from_mapping(output_details.__dict__, RESPONSE_USAGE_DETAILS_ATTRIBUTES)) + + else: + logger.debug( + f"[agentops.instrumentation.openai.response] '{output_details}' is not a recognized output details type." + ) + + return attributes diff --git a/tests/unit/instrumentation/providers/openai/test_response_issue_1285.py b/tests/unit/instrumentation/providers/openai/test_response_issue_1285.py new file mode 100644 index 000000000..b4b514649 --- /dev/null +++ b/tests/unit/instrumentation/providers/openai/test_response_issue_1285.py @@ -0,0 +1,94 @@ +""" +Regression test for issue #1285: +'NoneType' object has no attribute '__dict__' in get_response_tool_web/file_search_attributes + +When OpenAI Agents SDK tools have optional fields (user_location, filters, ranking_options) +set to None, hasattr() returns True but .__dict__ access crashes. +""" + +import pytest +from unittest.mock import MagicMock + +from agentops.instrumentation.providers.openai.attributes.response import ( + get_response_tool_web_search_attributes, + get_response_tool_file_search_attributes, +) + + +class MockUserLocation: + """Mock user_location object with a __dict__ attribute.""" + def __init__(self): + self.type = "approximate" + self.country = "US" + + +class MockRankingOptions: + """Mock ranking_options object with a __dict__ attribute.""" + def __init__(self): + self.score_threshold = 0.5 + + +class MockFilters: + """Mock filters object with a __dict__ attribute.""" + def __init__(self): + self.type = "and" + self.filters = [] + + +class TestIssue1285: + """Test that None optional fields don't crash attribute extraction.""" + + def test_web_search_tool_with_none_user_location(self): + """user_location=None should not cause AttributeError.""" + tool = MagicMock() + tool.search_context_size = 1024 + tool.user_location = None # This is the bug: hasattr returns True, but .__dict__ crashes + + # Should not raise AttributeError + result = get_response_tool_web_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_web_search_tool_with_valid_user_location(self): + """user_location with a real object should still work.""" + tool = MagicMock() + tool.search_context_size = 1024 + tool.user_location = MockUserLocation() + tool.user_location.__dict__ = {"type": "approximate", "country": "US"} + + result = get_response_tool_web_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_file_search_tool_with_none_filters(self): + """filters=None should not cause AttributeError.""" + tool = MagicMock() + tool.vector_store_ids = ["vs_123"] + tool.filters = None # Bug: hasattr returns True, .__dict__ crashes + tool.max_num_results = 5 + tool.ranking_options = None # Bug: same issue + + result = get_response_tool_file_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_file_search_tool_with_none_ranking_options(self): + """ranking_options=None should not cause AttributeError.""" + tool = MagicMock() + tool.vector_store_ids = ["vs_123"] + tool.filters = MockFilters() + tool.max_num_results = 5 + tool.ranking_options = None + + result = get_response_tool_file_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_file_search_tool_with_valid_fields(self): + """All valid fields should still work correctly.""" + tool = MagicMock() + tool.vector_store_ids = ["vs_123"] + tool.filters = MockFilters() + tool.filters.__dict__ = {"type": "and", "filters": []} + tool.max_num_results = 5 + tool.ranking_options = MockRankingOptions() + tool.ranking_options.__dict__ = {"score_threshold": 0.5} + + result = get_response_tool_file_search_attributes(tool, index=0) + assert isinstance(result, dict) pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy