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
32 changes: 23 additions & 9 deletions python/autonomous/github-trending/.env.template
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
# 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_ENDPOINT>>
AZURE_OPENAI_API_KEY=<<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=<<TENANT_ID>>
AGENT365_AGENT_ID=<<AGENT_IDENTITY_ID>>
AGENT365_BLUEPRINT_ID=<<BLUEPRINT_APP_ID>>
AGENT365_CLIENT_ID=<<BLUEPRINT_APP_ID>>
AGENT365_CLIENT_SECRET=<<BLUEPRINT_CLIENT_SECRET>>
AGENT365_AGENT_NAME=<<AGENT_NAME>>
AGENT365_AGENT_DESCRIPTION=<<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=<<TENANT_ID>>
AGENT365OBSERVABILITY__AGENTID=<<AGENT_IDENTITY_ID>>
AGENT365OBSERVABILITY__AGENTBLUEPRINTID=<<BLUEPRINT_APP_ID>>
AGENT365OBSERVABILITY__CLIENTID=<<BLUEPRINT_APP_ID>>
AGENT365OBSERVABILITY__CLIENTSECRET=<<BLUEPRINT_CLIENT_SECRET>>
AGENT365OBSERVABILITY__AGENTNAME=<<AGENT_NAME>>
AGENT365OBSERVABILITY__AGENTDESCRIPTION=<<AGENT_DESCRIPTION>>

# 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 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=false

# GitHub Trending
GITHUB_TRENDING_LANGUAGE=python
Expand Down
43 changes: 31 additions & 12 deletions python/autonomous/github-trending/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,42 @@ a365 setup permissions custom --agent-name <your-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

Expand All @@ -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
```
Expand Down
96 changes: 75 additions & 21 deletions python/autonomous/github-trending/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 ────────────────────────────────────────────────────
Expand Down Expand Up @@ -153,12 +179,40 @@ 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)
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
Comment thread
Yogeshp-MSFT marked this conversation as resolved.
use_foundry_v1_path = bool(
re.search(r"services\.ai\.azure\.com|cognitiveservices\.azure\.com", parsed.netloc, 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="placeholder-foundry-uses-api-key-header",
default_headers={"api-key": AZURE_OPENAI_API_KEY},
)
Comment thread
Yogeshp-MSFT marked this conversation as resolved.
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
Expand Down
Loading