Skip to content
Open
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
Expand Up @@ -386,7 +386,7 @@ async def process_activity(

if claims_identity.is_agent_claim():
outgoing_audience = claims_identity.get_token_audience()
activity.caller_id = f"{CallerIdConstants.agent_to_agent_prefix}{claims_identity.get_outgoing_app_id()}"
activity.caller_id = f"{CallerIdConstants.agent_to_agent_prefix.value}{claims_identity.get_outgoing_app_id()}"
else:
outgoing_audience = AuthenticationConstants.AGENTS_SDK_SCOPE

Expand Down
54 changes: 54 additions & 0 deletions test_samples/echo-a365-telemetry/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# =============================================================================
# M365 Agents SDK — service principal credentials
# =============================================================================
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=your-client-id
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=your-client-secret
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=your-tenant-id

# =============================================================================
# Azure OpenAI
# Endpoint format: https://<resource>.openai.azure.com
# =============================================================================
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini
AZURE_OPENAI_API_VERSION=2024-12-01-preview

# Optional: override the system prompt sent to the model
# AGENT_SYSTEM_PROMPT=You are a helpful assistant. Respond concisely.

# =============================================================================
# Agent settings
# =============================================================================
AGENT_NAME=EchoAgent
ENVIRONMENT=development
HOST=localhost
PORT=3978

# =============================================================================
# OpenTelemetry — exporters
# =============================================================================

# OTLP (Aspire dashboard, Jaeger, OpenTelemetry Collector)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
ENABLE_OTLP_EXPORTER=True
OTEL_EXPORTER_OTLP_INSECURE=true

# Capture all request/response headers in spans
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=".*"
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE=".*"

# Azure Monitor / Application Insights (optional — leave blank to skip)
# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=your-key-here

# =============================================================================
# A365 observability SDK settings
# =============================================================================
ENABLE_OBSERVABILITY=true
ENABLE_OPENTELEMETRY_SWITCH=true
ENABLE_OTEL=true
ENABLE_SENSITIVE_DATA=true
OBSERVABILITY_SERVICE_NAME=echo-a365-telemetry
OBSERVABILITY_SERVICE_NAMESPACE=agents-framework.samples
139 changes: 139 additions & 0 deletions test_samples/echo-a365-telemetry/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Echo Agent — Microsoft 365 Agents SDK with full A365 telemetry.

Startup order (mirrors the C# sample):
1. configure_otel_providers() — A365 observability framework bootstrap
2. configure_opentelemetry() — local TracerProvider / MeterProvider / log bridge
3. M365 SDK wiring — storage, adapter, auth, AgentApplication
4. Route registration — /api/messages, /health, /alive
"""

import logging
from os import environ, path

from aiohttp import web
from aiohttp.web import Application, Request, Response
from dotenv import load_dotenv

from agent_framework.observability import configure_otel_providers

# Phase 1 — must run before any OTel proxy object is accessed
configure_otel_providers()

logger = logging.getLogger(__name__)

load_dotenv(path.join(path.dirname(__file__), ".env"))

# ---------------------------------------------------------------------------
# Phase 2 — set up TracerProvider / MeterProvider / log bridge.
# OTel proxy objects in telemetry/agent_metrics.py resolve to real
# implementations once configure_opentelemetry() has run.
# ---------------------------------------------------------------------------
from telemetry import (
configure_opentelemetry,
create_aiohttp_tracing_middleware,
setup_health_routes,
)

configure_opentelemetry(
app_name=environ.get("AGENT_NAME", "EchoAgent"),
environment=environ.get("ENVIRONMENT", "development"),
otlp_endpoint=environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"),
azure_monitor_connection_string=environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING"),
)

# ---------------------------------------------------------------------------
# Phase 3 — M365 Agents SDK
# ---------------------------------------------------------------------------
from microsoft_agents.hosting.core import (
AgentApplication,
MemoryStorage,
TurnContext,
TurnState,
)
from microsoft_agents.hosting.aiohttp import (
CloudAdapter,
jwt_authorization_middleware,
start_agent_process,
)
from microsoft_agents.hosting.core.app.oauth.authorization import Authorization
from microsoft_agents.activity import ActivityTypes, load_configuration_from_env
from microsoft_agents.authentication.msal import MsalConnectionManager

from echo_agent import EchoAgent


async def messages_endpoint(request: Request) -> Response:
agent_app: AgentApplication = request.app["agent_app"]
adapter: CloudAdapter = request.app["adapter"]
return await start_agent_process(request, agent_app, adapter)


def create_app() -> Application:
agents_sdk_config = load_configuration_from_env(environ)

storage = MemoryStorage()
connection_manager = MsalConnectionManager(**agents_sdk_config)
adapter = CloudAdapter(connection_manager=connection_manager)
authorization = Authorization(storage, connection_manager, **agents_sdk_config)

agent_app = AgentApplication[TurnState](
storage=storage,
adapter=adapter,
authorization=authorization,
**agents_sdk_config,
)

# The echo agent receives user_authorization so the A365 wrapper can cache
# the observability token — equivalent to injecting IExporterTokenCache in C#.
echo = EchoAgent(user_authorization=agent_app.auth)

@agent_app.activity(ActivityTypes.message)
async def on_message(context: TurnContext, state: TurnState):
# Pre-fetch the agentic token so it is warm in the cache before the
# A365 wrapper tries to use it (mirrors the weather agent pattern).
# await agent_app.auth.get_token(context, "AGENTIC")
await echo.handle_message(context, state)

@agent_app.conversation_update("membersAdded")
async def on_members_added(context: TurnContext, state: TurnState):
await echo.send_welcome(context, state)

app = Application(
middlewares=[create_aiohttp_tracing_middleware(), jwt_authorization_middleware]
)

app.router.add_post("/api/messages", messages_endpoint)
app.router.add_get("/", lambda _: Response(text="Echo Agent is running", status=200))

# Register /health and /alive endpoints (development only).
is_development = environ.get("ENVIRONMENT", "development").lower() == "development"
setup_health_routes(app, development=is_development)

app["agent_app"] = agent_app
app["adapter"] = adapter
app["agent_configuration"] = connection_manager.get_default_connection_configuration()

return app


def main() -> None:
agent_name = environ.get("AGENT_NAME", "EchoAgent")
host = environ.get("HOST", "localhost")
port = int(environ.get("PORT", 3978))

print(f"\n{'='*60}")
print(f"Starting {agent_name} (M365 SDK + A365 Telemetry)")
print(f"{'='*60}")
print(f"Endpoint: http://{host}:{port}/api/messages")
print(f"Health: http://{host}:{port}/health")
print(f"Alive: http://{host}:{port}/alive")
print(f"{'='*60}\n")

web.run_app(create_app(), host=host, port=port)


if __name__ == "__main__":
main()
104 changes: 104 additions & 0 deletions test_samples/echo-a365-telemetry/echo_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Echo Agent — relays user text to an Azure OpenAI endpoint.

The entire agent logic is intentionally minimal so that the telemetry
infrastructure (spans, metrics, baggage, token caching) stays in the
foreground. The AI call is a single-turn, stateless chat completion:

User text → Azure OpenAI (openai) → reply to user

No conversation history, no tools, no state persistence.
"""

import logging
import traceback
from os import environ

from openai import AzureOpenAI

from microsoft_agents.hosting.core import Authorization, TurnContext, TurnState

from telemetry import invoke_observed_agent_operation_with_context

logger = logging.getLogger(__name__)

_SYSTEM_PROMPT = environ.get(
"AGENT_SYSTEM_PROMPT",
"You are a helpful assistant. Respond concisely to the user's message.",
)
_WELCOME_MESSAGE = environ.get(
"AGENT_WELCOME_MESSAGE",
"Hello! I'm the Echo Agent. Send me any message and I'll relay it to Azure OpenAI.",
)


def _build_client() -> AzureOpenAI:
"""Build an AzureOpenAI client.

Requires AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to be set.
"""
return AzureOpenAI(
azure_endpoint=environ["AZURE_OPENAI_ENDPOINT"],
api_key=environ["AZURE_OPENAI_API_KEY"],
api_version=environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"),
)


class EchoAgent:
"""Stateless agent that echoes user text through Azure OpenAI."""

def __init__(self, user_authorization: Authorization = None):
self._user_authorization = user_authorization
self._client = _build_client()
self._model = environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini")
logger.info("EchoAgent initialised (model=%s)", self._model)

async def send_welcome(self, context: TurnContext, state: TurnState) -> None:
"""Greet each new participant when they join the conversation."""
for member in context.activity.members_added or []:
if member.id != context.activity.recipient.id:
await context.send_activity(_WELCOME_MESSAGE)

async def handle_message(self, context: TurnContext, state: TurnState) -> None:
"""Process an incoming message and relay the OpenAI reply to the user.

The core logic is wrapped inside
``invoke_observed_agent_operation_with_context`` so that every
invocation produces:
- a distributed trace span with activity attributes
- W3C baggage carrying tenant.id / agent.id
- per-turn observability token caching
- message counters and duration histograms
"""
user_text = (context.activity.text or "").strip()
if not user_text:
return

logger.info("Received: %s", user_text)

async def _process() -> None:
response = self._client.chat.completions.create(
model=self._model,
messages=[
{"role": "system", "content": _SYSTEM_PROMPT},
{"role": "user", "content": user_text},
],
)
reply = response.choices[0].message.content
logger.info("OpenAI reply: %s", reply)
await context.send_activity(reply)

try:
await invoke_observed_agent_operation_with_context(
"OnMessageActivity",
context,
state,
_process,
user_authorization=self._user_authorization,
)
except Exception as exc:
logger.error("Error processing message: %s", exc)
traceback.print_exc()
await context.send_activity(f"Sorry, I encountered an error: {exc}")
39 changes: 39 additions & 0 deletions test_samples/echo-a365-telemetry/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# M365 Agents SDK
microsoft-agents-hosting-aiohttp
microsoft-agents-authentication-msal

# Azure OpenAI
openai

# Web framework
aiohttp

# Configuration
python-dotenv

# JWT decoding (for A365 observability wrapper)
PyJWT

# OpenTelemetry — core
opentelemetry-api
opentelemetry-sdk

# OpenTelemetry — exporters
# OTLP (traces, metrics, logs) — for Aspire dashboard, Jaeger, etc.
opentelemetry-exporter-otlp
# Azure Monitor / Application Insights
azure-monitor-opentelemetry-exporter

# OpenTelemetry — library auto-instrumentation
opentelemetry-instrumentation-aiohttp-server
opentelemetry-instrumentation-aiohttp-client
opentelemetry-instrumentation-requests
opentelemetry-instrumentation-logging

# A365 observability — AgentFramework automatic instrumentation
microsoft-agents-a365-runtime==0.2.1.dev43
microsoft-agents-a365-observability-core==0.2.1.dev43
microsoft-agents-a365-observability-extensions-agent-framework>=0.2.1.dev43

# Agent Framework (required for configure_otel_providers)
agent-framework-azure-ai
Loading
Loading