From f445fc5f12f3639df6fac6d3b09662ab5caaf636 Mon Sep 17 00:00:00 2001 From: etserend Date: Tue, 11 Aug 2026 20:32:23 -0500 Subject: [PATCH 1/3] feat(examples): add Microsoft 365 Agents SDK example with Splunk AO OTel instrumentation Adds a working Q&A agent example using the Microsoft 365 Agents SDK (microsoft-agents-hosting-aiohttp) wired to Splunk AO via stock SplunkAOSpanProcessor. opentelemetry-instrumentation-openai-v2 auto-instruments the Azure OpenAI call, emitting gen_ai.* spans. Supports both O11Y (SPLUNK_AO_REALM + token) and standalone (SPLUNK_AO_API_KEY) deployment modes. AnonymousTokenProvider for local testing with npx @microsoft/m365agentsplayground; MsalConnectionManager for authenticated Teams/Copilot channels. Co-Authored-By: Claude Opus 4.7 --- .../microsoft-365-agents-sdk/.env.example | 33 ++++++ .../agent/microsoft-365-agents-sdk/.npmrc | 1 + .../agent/microsoft-365-agents-sdk/app.py | 105 ++++++++++++++++++ .../microsoft-365-agents-sdk/pyproject.toml | 17 +++ .../microsoft-365-agents-sdk/start_server.py | 34 ++++++ .../agent/microsoft-365-agents-sdk/uv.toml | 3 + 6 files changed, 193 insertions(+) create mode 100644 examples/agent/microsoft-365-agents-sdk/.env.example create mode 100644 examples/agent/microsoft-365-agents-sdk/.npmrc create mode 100644 examples/agent/microsoft-365-agents-sdk/app.py create mode 100644 examples/agent/microsoft-365-agents-sdk/pyproject.toml create mode 100644 examples/agent/microsoft-365-agents-sdk/start_server.py create mode 100644 examples/agent/microsoft-365-agents-sdk/uv.toml diff --git a/examples/agent/microsoft-365-agents-sdk/.env.example b/examples/agent/microsoft-365-agents-sdk/.env.example new file mode 100644 index 0000000..9328c25 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/.env.example @@ -0,0 +1,33 @@ +# --- Splunk AO: Scenario A — O11Y (lab0 / us0 / us1) --- +SPLUNK_AO_REALM=your-realm # e.g. lab0, us0, us1 +SPLUNK_AO_O11Y_TOKEN=your-o11y-ingest-token +SPLUNK_AO_O11Y_API_TOKEN=your-o11y-api-token + +# --- Splunk AO: Scenario B — Standalone (commented out) --- +# SPLUNK_AO_API_KEY=your-splunk-ao-api-key +# SPLUNK_AO_CONSOLE_URL=https://console.your-splunk-ao-instance.com + +# --- Routing --- +SPLUNK_AO_PROJECT=your-project-name +SPLUNK_AO_AGENT_STREAM=your-agent-stream-name + +# --- Azure OpenAI --- +AZURE_OPENAI_ENDPOINT=https://your-resource.cognitiveservices.azure.com/ +AZURE_OPENAI_API_KEY=your-azure-openai-api-key +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +AZURE_OPENAI_API_VERSION=2024-12-01-preview + +# --- OTel: capture LLM prompt/completion content in spans --- +OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY + +# --- Microsoft 365 Agents SDK auth --- +# Leave empty for local unauthenticated testing with npx @microsoft/m365agentsplayground. +# For Teams / Copilot, fill in credentials from your Azure App Registration: +# portal.azure.com → Entra ID → App registrations → +# Supported account types: "Accounts in any org directory + personal Microsoft accounts" +MicrosoftAppId= +MicrosoftAppPassword= +MicrosoftAppTenantId= + +# --- macOS SSL (if needed) --- +# SSL_CERT_FILE=/opt/homebrew/etc/openssl@3/cert.pem diff --git a/examples/agent/microsoft-365-agents-sdk/.npmrc b/examples/agent/microsoft-365-agents-sdk/.npmrc new file mode 100644 index 0000000..214c29d --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/examples/agent/microsoft-365-agents-sdk/app.py b/examples/agent/microsoft-365-agents-sdk/app.py new file mode 100644 index 0000000..b0777ab --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/app.py @@ -0,0 +1,105 @@ +""" +Microsoft 365 Agents SDK Q&A agent with Splunk AO OTel instrumentation. + +Instrumentation: + - SplunkAOSpanProcessor() exports spans to Splunk AO via OTLP. + - opentelemetry-instrumentation-openai-v2 auto-instruments the Azure OpenAI call, + emitting gen_ai.* spans with input/output content. + +Auth: + - MicrosoftAppId/Password empty → AnonymousTokenProvider (local playground only). + - MicrosoftAppId/Password set → MsalConnectionManager (Teams / Copilot / production). + +Run: + python start_server.py +Test locally: + npx @microsoft/m365agentsplayground +""" + +import os + +from dotenv import load_dotenv + +load_dotenv(override=True) + +# --- OTel setup (must happen before any instrumented client is created) --- +from openai import AzureOpenAI +from opentelemetry import trace +from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider + +from splunk_ao.otel import SplunkAOSpanProcessor + +resource = Resource.create({"service.name": "microsoft-365-agents-sdk-example"}) +tracer_provider = TracerProvider(resource=resource) +tracer_provider.add_span_processor(SplunkAOSpanProcessor()) + +trace.set_tracer_provider(tracer_provider) + +OpenAIInstrumentor().instrument(tracer_provider=tracer_provider) + +# --- Azure OpenAI client --- +_openai_client = AzureOpenAI( + azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + api_key=os.environ["AZURE_OPENAI_API_KEY"], + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), +) +_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") + +# --- Microsoft 365 Agents SDK --- +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnContext, TurnState +from microsoft_agents.hosting.core.authorization import AgentAuthConfiguration, AnonymousTokenProvider, ConnectionManager + +_app_id = os.environ.get("MicrosoftAppId", "") +_app_password = os.environ.get("MicrosoftAppPassword", "") +_tenant_id = os.environ.get("MicrosoftAppTenantId", "") + +if _app_id and _app_password: + from microsoft_agents.authentication.msal import MsalConnectionManager + _connection_manager = MsalConnectionManager( + connections_configurations={ + "SERVICE_CONNECTION": AgentAuthConfiguration( + client_id=_app_id, + client_secret=_app_password, + tenant_id=_tenant_id or "common", + ) + } + ) +else: + _connection_manager = ConnectionManager( + provider_factory=lambda config: AnonymousTokenProvider(), + connections_configurations={"SERVICE_CONNECTION": AgentAuthConfiguration(anonymous_allowed=True)}, + ) + +STORAGE = MemoryStorage() +ADAPTER = CloudAdapter(connection_manager=_connection_manager) + +AGENT_APP = AgentApplication[TurnState]( + storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager +) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("Hello! Ask me anything and I'll answer using Azure OpenAI.") + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, state: TurnState): + user_text = context.activity.text or "" + if not user_text.strip(): + await context.send_activity("Please send a message.") + return + + response = _openai_client.chat.completions.create( + model=_deployment, + messages=[ + {"role": "system", "content": "You are a concise, helpful assistant."}, + {"role": "user", "content": user_text}, + ], + max_tokens=512, + ) + reply = response.choices[0].message.content or "" + await context.send_activity(reply) diff --git a/examples/agent/microsoft-365-agents-sdk/pyproject.toml b/examples/agent/microsoft-365-agents-sdk/pyproject.toml new file mode 100644 index 0000000..8a6cee1 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "microsoft-365-agents-sdk-example" +version = "0.1.0" +description = "Microsoft 365 Agents SDK example with Splunk AO OTel instrumentation" +requires-python = ">=3.11" +dependencies = [ + "splunk-ao[otel]", + "microsoft-agents-hosting-aiohttp>=1.1.0", + "microsoft-agents-authentication-msal>=1.1.0", + "openai>=1.0.0", + "opentelemetry-instrumentation-openai-v2>=2.1b0", + "azure-identity>=1.19.0", + "python-dotenv>=1.0.0", +] + +[tool.uv.sources] +splunk-ao = { path = "../../..", editable = true } diff --git a/examples/agent/microsoft-365-agents-sdk/start_server.py b/examples/agent/microsoft-365-agents-sdk/start_server.py new file mode 100644 index 0000000..c8db569 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/start_server.py @@ -0,0 +1,34 @@ +""" +aiohttp server entry point for the Microsoft 365 Agents SDK example. + +Starts the agent on http://localhost:3978/api/messages. +Test locally with: npx @microsoft/m365agentsplayground +""" + +import os + +from aiohttp.web import Application, Request, Response, run_app +from dotenv import load_dotenv + +load_dotenv() + +from app import ADAPTER, AGENT_APP +from microsoft_agents.hosting.aiohttp import start_agent_process + + +async def messages(request: Request) -> Response: + return await start_agent_process(request, AGENT_APP, ADAPTER) + + +def main() -> None: + web_app = Application() + web_app.router.add_post("/api/messages", messages) + + port = int(os.environ.get("PORT", 3978)) + print(f"Microsoft 365 Agents SDK example running on http://localhost:{port}/api/messages") + print("Test with: npx @microsoft/m365agentsplayground") + run_app(web_app, host="localhost", port=port) + + +if __name__ == "__main__": + main() diff --git a/examples/agent/microsoft-365-agents-sdk/uv.toml b/examples/agent/microsoft-365-agents-sdk/uv.toml new file mode 100644 index 0000000..5bdd9aa --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/uv.toml @@ -0,0 +1,3 @@ +[[index]] +url = "https://pypi.org/simple" +default = true From e0ecfff653e9d3be1a9d9ac026b29e9ff21f03ce Mon Sep 17 00:00:00 2001 From: etserend Date: Wed, 12 Aug 2026 15:54:16 -0500 Subject: [PATCH 2/3] feat(examples): add lab0 auto and manual instrumentation modes for M365 Agents SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app_auto_lab0.py: SplunkAOSpanProcessor + OpenAIAgentsTraceInstrumentor → Splunk AO lab0 app_manual_lab0.py: SplunkAOLogger with full 5-level span hierarchy matching auto trace Co-Authored-By: Claude Opus 4.7 --- .../microsoft-365-agents-sdk/app_auto_lab0.py | 140 +++++++++++++++ .../app_manual_lab0.py | 163 ++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 examples/agent/microsoft-365-agents-sdk/app_auto_lab0.py create mode 100644 examples/agent/microsoft-365-agents-sdk/app_manual_lab0.py diff --git a/examples/agent/microsoft-365-agents-sdk/app_auto_lab0.py b/examples/agent/microsoft-365-agents-sdk/app_auto_lab0.py new file mode 100644 index 0000000..27950ff --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/app_auto_lab0.py @@ -0,0 +1,140 @@ +""" +app_auto_lab0.py — Auto-instrumentation via SplunkAOSpanProcessor → Splunk AO lab0. + + OpenAIAgentsTraceInstrumentor hooks into the OpenAI Agents SDK and emits spans + automatically. SplunkAOSpanProcessor exports them to Splunk AO — no local collector + or OTEL_EXPORTER_OTLP_ENDPOINT needed. + + Prerequisites: + Copy .env.example to .env and fill in: + SPLUNK_AO_REALM, SPLUNK_AO_O11Y_TOKEN, SPLUNK_AO_PROJECT, SPLUNK_AO_AGENT_STREAM + AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY + + Run: APP_MODE=auto_lab0 uv run python start_server.py + Test: npx @microsoft/m365agentsplayground (or send a POST to /api/messages) + Find: Splunk AO → Agent Stream: microsoft-365-agents-sdk → service: m365-auto-lab0 +""" + +import os + +from dotenv import load_dotenv +from opentelemetry import context as otel_context + +load_dotenv(override=False) + +# --- MS A365 Observability SDK: configure TracerProvider first --------------- +from microsoft_agents_a365.observability.core import ( + AgentDetails, + Channel, + InvokeAgentScope, + InvokeAgentScopeDetails, + Request, + SpanDetails, + configure as a365_configure, + get_tracer_provider as a365_get_tracer_provider, +) + +a365_configure( + service_name=os.environ.get("OTEL_SERVICE_NAME", "m365-auto-lab0"), + service_namespace="splunk.ao.demo", +) + +# --- Attach SplunkAOSpanProcessor to the MS A365 TracerProvider -------------- +from splunk_ao.otel import SplunkAOSpanProcessor, add_splunk_ao_span_processor + +add_splunk_ao_span_processor( + a365_get_tracer_provider(), + SplunkAOSpanProcessor( + project=os.environ.get("SPLUNK_AO_PROJECT"), + agentstream=os.environ.get("SPLUNK_AO_AGENT_STREAM"), + ), +) + +# --- Auto-instrumentation: hooks into OpenAI Agents SDK tracing -------------- +from microsoft_agents_a365.observability.extensions.openai import OpenAIAgentsTraceInstrumentor + +OpenAIAgentsTraceInstrumentor().instrument() + +# --- OpenAI Agents SDK (wraps Azure OpenAI) ---------------------------------- +from agents import Agent, Runner +from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel +from openai import AsyncAzureOpenAI + +_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") +_azure_client = AsyncAzureOpenAI( + azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + api_key=os.environ["AZURE_OPENAI_API_KEY"], + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), +) +_agent = Agent( + name="QAAgent", + instructions="You are a concise, helpful assistant.", + model=OpenAIChatCompletionsModel(model=_deployment, openai_client=_azure_client), +) + +# --- Microsoft 365 Agents SDK ------------------------------------------------ +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnContext, TurnState +from microsoft_agents.hosting.core.authorization import ( + AgentAuthConfiguration, + AnonymousTokenProvider, + ConnectionManager, +) + +_app_id = os.environ.get("MicrosoftAppId", "") +_app_password = os.environ.get("MicrosoftAppPassword", "") +_tenant_id = os.environ.get("MicrosoftAppTenantId", "") + +if _app_id and _app_password: + from microsoft_agents.authentication.msal import MsalConnectionManager + _connection_manager = MsalConnectionManager( + connections_configurations={ + "SERVICE_CONNECTION": AgentAuthConfiguration( + client_id=_app_id, + client_secret=_app_password, + tenant_id=_tenant_id or "common", + ) + } + ) +else: + _connection_manager = ConnectionManager( + provider_factory=lambda config: AnonymousTokenProvider(), + connections_configurations={"SERVICE_CONNECTION": AgentAuthConfiguration(anonymous_allowed=True)}, + ) + +STORAGE = MemoryStorage() +ADAPTER = CloudAdapter(connection_manager=_connection_manager) +AGENT_APP = AgentApplication[TurnState](storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState) -> None: + await context.send_activity("Hello! MS A365 auto-instrumentation (lab0) demo. Ask me anything.") + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, state: TurnState) -> None: + user_text = context.activity.text or "" + if not user_text.strip(): + await context.send_activity("Please send a message.") + return + + conv_id = context.activity.conversation.id if context.activity.conversation else "unknown" + channel_id = context.activity.channel_id or "unknown" + + request = Request( + content=user_text, + session_id=conv_id, + conversation_id=conv_id, + channel=Channel(name=channel_id), + ) + agent_details = AgentDetails(agent_id="m365-splunk-ao-demo", agent_name="M365 Splunk AO Demo Agent") + + with InvokeAgentScope.start( + request, InvokeAgentScopeDetails(), agent_details, + span_details=SpanDetails(parent_context=otel_context.get_current()), + ): + result = await Runner.run(_agent, input=user_text) + reply = result.final_output + + await context.send_activity(reply) diff --git a/examples/agent/microsoft-365-agents-sdk/app_manual_lab0.py b/examples/agent/microsoft-365-agents-sdk/app_manual_lab0.py new file mode 100644 index 0000000..0bc8543 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/app_manual_lab0.py @@ -0,0 +1,163 @@ +""" +app_manual_lab0.py — Manual instrumentation via SplunkAOLogger → Splunk AO lab0. + + SplunkAOLogger is used directly in the turn handler to build the trace hierarchy: + trace → invoke_agent M365 Splunk AO Demo Agent + agent_span → Agent workflow + agent_span → invoke_agent QAAgent + agent_span → turn + llm_span → chat gpt-4o-mini + + Session is created once per conversation (conv_id as external_id) so multi-turn + messages in the same Teams conversation are grouped under one session. + + Prerequisites: + Copy .env.example to .env and fill in: + SPLUNK_AO_REALM, SPLUNK_AO_O11Y_TOKEN, SPLUNK_AO_PROJECT, SPLUNK_AO_AGENT_STREAM + AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY + + Run: APP_MODE=manual_lab0 uv run python start_server.py + Test: npx @microsoft/m365agentsplayground (or send a POST to /api/messages) + Find: Splunk AO → Agent Stream: microsoft-365-agents-sdk → service: m365-manual-lab0 +""" + +import os +import time + +from dotenv import load_dotenv + +load_dotenv(override=False) + +os.environ.setdefault("OTEL_SERVICE_NAME", "m365-manual-lab0") + +# --- Splunk AO Logger -------------------------------------------------------- +from splunk_ao import SplunkAOLogger + +_logger = SplunkAOLogger( + project=os.environ.get("SPLUNK_AO_PROJECT"), + agent_stream=os.environ.get("SPLUNK_AO_AGENT_STREAM"), +) + +# conv_id → session_id; avoids a network round-trip on every turn +_session_cache: dict[str, str] = {} + +# --- Direct Azure OpenAI client ---------------------------------------------- +from openai import AzureOpenAI + +_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") +_azure_client = AzureOpenAI( + azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + api_key=os.environ["AZURE_OPENAI_API_KEY"], + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), +) + +# --- Microsoft 365 Agents SDK ------------------------------------------------ +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnContext, TurnState +from microsoft_agents.hosting.core.authorization import ( + AgentAuthConfiguration, + AnonymousTokenProvider, + ConnectionManager, +) + +_app_id = os.environ.get("MicrosoftAppId", "") +_app_password = os.environ.get("MicrosoftAppPassword", "") +_tenant_id = os.environ.get("MicrosoftAppTenantId", "") + +if _app_id and _app_password: + from microsoft_agents.authentication.msal import MsalConnectionManager + _connection_manager = MsalConnectionManager( + connections_configurations={ + "SERVICE_CONNECTION": AgentAuthConfiguration( + client_id=_app_id, + client_secret=_app_password, + tenant_id=_tenant_id or "common", + ) + } + ) +else: + _connection_manager = ConnectionManager( + provider_factory=lambda config: AnonymousTokenProvider(), + connections_configurations={"SERVICE_CONNECTION": AgentAuthConfiguration(anonymous_allowed=True)}, + ) + +STORAGE = MemoryStorage() +ADAPTER = CloudAdapter(connection_manager=_connection_manager) +AGENT_APP = AgentApplication[TurnState](storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState) -> None: + await context.send_activity("Hello! MS 365 manual-instrumentation (lab0) demo. Ask me anything.") + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, state: TurnState) -> None: + user_text = context.activity.text or "" + if not user_text.strip(): + await context.send_activity("Please send a message.") + return + + conv_id = context.activity.conversation.id if context.activity.conversation else "unknown" + channel_id = context.activity.channel_id or "unknown" + + # --- Session: one per conversation, server-side deduped by conv_id ------- + if conv_id not in _session_cache: + session_id = _logger.start_session( + external_id=conv_id, + metadata={"channel": channel_id}, + ) + _session_cache[conv_id] = session_id + _logger.set_session(_session_cache[conv_id]) + + # ------------------------------------------------------------------------- + # Trace hierarchy (mirrors auto-instrumented span tree): + # + # start_trace "invoke_agent M365 Splunk AO Demo Agent" + # add_agent_span "Agent workflow" + # add_agent_span "invoke_agent QAAgent" + # add_agent_span "turn" + # add_llm_span gpt-4o-mini + # conclude ← turn + # conclude ← invoke_agent QAAgent + # conclude ← Agent workflow + # conclude ← trace + # ------------------------------------------------------------------------- + t0 = time.monotonic_ns() + + _logger.start_trace(input=user_text, name="invoke_agent M365 Splunk AO Demo Agent") + _logger.add_agent_span(input=user_text, name="Agent workflow") + _logger.add_agent_span(input=user_text, name="invoke_agent QAAgent") + _logger.add_agent_span(input=user_text, name="turn") + + t_llm = time.monotonic_ns() + response = _azure_client.chat.completions.create( + model=_deployment, + messages=[ + {"role": "system", "content": "You are a concise, helpful assistant."}, + {"role": "user", "content": user_text}, + ], + ) + llm_elapsed_ns = time.monotonic_ns() - t_llm + + reply = response.choices[0].message.content or "" + usage = response.usage + + _logger.add_llm_span( + input=user_text, + output=reply, + model=_deployment, + num_input_tokens=usage.prompt_tokens if usage else None, + num_output_tokens=usage.completion_tokens if usage else None, + total_tokens=usage.total_tokens if usage else None, + duration_ns=llm_elapsed_ns, + ) + + total_elapsed_ns = time.monotonic_ns() - t0 + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # turn + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # invoke_agent QAAgent + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # Agent workflow + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # trace + _logger.flush() + + await context.send_activity(reply) From 181b7bd191a69863a88670f04a2b0d39956c85f1 Mon Sep 17 00:00:00 2001 From: etserend Date: Wed, 12 Aug 2026 15:55:42 -0500 Subject: [PATCH 3/3] feat(examples): update M365 Agents SDK example with APP_MODE dispatch and MS A365 observability - start_server.py: APP_MODE dispatch (auto/manual/auto_lab0/manual_lab0/default) - app.py: rewrite to use MS A365 OpenAIAgentsTraceInstrumentor + InvokeAgentScope - pyproject.toml: replace opentelemetry-instrumentation-openai-v2 with ms-a365-observability deps Co-Authored-By: Claude Opus 4.7 --- .../agent/microsoft-365-agents-sdk/app.py | 134 ++++++++++++------ .../microsoft-365-agents-sdk/pyproject.toml | 11 +- .../microsoft-365-agents-sdk/start_server.py | 21 ++- 3 files changed, 114 insertions(+), 52 deletions(-) diff --git a/examples/agent/microsoft-365-agents-sdk/app.py b/examples/agent/microsoft-365-agents-sdk/app.py index b0777ab..f3e9995 100644 --- a/examples/agent/microsoft-365-agents-sdk/app.py +++ b/examples/agent/microsoft-365-agents-sdk/app.py @@ -1,56 +1,83 @@ """ -Microsoft 365 Agents SDK Q&A agent with Splunk AO OTel instrumentation. +Microsoft 365 Agents SDK demo — MS A365 auto-instrumentation via OpenAIAgentsTraceInstrumentor. Instrumentation: - - SplunkAOSpanProcessor() exports spans to Splunk AO via OTLP. - - opentelemetry-instrumentation-openai-v2 auto-instruments the Azure OpenAI call, - emitting gen_ai.* spans with input/output content. - -Auth: - - MicrosoftAppId/Password empty → AnonymousTokenProvider (local playground only). - - MicrosoftAppId/Password set → MsalConnectionManager (Teams / Copilot / production). - -Run: - python start_server.py -Test locally: - npx @microsoft/m365agentsplayground + - OpenAIAgentsTraceInstrumentor hooks into OpenAI Agents SDK tracing → emits gen_ai.* spans. + - InvokeAgentScope wraps each turn manually (no auto-instrumentor for M365 turn layer yet). + - All spans → OTLP → local collector. + +Run: uv run python start_server.py +Test: npx @microsoft/m365agentsplayground """ import os from dotenv import load_dotenv +from opentelemetry import context as otel_context + +load_dotenv(override=False) + +# --- MS A365 Observability SDK: configure first, then attach OTLP exporter --- +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +from microsoft_agents_a365.observability.core import ( + AgentDetails, + Channel, + InvokeAgentScope, + InvokeAgentScopeDetails, + Request, + SpanDetails, + configure as a365_configure, + get_tracer_provider as a365_get_tracer_provider, +) -load_dotenv(override=True) - -# --- OTel setup (must happen before any instrumented client is created) --- -from openai import AzureOpenAI -from opentelemetry import trace -from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider +a365_configure(service_name="m365-splunk-ao-demo", service_namespace="splunk.ao.demo") -from splunk_ao.otel import SplunkAOSpanProcessor +_otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") +a365_get_tracer_provider().add_span_processor( + BatchSpanProcessor(OTLPSpanExporter(endpoint=f"{_otlp_endpoint}/v1/traces")) +) -resource = Resource.create({"service.name": "microsoft-365-agents-sdk-example"}) -tracer_provider = TracerProvider(resource=resource) -tracer_provider.add_span_processor(SplunkAOSpanProcessor()) +# --- Auto-instrumentation: hooks into OpenAI Agents SDK tracing --- +from microsoft_agents_a365.observability.extensions.openai import OpenAIAgentsTraceInstrumentor -trace.set_tracer_provider(tracer_provider) +OpenAIAgentsTraceInstrumentor().instrument() -OpenAIInstrumentor().instrument(tracer_provider=tracer_provider) +# --- OpenAI Agents SDK (wraps Azure OpenAI) --- +from agents import Agent, Runner +from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel +from openai import AsyncAzureOpenAI -# --- Azure OpenAI client --- -_openai_client = AzureOpenAI( +_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") +_azure_client = AsyncAzureOpenAI( azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], api_key=os.environ["AZURE_OPENAI_API_KEY"], api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), ) -_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") +_agent = Agent( + name="QAAgent", + instructions="You are a concise, helpful assistant.", + model=OpenAIChatCompletionsModel(model=_deployment, openai_client=_azure_client), +) + +# --- Splunk AO Logger (optional — skipped without creds) --- +from splunk_ao import SplunkAOLogger + +_ao_logger: SplunkAOLogger | None = None +if os.environ.get("SPLUNK_AO_API_KEY") or os.environ.get("SPLUNK_AO_REALM"): + _ao_logger = SplunkAOLogger() +else: + print("[splunk-ao] No credentials — otel-tui-only mode.") # --- Microsoft 365 Agents SDK --- from microsoft_agents.hosting.aiohttp import CloudAdapter from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnContext, TurnState -from microsoft_agents.hosting.core.authorization import AgentAuthConfiguration, AnonymousTokenProvider, ConnectionManager +from microsoft_agents.hosting.core.authorization import ( + AgentAuthConfiguration, + AnonymousTokenProvider, + ConnectionManager, +) _app_id = os.environ.get("MicrosoftAppId", "") _app_password = os.environ.get("MicrosoftAppPassword", "") @@ -75,31 +102,46 @@ STORAGE = MemoryStorage() ADAPTER = CloudAdapter(connection_manager=_connection_manager) - -AGENT_APP = AgentApplication[TurnState]( - storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager -) +AGENT_APP = AgentApplication[TurnState](storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager) @AGENT_APP.conversation_update("membersAdded") -async def on_members_added(context: TurnContext, state: TurnState): - await context.send_activity("Hello! Ask me anything and I'll answer using Azure OpenAI.") +async def on_members_added(context: TurnContext, state: TurnState) -> None: + await context.send_activity("Hello! MS A365 auto-instrumentation demo. Ask me anything.") @AGENT_APP.activity("message") -async def on_message(context: TurnContext, state: TurnState): +async def on_message(context: TurnContext, state: TurnState) -> None: user_text = context.activity.text or "" if not user_text.strip(): await context.send_activity("Please send a message.") return - response = _openai_client.chat.completions.create( - model=_deployment, - messages=[ - {"role": "system", "content": "You are a concise, helpful assistant."}, - {"role": "user", "content": user_text}, - ], - max_tokens=512, + conv_id = context.activity.conversation.id if context.activity.conversation else "unknown" + channel_id = context.activity.channel_id or "unknown" + + request = Request( + content=user_text, + session_id=conv_id, + conversation_id=conv_id, + channel=Channel(name=channel_id), ) - reply = response.choices[0].message.content or "" + agent_details = AgentDetails(agent_id="m365-splunk-ao-demo", agent_name="M365 Splunk AO Demo Agent") + + # InvokeAgentScope: manual turn-level span (no auto-instrumentor for M365 turn layer) + with InvokeAgentScope.start( + request, InvokeAgentScopeDetails(), agent_details, + span_details=SpanDetails(parent_context=otel_context.get_current()), + ): + # OpenAI Agents SDK run — auto-instrumented by OpenAIAgentsTraceInstrumentor + result = await Runner.run(_agent, input=user_text) + reply = result.final_output + + # Splunk AO Logger (optional) + if _ao_logger: + _ao_logger.start_session(name=conv_id, metadata={"channel": channel_id}) + _ao_logger.start_trace(input=user_text, metadata={"conversation_id": conv_id}) + _ao_logger.conclude(output=reply) + _ao_logger.flush() + await context.send_activity(reply) diff --git a/examples/agent/microsoft-365-agents-sdk/pyproject.toml b/examples/agent/microsoft-365-agents-sdk/pyproject.toml index 8a6cee1..4a41a62 100644 --- a/examples/agent/microsoft-365-agents-sdk/pyproject.toml +++ b/examples/agent/microsoft-365-agents-sdk/pyproject.toml @@ -1,14 +1,17 @@ [project] name = "microsoft-365-agents-sdk-example" version = "0.1.0" -description = "Microsoft 365 Agents SDK example with Splunk AO OTel instrumentation" +description = "Microsoft 365 Agents SDK + MS A365 Observability + Splunk AO Logger demo" requires-python = ">=3.11" dependencies = [ - "splunk-ao[otel]", + "splunk-ao", + "openai>=1.0.0", + "opentelemetry-exporter-otlp-proto-http", "microsoft-agents-hosting-aiohttp>=1.1.0", "microsoft-agents-authentication-msal>=1.1.0", - "openai>=1.0.0", - "opentelemetry-instrumentation-openai-v2>=2.1b0", + "microsoft-agents-a365-observability-core>=1.0.0", + "microsoft-agents-a365-observability-hosting>=1.0.0", + "microsoft-agents-a365-observability-extensions-openai>=1.0.0", "azure-identity>=1.19.0", "python-dotenv>=1.0.0", ] diff --git a/examples/agent/microsoft-365-agents-sdk/start_server.py b/examples/agent/microsoft-365-agents-sdk/start_server.py index c8db569..65b7b66 100644 --- a/examples/agent/microsoft-365-agents-sdk/start_server.py +++ b/examples/agent/microsoft-365-agents-sdk/start_server.py @@ -3,6 +3,12 @@ Starts the agent on http://localhost:3978/api/messages. Test locally with: npx @microsoft/m365agentsplayground + +APP_MODE=auto → app_auto.py (auto-instrumented, local collector) +APP_MODE=manual → app_manual.py (manual InferenceScope, local collector) +APP_MODE=auto_lab0 → app_auto_lab0.py (SplunkAOSpanProcessor, auto-instrumented, lab0/staging) +APP_MODE=manual_lab0 → app_manual_lab0.py (SplunkAOLogger, manual instrumentation, lab0/staging) +APP_MODE= → app.py (default) """ import os @@ -10,9 +16,20 @@ from aiohttp.web import Application, Request, Response, run_app from dotenv import load_dotenv -load_dotenv() +load_dotenv(override=False) + +_mode = os.environ.get("APP_MODE", "").lower() +if _mode == "auto": + from app_auto import ADAPTER, AGENT_APP +elif _mode == "manual": + from app_manual import ADAPTER, AGENT_APP +elif _mode == "auto_lab0": + from app_auto_lab0 import ADAPTER, AGENT_APP +elif _mode == "manual_lab0": + from app_manual_lab0 import ADAPTER, AGENT_APP +else: + from app import ADAPTER, AGENT_APP -from app import ADAPTER, AGENT_APP from microsoft_agents.hosting.aiohttp import start_agent_process