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
139 changes: 139 additions & 0 deletions nodejs/google-adk/sample-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# =============================================================================
# Google ADK Sample Agent (Node.js) — Environment Configuration
# =============================================================================
# Copy this file to .env and fill in your values:
# cp .env.example .env
#
# All values marked <<...>> MUST be replaced before the agent will work.
# Run `a365 config init` first — it generates the config files referenced below.
# =============================================================================

# -----------------------------------------------------------------------------
# Google Gemini Configuration
# -----------------------------------------------------------------------------
# Set GOOGLE_GENAI_USE_VERTEXAI=TRUE to use Vertex AI (recommended for production).
# Set to FALSE to use the public Gemini API with GOOGLE_API_KEY.
GOOGLE_GENAI_USE_VERTEXAI=TRUE
GEMINI_MODEL=gemini-2.5-flash

# --- When GOOGLE_GENAI_USE_VERTEXAI=FALSE (public Gemini API) ---
# Get your API key from: https://aistudio.google.com/app/apikey
GOOGLE_API_KEY=<<YOUR_GOOGLE_API_KEY>>

# --- When GOOGLE_GENAI_USE_VERTEXAI=TRUE (Vertex AI) ---
# Project ID and region of your GCP project where Vertex AI is enabled.
GOOGLE_CLOUD_PROJECT=<<YOUR_GOOGLE_CLOUD_PROJECT>>
GOOGLE_CLOUD_LOCATION=<<YOUR_GOOGLE_CLOUD_LOCATION>>

# Path to the GCP service account JSON key file (Application Default Credentials).
GOOGLE_APPLICATION_CREDENTIALS=<<PATH_TO_SERVICE_ACCOUNT_KEY_JSON>>

# -----------------------------------------------------------------------------
# Agent365 Service Connection (OAuth client credentials)
# -----------------------------------------------------------------------------
# These values authenticate your agent with the Bot Framework and Agent 365.
#
# Where to find them (after running `a365 config init`):
# CLIENTID => a365.generated.config.json -> agentBlueprintId
# CLIENTSECRET => a365.generated.config.json -> agentBlueprintClientSecret
# TENANTID => a365.config.json -> tenantId
#
# IMPORTANT — Client Secret:
# The a365.generated.config.json stores the secret encrypted with Windows DPAPI.
# Use `a365 config display -g` to view the decrypted secret, and copy it here.
#
# IMPORTANT — Client ID and JWT Audience:
# CLIENTID is the blueprint/app-registration ID. Bot Framework tokens are issued
# with aud=CLIENTID, so this value is also used for JWT audience validation.
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<<YOUR_BLUEPRINT_ID>>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<<YOUR_PLAINTEXT_CLIENT_SECRET>>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<<YOUR_TENANT_ID>>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default

# Agentic user-authorization handler settings (do not change these defaults)
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default

# Connection map (do not change)
CONNECTIONSMAP__0__SERVICEURL=*
CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION

# -----------------------------------------------------------------------------
# Agent Identity
# -----------------------------------------------------------------------------
# These identify your agent in the Agent 365 ecosystem.
#
# Where to find them:
# AGENTIC_UPN => a365.config.json -> agentUserPrincipalName
# AGENTIC_NAME => a365.config.json -> agentUserDisplayName
# AGENTIC_USER_ID => a365.generated.config.json -> AgenticUserId
# AGENTIC_APP_ID => a365.generated.config.json -> AgenticAppId
# AGENTIC_TENANT_ID => a365.config.json -> tenantId
#
# NOTE: AGENTIC_APP_ID is the agentic app ID, which is different from the
# blueprint ID (CLIENTID above). Do not use AGENTIC_APP_ID for JWT validation.
AGENTIC_UPN=<<YOUR_AGENT_UPN>>
AGENTIC_NAME=<<YOUR_AGENT_NAME>>
AGENTIC_USER_ID=<<YOUR_AGENTIC_USER_ID>>
AGENTIC_APP_ID=<<YOUR_AGENTIC_APP_ID>>
AGENTIC_TENANT_ID=<<YOUR_TENANT_ID>>

