diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 051319926f..371f6db297 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -691,7 +691,7 @@ class ObservabilitySettings: Model diagnostics are enabled if either diagnostic is enabled or diagnostic with sensitive events is enabled. """ - return self.enable_instrumentation + return self.enable_instrumentation or self.enable_sensitive_data @property def SENSITIVE_DATA_ENABLED(self) -> bool: diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 71b59a351b..3bb8787de6 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -1015,6 +1015,30 @@ def test_observability_settings_is_setup_initial(monkeypatch): assert settings.is_setup is False +@pytest.mark.parametrize( + ("enable_instrumentation", "enable_sensitive_data", "expected"), + [ + (False, False, False), + (True, False, True), + (False, True, True), + (True, True, True), + ], +) +def test_observability_settings_enabled_property( + monkeypatch, enable_instrumentation: bool, enable_sensitive_data: bool, expected: bool +): + """ENABLED is True when either instrumentation or sensitive data is enabled.""" + from agent_framework.observability import ObservabilitySettings + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + settings = ObservabilitySettings( + enable_instrumentation=enable_instrumentation, + enable_sensitive_data=enable_sensitive_data, + ) + assert settings.ENABLED is expected + + # region Test enable_instrumentation function diff --git a/python/samples/02-agents/observability/README.md b/python/samples/02-agents/observability/README.md index 95aa3fc174..49a1c8a9d4 100644 --- a/python/samples/02-agents/observability/README.md +++ b/python/samples/02-agents/observability/README.md @@ -173,7 +173,7 @@ Finally we have `VS_CODE_EXTENSION_PORT` which you can set to a port, which can The framework will emit observability data when the `ENABLE_INSTRUMENTATION` environment variable is set to `true`. If both are `true` then it will also emit sensitive information. When these are not set, or set to false, you can use the `enable_instrumentation()` function from the `agent_framework.observability` module to turn on instrumentation programmatically. This is useful when you want to control this via code instead of environment variables. -> **Note**: Sensitive information includes prompts, responses, and more, and should only be enabled in a development or test environment. It is not recommended to enable this in production environments as it may expose sensitive data. +> **Note**: Sensitive information includes prompts, responses, and more, and should only be enabled in a development or test environment. It is not recommended to enable this in production environments as it may expose sensitive data. Enabling this will automatically enable instrumentation if it is not already enabled. The two other variables, `ENABLE_CONSOLE_EXPORTERS` and `VS_CODE_EXTENSION_PORT`, are used to configure where the observability data is sent. Those are only activated when calling `configure_otel_providers()`. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.dockerignore b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.dockerignore new file mode 100644 index 0000000000..008e6e6616 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.dockerignore @@ -0,0 +1,6 @@ +.venv +__pycache__ +*.pyc +*.pyo +*.pyd +.Python \ No newline at end of file diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example new file mode 100644 index 0000000000..27c4305120 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example @@ -0,0 +1,5 @@ +FOUNDRY_PROJECT_ENDPOINT="..." +AZURE_AI_MODEL_DEPLOYMENT_NAME="..." +ENABLE_INSTRUMENTATION=true +ENABLE_SENSITIVE_DATA=true +APPLICATIONINSIGHTS_CONNECTION_STRING="..." \ No newline at end of file diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/Dockerfile b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/Dockerfile new file mode 100644 index 0000000000..eaffb94f19 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY . user_agent/ +WORKDIR /app/user_agent + +RUN if [ -f requirements.txt ]; then \ + pip install -r requirements.txt; \ + else \ + echo "No requirements.txt found"; \ + fi + +EXPOSE 8088 + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md new file mode 100644 index 0000000000..84b8bd1bf6 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md @@ -0,0 +1,55 @@ +# What this sample demonstrates + +An instrumented [Agent Framework](https://github.com/microsoft/agent-framework) agent hosted using the **Responses protocol**. + +## How It Works + +### Model Integration + +The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes. + +See [main.py](main.py) for the full implementation. + +### Agent Hosting + +The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol. + +### Instrumentation + +Agent Framework is [**natively instrumented**](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) to capture diagnostics and telemetry for agent execution, but it's turned off by default. This sample demonstrates how to enable instrumentation via environment variables in `agent.manifest.yaml` and `anget.yaml`. The relevant environment variables are `ENABLE_INSTRUMENTATION` and `ENABLE_SENSITIVE_DATA`, which can be set to `true` to enable diagnostics and capture sensitive events respectively. + +Foundry Hosted Agent has built-in observability thus you don't need to set up exporters manually to capture telemetry from your code. The traces, metrics, and logs generated by the agent are automatically collected and made available through Foundry's observability stack via Azure Monitor/Application Insights. The `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable is injected when the agent is deployed to Foundry, however it is still required to be set in your local `.env` file or your environment if you want to run the agent host locally and have telemetry sent to Application Insights from your local environment. + +> Setting `ENABLE_SENSITIVE_DATA` to `true` will automatically enable `ENABLE_INSTRUMENTATION` as well, since capturing sensitive events requires that instrumentation be enabled. + +## Running the Agent Host + +Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host. + +## Interacting with the agent + +> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent. + +Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example: + +```bash +curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What is the current weather?"}' +``` + +A couple of spans will be created for this request from Agent Framework's instrumentation, representing the generation of the response by the agent: + +- `invoke_agent`: This span represents the invocation of the agent itself, capturing the start and end of the agent's processing for this request. +- `chat`: This span represents the call to the underlying model. +- `execute_tool`: This span represents the execution of any tools invoked by the agent as part of generating the response. + +> For more information on the spans, refer to the [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) + +## Deploying the Agent to Foundry + +To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory. + +### Viewing Telemetry in Foundry + +Once the agent is deployed to Foundry, the telemetry generated by the agent (traces, metrics, and logs) will be automatically collected and sent to Azure Monitor/Application Insights. You can view this telemetry by navigating to the Application Insights resource associated with your Foundry project or directly from the Foundry UI. + +In the Foundry UI, next to the **Playground** tab is the **Traces** tab, where you can find the conversations and their corresponding trace IDs. Clicking on a trace ID will allow you to drill into the detailed trace information for that particular conversation. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml new file mode 100644 index 0000000000..b96c34ad96 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml @@ -0,0 +1,27 @@ +name: agent-framework-agent-observability-responses +description: > + A basic Agent Framework agent hosted by Foundry. +metadata: + tags: + - Agent Framework + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming +template: + name: agent-framework-agent-observability-responses + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" + - name: ENABLE_INSTRUMENTATION + value: true + - name: ENABLE_SENSITIVE_DATA + value: true +resources: + - kind: model + id: gpt-4.1-mini + name: AZURE_AI_MODEL_DEPLOYMENT_NAME diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml new file mode 100644 index 0000000000..216dd415d6 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: agent-framework-agent-observability-responses +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: '0.25' + memory: '0.5Gi' +environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: ENABLE_INSTRUMENTATION + value: true + - name: ENABLE_SENSITIVE_DATA + value: true \ No newline at end of file diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/main.py new file mode 100644 index 0000000000..faf7767bfc --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/main.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from random import randint +from typing import Annotated + +from agent_framework import Agent, tool +from agent_framework.foundry import FoundryChatClient +from agent_framework_foundry_hosting import ResponsesHostServer +from azure.identity import DefaultAzureCredential +from dotenv import load_dotenv +from pydantic import Field + +# Load environment variables from .env file +load_dotenv() + + +@tool(approval_mode="never_require") +def get_current_location() -> str: + """Get the current working directory.""" + locations = ["New York", "London", "Paris", "Tokyo"] + return locations[randint(0, len(locations) - 1)] + + +@tool(approval_mode="never_require") +def get_weather( + location: Annotated[str, Field(description="The location to get the weather for.")], +) -> str: + """Get the weather for a given location.""" + conditions = ["sunny", "cloudy", "rainy", "stormy"] + return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." + + +async def main(): + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=DefaultAzureCredential(), + ) + + agent = Agent( + client=client, + instructions="You are a friendly assistant. Keep your answers brief.", + tools=[get_weather, get_current_location], + # History will be managed by the hosting infrastructure, thus there + # is no need to store history by the service. Learn more at: + # https://developers.openai.com/api/reference/resources/responses/methods/create + default_options={"store": False}, + ) + + server = ResponsesHostServer(agent) + await server.run_async() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/requirements.txt b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/requirements.txt new file mode 100644 index 0000000000..f7dc62f3e3 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/requirements.txt @@ -0,0 +1,2 @@ +agent-framework +agent-framework-foundry-hosting \ No newline at end of file