diff --git a/.github/skills/migrate-from-openinference/SKILL.md b/.github/skills/migrate-from-openinference/SKILL.md index 9703a0bed..a015e82d7 100644 --- a/.github/skills/migrate-from-openinference/SKILL.md +++ b/.github/skills/migrate-from-openinference/SKILL.md @@ -338,9 +338,9 @@ Drop every match. The mappings: `EmbeddingInvocation`, `ToolInvocation`, `WorkflowInvocation`, `AgentInvocation`, `Error`, `GenAIInvocation` - `opentelemetry.util.genai.types` — `InputMessage`, `OutputMessage`, - `Text`, `ToolCallRequest`, `ToolCallResponse`, `Reasoning`, - `ServerToolCall`, `ServerToolCallResponse`, `GenericPart`, `Blob`, - `File`, `Uri`, `Modality` + `TextPart`, `ToolCallRequestPart`, `ToolCallResponsePart`, `ReasoningPart`, + `ServerToolCallPart`, `ServerToolCallResponsePart`, `GenericPart`, `BlobPart`, + `FilePart`, `UriPart`, `Modality` - `opentelemetry.util.genai.completion_hook` - `opentelemetry.util.genai.environment_variables` @@ -401,15 +401,15 @@ right (all types from `opentelemetry.util.genai.types` unless noted): | Source request item | OTel construct | |---|---| -| User / assistant / system text message | `Input/OutputMessage(role=…, parts=[Text(content=…)])` | -| Assistant message containing a tool/function call | `Message(role="assistant", parts=[ToolCallRequest(name=…, id=…, arguments=…)])` | -| Tool/function result message | `Message(role="tool", parts=[ToolCallResponse(id=…, response=…)])` | -| Reasoning / thinking item | `Message(role="assistant", parts=[Reasoning(content=…)])` | -| Server-side tool call (web_search, file_search, code_interpreter, …) | `Message(parts=[ServerToolCall(name=…, server_tool_call=…, id=…)])` | -| Server-side tool call result | `Message(parts=[ServerToolCallResponse(server_tool_call_response=…, id=…)])` | -| Inline image / audio / video bytes | `Blob(mime_type=…, modality="image"\|"audio"\|"video", content=b"…")` | -| External media URL | `Uri(mime_type=…, modality=…, uri="…")` | -| File reference (e.g. OpenAI `file_id`) | `File(mime_type=…, modality=…, file_id="file-…")` | +| User / assistant / system text message | `Input/OutputMessage(role=…, parts=[TextPart(content=…)])` | +| Assistant message containing a tool/function call | `Message(role="assistant", parts=[ToolCallRequestPart(name=…, id=…, arguments=…)])` | +| Tool/function result message | `Message(role="tool", parts=[ToolCallResponsePart(id=…, response=…)])` | +| Reasoning / thinking item | `Message(role="assistant", parts=[ReasoningPart(content=…)])` | +| Server-side tool call (web_search, file_search, code_interpreter, …) | `Message(parts=[ServerToolCallPart(name=…, server_tool_call=…, id=…)])` | +| Server-side tool call result | `Message(parts=[ServerToolCallResponsePart(server_tool_call_response=…, id=…)])` | +| Inline image / audio / video bytes | `BlobPart(mime_type=…, modality="image"\|"audio"\|"video", content=b"…")` | +| External media URL | `UriPart(mime_type=…, modality=…, uri="…")` | +| File reference (e.g. OpenAI `file_id`) | `FilePart(mime_type=…, modality=…, file_id="file-…")` | | Provider-specific item with no semconv mapping | `GenericPart(value=…)` — never silently drop. Flag those in the review report. | Output messages mirror the input mapping — `OutputMessage` serializes with diff --git a/.github/skills/write-conformance-tests/SKILL.md b/.github/skills/write-conformance-tests/SKILL.md index 1c8360d7b..a6317daba 100644 --- a/.github/skills/write-conformance-tests/SKILL.md +++ b/.github/skills/write-conformance-tests/SKILL.md @@ -58,13 +58,13 @@ instruments: walk its wrappers (the step-6 mapping for a port) for which | Part `type` | util-genai type | Emitted when the library accepts… | |---|---|---| -| `text` | `Text` | plain text (always) | -| `tool_call` / `tool_call_response` | `ToolCallRequest` / `ToolCallResponse` | function/tool calling — covered by `tool_calling.py` | -| `server_tool_call` / `server_tool_call_response` | `ServerToolCall` / `ServerToolCallResponse` | vendor server-side tools (web_search, code_interpreter, …) | -| `reasoning` | `Reasoning` | reasoning / thinking items | -| `blob` | `Blob` | inline image/audio/video **bytes** (`modality` distinguishes them) | -| `uri` | `Uri` | an external media **URL** (`modality`) | -| `file` | `File` | a **file reference** / id (`modality`) | +| `text` | `TextPart` | plain text (always) | +| `tool_call` / `tool_call_response` | `ToolCallRequestPart` / `ToolCallResponsePart` | function/tool calling — covered by `tool_calling.py` | +| `server_tool_call` / `server_tool_call_response` | `ServerToolCallPart` / `ServerToolCallResponsePart` | vendor server-side tools (web_search, code_interpreter, …) | +| `reasoning` | `ReasoningPart` | reasoning / thinking items | +| `blob` | `BlobPart` | inline image/audio/video **bytes** (`modality` distinguishes them) | +| `uri` | `UriPart` | an external media **URL** (`modality`) | +| `file` | `FilePart` | a **file reference** / id (`modality`) | | `generic` | `GenericPart` | a provider item with no semconv mapping — flag, don't drop | Group by shared turn/cassette — typically one `multimodal.py` for the diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml b/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml index 059e13834..555ae7c71 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "opentelemetry-api ~= 1.43", "opentelemetry-instrumentation >= 0.64b0, <1", "opentelemetry-semantic-conventions >= 0.64b0, <1", - "opentelemetry-util-genai >= 1.0b0, <2", + "opentelemetry-util-genai >= 1.1b0.dev, <2", ] [project.optional-dependencies] diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py b/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py index 1c89fa155..d5a02657c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py @@ -24,7 +24,7 @@ from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, - Text, + TextPart, ) logger = logging.getLogger(__name__) @@ -151,7 +151,9 @@ def _set_invocation_input( if input_val is not None: content_str = _extract_input_content(input_val) invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content=content_str)]) + InputMessage( + role="user", parts=[TextPart(content=content_str)] + ) ] @@ -165,7 +167,7 @@ def _set_invocation_output( invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content=output_str)], + parts=[TextPart(content=output_str)], finish_reason="stop", ) ] diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py index e405d262d..b4f9b3c23 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py @@ -24,12 +24,12 @@ ) from opentelemetry.util.genai.types import ( - Blob, + BlobPart, MessagePart, - Reasoning, - Text, - ToolCallRequest, - ToolCallResponse, + ReasoningPart, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ) if TYPE_CHECKING: @@ -99,8 +99,8 @@ def _decode_base64(data: str) -> bytes | None: return None -def _extract_base64_blob(source: object, modality: str) -> Blob | None: - """Extract a Blob from a base64-encoded source dict.""" +def _extract_base64_blob(source: object, modality: str) -> BlobPart | None: + """Extract a BlobPart from a base64-encoded source dict.""" if not isinstance(source, dict): return None # source is a TypedDict (e.g. Base64ImageSourceParam) narrowed to dict; @@ -112,7 +112,7 @@ def _extract_base64_blob(source: object, modality: str) -> Blob | None: if decoded is None: return None media_type: object = source.get("media_type") # type: ignore[reportUnknownMemberType] - return Blob( + return BlobPart( mime_type=media_type if isinstance(media_type, str) else None, modality=modality, content=decoded, @@ -127,25 +127,27 @@ def _convert_dict_block_to_part( if block_type == "text": text = block.get("text") - return Text(content=str(text) if text is not None else "") + return TextPart(content=str(text) if text is not None else "") if block_type == "tool_use": inp = block.get("input") - return ToolCallRequest( + return ToolCallRequestPart( arguments=inp if isinstance(inp, dict) else None, name=str(block.get("name", "")), id=str(block.get("id", "")), ) if block_type == "tool_result": - return ToolCallResponse( + return ToolCallResponsePart( response=block.get("content"), id=str(block.get("tool_use_id", "")), ) if block_type in ("thinking", "redacted_thinking"): thinking = block.get("thinking") or block.get("data") - return Reasoning(content=str(thinking) if thinking is not None else "") + return ReasoningPart( + content=str(thinking) if thinking is not None else "" + ) if block_type in ("image", "audio", "video", "document", "file"): return _extract_base64_blob(block.get("source"), str(block_type)) @@ -158,10 +160,10 @@ def _convert_content_block_to_part( ) -> MessagePart | None: """Convert an Anthropic content block to a MessagePart.""" if isinstance(block, TextBlock): - return Text(content=block.text) + return TextPart(content=block.text) if isinstance(block, (ToolUseBlock, ServerToolUseBlock)): - return ToolCallRequest( + return ToolCallRequestPart( arguments=block.input, name=block.name, id=block.id ) @@ -169,10 +171,10 @@ def _convert_content_block_to_part( content = ( block.thinking if isinstance(block, ThinkingBlock) else block.data ) - return Reasoning(content=content) + return ReasoningPart(content=content) if isinstance(block, WebSearchToolResultBlock): - return ToolCallResponse( + return ToolCallResponsePart( response=block.model_dump().get("content"), id=block.tool_use_id, ) @@ -188,7 +190,7 @@ def convert_content_to_parts( if content is None: return [] if isinstance(content, str): - return [Text(content=content)] + return [TextPart(content=content)] parts: list[MessagePart] = [] for item in content: part = _convert_content_block_to_part(item) @@ -236,7 +238,7 @@ def update_stream_block_state( def stream_block_state_to_part(state: StreamBlockState) -> MessagePart | None: if state.type == "text": - return Text(content=state.text) + return TextPart(content=state.text) if state.type == "tool_use": arguments: str | dict[str, object] | None = state.tool_input @@ -245,13 +247,13 @@ def stream_block_state_to_part(state: StreamBlockState) -> MessagePart | None: arguments = json.loads(state.input_json) except ValueError: arguments = state.input_json - return ToolCallRequest( + return ToolCallRequestPart( arguments=arguments, name=state.tool_name, id=state.tool_id, ) if state.type in ("thinking", "redacted_thinking"): - return Reasoning(content=state.thinking) + return ReasoningPart(content=state.thinking) return None diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt index 8880e70bf..e2724ab2a 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt @@ -20,5 +20,4 @@ # factor, so they are NOT pinned here — pyproject.toml is the single source of truth. The # OpenTelemetry SDK and test utilities come transitively from opentelemetry-test-util-genai, which # every oldest env installs. Pin here only test-only deps that nothing else already provides. -# - +# \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py index c41f3a386..b205c0f6d 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py @@ -42,8 +42,8 @@ from opentelemetry.util.genai.types import ( MessagePart, OutputMessage, - Text, - ToolCallRequest, + TextPart, + ToolCallRequestPart, ) SUPPORTED_RAPI_RESPONSE_HEADERS = ("x-ms-served-model",) @@ -374,9 +374,9 @@ def on_llm_end( ) if finish_reason in ("tool_calls", "tool_use"): - tool_calls: list[ToolCallRequest] = [] + tool_calls: list[ToolCallRequestPart] = [] for tool_call in chat_generation.message.tool_calls: - tool_call_request = ToolCallRequest( + tool_call_request = ToolCallRequestPart( name=tool_call["name"], id=tool_call["id"], arguments=tool_call["args"], @@ -402,7 +402,7 @@ def on_llm_end( ) else: parts = [ - Text( + TextPart( content=chat_generation.message.content, type="text", ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py index ef726f1ac..9a2ff5e7b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py @@ -23,10 +23,10 @@ InputMessage, MessagePart, OutputMessage, - Reasoning, - Text, - ToolCallRequest, - ToolCallResponse, + ReasoningPart, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ToolDefinition, ) @@ -81,26 +81,26 @@ def _content_to_parts( """Convert a LangChain message ``content`` payload into ``MessagePart`` s. Content may be a plain string or a list of provider-specific block dicts - (e.g. Anthropic structured content). We extract :class:`Text` and - :class:`Reasoning` parts; ``tool_use`` blocks are intentionally ignored + (e.g. Anthropic structured content). We extract :class:`TextPart` and + :class:`ReasoningPart` parts; ``tool_use`` blocks are intentionally ignored here because LangChain consolidates them into ``message.tool_calls`` which is read separately. """ parts: list[MessagePart] = [] if isinstance(content, str): if content: - parts.append(Text(content=content)) + parts.append(TextPart(content=content)) return parts for item in content: if isinstance(item, str): if item: - parts.append(Text(content=item)) + parts.append(TextPart(content=item)) continue block_type = item.get("type") if block_type == "text": text_value = item.get("text") if isinstance(text_value, str) and text_value: - parts.append(Text(content=text_value)) + parts.append(TextPart(content=text_value)) elif block_type in ("thinking", "reasoning"): reasoning_value = ( item.get("thinking") @@ -108,14 +108,14 @@ def _content_to_parts( or item.get("text") ) if isinstance(reasoning_value, str) and reasoning_value: - parts.append(Reasoning(content=reasoning_value)) + parts.append(ReasoningPart(content=reasoning_value)) return parts def _legacy_function_call_request( message: AIMessage, -) -> ToolCallRequest | None: - """Extract a legacy OpenAI ``function_call`` as a :class:`ToolCallRequest`. +) -> ToolCallRequestPart | None: + """Extract a legacy OpenAI ``function_call`` as a :class:`ToolCallRequestPart`. Pre-tools OpenAI models return a single call under ``additional_kwargs['function_call']`` (``{"name", "arguments"}``) rather @@ -136,14 +136,14 @@ def _legacy_function_call_request( arguments = json.loads(raw_arguments) except (json.JSONDecodeError, ValueError): arguments = raw_arguments - return ToolCallRequest(arguments=arguments, name=name, id=None) + return ToolCallRequestPart(arguments=arguments, name=name, id=None) def _ai_message_parts(message: AIMessage) -> list[MessagePart]: """Build :class:`MessagePart` s for an :class:`AIMessage`. Includes any text/reasoning content followed by a - :class:`ToolCallRequest` for each entry in ``message.tool_calls``, plus a + :class:`ToolCallRequestPart` for each entry in ``message.tool_calls``, plus a legacy ``additional_kwargs['function_call']`` when present. """ parts: list[MessagePart] = _content_to_parts(message.content) @@ -152,7 +152,7 @@ def _ai_message_parts(message: AIMessage) -> list[MessagePart]: if not name: continue parts.append( - ToolCallRequest( + ToolCallRequestPart( arguments=call["args"], name=name, id=call["id"], @@ -169,7 +169,7 @@ def _tool_message_parts(message: ToolMessage) -> list[MessagePart]: """Build :class:`MessagePart` s for a :class:`ToolMessage` (tool result).""" tool_call_id = getattr(message, "tool_call_id", None) return [ - ToolCallResponse( + ToolCallResponsePart( response=message.content, id=tool_call_id if isinstance(tool_call_id, str) else None, ) @@ -294,7 +294,7 @@ def make_input_message(data: Any) -> list[InputMessage]: When no ``messages`` key exists (common in LangGraph state dicts), the remaining state fields are serialized as JSON and emitted as a single - user-role :class:`Text` part. + user-role :class:`TextPart` part. """ if not isinstance(data, dict): return [] @@ -318,7 +318,7 @@ def make_input_message(data: Any) -> list[InputMessage]: if input_data: serialized = serialize(input_data) if serialized: - return [InputMessage(role="user", parts=[Text(serialized)])] + return [InputMessage(role="user", parts=[TextPart(serialized)])] return [] diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py index 1632c21f3..8f753666b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py @@ -38,8 +38,8 @@ from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, - Text, - ToolCallRequest, + TextPart, + ToolCallRequestPart, ) # --------------------------------------------------------------------------- @@ -645,7 +645,7 @@ def test_messages_key_with_human_message(self): assert isinstance(result[0], InputMessage) assert result[0].role == "user" assert len(result[0].parts) == 1 - assert isinstance(result[0].parts[0], Text) + assert isinstance(result[0].parts[0], TextPart) assert result[0].parts[0].content == "Hello" def test_messages_key_skips_empty_content(self): @@ -1168,7 +1168,7 @@ def _make_handler_with_llm_invocation( class TestOnLlmEndToolCalls: def test_openai_tool_calls_finish_reason_produces_tool_call_request(self): - """finish_reason='tool_calls' (OpenAI) must produce ToolCallRequest parts.""" + """finish_reason='tool_calls' (OpenAI) must produce ToolCallRequestPart parts.""" run_id = _run_id() handler, _, llm_inv = _make_handler_with_llm_invocation(run_id) @@ -1192,13 +1192,13 @@ def test_openai_tool_calls_finish_reason_produces_tool_call_request(self): assert assigned[0].finish_reason == "tool_calls" assert len(assigned[0].parts) == 1 part = assigned[0].parts[0] - assert isinstance(part, ToolCallRequest) + assert isinstance(part, ToolCallRequestPart) assert part.name == "get_weather" assert part.id == "call_123" assert part.arguments == {"location": "Paris"} def test_bedrock_tool_use_finish_reason_produces_tool_call_request(self): - """finish_reason='tool_use' (Bedrock/Anthropic) must produce ToolCallRequest parts.""" + """finish_reason='tool_use' (Bedrock/Anthropic) must produce ToolCallRequestPart parts.""" run_id = _run_id() handler, _, llm_inv = _make_handler_with_llm_invocation(run_id) @@ -1223,7 +1223,7 @@ def test_bedrock_tool_use_finish_reason_produces_tool_call_request(self): assert assigned[0].finish_reason == "tool_use" assert len(assigned[0].parts) == 1 part = assigned[0].parts[0] - assert isinstance(part, ToolCallRequest) + assert isinstance(part, ToolCallRequestPart) assert part.name == "get_weather" assert part.id == "tooluse_abc" assert part.arguments == {"location": "London"} @@ -1561,7 +1561,7 @@ def test_extract_token_details_no_details_key(): def test_legacy_function_call_finish_reason_produces_tool_call_request( self, ): - """Pre-tools OpenAI ``function_call`` must surface as a ToolCallRequest.""" + """Pre-tools OpenAI ``function_call`` must surface as a ToolCallRequestPart.""" run_id = _run_id() handler, _, llm_inv = _make_handler_with_llm_invocation(run_id) @@ -1586,7 +1586,7 @@ def test_legacy_function_call_finish_reason_produces_tool_call_request( assert len(assigned) == 1 assert len(assigned[0].parts) == 1 part = assigned[0].parts[0] - assert isinstance(part, ToolCallRequest) + assert isinstance(part, ToolCallRequestPart) assert part.name == "get_weather" assert part.arguments == {"city": "Paris"} @@ -1607,7 +1607,7 @@ def test_legacy_function_call_dict_arguments(): }, ) call = _legacy_function_call_request(message) - assert isinstance(call, ToolCallRequest) + assert isinstance(call, ToolCallRequestPart) assert call.name == "get_weather" assert call.arguments == {"city": "New York"} @@ -1623,7 +1623,7 @@ def test_legacy_function_call_string_arguments_parsed(): }, ) call = _legacy_function_call_request(message) - assert isinstance(call, ToolCallRequest) + assert isinstance(call, ToolCallRequestPart) assert call.arguments == {"city": "New York"} @@ -1641,7 +1641,7 @@ def test_to_input_messages_includes_legacy_function_call(): messages = to_input_messages([message]) assert len(messages) == 1 assert any( - isinstance(p, ToolCallRequest) and p.name == "f" + isinstance(p, ToolCallRequestPart) and p.name == "f" for p in messages[0].parts ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py index 31954d0c7..872977389 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py @@ -19,8 +19,8 @@ ) from opentelemetry.util.genai.types import ( OutputMessage, - Text, - ToolCallRequest, + TextPart, + ToolCallRequestPart, ) from .chat_buffers import ChoiceBuffer @@ -123,7 +123,7 @@ def _set_output_messages(self) -> None: ) if choice.text_content: message.parts.append( - Text(content="".join(choice.text_content)) + TextPart(content="".join(choice.text_content)) ) if choice.tool_calls_buffers: tool_calls = [] @@ -135,7 +135,7 @@ def _set_output_messages(self) -> None: arguments = json.loads(arguments_str) except json.JSONDecodeError: arguments = arguments_str - tool_call_part = ToolCallRequest( + tool_call_part = ToolCallRequestPart( name=tool_call.function_name, id=tool_call.tool_call_id, arguments=arguments, diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py index 7a495b4dd..4fb5b870b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py @@ -31,7 +31,7 @@ Error, InputMessage, OutputMessage, - Text, + TextPart, ToolDefinition, ) @@ -67,11 +67,11 @@ GenericToolDefinition, InputMessage, OutputMessage, - Reasoning, - Text, + ReasoningPart, + TextPart, ) from opentelemetry.util.genai.types import ( - ToolCallRequest as ToolCall, + ToolCallRequestPart as ToolCall, ) except ImportError: Error = None @@ -79,8 +79,8 @@ GenericToolDefinition = None InputMessage = None OutputMessage = None - Reasoning = None - Text = None + ReasoningPart = None + TextPart = None ToolCall = None @@ -180,20 +180,22 @@ def extract_params( ) -def get_system_instruction(instructions: str | None) -> list[Text]: - if Text is None or instructions is None: +def get_system_instruction(instructions: str | None) -> list[TextPart]: + if TextPart is None or instructions is None: return [] - return [Text(content=instructions)] + return [TextPart(content=instructions)] def get_input_messages( input_value: str | Sequence[object] | None, ) -> list[InputMessage]: - if InputMessage is None or Text is None: + if InputMessage is None or TextPart is None: return [] if isinstance(input_value, str): - return [InputMessage(role="user", parts=[Text(content=input_value)])] + return [ + InputMessage(role="user", parts=[TextPart(content=input_value)]) + ] messages: list[InputMessage] = [] for item in _get_sequence(input_value): @@ -204,7 +206,7 @@ def get_input_messages( content = _get_field(item, "content") if isinstance(content, str): messages.append( - InputMessage(role=role, parts=[Text(content=content)]) + InputMessage(role=role, parts=[TextPart(content=content)]) ) continue @@ -212,27 +214,27 @@ def get_input_messages( for part in _get_sequence(content): text = _get_field(part, "text") if isinstance(text, str): - parts.append(Text(content=text)) + parts.append(TextPart(content=text)) if parts: messages.append(InputMessage(role=role, parts=parts)) return messages -def _extract_output_parts(content_blocks: Sequence[object]) -> list[Text]: +def _extract_output_parts(content_blocks: Sequence[object]) -> list[TextPart]: if ( - Text is None + TextPart is None or ResponseOutputText is None or ResponseOutputRefusal is None ): return [] - parts: list[Text] = [] + parts: list[TextPart] = [] for block in content_blocks: if isinstance(block, ResponseOutputText): - parts.append(Text(content=block.text)) + parts.append(TextPart(content=block.text)) elif isinstance(block, ResponseOutputRefusal): - parts.append(Text(content=block.refusal)) + parts.append(TextPart(content=block.refusal)) return parts @@ -248,19 +250,19 @@ def _parse_tool_call_arguments(arguments: str | None) -> object: def _extract_reasoning_parts( item: ResponseReasoningItem, -) -> list[Reasoning]: - if Reasoning is None: +) -> list[ReasoningPart]: + if ReasoningPart is None: return [] - parts: list[Reasoning] = [] + parts: list[ReasoningPart] = [] for block in item.summary: if isinstance(block.text, str): - parts.append(Reasoning(content=block.text)) + parts.append(ReasoningPart(content=block.text)) for block in item.content or []: if getattr(block, "type", None) == "reasoning_text" and isinstance( getattr(block, "text", None), str ): - parts.append(Reasoning(content=block.text)) + parts.append(ReasoningPart(content=block.text)) return parts @@ -352,7 +354,7 @@ def get_output_messages_from_response( not _response_types_available() or not isinstance(response, Response) or OutputMessage is None - or Text is None + or TextPart is None ): return [] diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py index 76b3ae603..79997fd0c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py @@ -25,9 +25,9 @@ FunctionToolDefinition, InputMessage, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ToolDefinition, ) @@ -205,22 +205,22 @@ def _prepare_input_messages(messages) -> list[InputMessage]: if tool_calls: chat_message.parts += extract_tool_calls_new(tool_calls) if _is_text_part(content): - chat_message.parts.append(Text(content=str(content))) + chat_message.parts.append(TextPart(content=str(content))) elif role == "tool": tool_call_id = get_property_value(message, "tool_call_id") chat_message.parts.append( - ToolCallResponse(id=tool_call_id, response=content) + ToolCallResponsePart(id=tool_call_id, response=content) ) else: # system, developer, user, fallback if _is_text_part(content): - chat_message.parts.append(Text(content=str(content))) + chat_message.parts.append(TextPart(content=str(content))) return chat_messages -def extract_tool_calls_new(tool_calls) -> list[ToolCallRequest]: +def extract_tool_calls_new(tool_calls) -> list[ToolCallRequestPart]: parts = [] for tool_call in tool_calls: call_id = get_property_value(tool_call, "id") @@ -239,7 +239,9 @@ def extract_tool_calls_new(tool_calls) -> list[ToolCallRequest]: # TODO: support custom parts.append( - ToolCallRequest(id=call_id, name=func_name, arguments=arguments) + ToolCallRequestPart( + id=call_id, name=func_name, arguments=arguments + ) ) return parts @@ -274,7 +276,7 @@ def _prepare_output_messages(choices) -> list[OutputMessage]: parts += extract_tool_calls_new(tool_calls) content = get_property_value(choice.message, "content") if _is_text_part(content): - parts.append(Text(content=str(content))) + parts.append(TextPart(content=str(content))) message = OutputMessage( finish_reason=choice.finish_reason or "error", diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py index 9faebeeda..3549fc7b1 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py @@ -323,7 +323,7 @@ def test_extract_output_type_handles_text_format_mapping(loaded_module): def test_extractors_handle_missing_genai_types_import(loaded_module): with ( - mock.patch.object(loaded_module, "Text", None), + mock.patch.object(loaded_module, "TextPart", None), mock.patch.object(loaded_module, "InputMessage", None), mock.patch.object(loaded_module, "OutputMessage", None), ): diff --git a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py index aad938d3a..6936e8071 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py @@ -16,9 +16,9 @@ InputMessage, MessagePart, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ) if TYPE_CHECKING: @@ -116,7 +116,7 @@ def find_tool_call_id( return None -def _function_call_part(function_call: Any) -> ToolCallRequest: +def _function_call_part(function_call: Any) -> ToolCallRequestPart: name = _field_value(function_call, "name") or "" arguments = _field_value(function_call, "arguments") or "{}" if isinstance(arguments, str): @@ -124,7 +124,7 @@ def _function_call_part(function_call: Any) -> ToolCallRequest: arguments = json.loads(arguments) except (json.JSONDecodeError, ValueError): pass - return ToolCallRequest(name=name, arguments=arguments, id=None) + return ToolCallRequestPart(name=name, arguments=arguments, id=None) def _tool_call_response_id(msg: Any) -> str: @@ -165,7 +165,7 @@ def convert_to_input_messages( # API converts it to role="tool"; handle both. if role in ("function", "tool") and content: parts.append( - ToolCallResponse( + ToolCallResponsePart( id=_tool_call_response_id(msg), response=_extract_content_text(content), ) @@ -173,7 +173,7 @@ def convert_to_input_messages( elif content: text = _extract_content_text(content) if text: - parts.append(Text(content=text)) + parts.append(TextPart(content=text)) if parts: input_messages.append(InputMessage(role=role, parts=parts)) @@ -210,10 +210,10 @@ def convert_to_output_messages( if content: text = _extract_content_text(content) if text: - parts.append(Text(content=text)) + parts.append(TextPart(content=text)) if not parts: - parts.append(Text(content="")) + parts.append(TextPart(content="")) output_messages.append( OutputMessage( @@ -293,6 +293,6 @@ def create_agent_invocation( # qwen-agent prepends to the LLM messages on every run. system_message = getattr(agent_instance, "system_message", None) if system_message: - invocation.system_instruction = [Text(content=system_message)] + invocation.system_instruction = [TextPart(content=system_message)] return invocation diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py index 17c93cbe8..b5d9e5607 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py @@ -99,11 +99,11 @@ class Stream: GenericToolDefinition, InputMessage, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ToolDefinition, - Uri, + UriPart, ) @@ -174,7 +174,9 @@ def _interactions_input_to_messages( if input_data is None: return [] if isinstance(input_data, str): - return [InputMessage(role="user", parts=[Text(content=input_data)])] + return [ + InputMessage(role="user", parts=[TextPart(content=input_data)]) + ] if not isinstance(input_data, Sequence): input_data = [input_data] @@ -186,22 +188,22 @@ def _interactions_input_to_messages( call_id = _get_field(item, "id") name = _get_field(item, "name") arguments = _get_field(item, "arguments") - part = ToolCallRequest( + part = ToolCallRequestPart( id=call_id, name=name or "", arguments=arguments ) parts.append(part) elif item_type == "function_result": call_id = _get_field(item, "call_id") result = _get_field(item, "result") - part = ToolCallResponse(id=call_id, response=result) + part = ToolCallResponsePart(id=call_id, response=result) parts.append(part) elif isinstance(item, str): - parts.append(Text(content=item)) + parts.append(TextPart(content=item)) elif item_type == "text": - part = Text(content=_get_field(item, "text") or "") + part = TextPart(content=_get_field(item, "text") or "") parts.append(part) elif item_type == "document": - part = Uri( + part = UriPart( mime_type=_get_field(item, "mime_type"), modality="document", uri=_get_field(item, "uri") or "", @@ -244,7 +246,7 @@ def _interactions_response_to_messages( return [ OutputMessage( role="assistant", - parts=[Text(content=output_text)], + parts=[TextPart(content=output_text)], finish_reason="stop", ) ] @@ -406,7 +408,9 @@ def _start_interactions_invocation( kwargs.get("input") ) if system_instruction := kwargs.get("system_instruction"): - invocation.system_instruction = [Text(content=system_instruction)] + invocation.system_instruction = [ + TextPart(content=system_instruction) + ] return invocation diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py index 865215474..001b466db 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py @@ -9,15 +9,15 @@ from google.genai import types as genai_types from opentelemetry.util.genai.types import ( - Blob, + BlobPart, FinishReason, InputMessage, MessagePart, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, - Uri, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, + UriPart, ) @@ -91,12 +91,12 @@ def tool_call_id(name: str | None) -> str: return f"{idx}" if (text := part.text) is not None: - return Text(content=text) + return TextPart(content=text) if inline_data := part.inline_data: mime_type = inline_data.mime_type or "" modality = mime_type.split("/")[0] if mime_type else "" - return Blob( + return BlobPart( mime_type=mime_type, modality=modality, content=inline_data.data or b"", @@ -105,21 +105,21 @@ def tool_call_id(name: str | None) -> str: if file_data := part.file_data: mime_type = file_data.mime_type or "" modality = mime_type.split("/")[0] if mime_type else "" - return Uri( + return UriPart( mime_type=mime_type, modality=modality, uri=file_data.file_uri or "", ) if call := part.function_call: - return ToolCallRequest( + return ToolCallRequestPart( id=call.id or tool_call_id(call.name), name=call.name or "", arguments=call.args, ) if response := part.function_response: - return ToolCallResponse( + return ToolCallResponsePart( id=response.id or tool_call_id(response.name), response=response.response, ) diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py b/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py index 1625a2ea2..0587fc290 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py @@ -13,10 +13,10 @@ ) from opentelemetry.util.genai.types import ( GenericPart, - Text, - ToolCallRequest, - ToolCallResponse, - Uri, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, + UriPart, ) @@ -32,22 +32,22 @@ def test_input_to_messages_str(self) -> None: messages = _interactions_input_to_messages("Hello world") self.assertEqual(messages[0].role, "user") self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello world") def test_input_to_messages_list_of_strings(self) -> None: messages = _interactions_input_to_messages(["Hello", "world"]) self.assertEqual(len(messages[0].parts), 2) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello") - self.assertIsInstance(messages[0].parts[1], Text) + self.assertIsInstance(messages[0].parts[1], TextPart) self.assertEqual(messages[0].parts[1].content, "world") def test_input_to_messages_text_step(self) -> None: steps = [{"type": "text", "text": "Hello text step"}] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello text step") def test_input_to_messages_document_step(self) -> None: @@ -60,7 +60,7 @@ def test_input_to_messages_document_step(self) -> None: ] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Uri) + self.assertIsInstance(messages[0].parts[0], UriPart) self.assertEqual(messages[0].parts[0].mime_type, "application/pdf") self.assertEqual(messages[0].parts[0].modality, "document") self.assertEqual( @@ -78,7 +78,7 @@ def test_input_to_messages_tool_call_step(self) -> None: ] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], ToolCallRequest) + self.assertIsInstance(messages[0].parts[0], ToolCallRequestPart) self.assertEqual(messages[0].parts[0].id, "call-123") self.assertEqual(messages[0].parts[0].name, "calc") self.assertEqual(messages[0].parts[0].arguments, {"x": 5}) @@ -93,7 +93,7 @@ def test_input_to_messages_tool_result_step(self) -> None: ] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], ToolCallResponse) + self.assertIsInstance(messages[0].parts[0], ToolCallResponsePart) self.assertEqual(messages[0].parts[0].id, "call-123") self.assertEqual(messages[0].parts[0].response, {"val": 10}) @@ -109,7 +109,7 @@ def test_input_to_messages_single_non_sequence_step(self) -> None: step = {"type": "text", "text": "Hello single step"} messages = _interactions_input_to_messages(step) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello single step") def test_input_to_messages_none_type_fall_through(self) -> None: @@ -126,5 +126,5 @@ def test_response_to_messages(self) -> None: self.assertEqual(messages[0].role, "assistant") self.assertEqual(messages[0].finish_reason, "stop") self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Model response text")