# A365 platform fallback vars (same values as AGENTIC_APP_ID and AGENTIC_USER_ID)
A365_AGENT_APP_INSTANCE_ID=<<YOUR_AGENTIC_APP_ID>>
A365_AGENTIC_USER_ID=<<YOUR_AGENTIC_USER_ID>>

# -----------------------------------------------------------------------------
# Local Development
# -----------------------------------------------------------------------------
# Bearer token for local dev / Playground — obtain with: a365 develop get-token -o raw
# Leave empty to run in bare LLM mode (no MCP tools)
BEARER_TOKEN=

# Authentication handler:
# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers.
# "" — local dev / Agents Playground. Allows anonymous access.
AUTH_HANDLER_NAME=

# USE_AGENTIC_AUTH=true can also be used to enable agentic auth
# USE_AGENTIC_AUTH=

# Agentic Authentication Options (do not change these defaults)
agentic_type=agentic
agentic_altBlueprintConnectionName=service_connection
agentic_scopes=https://graph.microsoft.com/.default
agentic_connectionName=AgenticAuthConnection

# -----------------------------------------------------------------------------
# Agent 365 Observability (stamped by `a365 setup all` for telemetry export)
# -----------------------------------------------------------------------------
agent365Observability__agentId=
agent365Observability__agentName=
agent365Observability__agentDescription=
agent365Observability__tenantId=
agent365Observability__agentBlueprintId=
agent365Observability__clientId=
agent365Observability__clientSecret=

# -----------------------------------------------------------------------------
# Observability
# -----------------------------------------------------------------------------
# ENABLE_OBSERVABILITY=true/false controls whether tracing is set up.
# ENABLE_A365_OBSERVABILITY_EXPORTER=true sends traces to the A365 backend;
# false falls back to the console exporter (expected in local/dev).
ENABLE_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY_EXPORTER=false

# Optional: Azure Monitor connection string for Application Insights
# APPLICATIONINSIGHTS_CONNECTION_STRING=

# A365 exporter log level — shows export HTTP responses, token details, errors.
# Supported: none, info, warn, error — use pipe (|) to combine: info|warn|error
A365_OBSERVABILITY_LOG_LEVEL=info|warn|error

# Logging level (debug, info, warn, error)
LOG_LEVEL=info

# Environment Settings
NODE_ENV=development
PORT=3978
36 changes: 36 additions & 0 deletions nodejs/google-adk/sample-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# A365 deploy artifacts — generated by `a365 deploy` / `a365 develop`
a365.config.json
a365.generated.config.json
app.zip
publish/
deploy.zip

# Manifest folder — generated during deploy
manifest/

# Node.js
node_modules/
dist/
*.js.map
*.d.ts

# Environment — contains secrets
.env
env/.env.playground.user
.localConfigs*

# Credentials — never commit
cred.json
*.pem

# Logs
*.log
log.json
log.txt

# OS files
.DS_Store
Thumbs.db

# IDE (keep .vscode/ for shared launch configs)
.idea/
6 changes: 6 additions & 0 deletions nodejs/google-adk/sample-agent/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"teamsdevapp.ms-teams-vscode-extension",
"ms-windows-ai-studio.windows-ai-studio"
]
}
44 changes: 44 additions & 0 deletions nodejs/google-adk/sample-agent/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Agent",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect=9239",
"--signal",
"SIGINT",
"-r",
"ts-node/register"
],
"args": ["index.ts"],
"env": {
"NODE_ENV": "development"
},
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"name": "Debug in Microsoft 365 Agents Playground",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect=9239",
"--signal",
"SIGINT",
"-r",
"ts-node/register"
],
"args": ["index.ts"],
"env": {
"NODE_ENV": "development"
},
"envFile": "${workspaceFolder}/env/.env.playground",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"preLaunchTask": "Start Agents Playground"
}
]
}
24 changes: 24 additions & 0 deletions nodejs/google-adk/sample-agent/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Agents Playground",
"type": "shell",
"command": "npm run dev:teamsfx:launch-playground",
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^.*$",
"file": 0,
"location": 1,
"message": 2
},
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
}
}
}
]
}
141 changes: 141 additions & 0 deletions nodejs/google-adk/sample-agent/Agent-Code-Walkthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Code Walkthrough: Google ADK Sample Agent

