Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .github/skills/migrate-from-openinference/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions .github/skills/write-conformance-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from opentelemetry.util.genai.types import (
InputMessage,
OutputMessage,
Text,
TextPart,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -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)]
)
]


Expand All @@ -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",
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
)

from opentelemetry.util.genai.types import (
Blob,
BlobPart,
MessagePart,
Reasoning,
Text,
ToolCallRequest,
ToolCallResponse,
ReasoningPart,
TextPart,
ToolCallRequestPart,
ToolCallResponsePart,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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))
Expand All @@ -131,21 +133,21 @@ 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
)

if isinstance(block, (ThinkingBlock, RedactedThinkingBlock)):
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,
)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
from opentelemetry.util.genai.types import (
MessagePart,
OutputMessage,
Text,
ToolCallRequest,
TextPart,
ToolCallRequestPart,
)

SUPPORTED_RAPI_RESPONSE_HEADERS = ("x-ms-served-model",)
Expand Down Expand Up @@ -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"],
Expand All @@ -402,7 +402,7 @@ def on_llm_end(
)
else:
parts = [
Text(
TextPart(
content=chat_generation.message.content,
type="text",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
InputMessage,
MessagePart,
OutputMessage,
Reasoning,
Text,
ToolCallRequest,
ToolCallResponse,
ReasoningPart,
TextPart,
ToolCallRequestPart,
ToolCallResponsePart,
ToolDefinition,
)

Expand Down Expand Up @@ -81,41 +81,41 @@ 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")
or item.get("reasoning")
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
Expand All @@ -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)
Expand All @@ -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"],
Expand All @@ -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,
)
Expand Down Expand Up @@ -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 []
Expand All @@ -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 []


Expand Down
Loading
Loading