From a2684fee254a90bb22b8d4f4b9886f595ea7004e Mon Sep 17 00:00:00 2001 From: prajapatiy9826 Date: Wed, 17 Jun 2026 12:52:23 +0530 Subject: [PATCH 1/2] python/autonomous/github-trending: support Foundry endpoints + wire observability flags * Auto-detect Azure OpenAI vs Foundry endpoints in main.py: - services.ai.azure.com / cognitiveservices.azure.com -> AsyncOpenAI with base_url=/openai/v1 and api-key header (no api-version) - openai.azure.com -> AsyncAzureOpenAI with api_version (existing) - Strips paths users may paste from the Foundry portal (e.g. /api/projects//openai/v1/responses) so any shape works. * Wire ENABLE_A365_OBSERVABILITY and ENABLE_A365_OBSERVABILITY_EXPORTER flags as the README documents. Previously the code ignored both, so docs and runtime behavior diverged. * Accept canonical AGENT365OBSERVABILITY__* env-var names (what `a365 setup all` writes to .env on CLI v1.1.206+) with fallback to legacy AGENT365_* aliases for backward compatibility. * Add AZURE_OPENAI_API_VERSION env var (default 2024-10-21) so users on classic Azure OpenAI can pin a specific version. Foundry ignores this value. * Update README configuration table and Quick-Start to document Foundry support, both env-var conventions side-by-side, and observability flag semantics. Validation: end-to-end run against a Foundry account with a fresh Agent 365 identity. chat.completions returned 200 OK, full A365 span tree (invoke_agent -> Chat -> execute_tool) generated and exported to the A365 backend; HTTP /api/health and / endpoints, heartbeat, and graceful shutdown all verified. --- .../autonomous/github-trending/.env.template | 30 +++++-- python/autonomous/github-trending/README.md | 43 ++++++--- python/autonomous/github-trending/main.py | 88 ++++++++++++++----- 3 files changed, 119 insertions(+), 42 deletions(-) diff --git a/python/autonomous/github-trending/.env.template b/python/autonomous/github-trending/.env.template index 80051925..9aba707a 100644 --- a/python/autonomous/github-trending/.env.template +++ b/python/autonomous/github-trending/.env.template @@ -1,23 +1,35 @@ # Azure OpenAI +# Both classic Azure OpenAI (.openai.azure.com) and Foundry endpoints +# (services.ai.azure.com / cognitiveservices.azure.com) are supported. +# Any path you paste (e.g. /api/projects/.../openai/v1/responses) is stripped +# automatically by main.py. AZURE_OPENAI_ENDPOINT=<> AZURE_OPENAI_API_KEY=<> AZURE_OPENAI_DEPLOYMENT=gpt-4o +# Pick an API version supported by your Azure OpenAI / Foundry resource (e.g. 2024-10-21 GA, 2024-11-20, 2025-01-01-preview). +# Foundry endpoints use /openai/v1 and ignore this value. +AZURE_OPENAI_API_VERSION=2024-10-21 # Agent 365 Observability -AGENT365_TENANT_ID=<> -AGENT365_AGENT_ID=<> -AGENT365_BLUEPRINT_ID=<> -AGENT365_CLIENT_ID=<> -AGENT365_CLIENT_SECRET=<> -AGENT365_AGENT_NAME=<> -AGENT365_AGENT_DESCRIPTION=<> +# Canonical (preferred) keys: agent365Observability__* convention used by Microsoft Agents SDK. +# Legacy AGENT365_* names are still accepted as a fallback by main.py. +AGENT365OBSERVABILITY__TENANTID=<> +AGENT365OBSERVABILITY__AGENTID=<> +AGENT365OBSERVABILITY__AGENTBLUEPRINTID=<> +AGENT365OBSERVABILITY__CLIENTID=<> +AGENT365OBSERVABILITY__CLIENTSECRET=<> +AGENT365OBSERVABILITY__AGENTNAME=<> +AGENT365OBSERVABILITY__AGENTDESCRIPTION=<> # Auth strategy: set to "true" for production (MSI), "false" for local dev (client secret) AGENT365_USE_MANAGED_IDENTITY=false -# Enables the A365 exporter to send spans to the observability endpoint -ENABLE_A365_OBSERVABILITY_EXPORTER=true +# Observability feature flags +# ENABLE_A365_OBSERVABILITY — master switch for the OpenTelemetry pipeline. +# ENABLE_A365_OBSERVABILITY_EXPORTER — when false, spans go to console only +# (no upload to the A365 backend). ENABLE_A365_OBSERVABILITY=true +ENABLE_A365_OBSERVABILITY_EXPORTER=true # GitHub Trending GITHUB_TRENDING_LANGUAGE=python diff --git a/python/autonomous/github-trending/README.md b/python/autonomous/github-trending/README.md index dc89b76e..d91ccdd4 100644 --- a/python/autonomous/github-trending/README.md +++ b/python/autonomous/github-trending/README.md @@ -54,27 +54,42 @@ a365 setup permissions custom --agent-name --resource-app-id 9 ### Configuration -Copy `.env.template` to `.env` and fill in the values from `a365 setup all` output: +Copy `.env.template` to `.env`. Most values are auto-stamped by `a365 setup all`; fill in the Azure OpenAI fields manually. ```bash cp .env.template .env ``` +#### Azure OpenAI + | Variable | Set by | Description | |----------|--------|-------------| -| `AZURE_OPENAI_ENDPOINT` | Manual | Azure OpenAI resource endpoint | +| `AZURE_OPENAI_ENDPOINT` | Manual | Azure OpenAI resource endpoint. Both classic Azure OpenAI (`*.openai.azure.com`) and Foundry / AI Services (`*.services.ai.azure.com`, `*.cognitiveservices.azure.com`) are supported. Any path you paste from the Foundry portal (e.g. `/api/projects/.../openai/v1/responses`) is stripped automatically. | | `AZURE_OPENAI_API_KEY` | Manual | Azure OpenAI API key | | `AZURE_OPENAI_DEPLOYMENT` | Manual | Model deployment name (default: `gpt-4o`) | -| `AGENT365_TENANT_ID` | CLI | Entra tenant ID | -| `AGENT365_AGENT_ID` | CLI | Agent identity ID (separate from blueprint) | -| `AGENT365_BLUEPRINT_ID` | CLI | Blueprint app registration ID | -| `AGENT365_CLIENT_ID` | CLI | Blueprint app ID (same as blueprint ID) | -| `AGENT365_CLIENT_SECRET` | CLI | Blueprint client secret | -| `AGENT365_AGENT_NAME` | CLI | Display name shown in traces | -| `AGENT365_AGENT_DESCRIPTION` | CLI | Agent description shown in traces | -| `AGENT365_USE_MANAGED_IDENTITY` | Manual | `true` for production (MSI), `false` for local dev | -| `ENABLE_A365_OBSERVABILITY_EXPORTER` | Manual | `true` to enable the A365 span exporter. Required for exporting traces to the A365 observability endpoint. | -| `ENABLE_A365_OBSERVABILITY` | Manual | `true` to enable SDK scope spans (`InvokeAgentScope`, `InferenceScope`, `ExecuteToolScope`). Required for the Python SDK. | +| `AZURE_OPENAI_API_VERSION` | Manual | API version for **classic** Azure OpenAI endpoints only. Foundry endpoints use `/openai/v1` and ignore this value. Default: `2024-10-21`. | + +#### Agent 365 observability credentials + +The Agent 365 CLI stamps these values directly into `.env` when you run `a365 setup all`. Both the canonical `AGENT365OBSERVABILITY__*` keys and legacy `AGENT365_*` aliases are accepted; the canonical keys are preferred and take precedence when both are present. + +| Variable (canonical) | Legacy alias | Set by | Description | +|----------------------|--------------|--------|-------------| +| `AGENT365OBSERVABILITY__TENANTID` | `AGENT365_TENANT_ID` | CLI | Entra tenant ID | +| `AGENT365OBSERVABILITY__AGENTID` | `AGENT365_AGENT_ID` | CLI | Agent identity ID (separate from blueprint) | +| `AGENT365OBSERVABILITY__AGENTBLUEPRINTID` | `AGENT365_BLUEPRINT_ID` | CLI | Blueprint app registration ID | +| `AGENT365OBSERVABILITY__CLIENTID` | `AGENT365_CLIENT_ID` | CLI | Blueprint app ID (same as blueprint ID) | +| `AGENT365OBSERVABILITY__CLIENTSECRET` | `AGENT365_CLIENT_SECRET` | CLI | Blueprint client secret | +| `AGENT365OBSERVABILITY__AGENTNAME` | `AGENT365_AGENT_NAME` | CLI | Display name shown in traces | +| `AGENT365OBSERVABILITY__AGENTDESCRIPTION` | `AGENT365_AGENT_DESCRIPTION` | CLI | Agent description shown in traces | +| `AGENT365_USE_MANAGED_IDENTITY` | — | Manual | `true` for production (MSI), `false` for local dev. **Defaults to `true` when unset.** | + +#### Observability feature flags + +| Variable | Default | Description | +|----------|---------|-------------| +| `ENABLE_A365_OBSERVABILITY` | `true` | Master switch for the OpenTelemetry pipeline. Set `false` to skip `use_microsoft_opentelemetry()` entirely (no spans generated). | +| `ENABLE_A365_OBSERVABILITY_EXPORTER` | `false` | When `true`, spans are uploaded to the A365 observability backend. When `false`, spans are produced but only printed to the console exporter. **Set to `true` to ship traces to the A365 portal.** | ### GitHub Trending Configuration @@ -94,7 +109,11 @@ The GitHub Search API is unauthenticated — no API key required (rate limit: 10 You can run the agent with **just Azure OpenAI credentials** — no Agent 365 setup required. Create a minimal `.env`: ```bash +# Classic Azure OpenAI: AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ +# — or Foundry / AI Services: +# AZURE_OPENAI_ENDPOINT=https://your-foundry-account.services.ai.azure.com/ + AZURE_OPENAI_API_KEY=your-api-key AZURE_OPENAI_DEPLOYMENT=gpt-4o ``` diff --git a/python/autonomous/github-trending/main.py b/python/autonomous/github-trending/main.py index b670a13b..ecc6bbe8 100644 --- a/python/autonomous/github-trending/main.py +++ b/python/autonomous/github-trending/main.py @@ -11,11 +11,13 @@ import asyncio import logging import os +import re from datetime import datetime, timezone +from urllib.parse import urlparse from aiohttp import web from dotenv import load_dotenv -from openai import AsyncAzureOpenAI +from openai import AsyncAzureOpenAI, AsyncOpenAI from microsoft.opentelemetry import use_microsoft_opentelemetry from microsoft.opentelemetry.a365.core import AgentDetails @@ -43,20 +45,32 @@ if not AZURE_OPENAI_API_KEY: raise SystemExit("AZURE_OPENAI_API_KEY environment variable is required but not set.") AZURE_OPENAI_DEPLOYMENT = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o") +# Only consumed by the classic AzureOpenAI client. Foundry's /openai/v1 path ignores +# (and in fact rejects) api-version, so this is effectively a default for non-Foundry endpoints. +AZURE_OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21") # Agent 365 Observability — optional. When these are missing or set to placeholders, # the agent runs without A365 observability export (spans go to console only). -TENANT_ID = os.environ.get("AGENT365_TENANT_ID", "") -AGENT_ID = os.environ.get("AGENT365_AGENT_ID", "") -BLUEPRINT_ID = os.environ.get("AGENT365_BLUEPRINT_ID", "") -CLIENT_ID = os.environ.get("AGENT365_CLIENT_ID", "") -CLIENT_SECRET = os.environ.get("AGENT365_CLIENT_SECRET", "") -AGENT_NAME = os.environ.get("AGENT365_AGENT_NAME", "github-trending") -AGENT_DESCRIPTION = os.environ.get("AGENT365_AGENT_DESCRIPTION", "") +# Read AGENT365OBSERVABILITY__* (canonical) with legacy AGENT365_* names as fallback +# for backward compatibility with older .env files. +TENANT_ID = os.environ.get("AGENT365OBSERVABILITY__TENANTID") or os.environ.get("AGENT365_TENANT_ID", "") +AGENT_ID = os.environ.get("AGENT365OBSERVABILITY__AGENTID") or os.environ.get("AGENT365_AGENT_ID", "") +BLUEPRINT_ID = os.environ.get("AGENT365OBSERVABILITY__AGENTBLUEPRINTID") or os.environ.get("AGENT365_BLUEPRINT_ID", "") +CLIENT_ID = os.environ.get("AGENT365OBSERVABILITY__CLIENTID") or os.environ.get("AGENT365_CLIENT_ID", "") +CLIENT_SECRET = os.environ.get("AGENT365OBSERVABILITY__CLIENTSECRET") or os.environ.get("AGENT365_CLIENT_SECRET", "") +AGENT_NAME = (os.environ.get("AGENT365OBSERVABILITY__AGENTNAME") or os.environ.get("AGENT365_AGENT_NAME", "github-trending")).strip('"') +AGENT_DESCRIPTION = os.environ.get("AGENT365OBSERVABILITY__AGENTDESCRIPTION") or os.environ.get("AGENT365_AGENT_DESCRIPTION", "") # Default to MSI in production (matches .NET appsettings.json default of true). # Local dev .env sets this to "false" to use client secret instead. USE_MANAGED_IDENTITY = os.environ.get("AGENT365_USE_MANAGED_IDENTITY", "true").lower() == "true" +# Observability feature flags (mirrors python/google-adk/sample-agent/main.py pattern). +# ENABLE_A365_OBSERVABILITY — master switch for the OpenTelemetry pipeline. +# ENABLE_A365_OBSERVABILITY_EXPORTER — when false, spans go to console only +# (no upload to the A365 backend). +ENABLE_A365_OBSERVABILITY = os.environ.get("ENABLE_A365_OBSERVABILITY", "true").lower() == "true" +ENABLE_A365_OBSERVABILITY_EXPORTER = os.environ.get("ENABLE_A365_OBSERVABILITY_EXPORTER", "false").lower() == "true" + def _has_a365_credentials() -> bool: """Check whether Agent 365 observability credentials are fully configured.""" required_values = [TENANT_ID, AGENT_ID, CLIENT_ID] @@ -85,18 +99,30 @@ def _has_a365_credentials() -> bool: ) A365_ENABLED = _has_a365_credentials() +# Exporter is only active when (a) the master observability flag is on, +# (b) credentials are configured, and (c) the exporter flag is on. +A365_EXPORTER_ENABLED = ENABLE_A365_OBSERVABILITY and A365_ENABLED and ENABLE_A365_OBSERVABILITY_EXPORTER # ── Microsoft OpenTelemetry Distro ─────────────────────────────────────────── # Equivalent to .NET's builder.UseMicrosoftOpenTelemetry(). # Token resolver reads from the in-memory cache populated by the background token service. -# When A365 credentials are not configured, the A365 exporter is disabled. - -use_microsoft_opentelemetry( - enable_a365=A365_ENABLED, - enable_azure_monitor=False, - a365_use_s2s_endpoint=True, - a365_token_resolver=lambda agent_id, tenant_id: token_cache.get_cached_token(agent_id, tenant_id) or "", -) +# When A365 credentials are not configured or observability is disabled, the +# A365 exporter is skipped and spans go to the console only. + +if ENABLE_A365_OBSERVABILITY: + use_microsoft_opentelemetry( + enable_a365=A365_EXPORTER_ENABLED, + enable_azure_monitor=False, + a365_use_s2s_endpoint=True, + a365_token_resolver=lambda agent_id, tenant_id: token_cache.get_cached_token(agent_id, tenant_id) or "", + ) + logger.info( + "Observability configured (a365_exporter=%s, credentials_present=%s)", + A365_EXPORTER_ENABLED, + A365_ENABLED, + ) +else: + logger.info("Observability disabled (ENABLE_A365_OBSERVABILITY=false)") # ── Health check endpoint ──────────────────────────────────────────────────── @@ -153,12 +179,32 @@ async def start_background_tasks(app: web.Application) -> None: # Heartbeat app["heartbeat_task"] = asyncio.create_task(heartbeat_loop(interval_seconds)) - # Azure OpenAI client — stored on the app for proper shutdown - client = AsyncAzureOpenAI( - azure_endpoint=AZURE_OPENAI_ENDPOINT, - api_key=AZURE_OPENAI_API_KEY, - api_version="2024-12-01-preview", + # OpenAI client — Foundry resources (services.ai.azure.com / cognitiveservices.azure.com) + # use the OpenAI-compatible /openai/v1 path which does NOT accept the api-version query. + # Classic Azure OpenAI resources (.openai.azure.com) use the legacy deployments path. + # This mirrors the Node.js sample at nodejs/autonomous/github-trending/src/github-trending-service.ts. + parsed = urlparse(AZURE_OPENAI_ENDPOINT) + resource_endpoint = f"{parsed.scheme}://{parsed.netloc}" # strip any path pasted from the portal + use_foundry_v1_path = bool( + re.search(r"services\.ai\.azure\.com|cognitiveservices\.azure\.com", parsed.netloc, re.IGNORECASE) + or re.fullmatch(r"preview", AZURE_OPENAI_API_VERSION, re.IGNORECASE) ) + + if use_foundry_v1_path: + client = AsyncOpenAI( + base_url=f"{resource_endpoint}/openai/v1", + api_key=AZURE_OPENAI_API_KEY, + default_headers={"api-key": AZURE_OPENAI_API_KEY}, + ) + logger.info("Using Foundry OpenAI-compatible client (base_url=%s/openai/v1)", resource_endpoint) + else: + client = AsyncAzureOpenAI( + azure_endpoint=resource_endpoint, + api_key=AZURE_OPENAI_API_KEY, + api_version=AZURE_OPENAI_API_VERSION, + ) + logger.info("Using classic Azure OpenAI client (endpoint=%s, api_version=%s)", resource_endpoint, AZURE_OPENAI_API_VERSION) + app["openai_client"] = client # Trending digest service From 8531363d5501d1febaa560f1f6a957c14c36b2a9 Mon Sep 17 00:00:00 2001 From: prajapatiy9826 Date: Wed, 17 Jun 2026 14:34:55 +0530 Subject: [PATCH 2/2] python/autonomous/github-trending: address Copilot review feedback Three small fixes from the Copilot AI review on PR #322: 1. Validate AZURE_OPENAI_ENDPOINT scheme + host - urlparse() silently produces empty scheme/netloc on input like "your-resource.openai.azure.com" (no https://), which would cascade into a malformed resource_endpoint such as "://". - Add an explicit check after urlparse() and raise SystemExit with a clear example URL so the failure mode is obvious. 2. Stop sending two conflicting auth headers to Foundry endpoints - The previous Foundry branch passed api_key=AZURE_OPENAI_API_KEY (which the OpenAI SDK turns into Authorization: Bearer ...) AND default_headers={"api-key": AZURE_OPENAI_API_KEY}, sending both headers on every request. - Foundry's /openai/v1 path expects the api-key header, so keep default_headers and pass a placeholder string for api_key (the SDK requires it to be set; Foundry ignores the placeholder Bearer token). 3. Drop the dead apiVersion=="preview" detection branch - The ^preview$ sentinel never matched documented values like 2024-08-01-preview and was never triggered in practice. Host detection alone is sufficient. 4. Align .env.template default with the README - README documents ENABLE_A365_OBSERVABILITY_EXPORTER default as false. - Template was set to true, which would silently start uploading traces when a user copies the template and fills in credentials. - Flip template default to false; users explicitly opt in by changing it. No functional change for users on classic Azure OpenAI endpoints. --- python/autonomous/github-trending/.env.template | 8 +++++--- python/autonomous/github-trending/main.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/python/autonomous/github-trending/.env.template b/python/autonomous/github-trending/.env.template index 9aba707a..f71d9856 100644 --- a/python/autonomous/github-trending/.env.template +++ b/python/autonomous/github-trending/.env.template @@ -26,10 +26,12 @@ AGENT365_USE_MANAGED_IDENTITY=false # Observability feature flags # ENABLE_A365_OBSERVABILITY — master switch for the OpenTelemetry pipeline. -# ENABLE_A365_OBSERVABILITY_EXPORTER — when false, spans go to console only -# (no upload to the A365 backend). +# ENABLE_A365_OBSERVABILITY_EXPORTER — when true, spans are uploaded to the A365 +# observability backend. When false (default), +# spans are produced but only printed to the +# console exporter. Set to true to ship traces. ENABLE_A365_OBSERVABILITY=true -ENABLE_A365_OBSERVABILITY_EXPORTER=true +ENABLE_A365_OBSERVABILITY_EXPORTER=false # GitHub Trending GITHUB_TRENDING_LANGUAGE=python diff --git a/python/autonomous/github-trending/main.py b/python/autonomous/github-trending/main.py index ecc6bbe8..7e13e6a0 100644 --- a/python/autonomous/github-trending/main.py +++ b/python/autonomous/github-trending/main.py @@ -184,16 +184,24 @@ async def start_background_tasks(app: web.Application) -> None: # Classic Azure OpenAI resources (.openai.azure.com) use the legacy deployments path. # This mirrors the Node.js sample at nodejs/autonomous/github-trending/src/github-trending-service.ts. parsed = urlparse(AZURE_OPENAI_ENDPOINT) + if not parsed.scheme or not parsed.netloc: + raise SystemExit( + f"AZURE_OPENAI_ENDPOINT must be an absolute URL with a scheme (e.g. " + f"https://your-resource.openai.azure.com/ or " + f"https://your-foundry-account.services.ai.azure.com/). Got: {AZURE_OPENAI_ENDPOINT!r}" + ) resource_endpoint = f"{parsed.scheme}://{parsed.netloc}" # strip any path pasted from the portal use_foundry_v1_path = bool( re.search(r"services\.ai\.azure\.com|cognitiveservices\.azure\.com", parsed.netloc, re.IGNORECASE) - or re.fullmatch(r"preview", AZURE_OPENAI_API_VERSION, re.IGNORECASE) ) if use_foundry_v1_path: + # Foundry's /openai/v1 path expects authentication via the `api-key` header. + # The OpenAI SDK requires api_key to be set, so pass a placeholder — the real + # credential is sent via default_headers and Foundry ignores the placeholder Bearer token. client = AsyncOpenAI( base_url=f"{resource_endpoint}/openai/v1", - api_key=AZURE_OPENAI_API_KEY, + api_key="placeholder-foundry-uses-api-key-header", default_headers={"api-key": AZURE_OPENAI_API_KEY}, ) logger.info("Using Foundry OpenAI-compatible client (base_url=%s/openai/v1)", resource_endpoint)