This document provides a detailed technical walkthrough of the Google ADK Sample Agent implementation, covering architecture, key components, and design decisions.

## 📁 File Structure Overview

```
sample-agent/
├── .vscode/
│ ├── extensions.json # Recommended VS Code extensions
│ ├── launch.json # Debug configurations
│ └── tasks.json # Pre-launch tasks
├── env/
│ ├── .env.playground # Playground-specific env vars
│ └── .env.playground.user # User secrets (gitignored)
├── images/
│ └── .gitkeep # Placeholder for future agent thumbnail assets
├── instrumentation.ts # 🔵 OpenTelemetry setup (loaded first)
├── index.ts # 🔵 Express server entry point
├── hosting.ts # 🔵 AgentApplication + handlers
├── agent.ts # 🔵 Google ADK agent + InferenceScope
├── agentInterface.ts # 🔵 Agent interface definition
├── mcpToolRegistrationService.ts # 🔵 MCP tool discovery + registration
├── .env.example # ⚙️ Environment template
├── ToolingManifest.json # 🔧 MCP tools definition
├── package.json # 📦 Dependencies and scripts
├── tsconfig.json # 🔧 TypeScript configuration
├── m365agents.yml # 🔧 Agents Toolkit config
└── m365agents.playground.yml # 🔧 Agents Playground config
```

## 🏗️ Architecture Overview

### Design Principles

1. **Google ADK Integration**: Uses Google's Agent Development Kit with Gemini models (Vertex AI or public API)
2. **Event-Driven**: Bot Framework activity handlers for messages, notifications, and install events
3. **Observability-First**: Microsoft OpenTelemetry Distro with `InferenceScope`, `BaggageBuilder`, and `AgenticTokenCacheInstance`
4. **MCP Tools**: Dynamic discovery and registration of MCP tool servers from the A365 gateway

### Request Flow

```
Teams Message → Express → authorizeJWT → CloudAdapter.process → AgentApplication.run
→ hosting.ts (baggage + observability token)
→ agent.ts (InferenceScope + Google ADK Runner)
→ mcpToolRegistrationService.ts (MCP tool discovery)
→ Gemini LLM (with MCP tools)
→ response → Teams
```

## 🔍 Core Components Deep Dive

### 1. instrumentation.ts — OpenTelemetry Setup

**Must be imported before all other modules** so the SDK can patch libraries (HTTP, Express).

- Loads `.env` via `configDotenv()` before `@microsoft/opentelemetry` reads `A365_OBSERVABILITY_LOG_LEVEL`
- Configures `useMicrosoftOpenTelemetry()` with `AgenticTokenCacheInstance` token resolver
- Enables console exporters in dev mode for local debugging
- Patches `Agent365Exporter.postWithRetries` to log HTTP response bodies (like the Python distro)

### 2. index.ts — Express Server

- Loads auth config via `loadAuthConfigFromEnv()` — always, not just in production
- Registers health endpoints (`/`, `/api/health`, `/robots933456.txt`) **before** JWT middleware
- `authorizeJWT(authConfig)` protects all routes after health endpoints
- Routes `POST /api/messages` through `CloudAdapter.process()` → `AgentApplication.run()`

### 3. hosting.ts — AgentApplication + Handlers

