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
33 changes: 26 additions & 7 deletions nodejs/autonomous/github-trending/.env.template
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
# 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 github-trending-service.ts.
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-08-01-preview, 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 src/index.ts.
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

# 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).
# Set ENABLE_A365_OBSERVABILITY_EXPORTER=true to ship traces to the A365 portal
# once your Agent 365 credentials above are configured.
ENABLE_A365_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY_EXPORTER=false

# GitHub Trending
GITHUB_TRENDING_LANGUAGE=typescript
GITHUB_TRENDING_MIN_STARS=5
Expand All @@ -24,3 +42,4 @@ GITHUB_TRENDING_MAX_RESULTS=10
HEARTBEAT_INTERVAL_MS=60000
PORT=3979
NODE_ENV=development

41 changes: 31 additions & 10 deletions nodejs/autonomous/github-trending/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,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. **Defaults to `true` when unset.** |
| `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 `useMicrosoftOpenTelemetry()` 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 @@ -93,7 +110,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
AGENT365_USE_MANAGED_IDENTITY=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* and how to call the GitHub Search API.
*/

import { AzureOpenAI } from 'openai';
import OpenAI, { AzureOpenAI } from 'openai';
import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';

import {
Expand All @@ -34,6 +34,12 @@ export interface TrendingServiceConfig {
endpoint: string;
apiKey: string;
deployment: string;
/**
* API version for the classic AzureOpenAI client. Ignored when the endpoint host
* matches a Foundry / AI Services pattern (the `/openai/v1` path rejects `api-version`).
* Defaults to `2024-10-21` when omitted.
*/
apiVersion?: string;
agentDetails: AgentDetails;
language: string;
minStars: number;
Expand All @@ -49,12 +55,49 @@ export interface TrendingServiceConfig {
* @param delayFirstRunMs Optional delay (ms) before the first cycle, e.g. to let token service warm up.
*/
export function startTrendingService(config: TrendingServiceConfig, delayFirstRunMs: number = 0): AbortController {
const client = new AzureOpenAI({
endpoint: config.endpoint,
apiKey: config.apiKey,
apiVersion: '2024-12-01-preview',
deployment: config.deployment,
});
const apiVersion = config.apiVersion ?? '2024-10-21';

// Normalize the endpoint to just <scheme>://<host> — strip any path the user may have
// pasted from the Foundry portal (e.g. /api/projects/<name>, /openai/v1, /openai/v1/responses).
// Validate up-front so misconfigured values fail with a clear message rather than a
// confusing TypeError from the URL constructor.
let parsedEndpoint: URL;
try {
parsedEndpoint = new URL(config.endpoint);
} catch {
throw new Error(
`AZURE_OPENAI_ENDPOINT must be an absolute URL (e.g. https://<your-resource>.openai.azure.com/), got: ${config.endpoint}`,
);
}
if (!/^https?:$/.test(parsedEndpoint.protocol) || !parsedEndpoint.host) {
throw new Error(
`AZURE_OPENAI_ENDPOINT must be an absolute http(s) URL with a host, got: ${config.endpoint}`,
);
}
const resourceEndpoint = `${parsedEndpoint.protocol}//${parsedEndpoint.host}`;

// 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.
const useFoundryV1Path = /services\.ai\.azure\.com|cognitiveservices\.azure\.com/i.test(parsedEndpoint.host);

const foundryBaseURL = `${resourceEndpoint}/openai/v1`;
// Foundry's /openai/v1 path authenticates via the `api-key` request header. The OpenAI SDK
// requires `apiKey` on construction, so we pass a placeholder string — the real credential
// is sent via `defaultHeaders["api-key"]` and Foundry ignores the placeholder Bearer token.
// (Mirrors the placeholder-auth pattern in python/autonomous/github-trending/main.py.)
const client: OpenAI = useFoundryV1Path
? new OpenAI({
baseURL: foundryBaseURL,
apiKey: 'placeholder-foundry-uses-api-key-header',
defaultHeaders: { 'api-key': config.apiKey },
})
: new AzureOpenAI({
endpoint: resourceEndpoint,
apiKey: config.apiKey,
apiVersion,
deployment: config.deployment,
});

console.log(`GitHubTrendingService started. Interval: ${config.intervalMs}ms`);

Expand All @@ -81,7 +124,7 @@ export function startTrendingService(config: TrendingServiceConfig, delayFirstRu
return controller;
}

