From 9e324067904aebbafbbd3789b16af82d8f2afe3c Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 11:30:43 -0500 Subject: [PATCH 01/17] Add af extension --- .../README.md | 0 .../extensions/agentframework/__init__.py | 1 + .../agentframework/span_processor.py | 36 +++++++++ .../agentframework/trace_instrumentor.py | 48 ++++++++++++ .../pyproject.toml | 75 +++++++++++++++++++ .../setup.py | 30 ++++++++ 6 files changed, 190 insertions(+) create mode 100644 libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md create mode 100644 libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py create mode 100644 libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py create mode 100644 libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py create mode 100644 libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml create mode 100644 libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md b/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md new file mode 100644 index 00000000..e69de29b diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py new file mode 100644 index 00000000..2a50eae8 --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py new file mode 100644 index 00000000..0dcd87f8 --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft. All rights reserved. + +# Custom Span Processor + +from opentelemetry.sdk.trace.export import SpanProcessor + +from microsoft_agents_a365.observability.core.constants import GEN_AI_OPERATION_NAME_KEY +from microsoft_agents_a365.observability.core.inference_operation_type import InferenceOperationType +from microsoft_agents_a365.observability.core.wrappers.utils import extract_model_name + + +class AgentFrameworkSpanProcessor(SpanProcessor): + """ + SpanProcessor for Agent Framework. + """ + + def __init__(self, service_name: str | None = None): + self.service_name = service_name + + def on_start(self, span, parent_context): + pass + + def on_end(self, span, parent_context): + EXECUTE_TOOL_OPERATION = "execute_tool" + TOOL_CALL_RESULT_TAG = "gen_ai.tool.call.result" + EVENT_CONTENT_TAG = "gen_ai.event.content" + if hasattr(span, "attributes"): + operation_name = span.attributes.get(GEN_AI_OPERATION_NAME_KEY) + if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION: + tool_call_result = span.attributes.get(TOOL_CALL_RESULT_TAG) + if tool_call_result is not None: + span.set_attribute(EVENT_CONTENT_TAG, tool_call_result) + + + + \ No newline at end of file diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py new file mode 100644 index 00000000..2bcb728a --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +from collections.abc import Collection +from typing import Any + +from microsoft_agents_a365.observability.core.config import get_tracer_provider, is_configured +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + +from microsoft_agents_a365.observability.extensions.agentframework.span_processor import ( + AgentFrameworkSpanProcessor, +) + +# ----------------------------- +# 3) The Instrumentor class +# ----------------------------- +_instruments = ("semantic-kernel >= 1.0.0",) + + +class AgentFrameworkInstrumentor(BaseInstrumentor): + """ + Instruments Agent Framework: + • Installs your custom OTel SpanProcessor + """ + + def __init__(self): + if not is_configured(): + raise RuntimeError( + "Agent365 (or your telemetry config) is not initialized. Configure it before instrumenting." + ) + super().__init__() + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs: Any) -> None: + """ + kwargs (all optional): + """ + + # Ensure we have an SDK TracerProvider + provider = get_tracer_provider() + self._processor = AgentFrameworkSpanProcessor() + provider.add_span_processor(self._processor) + + def _uninstrument(self, **kwargs: Any) -> None: + pass diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml b/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml new file mode 100644 index 00000000..d160874a --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml @@ -0,0 +1,75 @@ +[build-system] +requires = ["setuptools>=68", "wheel", "tzdata"] +build-backend = "setuptools.build_meta" + +[project] +name = "microsoft-agents-a365-observability-extensions-agent-framework" +dynamic = ["version"] +authors = [ + { name = "Microsoft", email = "support@microsoft.com" }, +] +description = "Agent Framwork observability and tracing extensions for Microsoft Agents A365" +readme = "README.md" +requires-python = ">=3.11" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: System :: Monitoring", +] +license = "MIT" +keywords = ["observability", "telemetry", "tracing", "opentelemetry", "agent-framework", "agents", "ai"] +dependencies = [ + "microsoft-agents-a365-observability-core >= 2025.10.16", + "opentelemetry-api >= 1.36.0", + "opentelemetry-sdk >= 1.36.0", + "opentelemetry-instrumentation >= 0.47b0", +] + +[project.urls] +Homepage = "https://github.com/microsoft/Agent365" +Repository = "https://github.com/microsoft/Agent365" +Issues = "https://github.com/microsoft/Agent365/issues" +Documentation = "https://github.com/microsoft/Agent365-python/tree/main/libraries/microsoft-agents-a365-observability-extensions-agentframework" + +[project.optional-dependencies] +dev = [ + "pytest >= 7.0.0", + "pytest-asyncio >= 0.21.0", + "ruff >= 0.1.0", + "black >= 23.0.0", + "mypy >= 1.0.0", +] +test = [ + "pytest >= 7.0.0", + "pytest-asyncio >= 0.21.0", +] + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools] +license-files = ["../../LICENSE"] +include-package-data = true + +[tool.setuptools.package-data] +"*" = ["../../LICENSE"] + +[tool.black] +line-length = 100 +target-version = ['py311'] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.mypy] +python_version = "3.11" +strict = true +warn_return_any = true +warn_unused_configs = true \ No newline at end of file diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py new file mode 100644 index 00000000..885895ea --- /dev/null +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os +from datetime import datetime +from zoneinfo import ZoneInfo + +from setuptools import setup + + +def build_version(): + """ + Example: 2025.10.3+preview.65532 (PEP 440 compliant; avoids hyphens) + Uses UTC. + """ + + if defined_version := os.getenv("A365_SDK_VERSION"): + return defined_version # For CI/CD to set a specific version. + + today = datetime.now(ZoneInfo("UTC")) + + return ( + f"{today.year}.{today.month}.{today.day}+preview.{today.hour}{today.minute}{today.second}" + ) + + +VERSION = build_version() + +setup( + version=VERSION, +) From 10386929f82d7a698b64fcf9391cbcd3950f5707 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 11:47:02 -0500 Subject: [PATCH 02/17] reformat --- .../agentframework/span_processor.py | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index 0dcd87f8..dbe91d7c 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -16,21 +16,18 @@ class AgentFrameworkSpanProcessor(SpanProcessor): def __init__(self, service_name: str | None = None): self.service_name = service_name - + super().__init__() + def on_start(self, span, parent_context): pass def on_end(self, span, parent_context): - EXECUTE_TOOL_OPERATION = "execute_tool" - TOOL_CALL_RESULT_TAG = "gen_ai.tool.call.result" - EVENT_CONTENT_TAG = "gen_ai.event.content" - if hasattr(span, "attributes"): - operation_name = span.attributes.get(GEN_AI_OPERATION_NAME_KEY) - if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION: - tool_call_result = span.attributes.get(TOOL_CALL_RESULT_TAG) - if tool_call_result is not None: - span.set_attribute(EVENT_CONTENT_TAG, tool_call_result) - - - - \ No newline at end of file + EXECUTE_TOOL_OPERATION = "execute_tool" + TOOL_CALL_RESULT_TAG = "gen_ai.tool.call.result" + EVENT_CONTENT_TAG = "gen_ai.event.content" + if hasattr(span, "attributes"): + operation_name = span.attributes.get(GEN_AI_OPERATION_NAME_KEY) + if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION: + tool_call_result = span.attributes.get(TOOL_CALL_RESULT_TAG) + if tool_call_result is not None: + span.set_attribute(EVENT_CONTENT_TAG, tool_call_result) \ No newline at end of file From bf9c4696540a237deee904061b4b1c059b1d8ab9 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 11:59:31 -0500 Subject: [PATCH 03/17] clean up unsused import --- .../extensions/agentframework/span_processor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index dbe91d7c..bfc21d2b 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -4,9 +4,10 @@ from opentelemetry.sdk.trace.export import SpanProcessor -from microsoft_agents_a365.observability.core.constants import GEN_AI_OPERATION_NAME_KEY -from microsoft_agents_a365.observability.core.inference_operation_type import InferenceOperationType -from microsoft_agents_a365.observability.core.wrappers.utils import extract_model_name +from microsoft_agents_a365.observability.core.constants import ( + GEN_AI_OPERATION_NAME_KEY, + EXECUTE_TOOL_OPERATION_NAME, + GEN_AI_EVENT_CONTENT) class AgentFrameworkSpanProcessor(SpanProcessor): @@ -22,12 +23,10 @@ def on_start(self, span, parent_context): pass def on_end(self, span, parent_context): - EXECUTE_TOOL_OPERATION = "execute_tool" TOOL_CALL_RESULT_TAG = "gen_ai.tool.call.result" - EVENT_CONTENT_TAG = "gen_ai.event.content" if hasattr(span, "attributes"): operation_name = span.attributes.get(GEN_AI_OPERATION_NAME_KEY) - if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION: + if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION_NAME: tool_call_result = span.attributes.get(TOOL_CALL_RESULT_TAG) if tool_call_result is not None: - span.set_attribute(EVENT_CONTENT_TAG, tool_call_result) \ No newline at end of file + span.set_attribute(GEN_AI_EVENT_CONTENT, tool_call_result) \ No newline at end of file From e6e337741fb2753170254f927a085f4d5b16742a Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 12:29:47 -0500 Subject: [PATCH 04/17] Clean up --- .../extensions/agentframework/span_processor.py | 8 +++++--- .../pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index bfc21d2b..51160997 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -7,7 +7,8 @@ from microsoft_agents_a365.observability.core.constants import ( GEN_AI_OPERATION_NAME_KEY, EXECUTE_TOOL_OPERATION_NAME, - GEN_AI_EVENT_CONTENT) + GEN_AI_EVENT_CONTENT +) class AgentFrameworkSpanProcessor(SpanProcessor): @@ -15,6 +16,8 @@ class AgentFrameworkSpanProcessor(SpanProcessor): SpanProcessor for Agent Framework. """ + TOOL_CALL_RESULT_TAG = "gen_ai.tool.call.result" + def __init__(self, service_name: str | None = None): self.service_name = service_name super().__init__() @@ -23,10 +26,9 @@ def on_start(self, span, parent_context): pass def on_end(self, span, parent_context): - TOOL_CALL_RESULT_TAG = "gen_ai.tool.call.result" if hasattr(span, "attributes"): operation_name = span.attributes.get(GEN_AI_OPERATION_NAME_KEY) if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION_NAME: - tool_call_result = span.attributes.get(TOOL_CALL_RESULT_TAG) + tool_call_result = span.attributes.get(self.TOOL_CALL_RESULT_TAG) if tool_call_result is not None: span.set_attribute(GEN_AI_EVENT_CONTENT, tool_call_result) \ No newline at end of file diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml b/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml index d160874a..c91b85fc 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] authors = [ { name = "Microsoft", email = "support@microsoft.com" }, ] -description = "Agent Framwork observability and tracing extensions for Microsoft Agents A365" +description = "Agent Framework observability and tracing extensions for Microsoft Agents A365" readme = "README.md" requires-python = ">=3.11" classifiers = [ From 708ba0c4108e178a04295f711a1fdbbf6e01f979 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 12:38:20 -0500 Subject: [PATCH 05/17] reformat --- .../observability/extensions/agentframework/span_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index 51160997..616d29b9 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -7,7 +7,7 @@ from microsoft_agents_a365.observability.core.constants import ( GEN_AI_OPERATION_NAME_KEY, EXECUTE_TOOL_OPERATION_NAME, - GEN_AI_EVENT_CONTENT + GEN_AI_EVENT_CONTENT, ) @@ -21,7 +21,7 @@ class AgentFrameworkSpanProcessor(SpanProcessor): def __init__(self, service_name: str | None = None): self.service_name = service_name super().__init__() - + def on_start(self, span, parent_context): pass From 28553931e32466285d0ac77eb69ce284bc485264 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 14:39:02 -0500 Subject: [PATCH 06/17] uv run --- .../agentframework/span_processor.py | 2 +- pyproject.toml | 1 + uv.lock | 52 +++++++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index 616d29b9..98f41225 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -31,4 +31,4 @@ def on_end(self, span, parent_context): if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION_NAME: tool_call_result = span.attributes.get(self.TOOL_CALL_RESULT_TAG) if tool_call_result is not None: - span.set_attribute(GEN_AI_EVENT_CONTENT, tool_call_result) \ No newline at end of file + span.set_attribute(GEN_AI_EVENT_CONTENT, tool_call_result) diff --git a/pyproject.toml b/pyproject.toml index a90d21f8..8b6a72d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ members = [ "libraries/microsoft-agents-a365-observability-extensions-langchain", "libraries/microsoft-agents-a365-observability-extensions-openai", "libraries/microsoft-agents-a365-observability-extensions-semantickernel", + "libraries/microsoft-agents-a365-observability-extensions-agentframework", "libraries/microsoft-agents-a365-runtime", "libraries/microsoft-agents-a365-tooling", "libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry", diff --git a/uv.lock b/uv.lock index 58b48ea7..22760430 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,7 @@ resolution-markers = [ members = [ "microsoft-agents-a365-notifications", "microsoft-agents-a365-observability-core", + "microsoft-agents-a365-observability-extensions-agent-framework", "microsoft-agents-a365-observability-extensions-langchain", "microsoft-agents-a365-observability-extensions-openai", "microsoft-agents-a365-observability-extensions-semantic-kernel", @@ -339,16 +340,16 @@ wheels = [ [[package]] name = "azure-ai-agents" -version = "1.2.0b6" +version = "1.2.0b5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/32/f4e534dc05dfb714705df56a190d690c5452cd4dd7e936612cb1adddc44f/azure_ai_agents-1.2.0b6.tar.gz", hash = "sha256:d3c10848c3b19dec98a292f8c10cee4ba4aac1050d4faabf9c2e2456b727f528", size = 396865, upload-time = "2025-10-24T18:04:47.877Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/57/8adeed578fa8984856c67b4229e93a58e3f6024417d448d0037aafa4ee9b/azure_ai_agents-1.2.0b5.tar.gz", hash = "sha256:1a16ef3f305898aac552269f01536c34a00473dedee0bca731a21fdb739ff9d5", size = 394876, upload-time = "2025-09-30T01:55:02.328Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/d0/930c522f5fa9da163de057e57f8b44539424e13f46618c52624ebc712293/azure_ai_agents-1.2.0b6-py3-none-any.whl", hash = "sha256:ce23ad8fb9791118905be1ec8eae5c907cca2e536a455f1d3b830062c72cf2a7", size = 217950, upload-time = "2025-10-24T18:04:49.72Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948, upload-time = "2025-09-30T01:55:04.155Z" }, ] [[package]] @@ -1583,6 +1584,9 @@ dev = [ { name = "pytest-asyncio" }, { name = "ruff" }, ] +jaeger = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, +] test = [ { name = "pytest" }, { name = "pytest-asyncio" }, @@ -1598,6 +1602,7 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "opentelemetry-api", specifier = ">=1.36.0" }, { name = "opentelemetry-exporter-otlp", specifier = ">=1.36.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'jaeger'", specifier = ">=1.36.0" }, { name = "opentelemetry-sdk", specifier = ">=1.36.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, @@ -1607,7 +1612,46 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, { name = "typing-extensions", specifier = ">=4.0.0" }, ] -provides-extras = ["azure", "dev", "test"] +provides-extras = ["azure", "jaeger", "dev", "test"] + +[[package]] +name = "microsoft-agents-a365-observability-extensions-agent-framework" +source = { editable = "libraries/microsoft-agents-a365-observability-extensions-agentframework" } +dependencies = [ + { name = "microsoft-agents-a365-observability-core" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "microsoft-agents-a365-observability-core", editable = "libraries/microsoft-agents-a365-observability-core" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "opentelemetry-api", specifier = ">=1.36.0" }, + { name = "opentelemetry-instrumentation", specifier = ">=0.47b0" }, + { name = "opentelemetry-sdk", specifier = ">=1.36.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.21.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, +] +provides-extras = ["dev", "test"] [[package]] name = "microsoft-agents-a365-observability-extensions-langchain" From 42624c3d48c4f8e347111cc006cef23b1155041e Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 15:48:38 -0500 Subject: [PATCH 07/17] update setup versioning format --- .../setup.py | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py index 885895ea..a04ba9a5 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/setup.py @@ -1,30 +1,13 @@ -# Copyright (c) Microsoft. All rights reserved. - -import os -from datetime import datetime -from zoneinfo import ZoneInfo +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +from os import environ from setuptools import setup - -def build_version(): - """ - Example: 2025.10.3+preview.65532 (PEP 440 compliant; avoids hyphens) - Uses UTC. - """ - - if defined_version := os.getenv("A365_SDK_VERSION"): - return defined_version # For CI/CD to set a specific version. - - today = datetime.now(ZoneInfo("UTC")) - - return ( - f"{today.year}.{today.month}.{today.day}+preview.{today.hour}{today.minute}{today.second}" - ) - - -VERSION = build_version() +# Get version from environment variable set by CI/CD +# This will be set by setuptools-git-versioning in the CI pipeline +package_version = environ.get("AGENT365_PYTHON_SDK_PACKAGE_VERSION", "0.0.0") setup( - version=VERSION, + version=package_version, ) From a38e4b2c2289e64987800b6143ce1525f84b8670 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 22:32:41 -0500 Subject: [PATCH 08/17] add integration test for agent framework --- pyproject.toml | 1 + .../test_agentframework_trace_processor.py | 307 ++++++++++++++++++ uv.lock | 1 + 3 files changed, 309 insertions(+) create mode 100644 tests/integration/test_agentframework_trace_processor.py diff --git a/pyproject.toml b/pyproject.toml index 8b6a72d4..836114ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dev-dependencies = [ "ruff>=0.1.0", "python-dotenv>=1.0.0", "openai>=1.0.0", + "agent-framework-azure-ai >= 0.1.0", "azure-identity>=1.12.0", "openai-agents >= 0.2.6", ] diff --git a/tests/integration/test_agentframework_trace_processor.py b/tests/integration/test_agentframework_trace_processor.py new file mode 100644 index 00000000..d33b9a42 --- /dev/null +++ b/tests/integration/test_agentframework_trace_processor.py @@ -0,0 +1,307 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import time + +import pytest +from microsoft_agents_a365.observability.core import configure, get_tracer_provider +from microsoft_agents_a365.observability.core.constants import ( + GEN_AI_AGENT_ID_KEY, + GEN_AI_INPUT_MESSAGES_KEY, + GEN_AI_OUTPUT_MESSAGES_KEY, + GEN_AI_REQUEST_MODEL_KEY, + GEN_AI_SYSTEM_KEY, + TENANT_ID_KEY, +) +from microsoft_agents_a365.observability.extensions.agentframework.trace_instrumentor import ( + AgentFrameworkInstrumentor, +) + +# AgentFramework SDK +try: + from agent_framework.azure import AzureOpenAIChatClient + from agent_framework import ChatAgent, ai_function + from azure.identity import AzureCliCredential + from agent_framework.observability import setup_observability +except ImportError: + pytest.skip( + "AgentFramework library and dependencies required for integration tests", + allow_module_level=True, + ) + + +@ai_function +def add_numbers(a: float, b: float) -> float: + """Add two numbers together. + + Args: + a: First number + b: Second number + + Returns: + The sum of a and b + """ + return a + b + + +@pytest.mark.integration +class TestAgentFrameworkTraceProcessorIntegration: + """Integration tests for AgentFramework trace processor with real Azure OpenAI.""" + + def setup_method(self): + """Set up test method with mock exporter.""" + self.captured_spans = [] + self.mock_exporter = MockAgent365Exporter(self.captured_spans) + + def test_agentframework_trace_processor_integration(self, azure_openai_config, agent365_config): + """Test AgentFramework trace processor with real Azure OpenAI call.""" + + + # Configure observability + configure( + service_name="integration-test-service", + service_namespace="agent365-tests", + logger_name="test-logger", + ) + + # Get the tracer provider and add our mock exporter + provider = get_tracer_provider() + provider.add_span_processor(self.mock_exporter) + + setup_observability() + + # Initialize the instrumentor + instrumentor = AgentFrameworkInstrumentor() + instrumentor.instrument() + + try: + # Create Azure OpenAI ChatClient + chat_client = AzureOpenAIChatClient( + endpoint=azure_openai_config["endpoint"], + credential=AzureCliCredential(), + deployment_name=azure_openai_config["deployment"], + api_version=azure_openai_config["api_version"], + ) + + # Create agent framework agent + agent = ChatAgent( + chat_client=chat_client, + instructions="You are a helpful assistant.", + tools=[], + ) + + # Execute a simple prompt using async runner + import asyncio + + async def run_agent(): + result = await agent.run("What can you do with agent framework?") + return result + + asyncio.run(setup_observability()) + response = asyncio.run(run_agent()) + + # Give some time for spans to be processed + time.sleep(1) + + # Verify that spans were captured + assert len(self.captured_spans) > 0, "No spans were captured" + + # Verify we have the expected span types + span_names = [span.name for span in self.captured_spans] + print(f"Captured spans: {span_names}") + + # Validate attributes on spans + self._validate_span_attributes(agent365_config) + + # Verify the response content + assert response is not None + assert len(response) > 0 + print(f"Agent response: {response}") + + finally: + # Clean up + instrumentor.uninstrument() + + def test_agentframework_trace_processor_with_tool_calls(self, azure_openai_config, agent365_config): + """Test AgentFramework trace processor with tool calls.""" + + # Configure observability + configure( + service_name="integration-test-service-tools", + service_namespace="agent365-tests", + logger_name="test-logger", + ) + + # Get the tracer provider and add our mock exporter + provider = get_tracer_provider() + provider.add_span_processor(self.mock_exporter) + + setup_observability() + + # Initialize the instrumentor + instrumentor = AgentFrameworkInstrumentor() + instrumentor.instrument() + + try: + # Create Azure OpenAI ChatClient + chat_client = AzureOpenAIChatClient( + endpoint=azure_openai_config["endpoint"], + credential=AzureCliCredential(), + deployment_name=azure_openai_config["deployment"], + api_version=azure_openai_config["api_version"], + ) + + # Create agent framework agent + agent = ChatAgent( + chat_client=chat_client, + instructions="You are a helpful agent framework assistant.", + tools=[add_numbers], + ) + + # Execute a prompt that requires tool usage + import asyncio + + + async def run_agent_with_tool(): + result = await agent.run("What is 15 + 27?") + return result + + response = asyncio.run(run_agent_with_tool()) + + # Give some time for spans to be processed + time.sleep(1) + + # Verify that spans were captured + assert len(self.captured_spans) > 0, "No spans were captured" + + # Verify we have the expected span types + span_names = [span.name for span in self.captured_spans] + print(f"Captured spans with tools: {span_names}") + + # Validate attributes on spans including tool calls + self._validate_tool_span_attributes(agent365_config) + + # Verify the response content includes the calculation result + assert response is not None + assert len(response) > 0 + assert "42" in response # 15 + 27 = 42 + print(f"Agent response with tool: {response}") + + finally: + # Clean up + instrumentor.uninstrument() + + def _validate_span_attributes(self, agent365_config): + """Validate that spans have the expected attributes.""" + llm_spans_found = 0 + agent_spans_found = 0 + + for span in self.captured_spans: + attributes = dict(span.attributes or {}) + print(f"Span '{span.name}' attributes: {list(attributes.keys())}") + + # Check common attributes + if TENANT_ID_KEY in attributes: + assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] + + if GEN_AI_AGENT_ID_KEY in attributes: + assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] + + # Check for LLM spans (generation spans) + if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai": + if GEN_AI_REQUEST_MODEL_KEY in attributes: + llm_spans_found += 1 + # Validate LLM span attributes + assert GEN_AI_REQUEST_MODEL_KEY in attributes + assert attributes[GEN_AI_REQUEST_MODEL_KEY] is not None + print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") + + # Check for input/output messages + if GEN_AI_INPUT_MESSAGES_KEY in attributes: + input_messages = attributes[GEN_AI_INPUT_MESSAGES_KEY] + assert input_messages is not None + print(f"✓ Input messages found: {input_messages[:100]}...") + + if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: + output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] + assert output_messages is not None + print(f"✓ Output messages found: {output_messages[:100]}...") + + # Check for agent spans + if "agent" in span.name.lower(): + agent_spans_found += 1 + print(f"✓ Found agent span: {span.name}") + + # Ensure we found at least some spans with telemetry data + assert len(self.captured_spans) > 0, "No spans were captured" + print(f"✓ Captured {len(self.captured_spans)} spans total") + print(f"✓ Found {llm_spans_found} LLM spans and {agent_spans_found} agent spans") + + def _validate_tool_span_attributes(self, agent365_config): + """Validate that spans have the expected attributes including tool calls.""" + llm_spans_found = 0 + agent_spans_found = 0 + tool_spans_found = 0 + + for span in self.captured_spans: + attributes = dict(span.attributes or {}) + print(f"Span '{span.name}' attributes: {list(attributes.keys())}") + + # Check common attributes + if TENANT_ID_KEY in attributes: + assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] + + if GEN_AI_AGENT_ID_KEY in attributes: + assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] + + # Check for LLM spans (generation spans) + if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai": + if GEN_AI_REQUEST_MODEL_KEY in attributes: + llm_spans_found += 1 + print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") + + # Check for tool calls in messages + if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: + output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] + if "tool_calls" in output_messages: + print("✓ Found tool calls in LLM output messages") + + # Check for agent spans + if "agent" in span.name.lower(): + agent_spans_found += 1 + print(f"✓ Found agent span: {span.name}") + + # Check for tool execution spans + if "execute_tool" in span.name.lower() or "calculator_tool" in span.name.lower(): + tool_spans_found += 1 + print(f"✓ Found tool execution span: {span.name}") + + # Ensure we found the expected span types + assert len(self.captured_spans) > 0, "No spans were captured" + print(f"✓ Captured {len(self.captured_spans)} spans total") + print( + f"✓ Found {llm_spans_found} LLM spans, {agent_spans_found} agent spans, and {tool_spans_found} tool spans" + ) + + +class MockAgent365Exporter: + """Mock span processor that captures spans instead of sending them.""" + + def __init__(self, captured_spans): + self.captured_spans = captured_spans + + def on_start(self, span, parent_context=None): + """Called when a span starts.""" + pass + + def on_end(self, span): + """Called when a span ends.""" + self.captured_spans.append(span) + + def shutdown(self): + """Mock shutdown.""" + pass + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Mock force flush.""" + return True diff --git a/uv.lock b/uv.lock index 22760430..ffb691a0 100644 --- a/uv.lock +++ b/uv.lock @@ -25,6 +25,7 @@ members = [ [manifest.dependency-groups] dev = [ + { name = "agent-framework-azure-ai", specifier = ">=0.1.0" }, { name = "azure-identity", specifier = ">=1.12.0" }, { name = "openai", specifier = ">=1.0.0" }, { name = "openai-agents", specifier = ">=0.2.6" }, From 9ca69260a10e8645ee2c9871188029c302afc171 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 23:07:24 -0500 Subject: [PATCH 09/17] Fix integration test --- .../agentframework/span_processor.py | 2 +- .../test_agentframework_trace_processor.py | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index 98f41225..916c7612 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -25,7 +25,7 @@ def __init__(self, service_name: str | None = None): def on_start(self, span, parent_context): pass - def on_end(self, span, parent_context): + def on_end(self, span): if hasattr(span, "attributes"): operation_name = span.attributes.get(GEN_AI_OPERATION_NAME_KEY) if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION_NAME: diff --git a/tests/integration/test_agentframework_trace_processor.py b/tests/integration/test_agentframework_trace_processor.py index d33b9a42..8fdabc19 100644 --- a/tests/integration/test_agentframework_trace_processor.py +++ b/tests/integration/test_agentframework_trace_processor.py @@ -96,10 +96,8 @@ def test_agentframework_trace_processor_integration(self, azure_openai_config, a async def run_agent(): result = await agent.run("What can you do with agent framework?") return result - - asyncio.run(setup_observability()) response = asyncio.run(run_agent()) - + print(f"Agent response: {response}") # Give some time for spans to be processed time.sleep(1) @@ -115,8 +113,8 @@ async def run_agent(): # Verify the response content assert response is not None - assert len(response) > 0 - print(f"Agent response: {response}") + assert len(response.text) > 0 + print(f"Agent response: {response.text}") finally: # Clean up @@ -183,9 +181,9 @@ async def run_agent_with_tool(): # Verify the response content includes the calculation result assert response is not None - assert len(response) > 0 - assert "42" in response # 15 + 27 = 42 - print(f"Agent response with tool: {response}") + assert len(response.text) > 0 + assert "42" in response.text # 15 + 27 = 42 + print(f"Agent response with tool: {response.text}") finally: # Clean up @@ -204,8 +202,8 @@ def _validate_span_attributes(self, agent365_config): if TENANT_ID_KEY in attributes: assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] - if GEN_AI_AGENT_ID_KEY in attributes: - assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] + #if GEN_AI_AGENT_ID_KEY in attributes: + # assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] # Check for LLM spans (generation spans) if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai": @@ -251,8 +249,8 @@ def _validate_tool_span_attributes(self, agent365_config): if TENANT_ID_KEY in attributes: assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] - if GEN_AI_AGENT_ID_KEY in attributes: - assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] + #if GEN_AI_AGENT_ID_KEY in attributes: + # assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] # Check for LLM spans (generation spans) if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai": From b704be79927af2ab42602ef8b914f5a46ab5217b Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 23:12:25 -0500 Subject: [PATCH 10/17] format --- .../test_agentframework_trace_processor.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/integration/test_agentframework_trace_processor.py b/tests/integration/test_agentframework_trace_processor.py index 8fdabc19..6ca856fd 100644 --- a/tests/integration/test_agentframework_trace_processor.py +++ b/tests/integration/test_agentframework_trace_processor.py @@ -56,7 +56,6 @@ def setup_method(self): def test_agentframework_trace_processor_integration(self, azure_openai_config, agent365_config): """Test AgentFramework trace processor with real Azure OpenAI call.""" - # Configure observability configure( service_name="integration-test-service", @@ -96,6 +95,7 @@ def test_agentframework_trace_processor_integration(self, azure_openai_config, a async def run_agent(): result = await agent.run("What can you do with agent framework?") return result + response = asyncio.run(run_agent()) print(f"Agent response: {response}") # Give some time for spans to be processed @@ -120,7 +120,9 @@ async def run_agent(): # Clean up instrumentor.uninstrument() - def test_agentframework_trace_processor_with_tool_calls(self, azure_openai_config, agent365_config): + def test_agentframework_trace_processor_with_tool_calls( + self, azure_openai_config, agent365_config + ): """Test AgentFramework trace processor with tool calls.""" # Configure observability @@ -159,7 +161,6 @@ def test_agentframework_trace_processor_with_tool_calls(self, azure_openai_confi # Execute a prompt that requires tool usage import asyncio - async def run_agent_with_tool(): result = await agent.run("What is 15 + 27?") return result @@ -202,9 +203,6 @@ def _validate_span_attributes(self, agent365_config): if TENANT_ID_KEY in attributes: assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] - #if GEN_AI_AGENT_ID_KEY in attributes: - # assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] - # Check for LLM spans (generation spans) if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai": if GEN_AI_REQUEST_MODEL_KEY in attributes: @@ -249,9 +247,6 @@ def _validate_tool_span_attributes(self, agent365_config): if TENANT_ID_KEY in attributes: assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] - #if GEN_AI_AGENT_ID_KEY in attributes: - # assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"] - # Check for LLM spans (generation spans) if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai": if GEN_AI_REQUEST_MODEL_KEY in attributes: From a6acd1a61fd3abd02e9df06f92240e33c4a0d891 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Tue, 4 Nov 2025 23:19:01 -0500 Subject: [PATCH 11/17] clean up --- .../extensions/agentframework/trace_instrumentor.py | 2 +- tests/integration/test_agentframework_trace_processor.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py index 2bcb728a..9caec227 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py @@ -15,7 +15,7 @@ # ----------------------------- # 3) The Instrumentor class # ----------------------------- -_instruments = ("semantic-kernel >= 1.0.0",) +_instruments = ("agent-framework-azure-ai >= 1.0.0",) class AgentFrameworkInstrumentor(BaseInstrumentor): diff --git a/tests/integration/test_agentframework_trace_processor.py b/tests/integration/test_agentframework_trace_processor.py index 6ca856fd..b8861b3c 100644 --- a/tests/integration/test_agentframework_trace_processor.py +++ b/tests/integration/test_agentframework_trace_processor.py @@ -6,7 +6,6 @@ import pytest from microsoft_agents_a365.observability.core import configure, get_tracer_provider from microsoft_agents_a365.observability.core.constants import ( - GEN_AI_AGENT_ID_KEY, GEN_AI_INPUT_MESSAGES_KEY, GEN_AI_OUTPUT_MESSAGES_KEY, GEN_AI_REQUEST_MODEL_KEY, From c2bd9de0de04e6ed2c54ee70f5aee6b0ef2998fe Mon Sep 17 00:00:00 2001 From: Jian Han Date: Wed, 5 Nov 2025 08:50:35 -0500 Subject: [PATCH 12/17] change dependency version --- .../pyproject.toml | 2 +- tests/integration/test_agentframework_trace_processor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml b/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml index c91b85fc..fe1aa3ee 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ license = "MIT" keywords = ["observability", "telemetry", "tracing", "opentelemetry", "agent-framework", "agents", "ai"] dependencies = [ - "microsoft-agents-a365-observability-core >= 2025.10.16", + "microsoft-agents-a365-observability-core >= 0.1.0", "opentelemetry-api >= 1.36.0", "opentelemetry-sdk >= 1.36.0", "opentelemetry-instrumentation >= 0.47b0", diff --git a/tests/integration/test_agentframework_trace_processor.py b/tests/integration/test_agentframework_trace_processor.py index b8861b3c..f3d3ab26 100644 --- a/tests/integration/test_agentframework_trace_processor.py +++ b/tests/integration/test_agentframework_trace_processor.py @@ -264,7 +264,7 @@ def _validate_tool_span_attributes(self, agent365_config): print(f"✓ Found agent span: {span.name}") # Check for tool execution spans - if "execute_tool" in span.name.lower() or "calculator_tool" in span.name.lower(): + if "execute_tool" in span.name.lower() or "add_numbers" in span.name.lower(): tool_spans_found += 1 print(f"✓ Found tool execution span: {span.name}") From a5a564f46e103b0dfd3374200afeb67013df1ae4 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Wed, 5 Nov 2025 09:11:37 -0500 Subject: [PATCH 13/17] update copyright --- .../observability/extensions/agentframework/__init__.py | 3 ++- .../observability/extensions/agentframework/span_processor.py | 3 ++- .../extensions/agentframework/trace_instrumentor.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py index 2a50eae8..59e481eb 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/__init__.py @@ -1 +1,2 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index 916c7612..0b3126f3 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -1,4 +1,5 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. # Custom Span Processor diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py index 9caec227..3a513983 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py @@ -1,4 +1,5 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. from __future__ import annotations From 14d16341216eabcd2adbcfc63d6a9ea5a6eea8bb Mon Sep 17 00:00:00 2001 From: Jian Han Date: Wed, 5 Nov 2025 12:22:34 -0500 Subject: [PATCH 14/17] move process logic to onstart --- .../extensions/agentframework/span_processor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py index 0b3126f3..0b41b761 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py @@ -24,12 +24,12 @@ def __init__(self, service_name: str | None = None): super().__init__() def on_start(self, span, parent_context): - pass - - def on_end(self, span): if hasattr(span, "attributes"): operation_name = span.attributes.get(GEN_AI_OPERATION_NAME_KEY) if isinstance(operation_name, str) and operation_name == EXECUTE_TOOL_OPERATION_NAME: tool_call_result = span.attributes.get(self.TOOL_CALL_RESULT_TAG) if tool_call_result is not None: span.set_attribute(GEN_AI_EVENT_CONTENT, tool_call_result) + + def on_end(self, span): + pass From cc5b2820619e8002572078d438906b7f0cc4ea1f Mon Sep 17 00:00:00 2001 From: Jian Han Date: Wed, 5 Nov 2025 13:50:37 -0500 Subject: [PATCH 15/17] reformat --- .../integration/test_agentframework_trace_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py b/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py index 60489b93..ad26d51c 100644 --- a/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py +++ b/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py @@ -294,4 +294,4 @@ def shutdown(self): def force_flush(self, timeout_millis: int = 30000) -> bool: """Mock force flush.""" - return True \ No newline at end of file + return True From da1759a06e7a4f2c4d3c5842a63e30842c3bf760 Mon Sep 17 00:00:00 2001 From: Jian Han Date: Wed, 5 Nov 2025 16:27:05 -0500 Subject: [PATCH 16/17] add env setting info to readme --- .../README.md | 3 +++ .../integration/test_agentframework_trace_processor.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md b/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md index e69de29b..c1434a51 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md @@ -0,0 +1,3 @@ +To allow the A365 observability SDK to capture input and output messages, please enable the flags in environment: +ENABLE_OTEL=true +ENABLE_SENSITIVE_DATA=true \ No newline at end of file diff --git a/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py b/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py index ad26d51c..f3e319cb 100644 --- a/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py +++ b/tests/observability/extensions/agentframework/integration/test_agentframework_trace_processor.py @@ -244,8 +244,8 @@ def _validate_tool_span_attributes(self, agent365_config): if TENANT_ID_KEY in attributes: assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] - # Check for LLM spans (generation spans) - if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai": + # Check for LLM spans + if "chat" in span.name.lower(): if GEN_AI_REQUEST_MODEL_KEY in attributes: llm_spans_found += 1 print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") From b37901077bc3bc135e4cfb55ddf52afd8c730e1c Mon Sep 17 00:00:00 2001 From: Jian Han Date: Wed, 5 Nov 2025 16:41:02 -0500 Subject: [PATCH 17/17] update readme --- .../README.md | 13 +++++++++++++ .../extensions/agentframework/trace_instrumentor.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md b/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md index c1434a51..8cf5f263 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/README.md @@ -1,3 +1,16 @@ +# microsoft-agents-a365-observability-extensions-agentframework + +## Overview + +This package provides Agent365 SDK observability extensions for the agent framework. + +## Features + +- Seamless integration with the Agent Framework +- Supports OpenTelemetry (OTEL) for distributed tracing +- Optional capture of sensitive data for deeper diagnostics +- Easy configuration via environment variables + To allow the A365 observability SDK to capture input and output messages, please enable the flags in environment: ENABLE_OTEL=true ENABLE_SENSITIVE_DATA=true \ No newline at end of file diff --git a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py index 3a513983..ad7d82fa 100644 --- a/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py +++ b/libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py @@ -16,7 +16,7 @@ # ----------------------------- # 3) The Instrumentor class # ----------------------------- -_instruments = ("agent-framework-azure-ai >= 1.0.0",) +_instruments = ("agent-framework-azure-ai >= 0.1.0",) class AgentFrameworkInstrumentor(BaseInstrumentor):