**MyAgent** extends `AgentApplication<TurnState>` and configures:

- `authorization: { agentic: { type: 'agentic' } }` — enables OBO token exchange
- `onActivity(ActivityTypes.Message, ...)` — message handling with baggage + typing loop
- `onAgentNotification("agents:*", ...)` — email, Word comment, lifecycle notifications
- `preloadObservabilityToken()` — refreshes exporter token via `AgenticTokenCacheInstance.refreshObservabilityToken()`
- `BaggageBuilderUtils.fromTurnContext()` — auto-populates tenant, agent, channel, conversation

### 4. agent.ts — Google ADK Agent

**GoogleADKAgent** implements the agent interface:

- **Personalized instructions**: Injects user display name per turn
- **MCP tool initialization**: Delegates to `McpToolRegistrationService` with 10s timeout
- **Google ADK Runner**: `runner.runEphemeral()` with `InMemorySessionService`
- **InferenceScope**: Wraps invocations with `recordInputMessages`, `recordOutputMessages`, `recordFinishReasons`
- **Baggage**: `BaggageBuilderUtils.fromTurnContext()` for auto-populated observability context

### 5. mcpToolRegistrationService.ts — MCP Tool Discovery

- Exchanges OBO token with MCP platform scope (`ea9ffc3e-.../.default`) via `ToolingConfiguration`
- Calls A365 gateway directly (bypasses SDK's `listToolServers` which has a response parsing bug)
- Handles both response shapes: raw array and `{ mcpServers: [...] }`
- Creates `MCPToolset` with `type: "StreamableHTTPConnectionParams"` + `header` auth

### 6. agentInterface.ts — Interface Contract

```typescript
export interface AgentInterface {
invokeAgent(message, auth, authHandlerName, context): Promise<string>;
invokeAgentWithScope(message, auth, authHandlerName, context): Promise<string>;
}
```

## 🔧 Observability

The sample uses the **Microsoft OpenTelemetry Distro** (`@microsoft/opentelemetry`) for end-to-end observability:

- **Token resolver**: `AgenticTokenCacheInstance.getObservabilityToken()` — built-in singleton
- **Token refresh**: `AgenticTokenCacheInstance.refreshObservabilityToken()` on each turn
- **Baggage**: `BaggageBuilderUtils.fromTurnContext()` auto-populates all identity fields
- **InferenceScope**: Wraps LLM calls with input/output messages and finish reasons
- **A365 Exporter**: Sends spans to the A365 backend (flashpoint, sentinel, esp sinks)
- **Console exporters**: Enabled in dev mode for local debugging
- **A365_OBSERVABILITY_LOG_LEVEL**: Set to `info|warn|error` for exporter diagnostics

## 🔐 Authentication

| Mode | Config | Description |
|------|--------|-------------|
| **Teams/dev tunnel** | `AUTH_HANDLER_NAME=AGENTIC` + service connection env vars | JWT validation + OBO token exchange |
| **Playground** | Via Agents Toolkit extension | Uses `env/.env.playground` values |
| **Bare local LLM** | No MCP token / no agentic handler | Runs without MCP tools |

## 📦 MCP Tools

The `ToolingManifest.json` defines available MCP servers. At runtime:

1. Agent discovers servers from the A365 gateway (`/agents/v2/{agenticAppId}/mcpServers`)
2. Creates `MCPToolset` instances with `StreamableHTTPConnectionParams` transport
3. Merges MCP tools with the Google ADK `Agent` tools array
4. Gemini can call tools like `SendEmailWithAttachments` via function calling

## 🔔 Notifications

| Type | Handler | Description |
|------|---------|-------------|
| `EmailNotification` | `handleEmailNotification` | Processes email content, responds via `createEmailResponseActivity()` |
| `WpxComment` | `handleWpxCommentNotification` | Retrieves Word doc content, processes comment |
| `AgentLifecycleNotification` | Log only | No reply needed |
Loading
Loading