Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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,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"
Comment thread
JianH marked this conversation as resolved.

def __init__(self, service_name: str | None = None):
self.service_name = service_name
Comment thread
JianH marked this conversation as resolved.
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):
Comment thread
JianH marked this conversation as resolved.
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 >= 1.0.0",)
Comment thread
JianH marked this conversation as resolved.
Outdated


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 = ["."]
Comment thread
JianH marked this conversation as resolved.

[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
Comment thread
JianH marked this conversation as resolved.
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