diff --git a/.gitignore b/.gitignore index 938dae64..7193dd15 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,12 @@ coverage/ **/.claude/ .idea/ +# A365 deploy artifacts — generated by `a365 deploy` / `a365 develop` +a365.config.json +a365.generated.config.json +app.zip +publish/ + # OS-specific files .DS_Store Thumbs.db diff --git a/nodejs/claude/sample-agent/package.json b/nodejs/claude/sample-agent/package.json index aa44ad32..00e58a49 100644 --- a/nodejs/claude/sample-agent/package.json +++ b/nodejs/claude/sample-agent/package.json @@ -16,15 +16,15 @@ "license": "MIT", "description": "", "dependencies": { - "@microsoft/agents-hosting": "^1.2.2", - "@microsoft/agents-activity": "^1.2.2", - "@microsoft/agents-a365-notifications": "^0.1.0-preview.30", - "@microsoft/agents-a365-observability": "^0.1.0-preview.30", - "@microsoft/agents-a365-runtime": "^0.1.0-preview.30", - "@microsoft/agents-a365-observability-hosting": "^0.1.0-preview.64", - "@microsoft/agents-a365-tooling": "^0.1.0-preview.30", - "@microsoft/agents-a365-tooling-extensions-claude": "^0.1.0-preview.30", "@anthropic-ai/claude-agent-sdk": "^0.1.1", + "@microsoft/agents-a365-notifications": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability-hosting": "^0.1.0-preview.125", + "@microsoft/agents-a365-runtime": "^0.1.0-preview.125", + "@microsoft/agents-a365-tooling": "^0.1.0-preview.125", + "@microsoft/agents-a365-tooling-extensions-claude": "^0.1.0-preview.125", + "@microsoft/agents-activity": "^1.2.2", + "@microsoft/agents-hosting": "^1.2.2", "dotenv": "^17.2.2", "express": "^5.1.0" }, diff --git a/nodejs/claude/sample-agent/src/client.ts b/nodejs/claude/sample-agent/src/client.ts index 76350ee6..93bb9e34 100644 --- a/nodejs/claude/sample-agent/src/client.ts +++ b/nodejs/claude/sample-agent/src/client.ts @@ -13,8 +13,8 @@ import { Builder, InferenceOperationType, AgentDetails, - TenantDetails, InferenceDetails, + Request, Agent365ExporterOptions, } from '@microsoft/agents-a365-observability'; import { AgenticTokenCacheInstance } from '@microsoft/agents-a365-observability-hosting'; @@ -141,17 +141,16 @@ class ClaudeClient implements Client { model: this.config.model || "", }; - const agentDetails: AgentDetails = { - agentId: 'claude-travel-agent', - agentName: 'Claude Travel Agent', + const request: Request = { conversationId: 'conv-12345', }; - const tenantDetails: TenantDetails = { - tenantId: 'claude-sample-tenant', + const agentDetails: AgentDetails = { + agentId: 'claude-travel-agent', + agentName: 'Claude Travel Agent', }; - const scope = InferenceScope.start(inferenceDetails, agentDetails, tenantDetails); + const scope = InferenceScope.start(request, inferenceDetails, agentDetails); const response = await this.invokeAgent(prompt); diff --git a/nodejs/langchain/sample-agent/package.json b/nodejs/langchain/sample-agent/package.json index 346868fc..b0717bcb 100644 --- a/nodejs/langchain/sample-agent/package.json +++ b/nodejs/langchain/sample-agent/package.json @@ -23,14 +23,14 @@ "@langchain/langgraph": "^1.0.2", "@langchain/mcp-adapters": "^1.0.0", "@langchain/openai": "^1.0.2", - "@microsoft/agents-a365-notifications": "^0.1.0-preview.30", - "@microsoft/agents-a365-observability": "^0.1.0-preview.30", - "@microsoft/agents-a365-runtime": "^0.1.0-preview.30", - "@microsoft/agents-a365-tooling": "^0.1.0-preview.30", - "@microsoft/agents-a365-tooling-extensions-langchain": "^0.1.0-preview.30", + "@microsoft/agents-a365-notifications": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability-hosting": "^0.1.0-preview.125", + "@microsoft/agents-a365-runtime": "^0.1.0-preview.125", + "@microsoft/agents-a365-tooling": "^0.1.0-preview.125", + "@microsoft/agents-a365-tooling-extensions-langchain": "^0.1.0-preview.125", "@microsoft/agents-activity": "^1.2.2", "@microsoft/agents-hosting": "^1.2.2", - "@microsoft/agents-a365-observability-hosting": "^0.1.0-preview.64", "dotenv": "^17.2.3", "express": "^5.1.0", "langchain": "^1.0.1", diff --git a/nodejs/langchain/sample-agent/src/client.ts b/nodejs/langchain/sample-agent/src/client.ts index b24e5072..c18445a3 100644 --- a/nodejs/langchain/sample-agent/src/client.ts +++ b/nodejs/langchain/sample-agent/src/client.ts @@ -16,8 +16,8 @@ import { Builder, InferenceOperationType, AgentDetails, - TenantDetails, InferenceDetails, + Request, Agent365ExporterOptions, } from '@microsoft/agents-a365-observability'; import { AgenticTokenCacheInstance } from '@microsoft/agents-a365-observability-hosting'; @@ -209,18 +209,18 @@ class LangChainClient implements Client { model: "gpt-4o-mini", }; - const agentDetails: AgentDetails = { - agentId: this.turnContext?.activity?.recipient?.agenticAppId || agentName, - agentName: agentName, + const request: Request = { conversationId: this.turnContext?.activity?.conversation?.id || `conv-${Date.now()}`, }; - const tenantDetails: TenantDetails = { + const agentDetails: AgentDetails = { + agentId: this.turnContext?.activity?.recipient?.agenticAppId || agentName, + agentName: agentName, tenantId: this.turnContext?.activity?.recipient?.tenantId || 'sample-tenant', }; let response = ''; - const scope = InferenceScope.start(inferenceDetails, agentDetails, tenantDetails); + const scope = InferenceScope.start(request, inferenceDetails, agentDetails); try { await scope.withActiveSpanAsync(async () => { response = await this.invokeAgent(prompt); diff --git a/nodejs/openai/sample-agent/package.json b/nodejs/openai/sample-agent/package.json index a07a3777..89df73ed 100644 --- a/nodejs/openai/sample-agent/package.json +++ b/nodejs/openai/sample-agent/package.json @@ -15,19 +15,19 @@ "license": "MIT", "description": "", "dependencies": { - "@microsoft/agents-hosting": "^1.2.2", + "@microsoft/agents-a365-notifications": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability-extensions-openai": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability-hosting": "^0.1.0-preview.125", + "@microsoft/agents-a365-runtime": "^0.1.0-preview.125", + "@microsoft/agents-a365-tooling": "^0.1.0-preview.125", + "@microsoft/agents-a365-tooling-extensions-openai": "^0.1.0-preview.125", "@microsoft/agents-activity": "^1.2.2", - "@microsoft/agents-a365-notifications": "^0.1.0-preview.30", - "@microsoft/agents-a365-observability": "^0.1.0-preview.30", - "@microsoft/agents-a365-runtime": "^0.1.0-preview.30", - "@microsoft/agents-a365-observability-hosting": "^0.1.0-preview.64", - "@microsoft/agents-a365-tooling": "^0.1.0-preview.30", - "@microsoft/agents-a365-tooling-extensions-openai": "^0.1.0-preview.30", - "@microsoft/agents-a365-observability-extensions-openai": "^0.1.0-preview.30", + "@microsoft/agents-hosting": "^1.2.2", "@openai/agents": "^0.1.11", - "openai": "^4.77.0", "dotenv": "^17.2.2", - "express": "^5.1.0" + "express": "^5.1.0", + "openai": "^4.77.0" }, "devDependencies": { "@microsoft/m365agentsplayground": "^0.2.18", diff --git a/nodejs/openai/sample-agent/src/client.ts b/nodejs/openai/sample-agent/src/client.ts index 428e9fbf..a59d3248 100644 --- a/nodejs/openai/sample-agent/src/client.ts +++ b/nodejs/openai/sample-agent/src/client.ts @@ -22,8 +22,8 @@ import { Builder, InferenceOperationType, AgentDetails, - TenantDetails, InferenceDetails, + Request, Agent365ExporterOptions, } from '@microsoft/agents-a365-observability'; import { OpenAIAgentsTraceInstrumentor } from '@microsoft/agents-a365-observability-extensions-openai'; @@ -147,17 +147,16 @@ class OpenAIClient implements Client { model: this.agent.model.toString(), }; + const request: Request = { + conversationId: 'conv-12345', + }; + const agentDetails: AgentDetails = { agentId: 'typescript-compliance-agent', agentName: 'TypeScript Compliance Agent', - conversationId: 'conv-12345', }; - const tenantDetails: TenantDetails = { - tenantId: 'typescript-sample-tenant', - }; - - const scope = InferenceScope.start(inferenceDetails, agentDetails, tenantDetails); + const scope = InferenceScope.start(request, inferenceDetails, agentDetails); try { await scope.withActiveSpanAsync(async () => { try { diff --git a/nodejs/vercel-sdk/sample-agent/package.json b/nodejs/vercel-sdk/sample-agent/package.json index 52bd2a27..69a48a93 100644 --- a/nodejs/vercel-sdk/sample-agent/package.json +++ b/nodejs/vercel-sdk/sample-agent/package.json @@ -20,9 +20,11 @@ "license": "MIT", "dependencies": { "@ai-sdk/anthropic": "^2.0.31", - "@microsoft/agents-a365-notifications": "^0.1.0-preview.30", - "@microsoft/agents-a365-observability": "^0.1.0-preview.30", - "@microsoft/agents-a365-runtime": "^0.1.0-preview.30", + "@microsoft/agents-a365-notifications": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability": "^0.1.0-preview.125", + "@microsoft/agents-a365-observability-hosting": "^0.1.0-preview.125", + "@microsoft/agents-a365-runtime": "^0.1.0-preview.125", + "@microsoft/agents-a365-tooling": "^0.1.0-preview.125", "@microsoft/agents-activity": "^1.1.0-alpha.85", "@microsoft/agents-hosting": "^1.1.0-alpha.85", "ai": "^5.0.72", diff --git a/nodejs/vercel-sdk/sample-agent/src/client.ts b/nodejs/vercel-sdk/sample-agent/src/client.ts index 039ae416..a727e9f5 100644 --- a/nodejs/vercel-sdk/sample-agent/src/client.ts +++ b/nodejs/vercel-sdk/sample-agent/src/client.ts @@ -12,8 +12,8 @@ import { Builder, InferenceOperationType, AgentDetails, - TenantDetails, - InferenceDetails + InferenceDetails, + Request } from '@microsoft/agents-a365-observability'; const modelName = 'claude-sonnet-4-20250514'; @@ -104,18 +104,17 @@ class VercelAiClient implements Client { model: modelName, }; - const agentDetails: AgentDetails = { - agentId: 'vercel-ai-sdk-agent', - agentName: 'Vercel AI SDK Agent', + const request: Request = { conversationId: 'conv-12345', }; - const tenantDetails: TenantDetails = { - tenantId: 'vercel-ai-sdk-sample-agent', + const agentDetails: AgentDetails = { + agentId: 'vercel-ai-sdk-agent', + agentName: 'Vercel AI SDK Agent', }; let response = ''; - const scope = InferenceScope.start(inferenceDetails, agentDetails, tenantDetails); + const scope = InferenceScope.start(request, inferenceDetails, agentDetails); try { await scope.withActiveSpanAsync(async () => { try { diff --git a/python/google-adk/sample-agent/.env.template b/python/google-adk/sample-agent/.env.template index 80dd31c5..fe39cd21 100644 --- a/python/google-adk/sample-agent/.env.template +++ b/python/google-adk/sample-agent/.env.template @@ -1,22 +1,100 @@ -GOOGLE_GENAI_USE_VERTEXAI=FALSE -GOOGLE_API_KEY= +# ============================================================================= +# Google ADK Sample Agent — Environment Configuration +# ============================================================================= +# Copy this file to .env and fill in your values: +# cp .env.template .env +# +# All values marked <<...>> MUST be replaced before the agent will work. +# Run `a365 config init` first — it generates the config files referenced below. +# ============================================================================= -# Agent365 Agentic Authentication Configuration -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= +# ----------------------------------------------------------------------------- +# Google Gemini Configuration +# ----------------------------------------------------------------------------- +GOOGLE_GENAI_USE_VERTEXAI=FALSE +GOOGLE_API_KEY=<> +GEMINI_MODEL=gemini-2.5-flash +GOOGLE_CLOUD_PROJECT=<> +GOOGLE_CLOUD_LOCATION=<> +GOOGLE_GENAI_USE_VERTEXAI=TRUE +# ----------------------------------------------------------------------------- +# 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=<> +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<> +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<> 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 -# These values are expected to be in the activity's recipient field -AGENTIC_UPN= -AGENTIC_NAME= -AGENTIC_USER_ID= -AGENTIC_APP_ID= -AGENTIC_TENANT_ID= \ No newline at end of file +# ----------------------------------------------------------------------------- +# 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=<> +AGENTIC_NAME=<> +AGENTIC_USER_ID=<> +AGENTIC_APP_ID=<> +AGENTIC_TENANT_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= + +# ----------------------------------------------------------------------------- +# Server +# ----------------------------------------------------------------------------- +# Port for the aiohttp server. +# Local dev default: 3978. Azure App Service injects PORT automatically. +PORT=3978 + +# Logging level: DEBUG, INFO, WARNING, ERROR +LOG_LEVEL=INFO + +# ----------------------------------------------------------------------------- +# Observability +# ----------------------------------------------------------------------------- +ENABLE_OBSERVABILITY=true +ENABLE_A365_OBSERVABILITY_EXPORTER=false +PYTHON_ENVIRONMENT=development +OBSERVABILITY_SERVICE_NAME=GoogleADKSampleAgent +OBSERVABILITY_SERVICE_NAMESPACE=GoogleADKTesting diff --git a/python/google-adk/sample-agent/.gitignore b/python/google-adk/sample-agent/.gitignore new file mode 100644 index 00000000..84cc73c9 --- /dev/null +++ b/python/google-adk/sample-agent/.gitignore @@ -0,0 +1,21 @@ +# A365 deploy artifacts — generated by `a365 deploy` / `a365 develop` +a365.config.json +a365.generated.config.json +app.zip +publish/ + +# Manifest folder — generated during deploy +manifest/ + +# Python virtual environment and caches +.venv/ +venv/ +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +uv.lock + +# Environment — contains secrets +.env diff --git a/python/google-adk/sample-agent/README.md b/python/google-adk/sample-agent/README.md index 7c555a34..71c73af3 100644 --- a/python/google-adk/sample-agent/README.md +++ b/python/google-adk/sample-agent/README.md @@ -11,17 +11,375 @@ This sample uses the [Microsoft Agent 365 SDK for Python](https://github.com/mic For comprehensive documentation and guidance on building agents with the Microsoft Agent 365 SDK, including how to add tooling, observability, and notifications, visit the [Microsoft Agent 365 Developer Documentation](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/). +--- + ## Prerequisites -- Python 3.x -- Microsoft Agent 365 SDK -- Google ADK SDK (google-adk) -- Google API credentials +- Python 3.11+ +- [uv](https://docs.astral.sh/uv/) package manager (recommended) or pip +- Google API key with Gemini access (paid tier recommended — free tier has low rate limits) +- Microsoft Agent 365 SDK credentials (for production / MCP tools) +- [Node.js](https://nodejs.org/) (for Agents Playground) + +--- + +## Quick Start — Local Development + +### 1. Clone and set up the environment + +```bash +cd python/google-adk/sample-agent + +# Create virtual environment and install dependencies +uv venv +uv sync + +# Bootstrap pip (required by the a365 CLI and some tools) +.venv/Scripts/python.exe -m ensurepip --upgrade # Windows +.venv/bin/python -m ensurepip --upgrade # Linux / macOS +``` + +### 2. Configure environment variables + +Copy the template and fill in your values: + +```bash +cp .env.template .env +``` + +Minimum required for local/Playground testing: + +```env +GOOGLE_API_KEY= +GEMINI_MODEL=gemini-2.5-flash +AUTH_HANDLER_NAME= # leave empty for Playground/local dev +``` + +> **Note**: `AUTH_HANDLER_NAME` must be **empty** for Agents Playground. Setting it to `AGENTIC` requires a real AAD token that Playground does not provide. + +### 3. Initialize A365 configuration + +The fastest way is the **AI-guided setup** — attach the instruction file to GitHub Copilot Chat (agent mode) and it walks you through every step automatically: + +``` +Follow the steps in #file:a365-setup-instructions.md +``` + +> See [AI-guided setup for Agent 365](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/ai-guided-setup) for full instructions and to download `a365-setup-instructions.md`. + +Alternatively, run the CLI manually: + +```bash +a365 config init +``` + +This creates `a365.config.json` with your agent configuration. For local dev or self-hosted servers (GCP, AWS), set `"needDeployment": false` to tell the CLI not to deploy to Azure: + +```json +{ + "messagingEndpoint": "https:///api/messages", + "needDeployment": false +} +``` + +> `"needDeployment": false` — **I host my own server; don't deploy to Azure.** Use this for local dev tunnels, GCP Cloud Run, AWS, or any non-Azure hosting. +> +> `"needDeployment": true` — **Deploy my code to Azure App Service.** Use this when you want `a365 deploy` to package and upload your agent. + +You can also run `a365 setup all` to provision all cloud resources in one step. + +### 4. Run the agent + +```bash +# Activate the virtual environment +.venv/Scripts/activate # Windows +source .venv/bin/activate # Linux / macOS + +# Start the server (listens on localhost:3978) +python main.py +``` + +You should see: + +``` +INFO main: Listening on localhost:3978/api/messages +INFO main: No auth handler configured — anonymous mode (Playground/local dev) +INFO main: No token and no auth handler — skipping MCP tools, running bare LLM +``` + +### 5. Get a bearer token for MCP tools (optional) + +To enable MCP tool access locally, get a fresh token using the A365 CLI: + +```bash +a365 develop get-token -o raw +``` + +Copy the output and set it in `.env`: + +```env +BEARER_TOKEN= +``` + +The token expires in ~90 minutes. The agent detects expiry automatically and falls back to bare LLM mode. + +--- + +## Testing with Agents Playground + +The Agents Playground is a local testing tool that connects directly to your running agent — **no tunnel or deployment required**. + +### Install + +```bash +# Via npm (recommended) +npm install -g @microsoft/m365agentsplayground + +# Or via winget (Windows) +winget install agentsplayground +``` + +### Run locally (anonymous mode) + +1. Start your agent: + +```bash +python main.py +``` + +2. In a separate terminal, launch the Playground: + +```bash +agentsplayground -e "http://localhost:3978/api/messages" -c "emulator" +``` + +3. The Playground opens in your browser — start chatting with your agent. + +### Run with authentication + +```bash +agentsplayground -e "http://localhost:3978/api/messages" -c "emulator" \ + --client-id "" \ + --client-secret "" \ + --tenant-id "" +``` + +### Key CLI options + +| Option | Description | +|--------|-------------| +| `-e` | Agent endpoint (e.g. `http://localhost:3978/api/messages`) | +| `-c` | Channel type: `emulator`, `webchat`, or `msteams` | +| `--client-id` | Entra ID client ID (for auth mode) | +| `--client-secret` | Client secret (for auth mode) | +| `--tenant-id` | Tenant ID (for auth mode) | + +Run `agentsplayground --help` for all options. + +> For full setup documentation see [Test your agent locally in Agents Playground](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/test-with-toolkit-project). + +### Testing checklist + +| Test | How | +|------|-----| +| Basic message | Send any text message in the Playground chat | +| Install/uninstall | Agents Playground → Mock an Activity → Install application | +| Typing indicator | Send a message — you should see "Got it — working on it…" then "..." animation | +| MCP tools | Set `BEARER_TOKEN` in `.env` and restart — tools listed in server logs | +| User identity | Check server logs for `Turn received from user — DisplayName:` | + +### Expected Playground behavior + +1. You send a message +2. Agent immediately replies: **"Got it — working on it…"** +3. Typing indicator (`...`) appears while Gemini processes +4. Agent sends the final response + +![Agents Playground - Google ADK Sample Agent](images/agents-playground.png) + +--- + +## Deploying to Production + +### Full lifecycle with A365 CLI + +```bash +# 1. Initialize config (first time only) +a365 config init + +# 2. Provision all cloud resources and set up the blueprint +a365 setup all + +# 3. Deploy agent code to Azure +a365 deploy + +# 4. Publish agent to Microsoft 365 admin center +a365 publish +``` + +### Running on Azure App Service + +See [Deploy agent to Azure](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/deploy-agent-azure?tabs=dotnet) for full instructions. + +Set `messagingEndpoint` in `a365.config.json` to your Azure Web App URL and `"needDeployment": true` (see [configuration reference above](#3-initialize-a365-configuration)). + +Set the Azure App Service **startup command** to: + +```bash +python main.py +``` + +> **Port**: Azure App Service injects `PORT=8000` automatically. The app reads it from the environment — do not hardcode `3978` in any startup command. + +### Configure Application Settings + +The `.env` file is **not** deployed. Set all variables as Azure App Service Application Settings. + +All values below come from `a365.config.json` and `a365.generated.config.json` (produced by `a365 setup all`). Run `a365 config display -g` to view the decrypted generated values. + +| Key | Source | Value | +|-----|--------|-------| +| `GOOGLE_API_KEY` | Google AI Studio | Your Google API key | +| `GOOGLE_GENAI_USE_VERTEXAI` | — | `FALSE` | +| `GEMINI_MODEL` | — | `gemini-2.5-flash` | +| `CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID` | `a365.generated.config.json` → `agentBlueprintId` | Blueprint App ID | +| `CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET` | `a365.generated.config.json` → `agentBlueprintClientSecret` | Blueprint client secret | +| `CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID` | `a365.config.json` → `tenantId` | Azure tenant ID | +| `CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES` | `a365.generated.config.json` → `agentBlueprintId` + `/.default` | `/.default` | +| `AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE` | — | `AgenticUserAuthorization` | +| `AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES` | — | `https://graph.microsoft.com/.default` | +| `AUTH_HANDLER_NAME` | — | `AGENTIC` | +| `AGENTIC_UPN` | `a365.config.json` → `agentUserPrincipalName` | Agent user principal name | +| `AGENTIC_NAME` | `a365.config.json` → `agentUserDisplayName` | Agent display name | +| `AGENTIC_APP_ID` | `a365.generated.config.json` → `agentBlueprintId` | Blueprint App ID | +| `AGENTIC_TENANT_ID` | `a365.config.json` → `tenantId` | Azure tenant ID | +| `AGENTIC_USER_ID` | `a365.generated.config.json` → `AgenticUserId` | Populated after Teams admin approves the agent instance | +| `ENABLE_OBSERVABILITY` | — | `true` | +| `OBSERVABILITY_SERVICE_NAME` | — | `GoogleADKSampleAgent` | + +### Running on GCP (Cloud Run) + +See [Deploy agent to GCP](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/deploy-agent-gcp) for full instructions. + +```bash +# Deploy to Cloud Run +gcloud run deploy gcp-a365-agent --source . --region us-central1 --platform managed --allow-unauthenticated +``` + +Set `a365.config.json` with your Cloud Run URL and `needDeployment: false`: + +```json +{ + "messagingEndpoint": "https://gcp-a365-agent-XXXX-uc.run.app/api/messages", + "needDeployment": false +} +``` + +> `"needDeployment": false` — tells the CLI "I host my own server; don't deploy to Azure." Use this for GCP, AWS, or any self-hosted server. + +Register only the messaging endpoint (skip Azure deploy): + +```bash +a365 setup blueprint --endpoint-only +``` + +### Messaging endpoint reference + +See [Configure messaging endpoint](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/agent-messaging-endpoint) for all hosting options. + +| Hosting | `messagingEndpoint` format | `needDeployment` | +|---------|--------------------------|-----------------| +| Azure App Service | `https://.azurewebsites.net/api/messages` | `true` | +| GCP Cloud Run | `https://.run.app/api/messages` | `false` | +| AWS | `https://.amazonaws.com/api/messages` | `false` | +| Dev Tunnel (local) | `https://.devtunnels.ms:3978/api/messages` | `false` | + +--- + +## After Publishing — Post-Deployment Steps + +After `a365 deploy` and `a365 publish` complete, the following steps require browser interaction and cannot be automated by the CLI. + +### Step 1: Configure in Teams Developer Portal + +1. Get your blueprint App ID: + ```bash + a365 config display -g + ``` + Copy the `agentBlueprintId` value from the output. + +2. Open your blueprint configuration page: + ``` + https://dev.teams.microsoft.com/tools/agent-blueprint//configuration + ``` + +3. Set **Agent Type** to `Bot Based` +4. Set **Bot ID** to your `agentBlueprintId` +5. Click **Save** + +> See [Configure agent in Teams Developer Portal](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/create-instance#1-configure-agent-in-teams-developer-portal) and [Publish agent](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/publish) for full instructions. + +### Step 2: Upload manifest to M365 Admin Center + +1. Go to [https://admin.microsoft.com](https://admin.microsoft.com) > **Agents** > **All agents** > **Upload custom agent** +2. Upload `manifest/manifest.zip` (created by `a365 publish`) + +### Step 3: Create agent instance + +1. In Microsoft Teams, go to **Apps** and search for your agent name +2. Select your agent and click **Request Instance** +3. A tenant admin must approve the request at: + ``` + https://admin.cloud.microsoft/#/agents/all/requested + ``` + +### Step 4: Update AGENTIC_USER_ID after approval + +Once the admin approves the agent instance, the agent user is created. Update `AGENTIC_USER_ID` in two places: + +1. Find the value in `a365.generated.config.json` → `AgenticUserId` + +2. Update `.env`: + ```env + AGENTIC_USER_ID= + ``` + +3. Update the Azure App Service Application Setting: + ```bash + az webapp config appsettings set \ + --name gemini-buddy-agent-webapp \ + --resource-group AgentSDKTestRG \ + --settings AGENTIC_USER_ID= + ``` + +> **Note:** The agent user creation is asynchronous — it can take a few minutes to a few hours to become searchable in Teams after the instance is approved. + +--- + +## Configuration Reference + +All configuration is via environment variables (`.env` for local, App Settings for Azure): + +| Variable | Default | Description | +|----------|---------|-------------| +| `GOOGLE_API_KEY` | — | **Required**. Google Gemini API key | +| `GEMINI_MODEL` | `gemini-2.5-flash` | Gemini model to use | +| `GOOGLE_GENAI_USE_VERTEXAI` | `FALSE` | Set `TRUE` to use Vertex AI instead of Gemini API | +| `AUTH_HANDLER_NAME` | _(empty)_ | Empty = anonymous (Playground/local), `AGENTIC` = production | +| `BEARER_TOKEN` | _(empty)_ | Token for MCP tool access. Get with `a365 develop get-token -o raw` | +| `AGENTIC_APP_ID` | — | Agent App ID from A365 portal | +| `AGENTIC_TENANT_ID` | — | Azure tenant ID | +| `AGENTIC_USER_ID` | — | Agent User ID from A365 portal | +| `PORT` | `3978` | Server port (Azure sets this to `8000` automatically) | +| `ENABLE_OBSERVABILITY` | `true` | Enable OpenTelemetry tracing | +| `ENABLE_A365_OBSERVABILITY_EXPORTER` | `false` | Send traces to A365 backend (`true` for production) | +| `LOG_LEVEL` | `INFO` | Logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | + +--- ## Working with User Identity -On every incoming message, the A365 platform populates `activity.from_property` with basic user -information — always available with no API calls or token acquisition: +On every incoming message, the A365 platform populates `activity.from_property` with basic user information — always available with no API calls or token acquisition: | Field | Description | |---|---| @@ -29,32 +387,28 @@ information — always available with no API calls or token acquisition: | `activity.from_property.name` | Display name as known to the channel | | `activity.from_property.aad_object_id` | Azure AD Object ID — use this to call Microsoft Graph | -The sample logs these fields at the start of every message turn and injects the display name -into the LLM system instructions for personalized responses. +The sample logs these fields at the start of every message turn and injects the display name into the LLM system instructions for personalized responses. + +--- ## Handling Agent Install and Uninstall -When a user installs (hires) or uninstalls (removes) the agent, the A365 platform sends an `InstallationUpdate` activity — also referred to as the `agentInstanceCreated` event. The sample handles this in `on_installation_update` in `hosting.py`: +When a user installs (hires) or uninstalls (removes) the agent, the A365 platform sends an `InstallationUpdate` activity. The sample handles this in `on_installation_update` in `hosting.py`: | Action | Description | |---|---| | `add` | Agent was installed — send a welcome message | | `remove` | Agent was uninstalled — send a farewell message | -```python -if action == "add": - await context.send_activity("Thank you for hiring me! Looking forward to assisting you in your professional journey!") -elif action == "remove": - await context.send_activity("Thank you for your time, I enjoyed working with you.") -``` +To test with Agents Playground, use **Mock an Activity → Install application**. -To test with Agents Playground, use **Mock an Activity → Install application** to send a simulated `installationUpdate` activity. +--- ## Sending Multiple Messages in Teams Agent365 agents can send multiple discrete messages in response to a single user prompt. This is the recommended pattern for agentic identities in Teams. -> **Important**: Streaming (SSE) is not supported for agentic identities in Teams. The SDK detects agentic identity and buffers streaming into a single message. Instead, call `send_activity` multiple times to send multiple messages. +> **Important**: Streaming (SSE) is not supported for agentic identities in Teams. Instead, call `send_activity` multiple times. ### Pattern @@ -65,9 +419,8 @@ Agent365 agents can send multiple discrete messages in response to a single user ### Typing Indicators - Typing indicators show a progress animation in Teams -- They have a built-in ~5-second visual timeout -- For long-running operations, re-send the typing indicator in a loop every ~4 seconds -- Typing indicators are only visible in 1:1 chats and small group chats (not channels) +- They have a built-in ~5-second visual timeout — re-send every ~4 seconds for long operations +- Only visible in 1:1 chats and small group chats (not channels) ### Code Example @@ -101,12 +454,86 @@ finally: pass ``` -## Running the Agent +--- + +## Troubleshooting + +### Agent not responding in Playground + +**Symptom**: Messages sent, no response appears. + +**Cause**: `AUTH_HANDLER_NAME=AGENTIC` is set. Playground does not provide a real AAD token, so the OBO exchange hangs and the handler never fires. + +**Fix**: Set `AUTH_HANDLER_NAME=` (empty) in `.env` for local/Playground testing. + +--- + +### "Retrieving agentic user token" in logs — agent hangs + +**Cause**: Same as above — `AUTH_HANDLER_NAME=AGENTIC` with no valid AAD token. + +**Fix**: Clear `AUTH_HANDLER_NAME` for Playground. For production with MCP tools, provide a fresh `BEARER_TOKEN`. + +--- + +### "Failed to create MCP session" error + +**Cause**: Expired or missing `BEARER_TOKEN` with no auth handler configured — the agent tries to connect to MCP servers with invalid credentials. + +**Fix**: Either refresh `BEARER_TOKEN` with `a365 develop get-token -o raw`, or set `AUTH_HANDLER_NAME=` to skip MCP tools entirely and run in bare LLM mode. + +--- + +### Getting HTTP 201 instead of 202 from `/api/messages` + +**Cause**: Python on Windows defaults to `WindowsProactorEventLoopPolicy`, which can break aiohttp socket writes. The `run_app()` call in `main.py` uses the correct event loop — no manual policy override needed. + +**Fix**: Ensure you are using `run_app()` from aiohttp (not `asyncio.run()`). Do not override the event loop policy manually. + +--- + +### Azure container startup timeout (230s) + +**Cause**: Port hardcoded to `3978` — Azure App Service injects `PORT=8000` and the app binds to the wrong port. + +**Fix**: Already handled in `main.py` — `port = int(os.getenv("PORT", 3978))`. + +--- + +### `pip not found` during `a365 deploy` + +**Cause**: `uv venv` / `uv sync` does not install pip by default. + +**Fix**: +```bash +.venv/Scripts/python.exe -m ensurepip --upgrade # Windows +.venv/bin/python -m ensurepip --upgrade # Linux / macOS +``` + +Note: Re-run this after every `uv sync` as uv removes pip. + +--- + +### `AttributeError: 'dict' object has no attribute 'TENANT_ID'` + +**Cause**: JWT middleware received a plain dict instead of a typed `AgentAuthConfiguration` object. + +**Fix**: Already handled in `main.py` — `AgentAuthConfiguration` is built explicitly from env vars. -To set up and test this agent, refer to the [Configure Agent Testing](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?tabs=python) guide for complete instructions. +--- + +### Gemini model 404 error + +**Cause**: `gemini-2.0-flash` is deprecated for new API key users. + +**Fix**: Use `GEMINI_MODEL=gemini-2.5-flash` (default) in `.env`. + +--- For a detailed explanation of the agent code and implementation, see the [Agent Code Walkthrough](AGENT-CODE-WALKTHROUGH.md). +--- + ## Support For issues, questions, or feedback: @@ -128,7 +555,13 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope - [Microsoft Agent 365 SDK - Python repository](https://github.com/microsoft/Agent365-python) - [Microsoft 365 Agents SDK - Python repository](https://github.com/Microsoft/Agents-for-python) - [Google ADK API documentation](https://google.github.io/adk-docs/) -- [Python API documentation](https://learn.microsoft.com/python/api/?view=m365-agents-sdk&preserve-view=true) +- [Configure messaging endpoint](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/agent-messaging-endpoint) +- [Deploy agent to Azure](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/deploy-agent-azure?tabs=dotnet) +- [Deploy agent to GCP](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/deploy-agent-gcp) +- [Publish agent](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/publish) +- [Configure agent in Teams Developer Portal](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/create-instance#1-configure-agent-in-teams-developer-portal) +- [Configure Agent Testing](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?tabs=python) +- [Test your agent locally in Agents Playground](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/test-with-toolkit-project) ## Trademarks diff --git a/python/google-adk/sample-agent/agent.py b/python/google-adk/sample-agent/agent.py index a3bb134e..23c6d61e 100644 --- a/python/google-adk/sample-agent/agent.py +++ b/python/google-adk/sample-agent/agent.py @@ -1,6 +1,9 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +import asyncio import os +import time from typing import Optional from google.adk.agents import Agent @@ -36,7 +39,7 @@ def _get_instruction(cls, user_name: str) -> str: def __init__( self, agent_name: str = "my_agent", - model: str = "gemini-2.0-flash", + model: str = os.getenv("GEMINI_MODEL", "gemini-2.5-flash"), description: str = "Agent to test Mcp tools.", instruction: str = """ You are a helpful AI assistant with access to external tools through MCP servers. @@ -122,12 +125,18 @@ async def invoke_agent( ) responses = [] - result = await runner.run_debug( - user_messages=[message] - ) + try: + result = await runner.run_debug( + user_messages=[message] + ) + except Exception as e: + logger.error("run_debug failed: %s", e) + await self._cleanup_agent(agent) + return "Sorry, I encountered an error while processing your request. Please try again." # Extract text responses from the result if not hasattr(result, '__iter__'): + await self._cleanup_agent(agent) return "I couldn't get a response from the agent. :(" for event in result: @@ -161,8 +170,11 @@ async def invoke_agent_with_scope( Returns: List of response messages from the agent """ - tenant_id = context.activity.recipient.tenant_id - agent_id = context.activity.recipient.agentic_user_id + # Playground sends a minimal recipient (id + name only). + # Fall back to env vars so observability baggage is still populated. + recipient = context.activity.recipient + tenant_id = getattr(recipient, "tenant_id", None) or os.getenv("AGENTIC_TENANT_ID", "") + agent_id = getattr(recipient, "agentic_user_id", None) or os.getenv("AGENTIC_USER_ID", "") with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build(): return await self.invoke_agent(message=message, auth=auth, auth_handler_name=auth_handler_name, context=context) @@ -175,17 +187,47 @@ async def _cleanup_agent(self, agent: Agent): async def _initialize_agent(self, agent, auth, auth_handler_name, turn_context): """Initialize the agent with MCP tools and authentication.""" + # Validate BEARER_TOKEN — pass empty string if expired so the SDK uses + # the proper auth handler instead of a stale token that triggers an OBO hang. + bearer_token = os.getenv("BEARER_TOKEN", "") + if bearer_token: + try: + from base64 import urlsafe_b64decode + import json as _json + payload = bearer_token.split(".")[1] + if len(payload) % 4 != 0: + payload += "=" * (4 - len(payload) % 4) + exp = _json.loads(urlsafe_b64decode(payload)).get("exp", 0) + if exp and time.time() > exp: + logger.warning("BEARER_TOKEN is expired — skipping token, will use auth handler") + bearer_token = "" + except Exception: + pass # non-JWT token format; pass it through as-is + + # Skip MCP init if there's no token and no auth handler — avoids MCP + # session errors when running locally/Playground without valid credentials. + if not bearer_token and not auth_handler_name: + logger.info("No token and no auth handler — skipping MCP tools, running bare LLM") + return agent + try: - # Add MCP tools to the agent tool_service = McpToolRegistrationService() - return await tool_service.add_tool_servers_to_agent( - agent=agent, - agentic_app_id=os.getenv("AGENTIC_APP_ID", "agent123"), - auth=auth, - auth_handler_name=auth_handler_name, - context=turn_context, - auth_token=os.getenv("BEARER_TOKEN", ""), + # Wrap in a timeout — if token exchange hangs (e.g. Playground user has + # no real AAD token for OBO), fall through to bare LLM mode after 10s. + return await asyncio.wait_for( + tool_service.add_tool_servers_to_agent( + agent=agent, + agentic_app_id=os.getenv("AGENTIC_APP_ID", "agent123"), + auth=auth, + auth_handler_name=auth_handler_name, + context=turn_context, + auth_token=bearer_token, + ), + timeout=10.0, ) + except asyncio.TimeoutError: + logger.warning("MCP tool initialization timed out — running without tools") + return agent except Exception as e: - logger.error(f"Error during agent initialization: {e}") + logger.error("Error during agent initialization: %s", e) return agent \ No newline at end of file diff --git a/python/google-adk/sample-agent/hosting.py b/python/google-adk/sample-agent/hosting.py index 4344cd9c..6d8c217d 100644 --- a/python/google-adk/sample-agent/hosting.py +++ b/python/google-adk/sample-agent/hosting.py @@ -73,14 +73,22 @@ def __init__(self, agent: AgentInterface): ) self.agent = agent - self.auth_handler_name = "AGENTIC" + # Read from AUTH_HANDLER_NAME env var. Set to "AGENTIC" for production + # agentic auth. Leave empty (default) for local dev and Agents Playground. + self.auth_handler_name = os.getenv("AUTH_HANDLER_NAME", "") or None + if self.auth_handler_name: + logger.info("Auth handler: %s", self.auth_handler_name) + else: + logger.info("No auth handler configured — anonymous mode (Playground/local dev)") self.agent_notification = AgentNotification(self) self._setup_handlers() def _setup_handlers(self): """Set up activity handlers for the agent.""" - auth_handlers = [self.auth_handler_name] + # Only enforce auth when AUTH_HANDLER_NAME is configured. + # Without it the Agents Playground (and local dev) can reach the handler. + handler_config = {"auth_handlers": [self.auth_handler_name]} if self.auth_handler_name else {} @self.conversation_update("membersAdded") async def help_handler(context: TurnContext, _: TurnState): @@ -107,7 +115,7 @@ async def on_installation_update(context: TurnContext, _: TurnState): elif action == "remove": await context.send_activity("Thank you for your time, I enjoyed working with you.") - @self.activity("message", auth_handlers=auth_handlers, rank=2) + @self.activity("message", **handler_config, rank=2) async def message_handler(context: TurnContext, _: TurnState): """Handle message activities.""" user_message = context.activity.text @@ -151,7 +159,7 @@ async def _typing_loop(): @self.agent_notification.on_agent_notification( channel_id=ChannelId(channel="agents", sub_channel="*"), - auth_handlers=auth_handlers, + **handler_config, rank=1 ) async def agent_notification_handler( diff --git a/python/google-adk/sample-agent/images/agents-playground.png b/python/google-adk/sample-agent/images/agents-playground.png new file mode 100644 index 00000000..4d648605 Binary files /dev/null and b/python/google-adk/sample-agent/images/agents-playground.png differ diff --git a/python/google-adk/sample-agent/main.py b/python/google-adk/sample-agent/main.py index f1e85977..d3378700 100644 --- a/python/google-adk/sample-agent/main.py +++ b/python/google-adk/sample-agent/main.py @@ -1,20 +1,19 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. # Internal imports import os from hosting import MyAgent from agent import GoogleADKAgent -import os - # Server imports from aiohttp.web import Application, Request, Response, run_app from aiohttp.web_middlewares import middleware as web_middleware # Microsoft Agents SDK imports from microsoft_agents.hosting.core import AgentApplication, ClaimsIdentity, AuthenticationConstants +from microsoft_agents.hosting.core.authorization import AgentAuthConfiguration from microsoft_agents.hosting.aiohttp import start_agent_process, jwt_authorization_middleware -from microsoft_agents.activity import load_configuration_from_env # Microsoft Agent 365 Observability Imports from microsoft_agents_a365.observability.core.config import configure @@ -23,13 +22,19 @@ from dotenv import load_dotenv load_dotenv() -# Logging +# Logging — respect LOG_LEVEL from .env import logging +log_level = getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO) +logging.basicConfig(level=log_level, format="%(asctime)s %(levelname)s %(name)s: %(message)s") logger = logging.getLogger(__name__) def start_server(agent_app: AgentApplication): """Start the agent application server.""" - isProduction = os.getenv("WEBSITE_SITE_NAME") is not None + isProduction = ( + os.getenv("WEBSITE_SITE_NAME") is not None # Azure App Service + or os.getenv("K_SERVICE") is not None # GCP Cloud Run + or os.getenv("ENVIRONMENT", "").lower() == "production" # Explicit flag + ) async def entry_point(req: Request) -> Response: return await start_agent_process(req, agent_app, agent_app.adapter) @@ -47,32 +52,105 @@ async def anonymous_claims(request, handler): ) return await handler(request) + # Build AgentAuthConfiguration — the JWT middleware requires an object with + # attribute access (.TENANT_ID, .ANONYMOUS_ALLOWED), not a plain dict. + # Read from CONNECTIONS__SERVICE_CONNECTION__SETTINGS__* (A365 format) or + # direct CLIENT_ID / TENANT_ID / CLIENT_SECRET vars as fallback. + # IMPORTANT: client_id for JWT validation must be the blueprint/app-registration ID + # (CLIENTID), NOT the AGENTIC_APP_ID. Bot Framework tokens have aud=blueprint ID. + agent_auth_config = None + client_id = ( + os.getenv("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID") + or os.getenv("CLIENT_ID") + or os.getenv("AGENTIC_APP_ID") + ) + tenant_id = ( + os.getenv("AGENTIC_TENANT_ID") + or os.getenv("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID") + or os.getenv("TENANT_ID") + ) + client_secret = ( + os.getenv("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET") + or os.getenv("CLIENT_SECRET") + ) + if client_id and tenant_id and client_secret: + try: + agent_auth_config = AgentAuthConfiguration( + client_id=client_id, + tenant_id=tenant_id, + client_secret=client_secret, + ) + logger.info("JWT auth configured (client_id=%s)", client_id) + except Exception as e: + logger.warning("Failed to build AgentAuthConfiguration, running anonymous: %s", e) + else: + logger.info("No auth credentials found — running in anonymous mode") + + # Wrap JWT middleware so it only applies to POST /api/messages. + # Azure App Service sends health probes (GET /robots933456.txt, GET /) + # that must return 200 without authentication. + @web_middleware + async def selective_jwt_auth(request, handler): + if request.method == "POST" and request.path == "/api/messages": + return await jwt_authorization_middleware(request, handler) + return await handler(request) + middlewares = [anonymous_claims] - auth_config = load_configuration_from_env(os.environ) - if (auth_config and isProduction): - middlewares.append(jwt_authorization_middleware) + if agent_auth_config and isProduction: + middlewares.append(selective_jwt_auth) + logger.info("JWT authorization middleware enabled (POST /api/messages only)") + + # Health / readiness endpoint — returns 200 for Azure App Service probes. + async def health_check(req: Request) -> Response: + return Response(text="OK", status=200) # Configure App app = Application(middlewares=middlewares) + app.router.add_get("/", health_check) + app.router.add_get("/robots933456.txt", health_check) app.router.add_post("/api/messages", entry_point) - app["agent_configuration"] = auth_config + app["agent_configuration"] = agent_auth_config try: host = "0.0.0.0" if isProduction else "localhost" - run_app(app, host=host, port=int(3978), handle_signals=True) + + # PORT environment variable is optional - defaults to 3978 for local dev + # Azure App Service automatically sets PORT=8000 + port_str = os.getenv("PORT") + if port_str: + try: + port = int(port_str) + logger.info("Using PORT from environment: %d", port) + except ValueError: + logger.warning("Invalid PORT value '%s', using default 3978", port_str) + port = 3978 + else: + port = 3978 + logger.info("PORT not set, using default: %d", port) + + logger.info("Listening on %s:%d/api/messages", host, port) + run_app(app, host=host, port=port, handle_signals=True) except KeyboardInterrupt: logger.info("\nShutting down server gracefully...") - except Exception as e: - logger.error(f"Server error: {e}") - raise e def main(): """Main function to run the sample agent application.""" - # Configure observability - configure( - service_name="GoogleADKSampleAgent", - service_namespace="GoogleADKTesting", - ) + # Configure observability from .env + # 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). + if os.getenv("ENABLE_OBSERVABILITY", "true").lower() == "true": + configure( + service_name=os.getenv("OBSERVABILITY_SERVICE_NAME", "GoogleADKSampleAgent"), + service_namespace=os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "GoogleADKTesting"), + ) + logger.info( + "Observability configured (service=%s, a365_exporter=%s)", + os.getenv("OBSERVABILITY_SERVICE_NAME", "GoogleADKSampleAgent"), + os.getenv("ENABLE_A365_OBSERVABILITY_EXPORTER", "false"), + ) + else: + logger.info("Observability disabled (ENABLE_OBSERVABILITY=false)") agent_application = MyAgent(GoogleADKAgent()) start_server(agent_application) diff --git a/python/google-adk/sample-agent/mcp_tool_registration_service.py b/python/google-adk/sample-agent/mcp_tool_registration_service.py index ab167173..9e50ea83 100644 --- a/python/google-adk/sample-agent/mcp_tool_registration_service.py +++ b/python/google-adk/sample-agent/mcp_tool_registration_service.py @@ -1,4 +1,5 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. from typing import Optional import logging @@ -74,9 +75,15 @@ async def add_tool_servers_to_agent( } for server_config in mcp_server_configs: + if not server_config.url: + self._logger.warning( + "Skipping MCP server '%s' — no URL configured (dev mode or manifest-only config).", + server_config.mcp_server_unique_name, + ) + continue server_info = McpToolset( connection_params=StreamableHTTPConnectionParams( - url=server_config.mcp_server_unique_name, + url=server_config.url, headers=mcp_server_headers ) ) @@ -89,5 +96,6 @@ async def add_tool_servers_to_agent( name=agent.name, model=agent.model, description=agent.description, + instruction=agent.instruction, tools=all_tools, ) diff --git a/python/google-adk/sample-agent/pyproject.toml b/python/google-adk/sample-agent/pyproject.toml index 286cc813..a95436f2 100644 --- a/python/google-adk/sample-agent/pyproject.toml +++ b/python/google-adk/sample-agent/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] dependencies = [ # Google ADK -- official package - "google-adk", + "google-adk>=1.18.0", # Microsoft Agents SDK - Official packages for hosting and integration "microsoft-agents-hosting-aiohttp", @@ -50,6 +50,26 @@ default = true # This ensures we always get the latest features and fixes [tool.uv] prerelease = "allow" +# Overrides to resolve conflicts between google-adk and agent-framework-core: +# - google-adk requires opentelemetry-sdk<1.39.0 +# - agent-framework-core requires opentelemetry-sdk>=1.39.0 +# google-adk is the binding constraint; the otel API is stable across these versions. +# Also overrides gradio's outdated pydantic upper bound and old ruff pin. +override-dependencies = [ + # Pin entire otel stack to 1.38.x — google-adk requires sdk<1.39.0 + # and all otel packages must be on the same minor version. + "opentelemetry-api>=1.38.0,<1.39.0", + "opentelemetry-sdk>=1.38.0,<1.39.0", + "opentelemetry-exporter-otlp>=1.38.0,<1.39.0", + "opentelemetry-exporter-otlp-proto-http>=1.38.0,<1.39.0", + "opentelemetry-exporter-otlp-proto-grpc>=1.38.0,<1.39.0", + "opentelemetry-exporter-otlp-proto-common>=1.38.0,<1.39.0", + "opentelemetry-proto>=1.38.0,<1.39.0", + "opentelemetry-semantic-conventions>=0.59b0,<0.60b0", + "opentelemetry-instrumentation>=0.59b0,<0.60b0", + "pydantic>=2.0.0", + "ruff>=0.9.3", +] [project.optional-dependencies] dev = [