Skip to content
Merged
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
43 changes: 40 additions & 3 deletions A365_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,22 +356,59 @@ Top-level agent invocation — wraps the entire request/response cycle:

```python
from microsoft.opentelemetry.a365.core import (
AgentDetails, CallerDetails, Channel, InvokeAgentScope,
InvokeAgentScopeDetails, Request, ServiceEndpoint, UserDetails,
AgentDetails, CallerDetails, Channel, GenAiRequestParameters,
GenAiResponseParameters, InvokeAgentScope, InvokeAgentScopeDetails,
Request, ServiceEndpoint, TextPart, UserDetails,
)

agent = AgentDetails(agent_id="agent-001", agent_name="My Agent", tenant_id="t1")

with InvokeAgentScope.start(
request=Request(content="Hello", session_id="s1", conversation_id="c1", channel=Channel(name="msteams")),
scope_details=InvokeAgentScopeDetails(endpoint=ServiceEndpoint(hostname="agent.contoso.com")),
scope_details=InvokeAgentScopeDetails(
endpoint=ServiceEndpoint(hostname="agent.contoso.com"),
request_parameters=GenAiRequestParameters(
model="gpt-4o",
max_tokens=256,
temperature=0.2,
stop_sequences=["END"],
output_type="text",
system_instructions=[TextPart(content="Be concise.")],
),
),
agent_details=agent,
caller_details=CallerDetails(user_details=UserDetails(user_id="u1", user_email="u@contoso.com")),
) as scope:
# ... do work ...
scope.record_response_parameters(
GenAiResponseParameters(
finish_reasons=["stop"],
input_tokens=42,
output_tokens=18,
cache_write_input_tokens=4,
)
)
scope.record_response("Here is the answer.")
```

`GenAiRequestParameters` values are recorded when the scope starts. Use
`scope.record_response_parameters()` for response values that are only known
after completion. Fields left as `None` are omitted from the span. Sequence
fields such as `stop_sequences` and `finish_reasons` are emitted as
OpenTelemetry string arrays.

Supported semantic attributes:

- Request: `gen_ai.request.model`, `gen_ai.request.seed`,
`gen_ai.request.choice.count`, `gen_ai.request.frequency_penalty`,
`gen_ai.request.max_tokens`, `gen_ai.request.presence_penalty`,
`gen_ai.request.stop_sequences`, `gen_ai.request.temperature`,
`gen_ai.request.top_p`, `gen_ai.data_source.id`, `gen_ai.output.type`,
`gen_ai.system_instructions`
- Response: `gen_ai.response.finish_reasons`, `gen_ai.usage.input_tokens`,
`gen_ai.usage.output_tokens`, `gen_ai.usage.cache_write.input_tokens`,
`gen_ai.usage.cache_read.input_tokens`

### ExecuteToolScope

```python
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Release History
# Unreleased
### Features Added
- Add Python-native `InvokeAgentScope` request and response parameter models
that emit OpenTelemetry GenAI semantic attributes, including structured
system instructions and cache read/write token counts, introduced by .NET
PR #120.

# 1.3.9 (2026-09-09)
### Features Added
- Update OpenTelemetry dependencies to latest versions, bump `langchain-core` minimum version to address S360, and support the new `httpx2` entry point exposed by `opentelemetry-instrumentation-httpx`.
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,49 @@ See the [A365 guide](https://github.com/microsoft/opentelemetry-distro-python/bl

> For A365 token resolver patterns, baggage, and scope classes, see the [A365 guide](https://github.com/microsoft/opentelemetry-distro-python/blob/main/A365_DOCUMENTATION.md).

### InvokeAgent semantic parameters

Manual `InvokeAgentScope` instrumentation accepts Python-native request and response parameter models:

```python
from microsoft.opentelemetry.a365.core import (
AgentDetails,
GenAiRequestParameters,
GenAiResponseParameters,
InvokeAgentScope,
InvokeAgentScopeDetails,
Request,
TextPart,
)

