diff --git a/.github/skills/migrate-from-openinference/SKILL.md b/.github/skills/migrate-from-openinference/SKILL.md index b99c98066..8043d5f30 100644 --- a/.github/skills/migrate-from-openinference/SKILL.md +++ b/.github/skills/migrate-from-openinference/SKILL.md @@ -337,9 +337,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` @@ -400,15 +400,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 a4b53d926..9fed56251 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-agno/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt index 8eecc5b0c..5358b2194 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt @@ -21,5 +21,5 @@ # 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. # -# There is currently nothing to pin: agno has no test-only dependency that isn't already -# provided by a declared bound or by the shared test fixtures. +# Drop this once opentelemetry-util-genai 1.1b0 is published. +-e util/opentelemetry-util-genai 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 1d58493b6..0a7e2f482 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: @@ -72,8 +72,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; @@ -85,7 +85,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, @@ -100,25 +100,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)) @@ -131,10 +133,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 ) @@ -142,10 +144,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, ) @@ -161,7 +163,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) @@ -209,7 +211,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 @@ -218,13 +220,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-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 14ceec1fb..fc50bf4c5 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 @@ -26,9 +26,9 @@ FunctionToolDefinition, InputMessage, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ToolDefinition, ) @@ -206,22 +206,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") @@ -240,7 +240,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 @@ -275,7 +277,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 a9adf3937..999a3ea76 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, ) _logger = logging.getLogger(__name__) @@ -104,7 +104,7 @@ def find_tool_call_id(messages: Any, tool_name: str) -> str | None: 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): @@ -112,7 +112,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: @@ -151,7 +151,7 @@ def convert_to_input_messages(messages: Any) -> list[InputMessage]: # 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), ) @@ -159,7 +159,7 @@ def convert_to_input_messages(messages: Any) -> list[InputMessage]: 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)) @@ -194,10 +194,10 @@ def convert_to_output_messages(messages: Any) -> list[OutputMessage]: 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( @@ -275,6 +275,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") diff --git a/util/opentelemetry-util-genai/.changelog/365.changed b/util/opentelemetry-util-genai/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/util/opentelemetry-util-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/util/opentelemetry-util-genai/README.rst b/util/opentelemetry-util-genai/README.rst index 0a9f8f971..850e6802d 100644 --- a/util/opentelemetry-util-genai/README.rst +++ b/util/opentelemetry-util-genai/README.rst @@ -10,7 +10,7 @@ Key Components -------------- - ``TelemetryHandler`` -- manages LLM invocation lifecycles (spans, metrics, events) -- ``InferenceInvocation`` and message types (``Text``, ``Reasoning``, ``Blob``, etc.) -- structured data model for GenAI interactions +- ``InferenceInvocation`` and message types (``TextPart``, ``ReasoningPart``, ``BlobPart``, etc.) -- structured data model for GenAI interactions - ``CompletionHook`` -- protocol for uploading content to external storage (built-in ``fsspec`` support) - Metrics -- ``gen_ai.client.operation.duration`` and ``gen_ai.client.token.usage`` histograms, plus the streaming timing histograms ``gen_ai.client.operation.time_to_first_chunk`` and diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py index b4f2a56c7..55e933348 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py @@ -33,7 +33,7 @@ def _any_value_to_attribute_value(value: AnyValue) -> AttributeValue | None: class ToolInvocation(GenAIInvocation): """Represents a tool call invocation for execute_tool span tracking. - Not used as a message part — use ToolCallRequest for that purpose. + Not used as a message part — use ToolCallRequestPart for that purpose. Use handler.tool(name) rather than constructing this directly. diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py index 3bde5f899..9900cf13d 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py @@ -83,7 +83,7 @@ def is_message_part_list_hashable( message_parts: list[types.MessagePart] | None, ) -> bool: return bool(message_parts) and all( - isinstance(x, types.Text) for x in message_parts + isinstance(x, types.TextPart) for x in message_parts ) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py index d3b751cbb..64e855ddc 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -50,7 +50,7 @@ class GenericPart: @dataclass() -class ToolCallRequest: +class ToolCallRequestPart: """Represents a tool call requested by the model (message part only). Use this for tool calls in message history. For execution tracking with spans @@ -67,7 +67,7 @@ class ToolCallRequest: @dataclass() -class ToolCallResponse: +class ToolCallResponsePart: """Represents a tool call result sent to the model or a built-in tool call outcome and details This model is specified as part of semconv in `GenAI messages Python models - ToolCallResponsePart @@ -80,7 +80,7 @@ class ToolCallResponse: @dataclass() -class ServerToolCall: +class ServerToolCallPart: """Represents a server-side tool call. Server tool calls are executed by the model provider on the server side rather @@ -98,7 +98,7 @@ class ServerToolCall: @dataclass() -class ServerToolCallResponse: +class ServerToolCallResponsePart: """Represents a server-side tool call response. Contains the outcome and details of a server tool execution. Provider-specific @@ -115,7 +115,7 @@ class ServerToolCallResponse: @dataclass() -class Text: +class TextPart: """Represents text content sent to or received from the model This model is specified as part of semconv in `GenAI messages Python models - TextPart @@ -127,7 +127,7 @@ class Text: @dataclass() -class Reasoning: +class ReasoningPart: """Represents reasoning/thinking content received from the model This model is specified as part of semconv in `GenAI messages Python models - ReasoningPart @@ -161,7 +161,7 @@ class CompactionPart: @dataclass() -class Blob: +class BlobPart: """Represents blob binary data sent inline to the model This model is specified as part of semconv in `GenAI messages Python models - BlobPart @@ -175,7 +175,7 @@ class Blob: @dataclass() -class File: +class FilePart: """Represents an external referenced file sent to the model by file id This model is specified as part of semconv in `GenAI messages Python models - FilePart @@ -189,7 +189,7 @@ class File: @dataclass() -class Uri: +class UriPart: """Represents an external referenced file sent to the model by URI This model is specified as part of semconv in `GenAI messages Python models - UriPart @@ -223,15 +223,15 @@ class GenericToolDefinition: ToolDefinition = Union[FunctionToolDefinition, GenericToolDefinition] MessagePart = Union[ - Text, - ToolCallRequest, - ToolCallResponse, - ServerToolCall, - ServerToolCallResponse, - Blob, - File, - Uri, - Reasoning, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, + ServerToolCallPart, + ServerToolCallResponsePart, + BlobPart, + FilePart, + UriPart, + ReasoningPart, CompactionPart, GenericPart, # For provider-specific types; prefer standard types above ] diff --git a/util/opentelemetry-util-genai/tests/test_handler_agent.py b/util/opentelemetry-util-genai/tests/test_handler_agent.py index 49b221e2c..42d882b39 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_agent.py +++ b/util/opentelemetry-util-genai/tests/test_handler_agent.py @@ -25,7 +25,7 @@ FunctionToolDefinition, InputMessage, OutputMessage, - Text, + TextPart, ) @@ -196,12 +196,12 @@ def test_default_values(self): def test_with_messages(self): invocation = self.handler.invoke_local_agent() invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="Hello")]) + InputMessage(role="user", parts=[TextPart(content="Hello")]) ] invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="Hi there!")], + parts=[TextPart(content="Hi there!")], finish_reason="stop", ) ] @@ -331,7 +331,7 @@ def setUp(self): def test_system_instruction_on_span(self, _mock_cap): invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ - Text(content="You are a helpful assistant."), + TextPart(content="You are a helpful assistant."), ] invocation.stop() @@ -362,12 +362,12 @@ def test_tool_definitions_on_span(self, _mock_cap): def test_messages_on_span(self, _mock_cap): invocation = self.handler.invoke_local_agent() invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="Hello")]) + InputMessage(role="user", parts=[TextPart(content="Hello")]) ] invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="Hi!")], + parts=[TextPart(content="Hi!")], finish_reason="stop", ) ] @@ -380,10 +380,10 @@ def test_messages_on_span(self, _mock_cap): def test_content_not_on_span_by_default(self): invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ - Text(content="You are a helpful assistant."), + TextPart(content="You are a helpful assistant."), ] invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="Hello")]) + InputMessage(role="user", parts=[TextPart(content="Hello")]) ] invocation.stop() diff --git a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py index bc4766c0c..dc276d56e 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py +++ b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py @@ -20,7 +20,7 @@ FunctionToolDefinition, InputMessage, OutputMessage, - Text, + TextPart, ) _CAPTURE_EVENT_ENV = { @@ -48,16 +48,16 @@ def test_hook_called_on_stop(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="hi")], + parts=[TextPart(content="hi")], finish_reason="stop", ) ] - system_instruction = [Text(content="be helpful")] + system_instruction = [TextPart(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", @@ -86,7 +86,7 @@ def test_hook_called_on_fail(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] invocation = handler.inference("openai", request_model="gpt-4o") @@ -158,12 +158,12 @@ def test_workflow_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="what is 2+2?")]) + InputMessage(role="user", parts=[TextPart(content="what is 2+2?")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="4")], + parts=[TextPart(content="4")], finish_reason="stop", ) ] @@ -189,7 +189,7 @@ def test_workflow_hook_called_on_fail(self): invocation = handler.workflow(name="my-workflow") invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] invocation.fail(RuntimeError("workflow failed")) @@ -213,16 +213,16 @@ def test_local_agent_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="what is 2+2?")]) + InputMessage(role="user", parts=[TextPart(content="what is 2+2?")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="4")], + parts=[TextPart(content="4")], finish_reason="stop", ) ] - system_instruction = [Text(content="be helpful")] + system_instruction = [TextPart(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", @@ -255,7 +255,7 @@ def test_local_agent_hook_called_on_fail(self): invocation = handler.invoke_local_agent(request_model="gpt-4") invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] invocation.fail(RuntimeError("agent failed")) @@ -268,12 +268,12 @@ def test_remote_agent_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="hi")]) + InputMessage(role="user", parts=[TextPart(content="hi")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="hello")], + parts=[TextPart(content="hello")], finish_reason="stop", ) ] diff --git a/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py b/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py index e94facb66..7878912d3 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py +++ b/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py @@ -32,7 +32,7 @@ Error, FunctionToolDefinition, OutputMessage, - Text, + TextPart, ) # TODO: use the semconv constants once these attributes are released in @@ -274,12 +274,12 @@ def _fetch_with_content(self) -> None: invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="This is a test.")], + parts=[TextPart(content="This is a test.")], finish_reason="stop", ) ] invocation.system_instruction = [ - Text(content="You are a helpful assistant.") + TextPart(content="You are a helpful assistant.") ] invocation.tool_definitions = [ FunctionToolDefinition( diff --git a/util/opentelemetry-util-genai/tests/test_handler_workflow.py b/util/opentelemetry-util-genai/tests/test_handler_workflow.py index cf45f90cb..7b701330d 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_workflow.py +++ b/util/opentelemetry-util-genai/tests/test_handler_workflow.py @@ -25,7 +25,7 @@ Error, InputMessage, OutputMessage, - Text, + TextPart, ) @@ -273,9 +273,11 @@ def test_workflow_context_manager_success_has_unset_status(self) -> None: self.assertEqual(spans[0].status.status_code, StatusCode.UNSET) def test_workflow_context_manager_with_messages(self) -> None: - inp = InputMessage(role="user", parts=[Text(content="hello")]) + inp = InputMessage(role="user", parts=[TextPart(content="hello")]) out = OutputMessage( - role="assistant", parts=[Text(content="hi")], finish_reason="stop" + role="assistant", + parts=[TextPart(content="hi")], + finish_reason="stop", ) with self.handler.workflow("msg_wf") as inv: inv.input_messages = [inp] diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 47b3c6de3..9dd32e5dd 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -1,7 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -"""Tests for ToolCallRequest and ToolInvocation inheritance structure""" +"""Tests for ToolCallRequestPart and ToolInvocation inheritance structure""" import os from unittest.mock import patch @@ -26,9 +26,9 @@ from opentelemetry.util.genai.types import ( CompactionPart, InputMessage, - ServerToolCall, - ServerToolCallResponse, - ToolCallRequest, + ServerToolCallPart, + ServerToolCallResponsePart, + ToolCallRequestPart, ) from opentelemetry.util.genai.utils import gen_ai_json_dumps @@ -38,8 +38,8 @@ def _make_handler() -> TelemetryHandler: def test_toolcallrequest_is_message_part(): - """ToolCallRequest is for message parts only""" - tcr = ToolCallRequest( + """ToolCallRequestPart is for message parts only""" + tcr = ToolCallRequestPart( arguments={"location": "Paris"}, name="get_weather", id="call_123" ) msg = InputMessage(role="user", parts=[tcr]) @@ -52,7 +52,7 @@ def test_toolcall_inherits_from_genaiinvocation(): tc = handler.tool("get_weather") tc.arguments = {"city": "Paris"} assert isinstance(tc, GenAIInvocation) - assert not isinstance(tc, ToolCallRequest) + assert not isinstance(tc, ToolCallRequestPart) tc.stop() @@ -66,13 +66,13 @@ def test_toolcall_has_attributes_dict(): def test_toolcallrequest_in_message_part_union(): - """ToolCallRequest (not ToolInvocation) is the correct type for message parts""" - tc = ToolCallRequest( + """ToolCallRequestPart (not ToolInvocation) is the correct type for message parts""" + tc = ToolCallRequestPart( name="get_weather", arguments={"city": "Paris"}, id="call_123" ) msg = InputMessage(role="assistant", parts=[tc]) assert len(msg.parts) == 1 - assert isinstance(msg.parts[0], ToolCallRequest) + assert isinstance(msg.parts[0], ToolCallRequestPart) assert not isinstance(msg.parts[0], GenAIInvocation) @@ -85,8 +85,8 @@ def test_toolcall_operation_name(): def test_server_tool_call_basic(): - """ServerToolCall can be created with required fields""" - stc = ServerToolCall( + """ServerToolCallPart can be created with required fields""" + stc = ServerToolCallPart( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "print(1)"}, ) @@ -100,8 +100,8 @@ def test_server_tool_call_basic(): def test_server_tool_call_with_id(): - """ServerToolCall can have an optional id""" - stc = ServerToolCall( + """ServerToolCallPart can have an optional id""" + stc = ServerToolCallPart( name="web_search", server_tool_call={"type": "web_search", "query": "weather"}, id="stc_001", @@ -110,8 +110,8 @@ def test_server_tool_call_with_id(): def test_server_tool_call_response_basic(): - """ServerToolCallResponse can be created with required fields""" - stcr = ServerToolCallResponse( + """ServerToolCallResponsePart can be created with required fields""" + stcr = ServerToolCallResponsePart( server_tool_call_response={ "type": "code_interpreter", "output": "1\n", @@ -126,19 +126,19 @@ def test_server_tool_call_response_basic(): def test_server_tool_call_in_message(): - """ServerToolCall and ServerToolCallResponse work as MessageParts""" - stc = ServerToolCall( + """ServerToolCallPart and ServerToolCallResponsePart work as MessageParts""" + stc = ServerToolCallPart( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "x = 1"}, ) - stcr = ServerToolCallResponse( + stcr = ServerToolCallResponsePart( server_tool_call_response={"type": "code_interpreter", "output": ""}, id="stc_001", ) msg = InputMessage(role="assistant", parts=[stc, stcr]) assert len(msg.parts) == 2 - assert isinstance(msg.parts[0], ServerToolCall) - assert isinstance(msg.parts[1], ServerToolCallResponse) + assert isinstance(msg.parts[0], ServerToolCallPart) + assert isinstance(msg.parts[1], ServerToolCallResponsePart) def test_compactionpart_is_message_part(): diff --git a/util/opentelemetry-util-genai/tests/test_upload.py b/util/opentelemetry-util-genai/tests/test_upload.py index 10474645e..57f5d6b5c 100644 --- a/util/opentelemetry-util-genai/tests/test_upload.py +++ b/util/opentelemetry-util-genai/tests/test_upload.py @@ -28,12 +28,12 @@ FAKE_INPUTS = [ types.InputMessage( role="user", - parts=[types.Text(content="What is the capital of France?")], + parts=[types.TextPart(content="What is the capital of France?")], ), types.InputMessage( role="assistant", parts=[ - types.ToolCallRequest( + types.ToolCallRequestPart( id="get_capital_0", name="get_capital", arguments={"city": "Paris"}, @@ -43,7 +43,7 @@ types.InputMessage( role="user", parts=[ - types.ToolCallResponse( + types.ToolCallResponsePart( id="get_capital_0", response={"capital": "Paris"} ) ], @@ -52,11 +52,13 @@ FAKE_OUTPUTS = [ types.OutputMessage( role="assistant", - parts=[types.Text(content="Paris")], + parts=[types.TextPart(content="Paris")], finish_reason="stop", ), ] -FAKE_SYSTEM_INSTRUCTION = [types.Text(content="You are a helpful assistant.")] +FAKE_SYSTEM_INSTRUCTION = [ + types.TextPart(content="You are a helpful assistant.") +] FAKE_TOOL_DEFINITIONS: list[types.ToolDefinition] = [ types.FunctionToolDefinition( @@ -174,7 +176,7 @@ def test_lru_cache_works(self): self.hook.on_completion( inputs=[], outputs=[], - system_instruction=[types.Text(content=str(iteration))], + system_instruction=[types.TextPart(content=str(iteration))], tool_definitions=[], ) self.hook.shutdown() @@ -351,8 +353,8 @@ def test_system_insruction_is_hashed_to_avoid_reupload(self): # FIle should exist. self.assertTrue(self.hook._file_exists(expected_file_name)) system_instructions = [ - types.Text(content="You are a helpful assistant."), - types.Text(content="You will do your best."), + types.TextPart(content="You are a helpful assistant."), + types.TextPart(content="You will do your best."), ] record = LogRecord() self.hook.on_completion( @@ -480,7 +482,9 @@ def test_upload_bytes(self) -> None: types.InputMessage( role="user", parts=[ - types.Text(content="What is the capital of France?"), + types.TextPart( + content="What is the capital of France?" + ), {"type": "generic_bytes", "bytes": b"hello"}, ], ) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index 127e9abae..a9f376fef 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -38,7 +38,7 @@ InputMessage, MessagePart, OutputMessage, - Text, + TextPart, ) from opentelemetry.util.genai.utils import ( get_content_capturing_mode, @@ -50,21 +50,23 @@ def _create_input_message( content: str = "hello world", role: str = "Human" ) -> InputMessage: - return InputMessage(role=role, parts=[Text(content=content)]) + return InputMessage(role=role, parts=[TextPart(content=content)]) def _create_output_message( content: str = "hello back", finish_reason: str = "stop", role: str = "AI" ) -> OutputMessage: return OutputMessage( - role=role, parts=[Text(content=content)], finish_reason=finish_reason + role=role, + parts=[TextPart(content=content)], + finish_reason=finish_reason, ) def _create_system_instruction( content: str = "You are a helpful assistant.", ) -> list[MessagePart]: - return [Text(content=content)] + return [TextPart(content=content)] def _get_single_span(span_exporter: InMemorySpanExporter) -> ReadableSpan: diff --git a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py index 6ddfd30a6..bcfda5c38 100644 --- a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py +++ b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py @@ -13,7 +13,7 @@ from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, - Text, + TextPart, ) @@ -42,7 +42,7 @@ def test_custom_name(self): assert invocation._name == "customer_support_pipeline" def test_with_input_messages(self): - msg = InputMessage(role="user", parts=[Text(content="hello")]) + msg = InputMessage(role="user", parts=[TextPart(content="hello")]) invocation = self.handler.workflow(name="test") invocation.input_messages = [msg] invocation.stop() @@ -51,7 +51,9 @@ def test_with_input_messages(self): def test_with_output_messages(self): msg = OutputMessage( - role="assistant", parts=[Text(content="hi")], finish_reason="stop" + role="assistant", + parts=[TextPart(content="hi")], + finish_reason="stop", ) invocation = self.handler.workflow(name="test") invocation.output_messages = [msg] @@ -85,10 +87,10 @@ def test_default_attributes_are_independent(self): inv2.stop() def test_full_construction(self): - inp = InputMessage(role="user", parts=[Text(content="query")]) + inp = InputMessage(role="user", parts=[TextPart(content="query")]) out = OutputMessage( role="assistant", - parts=[Text(content="answer")], + parts=[TextPart(content="answer")], finish_reason="stop", ) invocation = self.handler.workflow(name="my_workflow")