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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

# Custom Span Processor

from opentelemetry.sdk.trace.export import SpanProcessor

from microsoft_agents_a365.observability.core.constants import (
GEN_AI_OPERATION_NAME_KEY,
EXECUTE_TOOL_OPERATION_NAME,
GEN_AI_EVENT_CONTENT,
)


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__()

def on_start(self, span, parent_context):
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

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 = ("agent-framework-azure-ai >= 0.1.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
Original file line number Diff line number Diff line change
@@ -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 Framework 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 >= 0.1.0",
"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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from os import environ
from setuptools import setup

# 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=package_version,
)
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -37,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",
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Integration tests for Agent365 Python SDK."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import os
from pathlib import Path
from typing import Any

import pytest

# Load .env file if it exists (for local development)
try:
from dotenv import load_dotenv

# Look for .env file in tests directory
# Navigate from conftest.py location: integration -> openai -> extensions -> observability -> tests
current_file = Path(__file__)
tests_dir = current_file.parent.parent.parent.parent.parent # Go up to tests/ directory
env_file = tests_dir / ".env"
if env_file.exists():
load_dotenv(env_file)
except ImportError:
# python-dotenv not installed, skip loading .env file
pass


def pytest_configure(config):
"""Add integration marker."""
config.addinivalue_line("markers", "integration: marks tests as integration tests")


@pytest.fixture(scope="session")
def azure_openai_config() -> dict[str, Any]:
"""Azure OpenAI configuration for integration tests."""
api_key = os.getenv("AZURE_OPENAI_API_KEY")
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4")
api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2024-08-01-preview")

if not api_key or not endpoint:
pytest.skip("Integration tests require AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT")

return {
"api_key": api_key,
"endpoint": endpoint,
"deployment": deployment,
"api_version": api_version,
}


@pytest.fixture(scope="session")
def agent365_config() -> dict[str, Any]:
"""Agent365 configuration for integration tests."""
tenant_id = os.getenv("AGENT365_TEST_TENANT_ID", "4d44f041-f91e-4d00-b107-61e47b26f5a8")
agent_id = os.getenv("AGENT365_TEST_AGENT_ID", "3bccd52b-daaa-4b11-af40-47443852137c")

if not tenant_id:
pytest.skip("Integration tests require AGENT365_TEST_TENANT_ID")

return {"tenant_id": tenant_id, "agent_id": agent_id}
Loading
Loading