openinference-instrumentation-agent-framework 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ # vendored virtual environments
2
+ .venv
3
+
4
+ # gradio work directory
5
+ .gradio
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: openinference-instrumentation-agent-framework
3
+ Version: 0.1.0
4
+ Summary: OpenInference span processor for Microsoft Agent Framework - transforms native OpenTelemetry spans to OpenInference format
5
+ Project-URL: Homepage, https://github.com/Arize-ai/openinference/tree/main/python/instrumentation/openinference-instrumentation-agent-framework
6
+ Author-email: OpenInference Authors <oss@arize.com>
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: <3.15,>=3.10
19
+ Requires-Dist: openinference-semantic-conventions>=0.1.25
20
+ Requires-Dist: opentelemetry-api>=1.39.0
21
+ Requires-Dist: opentelemetry-sdk>=1.39.0
22
+ Requires-Dist: opentelemetry-semantic-conventions>=0.52b0
23
+ Requires-Dist: typing-extensions
24
+ Provides-Extra: instruments
25
+ Requires-Dist: agent-framework>=1.0.0b260130; extra == 'instruments'
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest; extra == 'test'
28
+ Requires-Dist: pytest-asyncio; extra == 'test'
29
+ Requires-Dist: pytest-recording; extra == 'test'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # OpenInference Microsoft Agent Framework Instrumentation
33
+
34
+ OpenInference span processor for Microsoft Agent Framework that transforms native OpenTelemetry spans to OpenInference format for compatibility with OpenInference-compliant backends like [Arize Phoenix](https://github.com/Arize-ai/phoenix).
35
+
36
+ **Tested with agent-framework `1.0.0b260130` (January 30, 2026)**
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install openinference-instrumentation-agent-framework
42
+ ```
43
+
44
+ Install with agent-framework:
45
+ ```bash
46
+ pip install openinference-instrumentation-agent-framework[instruments]
47
+ ```
48
+
49
+ ## Overview
50
+
51
+ Microsoft Agent Framework emits telemetry using GenAI semantic conventions (`gen_ai.*` attributes). This package provides a `SpanProcessor` that transforms these spans to OpenInference format, enabling compatibility with observability tools that support the OpenInference standard.
52
+
53
+ **Note:** Agent Framework is in beta and its API may change between versions. This instrumentation tracks the latest stable release.
54
+
55
+ ## Usage
56
+
57
+ ### Basic Setup
58
+
59
+ ```python
60
+ from opentelemetry import trace
61
+ from opentelemetry.sdk.trace import TracerProvider
62
+ from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
63
+ from agent_framework.observability import enable_instrumentation
64
+ from openinference.instrumentation.agent_framework import (
65
+ AgentFrameworkToOpenInferenceProcessor
66
+ )
67
+
68
+ # Create tracer provider
69
+ tracer_provider = TracerProvider()
70
+
71
+ # Add OpenInference processor to transform spans
72
+ tracer_provider.add_span_processor(
73
+ AgentFrameworkToOpenInferenceProcessor()
74
+ )
75
+
76
+ # Add exporter (Console for demo, use OTLP for Phoenix)
77
+ tracer_provider.add_span_processor(
78
+ SimpleSpanProcessor(ConsoleSpanExporter())
79
+ )
80
+
81
+ # Set as global tracer provider
82
+ trace.set_tracer_provider(tracer_provider)
83
+
84
+ # Enable agent-framework instrumentation
85
+ enable_instrumentation(enable_sensitive_data=True)
86
+
87
+ # Use framework normally - spans will be transformed automatically
88
+ from agent_framework.openai import OpenAIChatClient
89
+
90
+ client = OpenAIChatClient(model_id="gpt-4o-mini", api_key="your-key")
91
+ agent = client.as_agent(name="Assistant", instructions="You are helpful.")
92
+ response = await agent.run("Hello!")
93
+ ```
94
+
95
+ ### With Phoenix (Arize)
96
+
97
+ ```python
98
+ from opentelemetry import trace
99
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
100
+ from opentelemetry.sdk.trace import TracerProvider
101
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
102
+ from agent_framework.observability import enable_instrumentation
103
+ from openinference.instrumentation.agent_framework import (
104
+ AgentFrameworkToOpenInferenceProcessor
105
+ )
106
+
107
+ # Configure Phoenix endpoint
108
+ endpoint = "http://localhost:6006/v1/traces"
109
+
110
+ # Setup tracer with OpenInference processor
111
+ tracer_provider = TracerProvider()
112
+ tracer_provider.add_span_processor(AgentFrameworkToOpenInferenceProcessor())
113
+ tracer_provider.add_span_processor(
114
+ SimpleSpanProcessor(OTLPSpanExporter(endpoint=endpoint))
115
+ )
116
+ trace.set_tracer_provider(tracer_provider)
117
+
118
+ # Enable instrumentation
119
+ enable_instrumentation(enable_sensitive_data=True)
120
+
121
+ # Your agent code here
122
+ ```
123
+
124
+ ## Transformation Details
125
+
126
+ ### Span Kind Mapping
127
+
128
+ | MS Agent Framework Operation | OpenInference Span Kind |
129
+ |------------------------------|-------------------------|
130
+ | `chat` | LLM |
131
+ | `execute_tool` | TOOL |
132
+ | `invoke_agent` | AGENT |
133
+ | `workflow.run` | CHAIN |
134
+ | `executor.process` | CHAIN |
135
+
136
+ ### Attribute Mapping
137
+
138
+ | Source (GenAI) | Target (OpenInference) |
139
+ |----------------|------------------------|
140
+ | `gen_ai.request.model` | `llm.model_name` |
141
+ | `gen_ai.provider.name` | `llm.provider` |
142
+ | `gen_ai.usage.input_tokens` | `llm.token_count.prompt` |
143
+ | `gen_ai.usage.output_tokens` | `llm.token_count.completion` |
144
+ | `gen_ai.input.messages` | `llm.input_messages.*` (flattened) |
145
+ | `gen_ai.output.messages` | `llm.output_messages.*` (flattened) |
146
+ | `gen_ai.tool.name` | `tool.name` |
147
+ | `gen_ai.tool.call.id` | `tool.call_id` |
148
+ | `gen_ai.tool.call.arguments` | `tool.parameters` |
149
+ | `gen_ai.conversation.id` | `session.id` |
150
+
151
+ ### Message Format Transformation
152
+
153
+ MS Agent Framework messages:
154
+ ```json
155
+ {
156
+ "role": "user",
157
+ "parts": [
158
+ {"type": "text", "content": "Hello"}
159
+ ]
160
+ }
161
+ ```
162
+
163
+ Transformed to OpenInference flattened format:
164
+ ```
165
+ llm.input_messages.0.message.role = "user"
166
+ llm.input_messages.0.message.content = "Hello"
167
+ ```
168
+
169
+ ## Debug Mode
170
+
171
+ Enable debug mode to log transformation details:
172
+
173
+ ```python
174
+ processor = AgentFrameworkToOpenInferenceProcessor(debug=True)
175
+ ```
176
+
177
+ ## Requirements
178
+
179
+ - Python >= 3.10, < 3.15
180
+ - opentelemetry-api >= 1.39.0
181
+ - opentelemetry-sdk >= 1.39.0
182
+ - openinference-semantic-conventions >= 0.1.25
183
+ - agent-framework >= 1.0.0b260130 (optional, install with `[instruments]` extra)
184
+
185
+ ## Important Notes
186
+
187
+ ### Agent Framework API Stability
188
+
189
+ Microsoft Agent Framework is in active beta development. API changes between versions are possible:
190
+ - This instrumentation is tested against `agent-framework==1.0.0b260130`
191
+ - The `-latest` test variant tracks breaking changes in new releases
192
+ - If you encounter API compatibility issues, pin to the tested version:
193
+ ```bash
194
+ pip install agent-framework==1.0.0b260130
195
+ ```
196
+
197
+ ### Sensitive Data
198
+
199
+ Set `enable_sensitive_data=True` when calling `enable_instrumentation()` to capture message content in traces. This is required for full observability but may include PII.
200
+
201
+ ## Development
202
+
203
+ ### Running Tests
204
+
205
+ Tests use VCR cassettes to replay recorded API interactions:
206
+
207
+ ```bash
208
+ # Run with recorded cassettes (no API key needed)
209
+ pytest tests/test_processor.py -v
210
+
211
+ # Re-record cassettes (requires OPENAI_API_KEY)
212
+ export OPENAI_API_KEY=your_key
213
+ rm -rf tests/cassettes/
214
+ pytest tests/test_processor.py -v --record-mode=rewrite
215
+ ```
216
+
217
+ ## License
218
+
219
+ Apache-2.0
@@ -0,0 +1,188 @@
1
+ # OpenInference Microsoft Agent Framework Instrumentation
2
+
3
+ OpenInference span processor for Microsoft Agent Framework that transforms native OpenTelemetry spans to OpenInference format for compatibility with OpenInference-compliant backends like [Arize Phoenix](https://github.com/Arize-ai/phoenix).
4
+
5
+ **Tested with agent-framework `1.0.0b260130` (January 30, 2026)**
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install openinference-instrumentation-agent-framework
11
+ ```
12
+
13
+ Install with agent-framework:
14
+ ```bash
15
+ pip install openinference-instrumentation-agent-framework[instruments]
16
+ ```
17
+
18
+ ## Overview
19
+
20
+ Microsoft Agent Framework emits telemetry using GenAI semantic conventions (`gen_ai.*` attributes). This package provides a `SpanProcessor` that transforms these spans to OpenInference format, enabling compatibility with observability tools that support the OpenInference standard.
21
+
22
+ **Note:** Agent Framework is in beta and its API may change between versions. This instrumentation tracks the latest stable release.
23
+
24
+ ## Usage
25
+
26
+ ### Basic Setup
27
+
28
+ ```python
29
+ from opentelemetry import trace
30
+ from opentelemetry.sdk.trace import TracerProvider
31
+ from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
32
+ from agent_framework.observability import enable_instrumentation
33
+ from openinference.instrumentation.agent_framework import (
34
+ AgentFrameworkToOpenInferenceProcessor
35
+ )
36
+
37
+ # Create tracer provider
38
+ tracer_provider = TracerProvider()
39
+
40
+ # Add OpenInference processor to transform spans
41
+ tracer_provider.add_span_processor(
42
+ AgentFrameworkToOpenInferenceProcessor()
43
+ )
44
+
45
+ # Add exporter (Console for demo, use OTLP for Phoenix)
46
+ tracer_provider.add_span_processor(
47
+ SimpleSpanProcessor(ConsoleSpanExporter())
48
+ )
49
+
50
+ # Set as global tracer provider
51
+ trace.set_tracer_provider(tracer_provider)
52
+
53
+ # Enable agent-framework instrumentation
54
+ enable_instrumentation(enable_sensitive_data=True)
55
+
56
+ # Use framework normally - spans will be transformed automatically
57
+ from agent_framework.openai import OpenAIChatClient
58
+
59
+ client = OpenAIChatClient(model_id="gpt-4o-mini", api_key="your-key")
60
+ agent = client.as_agent(name="Assistant", instructions="You are helpful.")
61
+ response = await agent.run("Hello!")
62
+ ```
63
+
64
+ ### With Phoenix (Arize)
65
+
66
+ ```python
67
+ from opentelemetry import trace
68
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
69
+ from opentelemetry.sdk.trace import TracerProvider
70
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
71
+ from agent_framework.observability import enable_instrumentation
72
+ from openinference.instrumentation.agent_framework import (
73
+ AgentFrameworkToOpenInferenceProcessor
74
+ )
75
+
76
+ # Configure Phoenix endpoint
77
+ endpoint = "http://localhost:6006/v1/traces"
78
+
79
+ # Setup tracer with OpenInference processor
80
+ tracer_provider = TracerProvider()
81
+ tracer_provider.add_span_processor(AgentFrameworkToOpenInferenceProcessor())
82
+ tracer_provider.add_span_processor(
83
+ SimpleSpanProcessor(OTLPSpanExporter(endpoint=endpoint))
84
+ )
85
+ trace.set_tracer_provider(tracer_provider)
86
+
87
+ # Enable instrumentation
88
+ enable_instrumentation(enable_sensitive_data=True)
89
+
90
+ # Your agent code here
91
+ ```
92
+
93
+ ## Transformation Details
94
+
95
+ ### Span Kind Mapping
96
+
97
+ | MS Agent Framework Operation | OpenInference Span Kind |
98
+ |------------------------------|-------------------------|
99
+ | `chat` | LLM |
100
+ | `execute_tool` | TOOL |
101
+ | `invoke_agent` | AGENT |
102
+ | `workflow.run` | CHAIN |
103
+ | `executor.process` | CHAIN |
104
+
105
+ ### Attribute Mapping
106
+
107
+ | Source (GenAI) | Target (OpenInference) |
108
+ |----------------|------------------------|
109
+ | `gen_ai.request.model` | `llm.model_name` |
110
+ | `gen_ai.provider.name` | `llm.provider` |
111
+ | `gen_ai.usage.input_tokens` | `llm.token_count.prompt` |
112
+ | `gen_ai.usage.output_tokens` | `llm.token_count.completion` |
113
+ | `gen_ai.input.messages` | `llm.input_messages.*` (flattened) |
114
+ | `gen_ai.output.messages` | `llm.output_messages.*` (flattened) |
115
+ | `gen_ai.tool.name` | `tool.name` |
116
+ | `gen_ai.tool.call.id` | `tool.call_id` |
117
+ | `gen_ai.tool.call.arguments` | `tool.parameters` |
118
+ | `gen_ai.conversation.id` | `session.id` |
119
+
120
+ ### Message Format Transformation
121
+
122
+ MS Agent Framework messages:
123
+ ```json
124
+ {
125
+ "role": "user",
126
+ "parts": [
127
+ {"type": "text", "content": "Hello"}
128
+ ]
129
+ }
130
+ ```
131
+
132
+ Transformed to OpenInference flattened format:
133
+ ```
134
+ llm.input_messages.0.message.role = "user"
135
+ llm.input_messages.0.message.content = "Hello"
136
+ ```
137
+
138
+ ## Debug Mode
139
+
140
+ Enable debug mode to log transformation details:
141
+
142
+ ```python
143
+ processor = AgentFrameworkToOpenInferenceProcessor(debug=True)
144
+ ```
145
+
146
+ ## Requirements
147
+
148
+ - Python >= 3.10, < 3.15
149
+ - opentelemetry-api >= 1.39.0
150
+ - opentelemetry-sdk >= 1.39.0
151
+ - openinference-semantic-conventions >= 0.1.25
152
+ - agent-framework >= 1.0.0b260130 (optional, install with `[instruments]` extra)
153
+
154
+ ## Important Notes
155
+
156
+ ### Agent Framework API Stability
157
+
158
+ Microsoft Agent Framework is in active beta development. API changes between versions are possible:
159
+ - This instrumentation is tested against `agent-framework==1.0.0b260130`
160
+ - The `-latest` test variant tracks breaking changes in new releases
161
+ - If you encounter API compatibility issues, pin to the tested version:
162
+ ```bash
163
+ pip install agent-framework==1.0.0b260130
164
+ ```
165
+
166
+ ### Sensitive Data
167
+
168
+ Set `enable_sensitive_data=True` when calling `enable_instrumentation()` to capture message content in traces. This is required for full observability but may include PII.
169
+
170
+ ## Development
171
+
172
+ ### Running Tests
173
+
174
+ Tests use VCR cassettes to replay recorded API interactions:
175
+
176
+ ```bash
177
+ # Run with recorded cassettes (no API key needed)
178
+ pytest tests/test_processor.py -v
179
+
180
+ # Re-record cassettes (requires OPENAI_API_KEY)
181
+ export OPENAI_API_KEY=your_key
182
+ rm -rf tests/cassettes/
183
+ pytest tests/test_processor.py -v --record-mode=rewrite
184
+ ```
185
+
186
+ ## License
187
+
188
+ Apache-2.0
@@ -0,0 +1,95 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "openinference-instrumentation-agent-framework"
7
+ dynamic = ["version"]
8
+ description = "OpenInference span processor for Microsoft Agent Framework - transforms native OpenTelemetry spans to OpenInference format"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10, <3.15"
12
+ authors = [
13
+ { name = "OpenInference Authors", email = "oss@arize.com" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ ]
27
+ dependencies = [
28
+ "opentelemetry-api>=1.39.0",
29
+ "opentelemetry-sdk>=1.39.0",
30
+ "opentelemetry-semantic-conventions>=0.52b0",
31
+ "openinference-semantic-conventions>=0.1.25",
32
+ "typing-extensions",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ instruments = [
37
+ "agent-framework >= 1.0.0b260130",
38
+ ]
39
+ test = [
40
+ "pytest",
41
+ "pytest-asyncio",
42
+ "pytest-recording",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/Arize-ai/openinference/tree/main/python/instrumentation/openinference-instrumentation-agent-framework"
47
+
48
+ [tool.hatch.version]
49
+ path = "src/openinference/instrumentation/agent_framework/__init__.py"
50
+ pattern = '__version__ = "(?P<version>[^"]+)"'
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ include = [
54
+ "/src",
55
+ ]
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["src/openinference"]
59
+
60
+ [tool.pytest.ini_options]
61
+ asyncio_mode = "auto"
62
+ asyncio_default_fixture_loop_scope = "function"
63
+ testpaths = [
64
+ "tests",
65
+ ]
66
+
67
+ [tool.mypy]
68
+ strict = true
69
+ explicit_package_bases = true
70
+ exclude = [
71
+ "examples",
72
+ "dist",
73
+ "sdist",
74
+ "tests",
75
+ ]
76
+
77
+ [[tool.mypy.overrides]]
78
+ ignore_missing_imports = true
79
+ module = [
80
+ "agent_framework.*",
81
+ "openinference.instrumentation.agent_framework.*",
82
+ ]
83
+
84
+ [tool.ruff]
85
+ line-length = 100
86
+ target-version = "py310"
87
+
88
+ [tool.ruff.lint.per-file-ignores]
89
+ "*.ipynb" = ["E402", "E501"]
90
+
91
+ [tool.ruff.lint]
92
+ select = ["E", "F", "W", "I"]
93
+
94
+ [tool.ruff.lint.isort]
95
+ force-single-line = false
@@ -0,0 +1,47 @@
1
+ """OpenInference instrumentation for Microsoft Agent Framework.
2
+
3
+ This package provides a SpanProcessor that transforms Microsoft Agent Framework's
4
+ native OpenTelemetry spans (using GenAI semantic conventions) to OpenInference format
5
+ for compatibility with OpenInference-compliant backends like Arize Phoenix.
6
+
7
+ Usage:
8
+ ```python
9
+ from opentelemetry import trace
10
+ from opentelemetry.sdk.trace import TracerProvider
11
+ from agent_framework.observability import configure_otel_providers
12
+ from openinference.instrumentation.agent_framework import (
13
+ AgentFrameworkToOpenInferenceProcessor
14
+ )
15
+
16
+ # Configure MS Agent Framework's native telemetry
17
+ # Set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT env var for Phoenix endpoint
18
+ configure_otel_providers(enable_sensitive_data=True)
19
+
20
+ # Add OpenInference processor to transform spans
21
+ tracer_provider = trace.get_tracer_provider()
22
+ if isinstance(tracer_provider, TracerProvider):
23
+ tracer_provider.add_span_processor(
24
+ AgentFrameworkToOpenInferenceProcessor()
25
+ )
26
+
27
+ # Use framework normally - spans will be transformed automatically
28
+ from agent_framework.openai import OpenAIChatClient
29
+
30
+ client = OpenAIChatClient(model_id="gpt-4o-mini")
31
+ agent = client.create_agent(name="Assistant")
32
+ response = await agent.run("Hello!")
33
+ ```
34
+ """
35
+
36
+ __version__ = "0.1.0"
37
+
38
+ from openinference.instrumentation.agent_framework.processor import (
39
+ AgentFrameworkToOpenInferenceProcessor,
40
+ )
41
+ from openinference.instrumentation.agent_framework.utils import is_openinference_span
42
+
43
+ __all__ = [
44
+ "AgentFrameworkToOpenInferenceProcessor",
45
+ "is_openinference_span",
46
+ "__version__",
47
+ ]
@@ -0,0 +1,2 @@
1
+ _instruments = ("agent-framework >= 0.1.0",)
2
+ _supports_metrics = False
@@ -0,0 +1,138 @@
1
+ """Microsoft Agent Framework to OpenInference Span Processor.
2
+
3
+ This module provides a span processor that converts Microsoft Agent Framework's native
4
+ OpenTelemetry spans (using GenAI semantic conventions) to OpenInference format for
5
+ compatibility with OpenInference-compliant backends like Arize Phoenix.
6
+
7
+ The processor transforms:
8
+ - GenAI attributes (gen_ai.*) to OpenInference attributes (llm.*, tool.*, etc.)
9
+ - Span names to OpenInference span kinds (AGENT, CHAIN, TOOL, LLM)
10
+ - Message structures to OpenInference flattened format
11
+ - Token usage attributes to OpenInference format
12
+ """
13
+
14
+ import logging
15
+ from typing import Any, Dict, Optional
16
+
17
+ from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor
18
+ from opentelemetry.trace import Status, StatusCode
19
+
20
+ from openinference.instrumentation.agent_framework import __version__
21
+ from openinference.instrumentation.agent_framework.semantic_conventions import get_attributes
22
+ from openinference.instrumentation.agent_framework.utils import SpanFilter, should_export_span
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class AgentFrameworkToOpenInferenceProcessor(SpanProcessor):
28
+ """
29
+ SpanProcessor that converts Microsoft Agent Framework telemetry attributes
30
+ to OpenInference format for compatibility with OpenInference-compliant backends.
31
+
32
+ This processor intercepts spans on completion and transforms their attributes
33
+ from the GenAI semantic conventions used by Microsoft Agent Framework to the
34
+ OpenInference semantic conventions.
35
+
36
+ Usage:
37
+ ```python
38
+ from opentelemetry.sdk.trace import TracerProvider
39
+ from openinference.instrumentation.agent_framework import (
40
+ AgentFrameworkToOpenInferenceProcessor
41
+ )
42
+
43
+ provider = TracerProvider()
44
+ provider.add_span_processor(
45
+ AgentFrameworkToOpenInferenceProcessor(debug=False)
46
+ )
47
+ ```
48
+ """
49
+
50
+ def __init__(self, debug: bool = False, span_filter: Optional[SpanFilter] = None) -> None:
51
+ """
52
+ Initialize the processor.
53
+
54
+ Args:
55
+ debug: Whether to log debug information about transformations
56
+ span_filter: Optional filter function to determine if a span should be processed
57
+ """
58
+ super().__init__()
59
+ self.debug = debug
60
+ self._span_filter = span_filter
61
+
62
+ def on_start(self, span: ReadableSpan, parent_context: Any = None) -> None:
63
+ """Called when a span is started. No-op for this processor."""
64
+ pass
65
+
66
+ def on_end(self, span: ReadableSpan) -> None:
67
+ """
68
+ Called when a span ends. Transform the span attributes from Microsoft Agent
69
+ Framework GenAI format to OpenInference format.
70
+ """
71
+ if not hasattr(span, "_attributes") or not span._attributes:
72
+ return
73
+
74
+ try:
75
+ # Get span context information
76
+ span_id = span.get_span_context().span_id # type: ignore[no-untyped-call]
77
+
78
+ # Get OpenInference attributes from the transformation function
79
+ openinference_attributes_iter = get_attributes(
80
+ dict(span._attributes), span.name, span_id
81
+ )
82
+ openinference_attributes = dict(openinference_attributes_iter)
83
+
84
+ # Merge with original attributes to preserve GenAI attributes that weren't transformed
85
+ span._attributes = {**span.attributes, **openinference_attributes} # type: ignore[dict-item]
86
+
87
+ # MS Agent Framework only sets ERROR status, not OK - set OK for successful spans
88
+ if not span.status.status_code == StatusCode.ERROR:
89
+ span._status = Status(status_code=StatusCode.OK)
90
+
91
+ # Determine if the span should be exported
92
+ if should_export_span(span, self._span_filter):
93
+ super().on_end(span)
94
+
95
+ if self.debug:
96
+ logger.info(
97
+ "span_name=<%s>, trans_attrs=<%d> | transformed span",
98
+ span.name,
99
+ len(openinference_attributes),
100
+ )
101
+
102
+ except Exception as e:
103
+ span._status = Status(status_code=StatusCode.ERROR, description=str(e))
104
+ logger.exception(e)
105
+ logger.warning(f"Error processing span in AgentFrameworkToOpenInferenceProcessor: {e}")
106
+
107
+ def shutdown(self) -> None:
108
+ """Shutdown the processor."""
109
+ pass
110
+
111
+ def force_flush(self, timeout_millis: Optional[int] = None) -> bool:
112
+ """Force flush any pending data."""
113
+ return True
114
+
115
+ def get_processor_info(self) -> Dict[str, Any]:
116
+ """Get information about this processor's capabilities."""
117
+ return {
118
+ "processor_name": "AgentFrameworkToOpenInferenceProcessor",
119
+ "version": __version__,
120
+ "debug_enabled": self.debug,
121
+ "supported_span_kinds": ["LLM", "AGENT", "CHAIN", "TOOL"],
122
+ "supported_operations": [
123
+ "chat",
124
+ "execute_tool",
125
+ "invoke_agent",
126
+ "create_agent",
127
+ "workflow.run",
128
+ "executor.process",
129
+ ],
130
+ "features": [
131
+ "Message extraction and transformation",
132
+ "Token usage mapping",
133
+ "Tool call processing",
134
+ "Graph node hierarchy mapping",
135
+ "Workflow/executor span support",
136
+ "Invocation parameters mapping",
137
+ ],
138
+ }
@@ -0,0 +1,587 @@
1
+ """Semantic conventions for Microsoft Agent Framework telemetry.
2
+
3
+ This module defines attribute constants used by Microsoft Agent Framework's
4
+ OpenTelemetry instrumentation and provides transformation functions to convert
5
+ GenAI attributes to OpenInference format.
6
+
7
+ Reference: agent_framework/observability.py OtelAttr enum
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ from typing import Any, Dict, Iterator, List, Optional, Tuple
13
+
14
+ logger = logging.getLogger(__name__)
15
+ logger.addHandler(logging.NullHandler())
16
+
17
+ # Operation names
18
+ OPERATION = "gen_ai.operation.name"
19
+ CHAT_COMPLETION_OPERATION = "chat"
20
+ TOOL_EXECUTION_OPERATION = "execute_tool"
21
+ AGENT_INVOKE_OPERATION = "invoke_agent"
22
+
23
+ # Provider and system
24
+ PROVIDER_NAME = "gen_ai.provider.name"
25
+
26
+ # Request attributes
27
+ LLM_REQUEST_MODEL = "gen_ai.request.model"
28
+ LLM_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens"
29
+ LLM_REQUEST_TEMPERATURE = "gen_ai.request.temperature"
30
+ LLM_REQUEST_TOP_P = "gen_ai.request.top_p"
31
+
32
+ # Response attributes
33
+ LLM_RESPONSE_MODEL = "gen_ai.response.model"
34
+ FINISH_REASONS = "gen_ai.response.finish_reasons"
35
+ RESPONSE_ID = "gen_ai.response.id"
36
+
37
+ # Usage attributes
38
+ INPUT_TOKENS = "gen_ai.usage.input_tokens"
39
+ OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
40
+
41
+ # Tool attributes
42
+ TOOL_CALL_ID = "gen_ai.tool.call.id"
43
+ TOOL_DESCRIPTION = "gen_ai.tool.description"
44
+ TOOL_NAME = "gen_ai.tool.name"
45
+ TOOL_DEFINITIONS = "gen_ai.tool.definitions"
46
+ TOOL_ARGUMENTS = "gen_ai.tool.call.arguments"
47
+ TOOL_RESULT = "gen_ai.tool.call.result"
48
+
49
+ # Agent attributes
50
+ AGENT_ID = "gen_ai.agent.id"
51
+ AGENT_NAME = "gen_ai.agent.name"
52
+ AGENT_DESCRIPTION = "gen_ai.agent.description"
53
+ CONVERSATION_ID = "gen_ai.conversation.id"
54
+
55
+ # Message attributes
56
+ INPUT_MESSAGES = "gen_ai.input.messages"
57
+ OUTPUT_MESSAGES = "gen_ai.output.messages"
58
+ SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions"
59
+
60
+ # Workflow attributes
61
+ WORKFLOW_ID = "workflow.id"
62
+ WORKFLOW_NAME = "workflow.name"
63
+ WORKFLOW_RUN_SPAN = "workflow.run"
64
+
65
+ # Executor attributes
66
+ EXECUTOR_ID = "executor.id"
67
+ EXECUTOR_TYPE = "executor.type"
68
+ EXECUTOR_PROCESS_SPAN = "executor.process"
69
+
70
+ # Edge group attributes
71
+ EDGE_GROUP_TYPE = "edge_group.type"
72
+ EDGE_GROUP_ID = "edge_group.id"
73
+
74
+
75
+ def safe_json_dumps(obj: Any) -> str:
76
+ """Safely serialize an object to JSON string.
77
+
78
+ Args:
79
+ obj: Object to serialize
80
+
81
+ Returns:
82
+ JSON string representation
83
+ """
84
+ try:
85
+ return json.dumps(obj, default=str, ensure_ascii=False)
86
+ except (TypeError, ValueError):
87
+ return str(obj)
88
+
89
+
90
+ def get_attributes(
91
+ attrs: Dict[str, Any], span_name: str, span_id: int
92
+ ) -> Iterator[Tuple[str, Any]]:
93
+ """
94
+ Extract OpenInference attributes from Microsoft Agent Framework GenAI attributes.
95
+
96
+ Args:
97
+ attrs: Original span attributes with GenAI semantic conventions
98
+ span_name: The span name
99
+ span_id: The span context ID
100
+
101
+ Returns:
102
+ Iterator of (key, value) pairs for OpenInference attributes
103
+ """
104
+ # Determine span kind first as it affects other attribute extraction
105
+ span_kind = _determine_span_kind(span_name, attrs)
106
+ yield "openinference.span.kind", span_kind
107
+
108
+ # Extract graph node attributes for visualization hierarchy
109
+ yield from _extract_graph_node_attributes(span_id, attrs, span_kind)
110
+
111
+ # Extract model and provider info
112
+ yield from _extract_model_info(attrs)
113
+
114
+ # Extract messages (input and output)
115
+ input_messages, output_messages = _extract_messages(attrs)
116
+
117
+ # Extract token usage
118
+ yield from _extract_token_usage(attrs)
119
+
120
+ # Extract span-kind specific attributes
121
+ if span_kind in ["LLM", "AGENT"]:
122
+ yield from _extract_llm_agent_attributes(attrs, input_messages, output_messages, span_kind)
123
+ elif span_kind == "TOOL":
124
+ yield from _extract_tool_attributes(attrs)
125
+ elif span_kind == "CHAIN":
126
+ yield from _extract_chain_attributes(attrs, input_messages, output_messages)
127
+
128
+ # Extract session and invocation parameters
129
+ yield from _extract_session_info(attrs)
130
+ yield from _extract_invocation_parameters(attrs)
131
+
132
+ # Add remaining attributes as metadata
133
+ yield from _extract_metadata(attrs)
134
+
135
+
136
+ def _determine_span_kind(span_name: str, attrs: Dict[str, Any]) -> str:
137
+ """Determine the OpenInference span kind based on MS Agent Framework operation."""
138
+ operation = attrs.get(OPERATION, "")
139
+
140
+ if operation == CHAT_COMPLETION_OPERATION:
141
+ return "LLM"
142
+ elif operation == TOOL_EXECUTION_OPERATION:
143
+ return "TOOL"
144
+ elif operation == AGENT_INVOKE_OPERATION:
145
+ return "AGENT"
146
+
147
+ if span_name.startswith("chat "):
148
+ return "LLM"
149
+ elif span_name.startswith("execute_tool "):
150
+ return "TOOL"
151
+ elif span_name.startswith("invoke_agent "):
152
+ return "AGENT"
153
+ elif span_name.startswith(WORKFLOW_RUN_SPAN):
154
+ return "CHAIN"
155
+ elif span_name.startswith(EXECUTOR_PROCESS_SPAN):
156
+ return "CHAIN"
157
+
158
+ if attrs.get(AGENT_NAME) or attrs.get(AGENT_ID):
159
+ return "AGENT"
160
+ if attrs.get(WORKFLOW_ID) or attrs.get(WORKFLOW_NAME):
161
+ return "CHAIN"
162
+ if attrs.get(EXECUTOR_ID) or attrs.get(EXECUTOR_TYPE):
163
+ return "CHAIN"
164
+
165
+ return "CHAIN"
166
+
167
+
168
+ def _extract_graph_node_attributes(
169
+ span_id: int, attrs: Dict[str, Any], span_kind: str
170
+ ) -> Iterator[Tuple[str, Any]]:
171
+ """Set graph node attributes for visualization hierarchy."""
172
+ if span_kind == "AGENT":
173
+ agent_id = attrs.get(AGENT_ID, span_id)
174
+ yield "graph.node.id", f"agent_{agent_id}"
175
+ if agent_name := attrs.get(AGENT_NAME):
176
+ yield "graph.node.name", agent_name
177
+
178
+ elif span_kind == "LLM":
179
+ yield "graph.node.id", f"llm_{span_id}"
180
+ if agent_id := attrs.get(AGENT_ID):
181
+ yield "graph.node.parent_id", f"agent_{agent_id}"
182
+
183
+ elif span_kind == "TOOL":
184
+ tool_name = attrs.get(TOOL_NAME, "unknown_tool")
185
+ yield "graph.node.id", f"tool_{tool_name}_{span_id}"
186
+ yield "graph.node.name", tool_name
187
+
188
+ elif span_kind == "CHAIN":
189
+ if workflow_id := attrs.get(WORKFLOW_ID):
190
+ yield "graph.node.id", f"workflow_{workflow_id}"
191
+ if workflow_name := attrs.get(WORKFLOW_NAME):
192
+ yield "graph.node.name", workflow_name
193
+ elif executor_id := attrs.get(EXECUTOR_ID):
194
+ yield "graph.node.id", f"executor_{executor_id}"
195
+ if executor_type := attrs.get(EXECUTOR_TYPE):
196
+ yield "graph.node.name", executor_type
197
+ elif edge_group_id := attrs.get(EDGE_GROUP_ID):
198
+ yield "graph.node.id", f"edge_group_{edge_group_id}"
199
+ if edge_group_type := attrs.get(EDGE_GROUP_TYPE):
200
+ yield "graph.node.name", edge_group_type
201
+ else:
202
+ yield "graph.node.id", f"chain_{span_id}"
203
+
204
+
205
+ def _extract_model_info(attrs: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
206
+ """Map model and provider information to OpenInference format."""
207
+ model_name = attrs.get(LLM_REQUEST_MODEL) or attrs.get(LLM_RESPONSE_MODEL)
208
+ if model_name:
209
+ yield "llm.model_name", model_name
210
+
211
+ if provider := attrs.get(PROVIDER_NAME):
212
+ yield "llm.provider", provider
213
+
214
+
215
+ def _extract_messages(attrs: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
216
+ """Extract input and output messages from attributes."""
217
+ input_messages: List[Dict[str, Any]] = []
218
+ output_messages: List[Dict[str, Any]] = []
219
+
220
+ if input_msgs_raw := attrs.get(INPUT_MESSAGES):
221
+ input_messages = _parse_messages(input_msgs_raw)
222
+ if output_msgs_raw := attrs.get(OUTPUT_MESSAGES):
223
+ output_messages = _parse_messages(output_msgs_raw)
224
+
225
+ return input_messages, output_messages
226
+
227
+
228
+ def _parse_messages(messages_raw: Any) -> List[Dict[str, Any]]:
229
+ """Parse messages from MS Agent Framework format."""
230
+ messages: List[Dict[str, Any]] = []
231
+
232
+ try:
233
+ if isinstance(messages_raw, str):
234
+ messages_data = json.loads(messages_raw)
235
+ else:
236
+ messages_data = messages_raw
237
+
238
+ if not isinstance(messages_data, list):
239
+ return messages
240
+
241
+ for msg in messages_data:
242
+ if not isinstance(msg, dict):
243
+ continue
244
+
245
+ parsed_msg = _parse_single_message(msg)
246
+ if parsed_msg:
247
+ messages.append(parsed_msg)
248
+
249
+ except (json.JSONDecodeError, TypeError) as e:
250
+ logger.debug(f"Failed to parse messages: {e}")
251
+
252
+ return messages
253
+
254
+
255
+ def _parse_single_message(msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
256
+ """Parse a single message from MS Agent Framework format to OpenInference format."""
257
+ role = msg.get("role", "user")
258
+ parts = msg.get("parts") or []
259
+
260
+ result: Dict[str, Any] = {"message.role": role}
261
+
262
+ text_content: List[str] = []
263
+ tool_calls: List[Dict[str, Any]] = []
264
+
265
+ for part in parts:
266
+ if not isinstance(part, dict):
267
+ continue
268
+
269
+ part_type = part.get("type", "")
270
+
271
+ if part_type == "text":
272
+ if content := part.get("content"):
273
+ text_content.append(str(content))
274
+
275
+ elif part_type == "reasoning":
276
+ if content := part.get("content"):
277
+ text_content.append(str(content))
278
+
279
+ elif part_type == "tool_call":
280
+ tool_call = {
281
+ "tool_call.id": part.get("id", ""),
282
+ "tool_call.function.name": part.get("name", ""),
283
+ "tool_call.function.arguments": safe_json_dumps(part.get("arguments", {})),
284
+ }
285
+ tool_calls.append(tool_call)
286
+
287
+ elif part_type == "tool_call_response":
288
+ result["message.role"] = "tool"
289
+ result["message.tool_call_id"] = part.get("id", "")
290
+ response = part.get("response", {})
291
+ if isinstance(response, dict):
292
+ text_content.append(safe_json_dumps(response))
293
+ else:
294
+ text_content.append(str(response))
295
+
296
+ if text_content:
297
+ result["message.content"] = " ".join(text_content)
298
+ if tool_calls:
299
+ result["message.tool_calls"] = tool_calls
300
+
301
+ if "message.content" not in result and "message.tool_calls" not in result:
302
+ return None
303
+
304
+ return result
305
+
306
+
307
+ def _extract_token_usage(attrs: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
308
+ """Map token usage from GenAI format to OpenInference format."""
309
+ input_tokens = attrs.get(INPUT_TOKENS)
310
+ if input_tokens is not None:
311
+ yield "llm.token_count.prompt", input_tokens
312
+ output_tokens = attrs.get(OUTPUT_TOKENS)
313
+ if output_tokens is not None:
314
+ yield "llm.token_count.completion", output_tokens
315
+ if input_tokens is not None and output_tokens is not None:
316
+ yield "llm.token_count.total", input_tokens + output_tokens
317
+
318
+
319
+ def _extract_llm_agent_attributes(
320
+ attrs: Dict[str, Any],
321
+ input_messages: List[Dict[str, Any]],
322
+ output_messages: List[Dict[str, Any]],
323
+ span_kind: str,
324
+ ) -> Iterator[Tuple[str, Any]]:
325
+ """Handle LLM and AGENT span attributes."""
326
+ if input_messages:
327
+ yield "llm.input_messages", safe_json_dumps(input_messages)
328
+ yield from _flatten_messages(input_messages, "llm.input_messages")
329
+
330
+ if output_messages:
331
+ yield "llm.output_messages", safe_json_dumps(output_messages)
332
+ yield from _flatten_messages(output_messages, "llm.output_messages")
333
+
334
+ yield from _create_input_output_values(attrs, input_messages, output_messages, span_kind)
335
+
336
+ if attrs.get(SYSTEM_INSTRUCTIONS) or attrs.get(AGENT_NAME):
337
+ yield "llm.system", "microsoft.agent_framework"
338
+
339
+ if tool_defs := attrs.get(TOOL_DEFINITIONS):
340
+ yield from _map_tools(tool_defs)
341
+
342
+
343
+ def _flatten_messages(messages: List[Dict[str, Any]], key_prefix: str) -> Iterator[Tuple[str, Any]]:
344
+ """Flatten messages to dotted attribute notation for OpenInference."""
345
+ for idx, msg in enumerate(messages):
346
+ for key, value in msg.items():
347
+ clean_key = key.replace("message.", "") if key.startswith("message.") else key
348
+ dotted_key = f"{key_prefix}.{idx}.message.{clean_key}"
349
+
350
+ if clean_key == "tool_calls" and isinstance(value, list):
351
+ for tool_idx, tool_call in enumerate(value):
352
+ if isinstance(tool_call, dict):
353
+ for tool_key, tool_val in tool_call.items():
354
+ tool_dotted_key = (
355
+ f"{key_prefix}.{idx}.message.tool_calls.{tool_idx}.{tool_key}"
356
+ )
357
+ yield tool_dotted_key, _serialize_value(tool_val)
358
+ else:
359
+ yield dotted_key, _serialize_value(value)
360
+
361
+
362
+ def _create_input_output_values(
363
+ attrs: Dict[str, Any],
364
+ input_messages: List[Dict[str, Any]],
365
+ output_messages: List[Dict[str, Any]],
366
+ span_kind: str,
367
+ ) -> Iterator[Tuple[str, Any]]:
368
+ """Create input.value and output.value attributes."""
369
+ model_name = attrs.get(LLM_REQUEST_MODEL, "unknown")
370
+
371
+ if span_kind in ["LLM", "AGENT"]:
372
+ if input_messages:
373
+ if len(input_messages) == 1 and input_messages[0].get("message.role") == "user":
374
+ yield "input.value", input_messages[0].get("message.content", "")
375
+ yield "input.mime_type", "text/plain"
376
+ else:
377
+ input_structure = {"messages": input_messages, "model": model_name}
378
+ yield "input.value", safe_json_dumps(input_structure)
379
+ yield "input.mime_type", "application/json"
380
+
381
+ if output_messages:
382
+ last_message = output_messages[-1]
383
+ content = last_message.get("message.content", "")
384
+
385
+ if span_kind == "LLM":
386
+ finish_reasons = attrs.get(FINISH_REASONS)
387
+ finish_reason = "stop"
388
+ if finish_reasons:
389
+ try:
390
+ reasons = (
391
+ json.loads(finish_reasons)
392
+ if isinstance(finish_reasons, str)
393
+ else finish_reasons
394
+ )
395
+ if isinstance(reasons, list) and reasons:
396
+ finish_reason = reasons[0]
397
+ except (json.JSONDecodeError, TypeError):
398
+ pass
399
+
400
+ # Get token counts if available
401
+ completion_tokens = attrs.get(OUTPUT_TOKENS)
402
+ prompt_tokens = attrs.get(INPUT_TOKENS)
403
+ total_tokens = None
404
+ if completion_tokens is not None and prompt_tokens is not None:
405
+ total_tokens = completion_tokens + prompt_tokens
406
+
407
+ output_structure = {
408
+ "choices": [
409
+ {
410
+ "finish_reason": finish_reason,
411
+ "index": 0,
412
+ "message": {
413
+ "content": content,
414
+ "role": last_message.get("message.role", "assistant"),
415
+ },
416
+ }
417
+ ],
418
+ "model": model_name,
419
+ "usage": {
420
+ "completion_tokens": completion_tokens,
421
+ "prompt_tokens": prompt_tokens,
422
+ "total_tokens": total_tokens,
423
+ },
424
+ }
425
+ yield "output.value", safe_json_dumps(output_structure)
426
+ yield "output.mime_type", "application/json"
427
+ else:
428
+ yield "output.value", content
429
+ yield "output.mime_type", "text/plain"
430
+
431
+
432
+ def _extract_tool_attributes(attrs: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
433
+ """Handle TOOL span attributes."""
434
+ if tool_name := attrs.get(TOOL_NAME):
435
+ yield "tool.name", tool_name
436
+ if tool_call_id := attrs.get(TOOL_CALL_ID):
437
+ yield "tool.call_id", tool_call_id
438
+ if tool_desc := attrs.get(TOOL_DESCRIPTION):
439
+ yield "tool.description", tool_desc
440
+
441
+ if tool_args := attrs.get(TOOL_ARGUMENTS):
442
+ if isinstance(tool_args, str):
443
+ yield "tool.parameters", tool_args
444
+ yield "input.value", tool_args
445
+ else:
446
+ yield "tool.parameters", safe_json_dumps(tool_args)
447
+ yield "input.value", safe_json_dumps(tool_args)
448
+ yield "input.mime_type", "application/json"
449
+
450
+ if tool_result := attrs.get(TOOL_RESULT):
451
+ if isinstance(tool_result, str):
452
+ yield "output.value", tool_result
453
+ else:
454
+ yield "output.value", safe_json_dumps(tool_result)
455
+ yield "output.mime_type", "text/plain"
456
+
457
+
458
+ def _extract_chain_attributes(
459
+ attrs: Dict[str, Any],
460
+ input_messages: List[Dict[str, Any]],
461
+ output_messages: List[Dict[str, Any]],
462
+ ) -> Iterator[Tuple[str, Any]]:
463
+ """Handle CHAIN span attributes (workflows, executors)."""
464
+ if input_messages:
465
+ for msg in input_messages:
466
+ if msg.get("message.role") == "user":
467
+ if content := msg.get("message.content"):
468
+ yield "input.value", content
469
+ yield "input.mime_type", "text/plain"
470
+ break
471
+
472
+ if output_messages:
473
+ for msg in reversed(output_messages):
474
+ if msg.get("message.role") == "assistant":
475
+ if content := msg.get("message.content"):
476
+ yield "output.value", content
477
+ yield "output.mime_type", "text/plain"
478
+ break
479
+
480
+
481
+ def _extract_session_info(attrs: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
482
+ """Map session and conversation info."""
483
+ if conversation_id := attrs.get(CONVERSATION_ID):
484
+ yield "session.id", conversation_id
485
+
486
+
487
+ def _extract_invocation_parameters(attrs: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
488
+ """Map invocation parameters to OpenInference format."""
489
+ params: Dict[str, Any] = {}
490
+
491
+ param_mappings = {
492
+ LLM_REQUEST_MAX_TOKENS: "max_tokens",
493
+ LLM_REQUEST_TEMPERATURE: "temperature",
494
+ LLM_REQUEST_TOP_P: "top_p",
495
+ }
496
+
497
+ for attr_key, param_key in param_mappings.items():
498
+ if attr_key in attrs:
499
+ params[param_key] = attrs[attr_key]
500
+
501
+ if params:
502
+ yield "llm.invocation_parameters", safe_json_dumps(params)
503
+
504
+
505
+ def _map_tools(tools_data: Any) -> Iterator[Tuple[str, Any]]:
506
+ """Map tool definitions to OpenInference format."""
507
+ try:
508
+ if isinstance(tools_data, str):
509
+ tools_data = json.loads(tools_data)
510
+
511
+ if not isinstance(tools_data, list):
512
+ return
513
+
514
+ for idx, tool in enumerate(tools_data):
515
+ if isinstance(tool, dict):
516
+ if name := tool.get("name"):
517
+ yield f"llm.tools.{idx}.tool.name", name
518
+ if desc := tool.get("description"):
519
+ yield f"llm.tools.{idx}.tool.description", desc
520
+ # Handle function schema
521
+ if "function" in tool:
522
+ func = tool["function"]
523
+ if isinstance(func, dict):
524
+ if fname := func.get("name"):
525
+ yield f"llm.tools.{idx}.tool.name", fname
526
+ if fdesc := func.get("description"):
527
+ yield f"llm.tools.{idx}.tool.description", fdesc
528
+ if params := func.get("parameters"):
529
+ yield f"llm.tools.{idx}.tool.json_schema", safe_json_dumps(params)
530
+ elif params := tool.get("parameters"):
531
+ yield f"llm.tools.{idx}.tool.json_schema", safe_json_dumps(params)
532
+ elif input_schema := tool.get("input_schema"):
533
+ yield f"llm.tools.{idx}.tool.json_schema", safe_json_dumps(input_schema)
534
+
535
+ except (json.JSONDecodeError, TypeError) as e:
536
+ logger.debug(f"Failed to parse tools: {e}")
537
+
538
+
539
+ def _extract_metadata(attrs: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
540
+ """Add remaining attributes as metadata."""
541
+ metadata: Dict[str, Any] = {}
542
+ skip_keys = {
543
+ OPERATION,
544
+ PROVIDER_NAME,
545
+ LLM_REQUEST_MODEL,
546
+ LLM_RESPONSE_MODEL,
547
+ INPUT_TOKENS,
548
+ OUTPUT_TOKENS,
549
+ INPUT_MESSAGES,
550
+ OUTPUT_MESSAGES,
551
+ TOOL_NAME,
552
+ TOOL_CALL_ID,
553
+ TOOL_ARGUMENTS,
554
+ TOOL_RESULT,
555
+ TOOL_DESCRIPTION,
556
+ TOOL_DEFINITIONS,
557
+ AGENT_ID,
558
+ AGENT_NAME,
559
+ AGENT_DESCRIPTION,
560
+ CONVERSATION_ID,
561
+ FINISH_REASONS,
562
+ RESPONSE_ID,
563
+ SYSTEM_INSTRUCTIONS,
564
+ LLM_REQUEST_MAX_TOKENS,
565
+ LLM_REQUEST_TEMPERATURE,
566
+ LLM_REQUEST_TOP_P,
567
+ WORKFLOW_ID,
568
+ WORKFLOW_NAME,
569
+ EXECUTOR_ID,
570
+ EXECUTOR_TYPE,
571
+ EDGE_GROUP_ID,
572
+ EDGE_GROUP_TYPE,
573
+ }
574
+
575
+ for key, value in attrs.items():
576
+ if key not in skip_keys:
577
+ metadata[key] = _serialize_value(value)
578
+
579
+ if metadata:
580
+ yield "metadata", safe_json_dumps(metadata)
581
+
582
+
583
+ def _serialize_value(value: Any) -> Any:
584
+ """Serialize a value for span attributes."""
585
+ if isinstance(value, (str, int, float, bool)) or value is None:
586
+ return value
587
+ return safe_json_dumps(value)
@@ -0,0 +1,23 @@
1
+ from typing import Callable, Optional
2
+
3
+ from opentelemetry.sdk.trace import ReadableSpan
4
+
5
+ from openinference.semconv.trace import SpanAttributes
6
+
7
+ # Define types for span filtering
8
+ SpanFilter = Callable[[ReadableSpan], bool]
9
+
10
+
11
+ def is_openinference_span(span: ReadableSpan) -> bool:
12
+ """Check if a span is an OpenInference span."""
13
+ if span.attributes is None:
14
+ return False
15
+ return SpanAttributes.OPENINFERENCE_SPAN_KIND in span.attributes
16
+
17
+
18
+ def should_export_span(span: ReadableSpan, span_filter: Optional[SpanFilter] = None) -> bool:
19
+ """Determine if a span should be exported based on a filter."""
20
+ if span_filter is None:
21
+ return True
22
+
23
+ return span_filter(span)