request = Request(content="Hello")
agent_details = AgentDetails(agent_id="agent-001")
details = InvokeAgentScopeDetails(
request_parameters=GenAiRequestParameters(
model="gpt-4o",
max_tokens=256,
stop_sequences=["END"],
system_instructions=[TextPart(content="Be concise.")],
)
)

with InvokeAgentScope.start(request, details, agent_details) as scope:
# Invoke your agent here.
scope.record_response_parameters(
GenAiResponseParameters(
finish_reasons=["stop"],
output_tokens=18,
cache_write_input_tokens=4,
)
)
```

Unset fields are omitted. Supported attributes include `gen_ai.request.*`,
`gen_ai.data_source.id`, `gen_ai.output.type`, `gen_ai.system_instructions`,
`gen_ai.response.finish_reasons`, and `gen_ai.usage.*` token counts. See the
[A365 guide](https://github.com/microsoft/opentelemetry-distro-python/blob/main/A365_DOCUMENTATION.md#invokeagentscope)
for the full list.

### Sampling

Configured via standard OpenTelemetry environment variables:
Expand Down
4 changes: 3 additions & 1 deletion src/microsoft/opentelemetry/a365/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ Core tracing primitives — scopes, configuration, data models, and internal uti
| `agent_details.py` | `AgentDetails` dataclass — metadata about an AI agent (ID, name, description, blueprint/platform IDs, tenant, version). |
| `channel.py` | `Channel` dataclass — channel context (name, link) for agent execution. |
| `constants.py` | Core-level constants for span operations, OTel conventions, feature switches, and error types. |
| `gen_ai_request_parameters.py` | GenAI request parameter models for generation settings and related semantic attributes. |
| `gen_ai_response_parameters.py` | GenAI response parameter models for finish reasons and token usage attributes. |
| `execute_tool_scope.py` | `ExecuteToolScope` — tracing scope for AI tool executions. Records tool name, arguments, call ID, type, and endpoint. |
| `inference_call_details.py` | `InferenceCallDetails` dataclass — LLM call metadata (model, provider, token counts, finish reasons, endpoint). |
| `inference_operation_type.py` | `InferenceOperationType` enum — Chat, TextCompletion, GenerateContent. |
| `inference_scope.py` | `InferenceScope` — tracing scope for LLM/AI inference operations. Records input/output messages, model details, token usage, and user info. |
| `invoke_agent_details.py` | `InvokeAgentScopeDetails` dataclass — configuration for agent invocation tracing (endpoint). |
| `invoke_agent_details.py` | `InvokeAgentScopeDetails` dataclass — configuration for agent invocation tracing (endpoint, optional request parameters, optional response parameters). |
| `invoke_agent_scope.py` | `InvokeAgentScope` — tracing scope for agent invocations. Records request/response, caller details (human and agent-to-agent), channel, and endpoint info. |
| `message_utils.py` | Conversion and serialization helpers for OTel gen-ai message format. Normalizes strings/lists to structured `InputMessages`/`OutputMessages`. |
| `opentelemetry_scope.py` | `OpenTelemetryScope` — base class for all tracing scopes. Manages span creation, attribute setting, context management, and baggage building. |
Expand Down
9 changes: 9 additions & 0 deletions src/microsoft/opentelemetry/a365/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,15 @@
GEN_AI_CLIENT_OPERATION_DURATION_METRIC_NAME = "gen_ai.client.operation.duration"
GEN_AI_CLIENT_TOKEN_USAGE_METRIC_NAME = "gen_ai.client.token.usage"
GEN_AI_OPERATION_NAME_KEY = "gen_ai.operation.name"
GEN_AI_DATA_SOURCE_ID_KEY = "gen_ai.data_source.id"
GEN_AI_OUTPUT_TYPE_KEY = "gen_ai.output.type"
GEN_AI_REQUEST_CHOICE_COUNT_KEY = "gen_ai.request.choice.count"
GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY = "gen_ai.request.frequency_penalty"
GEN_AI_REQUEST_MAX_TOKENS_KEY = "gen_ai.request.max_tokens"
GEN_AI_REQUEST_MODEL_KEY = "gen_ai.request.model"
GEN_AI_REQUEST_PRESENCE_PENALTY_KEY = "gen_ai.request.presence_penalty"
GEN_AI_REQUEST_SEED_KEY = "gen_ai.request.seed"
GEN_AI_REQUEST_STOP_SEQUENCES_KEY = "gen_ai.request.stop_sequences"
GEN_AI_REQUEST_TEMPERATURE_KEY = "gen_ai.request.temperature"
GEN_AI_REQUEST_TOP_P_KEY = "gen_ai.request.top_p"
GEN_AI_RESPONSE_FINISH_REASONS_KEY = "gen_ai.response.finish_reasons"
Expand All @@ -52,6 +59,8 @@
GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id"
GEN_AI_CONVERSATION_ITEM_LINK_KEY = "microsoft.conversation.item.link"
GEN_AI_TOKEN_TYPE_KEY = "gen_ai.token.type"
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens"
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY = "gen_ai.usage.cache_read.input_tokens"
GEN_AI_USAGE_INPUT_TOKENS_KEY = "gen_ai.usage.input_tokens"
GEN_AI_USAGE_OUTPUT_TOKENS_KEY = "gen_ai.usage.output_tokens"
GEN_AI_CHOICE = "gen_ai.choice"
Expand Down
4 changes: 4 additions & 0 deletions src/microsoft/opentelemetry/a365/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from microsoft.opentelemetry.a365.core.guardrail_finding import GuardrailFinding
from microsoft.opentelemetry.a365.core.guardrail_risk_severity import GuardrailRiskSeverity
from microsoft.opentelemetry.a365.core.guardrail_target_type import GuardrailTargetType
from microsoft.opentelemetry.a365.core.gen_ai_request_parameters import GenAiRequestParameters
from microsoft.opentelemetry.a365.core.gen_ai_response_parameters import GenAiResponseParameters
from microsoft.opentelemetry.a365.core.inference_call_details import InferenceCallDetails
from microsoft.opentelemetry.a365.core.models.service_endpoint import ServiceEndpoint
from microsoft.opentelemetry.a365.core.inference_operation_type import InferenceOperationType
Expand Down Expand Up @@ -84,6 +86,8 @@
"SpanDetails",
"InferenceCallDetails",
"ServiceEndpoint",
"GenAiRequestParameters",
"GenAiResponseParameters",
# Enums
"InferenceOperationType",
"ToolType",
Expand Down
9 changes: 9 additions & 0 deletions src/microsoft/opentelemetry/a365/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@
GEN_AI_CLIENT_OPERATION_DURATION_METRIC_NAME = "gen_ai.client.operation.duration"
GEN_AI_CLIENT_TOKEN_USAGE_METRIC_NAME = "gen_ai.client.token.usage"
GEN_AI_OPERATION_NAME_KEY = "gen_ai.operation.name"
GEN_AI_DATA_SOURCE_ID_KEY = "gen_ai.data_source.id"
GEN_AI_OUTPUT_TYPE_KEY = "gen_ai.output.type"
GEN_AI_REQUEST_CHOICE_COUNT_KEY = "gen_ai.request.choice.count"
GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY = "gen_ai.request.frequency_penalty"
GEN_AI_REQUEST_MAX_TOKENS_KEY = "gen_ai.request.max_tokens"
GEN_AI_REQUEST_MODEL_KEY = "gen_ai.request.model"
GEN_AI_REQUEST_PRESENCE_PENALTY_KEY = "gen_ai.request.presence_penalty"
GEN_AI_REQUEST_SEED_KEY = "gen_ai.request.seed"
GEN_AI_REQUEST_STOP_SEQUENCES_KEY = "gen_ai.request.stop_sequences"
GEN_AI_REQUEST_TEMPERATURE_KEY = "gen_ai.request.temperature"
GEN_AI_REQUEST_TOP_P_KEY = "gen_ai.request.top_p"
GEN_AI_RESPONSE_FINISH_REASONS_KEY = "gen_ai.response.finish_reasons"
Expand All @@ -49,6 +56,8 @@
GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id"
GEN_AI_CONVERSATION_ITEM_LINK_KEY = "microsoft.conversation.item.link"
GEN_AI_TOKEN_TYPE_KEY = "gen_ai.token.type"
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY = "gen_ai.usage.cache_write.input_tokens"
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY = "gen_ai.usage.cache_read.input_tokens"
GEN_AI_USAGE_INPUT_TOKENS_KEY = "gen_ai.usage.input_tokens"
GEN_AI_USAGE_OUTPUT_TOKENS_KEY = "gen_ai.usage.output_tokens"
GEN_AI_CHOICE = "gen_ai.choice"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""GenAI request parameters for InvokeAgent telemetry."""

from dataclasses import dataclass
from typing import Sequence

from microsoft.opentelemetry.a365.core.models.messages import TextPart


@dataclass
class GenAiRequestParameters:
"""Optional GenAI request parameters recorded on InvokeAgent spans."""

model: str | None = None
seed: int | None = None
choice_count: int | None = None
frequency_penalty: float | None = None
max_tokens: int | None = None
presence_penalty: float | None = None
stop_sequences: Sequence[str] | None = None
temperature: float | None = None
top_p: float | None = None
data_source_id: str | None = None
output_type: str | None = None
system_instructions: Sequence[TextPart] | None = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""GenAI response parameters for InvokeAgent telemetry."""

from dataclasses import dataclass
from typing import Sequence


@dataclass
class GenAiResponseParameters:
"""Optional GenAI response parameters recorded on InvokeAgent spans."""

finish_reasons: Sequence[str] | None = None
input_tokens: int | None = None
output_tokens: int | None = None
cache_write_input_tokens: int | None = None
cache_read_input_tokens: int | None = None
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from dataclasses import dataclass

from microsoft.opentelemetry.a365.core.gen_ai_request_parameters import GenAiRequestParameters
from microsoft.opentelemetry.a365.core.gen_ai_response_parameters import GenAiResponseParameters
from microsoft.opentelemetry.a365.core.models.service_endpoint import ServiceEndpoint


Expand All @@ -13,3 +15,5 @@ class InvokeAgentScopeDetails:
"""Scope-level configuration for agent invocation tracing."""

endpoint: ServiceEndpoint | None = None
request_parameters: GenAiRequestParameters | None = None
response_parameters: GenAiResponseParameters | None = None
66 changes: 66 additions & 0 deletions src/microsoft/opentelemetry/a365/core/invoke_agent_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,25 @@
GEN_AI_CALLER_AGENT_VERSION_KEY,
GEN_AI_CALLER_CLIENT_IP_KEY,
GEN_AI_CONVERSATION_ID_KEY,
GEN_AI_DATA_SOURCE_ID_KEY,
GEN_AI_INPUT_MESSAGES_KEY,
GEN_AI_OUTPUT_MESSAGES_KEY,
GEN_AI_OUTPUT_TYPE_KEY,
GEN_AI_REQUEST_CHOICE_COUNT_KEY,
GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY,
GEN_AI_REQUEST_MAX_TOKENS_KEY,
GEN_AI_REQUEST_MODEL_KEY,
GEN_AI_REQUEST_PRESENCE_PENALTY_KEY,
GEN_AI_REQUEST_SEED_KEY,
GEN_AI_REQUEST_STOP_SEQUENCES_KEY,
GEN_AI_REQUEST_TEMPERATURE_KEY,
GEN_AI_REQUEST_TOP_P_KEY,
GEN_AI_RESPONSE_FINISH_REASONS_KEY,
GEN_AI_SYSTEM_INSTRUCTIONS_KEY,
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY,
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY,
GEN_AI_USAGE_INPUT_TOKENS_KEY,
GEN_AI_USAGE_OUTPUT_TOKENS_KEY,
INVOKE_AGENT_OPERATION_NAME,
SERVER_ADDRESS_KEY,
SERVER_PORT_KEY,
Expand All @@ -30,11 +47,14 @@
USER_ID_KEY,
USER_NAME_KEY,
)
from microsoft.opentelemetry.a365.core.gen_ai_request_parameters import GenAiRequestParameters
from microsoft.opentelemetry.a365.core.gen_ai_response_parameters import GenAiResponseParameters
from microsoft.opentelemetry.a365.core.invoke_agent_details import InvokeAgentScopeDetails
from microsoft.opentelemetry.a365.core.message_utils import (
normalize_input_messages,
normalize_output_messages,
serialize_messages,
serialize_system_instructions,
)
from microsoft.opentelemetry.a365.core.models.caller_details import CallerDetails
from microsoft.opentelemetry.a365.core.models.messages import InputMessagesParam, OutputMessagesParam
Expand Down Expand Up @@ -129,6 +149,11 @@ def __init__(
if endpoint.port and endpoint.port != 443:
self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port)

if scope_details.request_parameters:
self._record_request_parameters(scope_details.request_parameters)
if scope_details.response_parameters:
self._record_response_parameters(scope_details.response_parameters)

# Set request metadata
if request.channel:
self.set_tag_maybe(CHANNEL_NAME_KEY, request.channel.name)
Expand Down Expand Up @@ -182,6 +207,10 @@ def record_response(self, response: str) -> None:
"""
self.record_output_messages([response])

def record_response_parameters(self, parameters: GenAiResponseParameters) -> None:
"""Record GenAI response parameters that are only known after completion."""
self._record_response_parameters(parameters)

def record_input_messages(self, messages: InputMessagesParam) -> None:
"""Record the input messages for telemetry tracking.

Expand All @@ -205,3 +234,40 @@ def record_output_messages(self, messages: OutputMessagesParam) -> None:
"""
wrapper = normalize_output_messages(messages)
self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, serialize_messages(wrapper))

def _record_request_parameters(self, parameters: GenAiRequestParameters) -> None:
self.set_tag_maybe(GEN_AI_REQUEST_MODEL_KEY, parameters.model)
self.set_tag_maybe(GEN_AI_REQUEST_SEED_KEY, parameters.seed)
self.set_tag_maybe(GEN_AI_REQUEST_CHOICE_COUNT_KEY, parameters.choice_count)
self.set_tag_maybe(GEN_AI_REQUEST_FREQUENCY_PENALTY_KEY, parameters.frequency_penalty)
self.set_tag_maybe(GEN_AI_REQUEST_MAX_TOKENS_KEY, parameters.max_tokens)
self.set_tag_maybe(GEN_AI_REQUEST_PRESENCE_PENALTY_KEY, parameters.presence_penalty)
self.set_tag_maybe(
GEN_AI_REQUEST_STOP_SEQUENCES_KEY,
tuple(parameters.stop_sequences) if parameters.stop_sequences is not None else None,
)
self.set_tag_maybe(GEN_AI_REQUEST_TEMPERATURE_KEY, parameters.temperature)
self.set_tag_maybe(GEN_AI_REQUEST_TOP_P_KEY, parameters.top_p)
self.set_tag_maybe(GEN_AI_DATA_SOURCE_ID_KEY, parameters.data_source_id)
self.set_tag_maybe(GEN_AI_OUTPUT_TYPE_KEY, parameters.output_type)
if parameters.system_instructions is not None:
self.set_tag_maybe(
GEN_AI_SYSTEM_INSTRUCTIONS_KEY,
serialize_system_instructions(parameters.system_instructions),
)

def _record_response_parameters(self, parameters: GenAiResponseParameters) -> None:
self.set_tag_maybe(
GEN_AI_RESPONSE_FINISH_REASONS_KEY,
tuple(parameters.finish_reasons) if parameters.finish_reasons is not None else None,
)
self.set_tag_maybe(GEN_AI_USAGE_INPUT_TOKENS_KEY, parameters.input_tokens)
self.set_tag_maybe(GEN_AI_USAGE_OUTPUT_TOKENS_KEY, parameters.output_tokens)
self.set_tag_maybe(
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY,
parameters.cache_write_input_tokens,
)
self.set_tag_maybe(
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY,
parameters.cache_read_input_tokens,
)
Loading