async function runCycle(client: AzureOpenAI, config: TrendingServiceConfig): Promise<void> {
async function runCycle(client: OpenAI, config: TrendingServiceConfig): Promise<void> {
const { deployment, agentDetails, endpoint, language, minStars, maxResults } = config;

// A365 Observability — propagate baggage context for this cycle.
Expand Down
44 changes: 33 additions & 11 deletions nodejs/autonomous/github-trending/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,28 @@ const shutdownHandles: { intervals: ReturnType<typeof setInterval>[]; controller
const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT!;
const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY!;
const AZURE_OPENAI_DEPLOYMENT = process.env.AZURE_OPENAI_DEPLOYMENT || 'gpt-4o';
const AZURE_OPENAI_API_VERSION = process.env.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).
const TENANT_ID = process.env.AGENT365_TENANT_ID || '';
const AGENT_ID = process.env.AGENT365_AGENT_ID || '';
const BLUEPRINT_ID = process.env.AGENT365_BLUEPRINT_ID || '';
const CLIENT_ID = process.env.AGENT365_CLIENT_ID || '';
const CLIENT_SECRET = process.env.AGENT365_CLIENT_SECRET || '';
const AGENT_NAME = process.env.AGENT365_AGENT_NAME || 'github-trending';
const AGENT_DESCRIPTION = process.env.AGENT365_AGENT_DESCRIPTION || '';
// Read agent365Observability__* (canonical) with legacy AGENT365_* names as fallback
// for backward compatibility with older .env files.
const TENANT_ID = process.env.agent365Observability__tenantId || process.env.AGENT365_TENANT_ID || '';
const AGENT_ID = process.env.agent365Observability__agentId || process.env.AGENT365_AGENT_ID || '';
const BLUEPRINT_ID = process.env.agent365Observability__agentBlueprintId || process.env.AGENT365_BLUEPRINT_ID || '';
const CLIENT_ID = process.env.agent365Observability__clientId || process.env.AGENT365_CLIENT_ID || '';
const CLIENT_SECRET = process.env.agent365Observability__clientSecret || process.env.AGENT365_CLIENT_SECRET || '';
const AGENT_NAME = process.env.agent365Observability__agentName || process.env.AGENT365_AGENT_NAME || 'github-trending';
const AGENT_DESCRIPTION = process.env.agent365Observability__agentDescription || process.env.AGENT365_AGENT_DESCRIPTION || '';
const USE_MANAGED_IDENTITY = (process.env.AGENT365_USE_MANAGED_IDENTITY || 'true').toLowerCase() === 'true';

// Observability feature flags (mirrors python/autonomous/github-trending/main.py).
// 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).
const ENABLE_A365_OBSERVABILITY = (process.env.ENABLE_A365_OBSERVABILITY || 'true').toLowerCase() === 'true';
const ENABLE_A365_OBSERVABILITY_EXPORTER = (process.env.ENABLE_A365_OBSERVABILITY_EXPORTER || 'false').toLowerCase() === 'true';

function hasA365Credentials(): boolean {
const requiredValues = [TENANT_ID, AGENT_ID, CLIENT_ID];
const hasRequiredValues = requiredValues.every(v => v && !v.startsWith('<<'));
Expand All @@ -61,6 +71,9 @@ function hasA365Credentials(): boolean {
}

const A365_ENABLED = hasA365Credentials();
// Exporter is only active when (a) the master observability flag is on,
// (b) credentials are configured, and (c) the exporter flag is on.
const A365_EXPORTER_ENABLED = ENABLE_A365_OBSERVABILITY && A365_ENABLED && ENABLE_A365_OBSERVABILITY_EXPORTER;

const LANGUAGE = process.env.GITHUB_TRENDING_LANGUAGE || 'typescript';
const MIN_STARS = parseInt(process.env.GITHUB_TRENDING_MIN_STARS || '5', 10);
Expand All @@ -87,7 +100,8 @@ const agentDetails: AgentDetails = {
// Build A365 span processors manually so we can set useS2SEndpoint (autonomous S2S scenario).
// The distro's a365 option doesn't yet expose useS2SEndpoint, so we create the exporter ourselves
// and pass it via spanProcessors, leaving a365 unset to avoid a duplicate exporter.
const a365SpanProcessors = A365_ENABLED
// Exporter is only attached when both credentials are present AND the exporter flag is on.
const a365SpanProcessors = A365_EXPORTER_ENABLED
? [
new A365SpanProcessor(),
new BatchSpanProcessor(new Agent365Exporter({
Expand All @@ -98,9 +112,16 @@ const a365SpanProcessors = A365_ENABLED
]
: [];

useMicrosoftOpenTelemetry({
spanProcessors: a365SpanProcessors,
});
if (ENABLE_A365_OBSERVABILITY) {
useMicrosoftOpenTelemetry({
spanProcessors: a365SpanProcessors,
});
console.log(
`Observability configured (a365_exporter=${A365_EXPORTER_ENABLED}, credentials_present=${A365_ENABLED})`
);
} else {
console.log('Observability disabled (ENABLE_A365_OBSERVABILITY=false)');
}

// ── Express server ───────────────────────────────────────────────────────────

Expand Down Expand Up @@ -149,6 +170,7 @@ server.listen(PORT, host, () => {
endpoint: AZURE_OPENAI_ENDPOINT,
apiKey: AZURE_OPENAI_API_KEY,
deployment: AZURE_OPENAI_DEPLOYMENT,
apiVersion: AZURE_OPENAI_API_VERSION,
agentDetails,
language: LANGUAGE,
minStars: MIN_STARS,
Expand Down
Loading