diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index cc0577e0..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,414 +0,0 @@ -# GitHub Copilot Instructions for Agent365-dotnet - -## Code Review Rules - -### Rule 1: Check for "Kairo" Keyword -- **Description**: Scan code for any occurrence of the keyword "Kairo" -- **Action**: If "Kairo" is found in any code file: - - Flag it for review - - Suggest removal or replacement with appropriate terminology - - Check if it's a legacy reference that needs to be updated -- **Files to check**: All source files (`.cs`, `.csx`, `.py`, `.js`, `.ts`, `.mjs`) in the repository - -### Rule 2: Verify Copyright Headers -- **Description**: Ensure all source code files have proper Microsoft copyright headers -- **Action**: If a source file is missing a copyright header: - - Add the Microsoft copyright header at the top of the file - - The header should be placed before any using/import statements or code - - Maintain proper formatting and spacing - - Use language-appropriate comment syntax - -#### Required Copyright Header Format - -**C# Files (`.cs`, `.csx`):** -```csharp -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -``` - -**Python Files (`.py`):** -```python -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -``` - -**JavaScript/TypeScript Files (`.js`, `.ts`, `.mjs`):** -```javascript -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -``` - -### Implementation Guidelines - -#### When Reviewing Code: -1. **Kairo Check**: - - Search for case-insensitive matches of "Kairo" - - Review context to determine if it's: - - A class name - - A namespace - - A variable name - - A comment reference - - A using statement - - A string literal - - Suggest appropriate alternatives based on the context - -2. **Header Check**: - - Verify the first non-empty lines of source files - - If missing, prepend the copyright header with appropriate comment syntax - - Ensure there's a blank line after the header before other content - - Do not add headers to: - - Auto-generated files: - - C#: Marked with `` or `// `, `.Designer.cs`, `.g.cs` - - Python: Files with auto-generated markers - - JavaScript/TypeScript: `.d.ts` type definitions, files with `@generated` marker - - Files with `#pragma warning disable` at the top for generated code - - Third-party/vendored code - -#### Example of Proper File Structure: - -**C# (`.cs`):** -```csharp -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; - -namespace MyNamespace -{ - /// - /// Class documentation - /// - public class MyClass - { - // Rest of the code... - } -} -``` - -**Python (`.py`):** -```python -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import os -import logging -from typing import Optional - -class MyClass: - """Class documentation""" - - def __init__(self): - # Rest of the code... - pass -``` - -**JavaScript/TypeScript (`.js`, `.ts`):** -```javascript -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import express from 'express'; -import { Logger } from './logger'; - -/** - * Class documentation - */ -class MyClass { - // Rest of the code... -} -``` - -#### Example with File-Scoped Namespace (C# 10+): -```csharp -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading.Tasks; - -namespace MyNamespace; - -/// -/// Class documentation -/// -public class MyClass -{ - // Rest of the code... -} -``` - -#### Example with Top-Level Statements (C# 9+): -```csharp -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -var builder = WebApplication.CreateBuilder(args); - -// Rest of the code... -``` - -### Auto-fix Behavior -When Copilot detects violations: -- **Kairo keyword**: Suggest inline replacement or flag for manual review -- **Missing header**: Automatically suggest adding the copyright header - -### Exclusions -- Test files in `Tests/`, `test/`, `tests/`, or files ending with `.Tests.cs`, `.Test.cs`, `_test.py`, `test_*.py`, `.test.js`, `.spec.js` may have relaxed header requirements (but headers are still recommended) -- Auto-generated files: - - C#: `.g.cs`, `.designer.cs`, files with auto-generated markers - - Python: Files with auto-generated comments - - JavaScript/TypeScript: `.d.ts`, files with `@generated` marker -- Third-party code or vendored dependencies should not be modified -- Configuration files (`.json`, `.xml`, `.yaml`, `.yml`, `.toml`, `.md`, `.txt`) do not require copyright headers -- Project metadata files: `.csproj`, `.sln`, `package.json`, `pyproject.toml`, `requirements.txt`, `setup.py` -- Build output directories: - - C#: `bin/`, `obj/` - - Python: `__pycache__/`, `*.pyc`, `dist/`, `build/` - - JavaScript/TypeScript: `node_modules/`, `dist/`, `build/` -- Environment files: `.env`, `.env.example`, `.env.local` -- AssemblyInfo.cs files that are auto-generated - ---- - -## Rule 3: Agent 365 Sample Review and Validation - -### Description -When a developer is adding or updating Agent 365 samples, Copilot must actively validate that the sample follows Agent 365 standards and best practices. This rule applies to any work in sample directories or when README files for samples are being created/modified. - -### When to Apply This Rule -- When user is creating a new sample project -- When user is updating an existing sample -- When user asks for help with sample documentation -- When user requests review of sample code -- When README.md files in sample directories are being edited - -### Validation Actions - -When working with Agent 365 samples, Copilot should **automatically check and report** on the following: - -#### 1. Documentation Validation -**Scan for:** -- [ ] README.md exists in sample directory -- [ ] README has "Demonstrates" section explaining what the sample shows -- [ ] Prerequisites section lists all required tools/services with links: - - Language runtime version (e.g., .NET 8.0+, Python 3.10+, Node.js 18+) - - Package manager (e.g., npm, pip, dotnet) - - Azure OpenAI or OpenAI API key - - Optional: Microsoft 365 Agents Playground - - Optional: dev tunnel -- [ ] Configuration section with example config file snippets: - - C#: `appsettings.json` example - - Python: `.env` or config file example - - JavaScript/TypeScript: `.env` or `config.json` example -- [ ] "How to run this sample" section with step-by-step instructions (language-specific commands) -- [ ] Multiple testing options documented (Playground, WebChat, Teams/M365) -- [ ] Troubleshooting section with common errors and solutions (language-specific) -- [ ] Links to official Agent 365 documentation - -**Action:** If missing, suggest adding the missing sections with appropriate content for the detected language. - -#### 2. Configuration File Validation -**Scan for:** -- [ ] Configuration file exists: - - C#: `appsettings.json` - - Python: `.env`, `config.py`, or `settings.py` - - JavaScript/TypeScript: `.env`, `config.js`, or `config.json` -- [ ] OpenAI/Azure OpenAI configuration section present -- [ ] Token validation settings documented (where applicable) -- [ ] No hardcoded API keys or secrets in committed files -- [ ] Placeholder values use clear naming (e.g., `<>`, `<>`, or empty strings) -- [ ] Example configuration provided in README -- [ ] `.env.example` or equivalent template file provided for environment variables - -**Action:** If secrets detected, **immediately warn** and suggest using user secrets, environment variables, or key vault. - -#### 3. Authentication Configuration Check -**Scan for:** -- [ ] Documentation covers supported authentication types -- [ ] Limitations clearly stated (e.g., "Federated Credentials don't work with dev tunnels") -- [ ] Token validation can be enabled/disabled via configuration -- [ ] Azure Bot setup instructions provided with links -- [ ] Authentication context properly propagated in code - -**Action:** Suggest missing authentication documentation or code patterns. - -#### 4. Code Quality Validation -**Scan for:** -- [ ] All source files have Microsoft copyright headers (`.cs`, `.py`, `.js`, `.ts`, etc.) - - C#: `// Copyright (c) Microsoft Corporation.` (Rule 2) - - Python: `# Copyright (c) Microsoft Corporation.` - - JavaScript/TypeScript: `// Copyright (c) Microsoft Corporation.` -- [ ] No "Kairo" legacy references in any language (Rule 1) -- [ ] Proper error handling with try-catch/try-except where appropriate -- [ ] Logging statements use appropriate framework (ILogger, logging module, console, etc.) -- [ ] No compiler/linter warnings in code -- [ ] Async/await patterns used correctly (for languages that support it) -- [ ] Resource cleanup patterns followed (Dispose, context managers, cleanup functions) - -**Action:** Flag violations and suggest fixes inline. - -#### 5. Project Structure Validation -**Scan for:** -- [ ] Project/dependency file properly configured: - - C#: `.csproj` with required packages - - Python: `requirements.txt` or `pyproject.toml` with dependencies - - JavaScript: `package.json` with dependencies - - TypeScript: `package.json` with dependencies and `tsconfig.json` -- [ ] Launch/debug configuration exists: - - C#: `Properties/launchSettings.json` - - Python: `.vscode/launch.json` (optional but recommended) - - JavaScript/TypeScript: `package.json` scripts or `.vscode/launch.json` -- [ ] `appManifest/` folder exists with: - - `manifest.json` with proper placeholders - - `color.png` icon (192x192) - - `outline.png` icon (32x32) -- [ ] `.gitignore` excludes build artifacts and secrets: - - C#: `bin/`, `obj/`, `*.user` - - Python: `__pycache__/`, `*.pyc`, `.env`, `venv/`, `.venv/` - - JavaScript/TypeScript: `node_modules/`, `dist/`, `.env` - -**Action:** Report missing files and offer to create them. - -#### 6. Observability Implementation Check -**Scan code for:** -- [ ] Activity/tracing framework configured (e.g., `Activity.Current`) -- [ ] Custom spans created for key operations -- [ ] Exception tracking implemented -- [ ] Logging configured at startup -- [ ] Observability appears in configuration - -**Action:** If observability is minimal or missing, suggest patterns to add it. - -#### 7. Server Integration Check -**Scan code for:** -- [ ] Microsoft 365 Agents SDK endpoint configured (typically `/api/messages`) -- [ ] Proper HTTP status codes returned -- [ ] Request/response handling follows SDK patterns -- [ ] Concurrent request handling considered - -**Action:** Validate endpoint configuration and suggest improvements. - -#### 8. Testing Validation -**Check documentation for:** -- [ ] Local testing steps with language-specific commands: - - C#: Visual Studio, VS Code, `dotnet run` - - Python: VS Code, command line with `python main.py` or `uvicorn` - - JavaScript/TypeScript: VS Code, `npm start` or `node index.js` -- [ ] Dev tunnels usage documented (if applicable) -- [ ] Azure deployment steps provided -- [ ] WebChat testing steps included -- [ ] Teams/M365 integration steps with manifest upload instructions -- [ ] Virtual environment setup documented (Python: venv, C#: not needed, JS: not needed) - -**Action:** Flag missing testing scenarios and suggest documentation. - -#### 9. Troubleshooting Documentation -**Scan README for:** -- [ ] Troubleshooting section exists -- [ ] Common configuration errors documented -- [ ] Missing API key errors explained -- [ ] Authentication errors covered -- [ ] Solutions provided (not just error descriptions) - -**Action:** Suggest adding common issues from known sample patterns. - -### Automated Reporting Format - -When reviewing a sample, Copilot should provide a **summary report** like this: - -``` -✅ Agent 365 Sample Validation Report - -Documentation: -✅ README.md exists with all required sections -⚠️ Missing troubleshooting section - suggest adding common errors - -Configuration: -✅ Configuration file present with proper structure (appsettings.json / .env / config.json) -❌ API key placeholder not clear - suggest using "<>" - -Authentication: -✅ Multiple auth types documented -✅ Limitations clearly stated - -Code Quality: -✅ All files have copyright headers -✅ No "Kairo" references found -⚠️ Missing error handling in agent.py line 45 (or MyAgent.cs, index.ts, etc.) - -Project Structure: -✅ All required files present -✅ appManifest folder configured correctly - -Observability: -✅ Logging framework configured -⚠️ Consider adding custom spans for tool execution - -Recommendations: -1. Add troubleshooting section to README with common configuration errors -2. Clarify API key placeholder in configuration file (appsettings.json / .env / config.json) -3. Add error handling around LLM call in agent file (try-catch for C#, try-except for Python, try-catch for JS/TS) -4. Consider adding observability spans for better tracing (Activity for C#, custom spans for Python/JS) -``` - -### Language-Specific Considerations - -**C# Samples:** -- Copyright header: `// Copyright (c) Microsoft Corporation.` -- Config file: `appsettings.json` -- Project file: `*.csproj` -- Run command: `dotnet run` -- Dependencies: NuGet packages - -**Python Samples:** -- Copyright header: `# Copyright (c) Microsoft Corporation.` -- Config file: `.env` or `config.py` -- Dependencies file: `requirements.txt` or `pyproject.toml` -- Run command: `python main.py` or `uvicorn main:app` -- Virtual environment: `venv` or `virtualenv` -- Check for `__pycache__/` in `.gitignore` - -**JavaScript/TypeScript Samples:** -- Copyright header: `// Copyright (c) Microsoft Corporation.` -- Config file: `.env` or `config.json` -- Project file: `package.json` -- TypeScript config: `tsconfig.json` -- Run command: `npm start` or `node index.js` / `ts-node index.ts` -- Dependencies: npm packages -- Check for `node_modules/` in `.gitignore` - -### Proactive Suggestions - -When user says: -- **"Create a sample"** → Ask about which concepts to demonstrate (Notifications, Observability, Tooling) and suggest starting with template structure -- **"Update README"** → Validate against checklist and suggest missing sections -- **"Review my sample"** → Run full validation report -- **"Add authentication"** → Suggest proper patterns and documentation requirements -- **"Deploy to Azure"** → Check for deployment documentation and Azure Bot setup - -### Files to Monitor -- Any `README.md` in sample directories -- Configuration files: - - C#: `appsettings.json`, `appsettings.Development.json` - - Python: `.env`, `config.py`, `settings.py` - - JavaScript/TypeScript: `.env`, `config.js`, `config.json` -- Source files: - - C#: `*.cs`, `*.csx` - - Python: `*.py` - - JavaScript: `*.js`, `*.mjs` - - TypeScript: `*.ts` -- Project files: - - C#: `*.csproj`, `launchSettings.json` - - Python: `requirements.txt`, `pyproject.toml` - - JavaScript/TypeScript: `package.json`, `tsconfig.json` -- `manifest.json` in `appManifest/` folders -- `.gitignore` files in sample directories - -### Integration with Existing Rules -- This rule **extends** Rule 1 (Kairo check) and Rule 2 (Copyright headers) -- Apply all three rules together when reviewing sample code -- Prioritize security checks (no secrets) above all other validations diff --git a/.gitignore b/.gitignore index ab02d105..cffae76e 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,7 @@ publish/ # OS-specific files .DS_Store Thumbs.db + +# Agent 365 generated config (contains secrets and environment-specific values) +a365.config.json +a365.generated.config.json diff --git a/nodejs/langchain/sample-agent/.env.example b/nodejs/langchain/sample-agent/.env.example index 12b8531f..042ee968 100644 --- a/nodejs/langchain/sample-agent/.env.example +++ b/nodejs/langchain/sample-agent/.env.example @@ -50,3 +50,10 @@ agent365Observability__tenantId= agent365Observability__agentBlueprintId= agent365Observability__clientId= agent365Observability__clientSecret= + +# Runtime exporter gate. true for prod / dev tunnel; false keeps traces console-only. +ENABLE_A365_OBSERVABILITY_EXPORTER=true + +# Uncomment to debug exporter behaviour (both are required together). +# OTEL_LOG_LEVEL=INFO +# A365_OBSERVABILITY_LOG_LEVEL=debug diff --git a/nodejs/langchain/sample-agent/.gitignore b/nodejs/langchain/sample-agent/.gitignore index 75a5c39d..30763ae2 100644 --- a/nodejs/langchain/sample-agent/.gitignore +++ b/nodejs/langchain/sample-agent/.gitignore @@ -21,4 +21,8 @@ node_modules/ dist/ # Dev tool directories -/devTools/ \ No newline at end of file +/devTools/ + +# Local test artifacts +log.txt +*.csv \ No newline at end of file diff --git a/nodejs/langchain/sample-agent/ToolingManifest.json b/nodejs/langchain/sample-agent/ToolingManifest.json index e842561c..4e5699ea 100644 --- a/nodejs/langchain/sample-agent/ToolingManifest.json +++ b/nodejs/langchain/sample-agent/ToolingManifest.json @@ -5,7 +5,7 @@ "mcpServerUniqueName": "mcp_MailTools", "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", "scope": "McpServers.Mail.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad" } ] } \ No newline at end of file diff --git a/nodejs/langchain/sample-agent/package.json b/nodejs/langchain/sample-agent/package.json index fac6cd37..1b907bd8 100644 --- a/nodejs/langchain/sample-agent/package.json +++ b/nodejs/langchain/sample-agent/package.json @@ -25,13 +25,14 @@ "@langchain/langgraph": "^1.0.2", "@langchain/mcp-adapters": "^1.0.0", "@langchain/openai": "^1.0.2", - "@microsoft/agents-a365-notifications": "^1.0.0", - "@microsoft/agents-a365-runtime": "^1.0.0", - "@microsoft/agents-a365-tooling": "^1.0.0", - "@microsoft/agents-a365-tooling-extensions-langchain": "^1.0.0", + "@microsoft/agents-a365-notifications": "1.1.0-preview.7", + "@microsoft/agents-a365-runtime": "1.1.0-preview.7", + "@microsoft/agents-a365-tooling": "1.1.0-preview.7", + "@microsoft/agents-a365-tooling-extensions-langchain": "1.1.0-preview.7", "@microsoft/agents-activity": "^1.2.2", "@microsoft/agents-hosting": "^1.2.2", "@microsoft/opentelemetry": "^1.0.0", + "axios": "^1.19.0", "dotenv": "^17.2.3", "express": "^5.1.0", "langchain": "^1.0.1", @@ -46,8 +47,9 @@ "@microsoft/m365agentsplayground": "^0.2.18", "@types/express": "^4.17.21", "@types/node": "^20.14.9", + "env-cmd": "^11.0.0", "nodemon": "^3.1.10", "ts-node": "^10.9.2", - "env-cmd": "^11.0.0" + "typescript": "~5.9.3" } } diff --git a/nodejs/langchain/sample-agent/src/agent.ts b/nodejs/langchain/sample-agent/src/agent.ts index b8d21fe4..21f0488c 100644 --- a/nodejs/langchain/sample-agent/src/agent.ts +++ b/nodejs/langchain/sample-agent/src/agent.ts @@ -8,11 +8,15 @@ import { Activity, ActivityTypes } from '@microsoft/agents-activity'; import '@microsoft/agents-a365-notifications'; import { AgentNotificationActivity, NotificationType, createEmailResponseActivity } from '@microsoft/agents-a365-notifications'; // Observability Imports -import { BaggageBuilder, AgenticTokenCacheInstance, BaggageBuilderUtils } from '@microsoft/opentelemetry'; +import { BaggageBuilder, AgenticTokenCacheInstance, BaggageBuilderUtils, InvokeAgentScope } from '@microsoft/opentelemetry'; +import type { A365Request, CallerDetails, InvokeAgentScopeDetails } from '@microsoft/opentelemetry'; import { getObservabilityAuthenticationScope } from '@microsoft/agents-a365-runtime'; import tokenCache, { createAgenticTokenCacheKey } from './token-cache'; +import { buildAgentDetails, resolveChannelName } from './observability'; import { Client, getClient } from './client'; +const EMAIL_CHANNEL_NAME = 'outlook'; + export class A365Agent extends AgentApplication { static authHandlerName: string = 'agentic'; @@ -49,6 +53,8 @@ export class A365Agent extends AgentApplication { const from = turnContext.activity?.from; console.log(`Turn received from user — DisplayName: '${from?.name ?? "(unknown)"}', UserId: '${from?.id ?? "(unknown)"}', AadObjectId: '${from?.aadObjectId ?? "(none)"}'`); + // This is the id MAC/Defender reporting groups on — not the blueprint id stamped into .env. + console.log(`Runtime agent identity for this turn — agenticAppId: '${(turnContext.activity?.recipient as any)?.agenticAppId ?? "(none)"}'`); const displayName = from?.name ?? 'unknown'; if (!userMessage) { @@ -75,33 +81,96 @@ export class A365Agent extends AgentApplication { startTypingLoop(); + try { + await this.runTraced(turnContext, userMessage, async (scope) => { + scope?.recordInputMessages([userMessage]); + const client: Client = await getClient(this.authorization, A365Agent.authHandlerName, turnContext, displayName); + const response = await client.invokeInferenceScope(userMessage); + scope?.recordOutputMessages([response]); + await turnContext.sendActivity(response); + }); + } catch (error) { + console.error('LLM query error:', error); + const err = error as any; + await turnContext.sendActivity(`Error: ${err.message || err}`); + } finally { + stopTypingLoop(); + } + } + + /** + * Runs `work` under the full A365 trace context: refreshed exporter token, turn baggage, + * and a root `invoke_agent` scope. Every path that calls the LLM must go through this, + * otherwise its spans have no identity group and are dropped before export. + */ + private async runTraced( + turnContext: TurnContext, + inputText: string, + work: (scope: InvokeAgentScope | null) => Promise, + channelName?: string + ): Promise { + await this.preloadObservabilityToken(turnContext); + const baggageScope = BaggageBuilderUtils.fromTurnContext( new BaggageBuilder(), turnContext as any ).sessionDescription('Initial onboarding session') + .channelName(resolveChannelName(turnContext, channelName)) .build(); - // Preload/refresh exporter token - await this.preloadObservabilityToken(turnContext); - try { - await baggageScope.run(async () => { + return await baggageScope.run(async () => { + const scope = this.startInvokeAgentScope(turnContext, inputText, channelName); try { - const client: Client = await getClient(this.authorization, A365Agent.authHandlerName, turnContext, displayName); - const response = await client.invokeInferenceScope(userMessage); - await turnContext.sendActivity(response); + return scope ? await scope.withActiveSpanAsync(() => work(scope)) : await work(null); } catch (error) { - console.error('LLM query error:', error); - const err = error as any; - await turnContext.sendActivity(`Error: ${err.message || err}`); + scope?.recordError(error as Error); + throw error; + } finally { + scope?.dispose(); } }); } finally { - stopTypingLoop(); baggageScope.dispose(); } } + /** + * Opens the root `invoke_agent` scope for the turn. + * Returns null when the turn carries no real agent identity — a synthetic id would + * produce spans the exporter cannot authenticate, so the turn runs untraced instead. + */ + private startInvokeAgentScope(turnContext: TurnContext, userMessage: string, channelName?: string): InvokeAgentScope | null { + const agentDetails = buildAgentDetails(turnContext); + if (!agentDetails) { + return null; + } + + const request: A365Request = { + content: userMessage, + conversationId: turnContext.activity?.conversation?.id, + channel: { name: resolveChannelName(turnContext, channelName) }, + }; + + const from = turnContext.activity?.from; + const callerDetails: CallerDetails = { + userDetails: { + userId: from?.aadObjectId || from?.id || '', + userName: from?.name || '', + tenantId: agentDetails.tenantId, + }, + }; + + const scopeDetails: InvokeAgentScopeDetails = { + endpoint: { + host: process.env.WEBSITE_HOSTNAME || 'localhost', + port: Number(process.env.PORT) || 3978, + }, + }; + + return InvokeAgentScope.start(request, scopeDetails, agentDetails, callerDetails); + } + /** * Preloads or refreshes the Observability token used by the Agent 365 Observability exporter. */ @@ -145,22 +214,28 @@ export class A365Agent extends AgentApplication { return; } + const retrievePrompt = + `You have a new email from ${context.activity.from?.name} with id '${emailNotification.id}', ` + + `ConversationId '${emailNotification.conversationId}'. Please retrieve this message and return it in text format.`; + try { - const client: Client = await getClient(this.authorization, A365Agent.authHandlerName, context); + await this.runTraced(context, retrievePrompt, async (scope) => { + scope?.recordInputMessages([retrievePrompt]); + const client: Client = await getClient(this.authorization, A365Agent.authHandlerName, context); - // First, retrieve the email content - const emailContent = await client.invokeInferenceScope( - `You have a new email from ${context.activity.from?.name} with id '${emailNotification.id}', ` + - `ConversationId '${emailNotification.conversationId}'. Please retrieve this message and return it in text format.` - ); + // First, retrieve the email content + const emailContent = await client.invokeInferenceScope(retrievePrompt, EMAIL_CHANNEL_NAME); - // Then process the email - const response = await client.invokeInferenceScope( - `You have received the following email. Please follow any instructions in it. ${emailContent}` - ); + // Then process the email + const response = await client.invokeInferenceScope( + `You have received the following email. Please follow any instructions in it. ${emailContent}`, + EMAIL_CHANNEL_NAME + ); - const emailResponseActivity = createEmailResponseActivity(response || 'I have processed your email but do not have a response at this time.'); - await context.sendActivity(emailResponseActivity); + const finalResponse = response || 'I have processed your email but do not have a response at this time.'; + scope?.recordOutputMessages([finalResponse]); + await context.sendActivity(createEmailResponseActivity(finalResponse)); + }, EMAIL_CHANNEL_NAME); } catch (error) { console.error('Email notification error:', error); const errorResponse = createEmailResponseActivity('Unable to process your email at this time.'); diff --git a/nodejs/langchain/sample-agent/src/client.ts b/nodejs/langchain/sample-agent/src/client.ts index 404ae83c..f745eb36 100644 --- a/nodejs/langchain/sample-agent/src/client.ts +++ b/nodejs/langchain/sample-agent/src/client.ts @@ -13,13 +13,13 @@ import { Authorization, TurnContext } from '@microsoft/agents-hosting'; import { InferenceScope, InferenceOperationType, - AgentDetails, InferenceDetails, A365Request, } from '@microsoft/opentelemetry'; +import { buildAgentDetails, resolveChannelName } from './observability'; export interface Client { - invokeInferenceScope(prompt: string): Promise; + invokeInferenceScope(prompt: string, channelName?: string): Promise; } // Observability is initialized by the Microsoft OpenTelemetry distro in index.ts. @@ -29,6 +29,13 @@ const toolService = new McpToolRegistrationService(); const agentName = "LangChainA365Agent"; +/** Mirrors createChatModel()'s branch so the span provider matches the client actually used. */ +function getProviderName(): string { + return process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_OPENAI_ENDPOINT && process.env.AZURE_OPENAI_DEPLOYMENT + ? 'azure.ai.openai' + : 'openai'; +} + /** * Creates the appropriate chat model based on available environment variables. * Supports both Azure OpenAI and regular OpenAI. @@ -36,8 +43,28 @@ const agentName = "LangChainA365Agent"; function createChatModel(): BaseChatModel { // Check for Azure OpenAI configuration first if (process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_OPENAI_ENDPOINT && process.env.AZURE_OPENAI_DEPLOYMENT) { + let endpoint = process.env.AZURE_OPENAI_ENDPOINT; + + // Azure AI Foundry endpoints use a /v1 path and are OpenAI-compatible. + // They do not accept the api-version query parameter, so use ChatOpenAI + // with a custom baseURL instead of AzureChatOpenAI. + if (endpoint.includes('/v1')) { + console.log('Using Azure AI Foundry OpenAI-compatible endpoint'); + const baseURL = endpoint.substring(0, endpoint.indexOf('/v1') + 3); + return new ChatOpenAI({ + openAIApiKey: process.env.AZURE_OPENAI_API_KEY, + modelName: process.env.AZURE_OPENAI_DEPLOYMENT, + temperature: 0, + configuration: { + baseURL, + apiKey: process.env.AZURE_OPENAI_API_KEY, + defaultHeaders: { 'api-key': process.env.AZURE_OPENAI_API_KEY }, + }, + }); + } + console.log('Using Azure OpenAI'); - const endpoint = process.env.AZURE_OPENAI_ENDPOINT.replace(/\/$/, ''); + endpoint = process.env.AZURE_OPENAI_ENDPOINT.replace(/\/$/, ''); const deployment = process.env.AZURE_OPENAI_DEPLOYMENT; const apiVersion = process.env.AZURE_OPENAI_API_VERSION || "2025-03-01-preview"; return new AzureChatOpenAI({ @@ -199,23 +226,28 @@ class LangChainClient implements Client { return { content, inputTokens, outputTokens, finishReason }; } - async invokeInferenceScope(prompt: string) { + async invokeInferenceScope(prompt: string, channelName?: string) { + const agentDetails = buildAgentDetails(this.turnContext); + + // Without a real agent identity the exporter cannot authenticate the span, so run + // untraced rather than emitting one under a synthetic id. + if (!agentDetails) { + const untraced = await this.invokeAgent(prompt); + return untraced.content; + } + // Mirror createChatModel()'s defaults so the manual InferenceScope records // the same model identifier the underlying client actually uses. const modelName = process.env.AZURE_OPENAI_DEPLOYMENT || process.env.OPENAI_MODEL || 'gpt-4o'; const inferenceDetails: InferenceDetails = { operationName: InferenceOperationType.CHAT, model: modelName, + providerName: getProviderName(), }; const request: A365Request = { conversationId: this.turnContext?.activity?.conversation?.id || `conv-${Date.now()}`, - }; - - const agentDetails: AgentDetails = { - agentId: this.turnContext?.activity?.recipient?.agenticAppId || agentName, - agentName: agentName, - tenantId: this.turnContext?.activity?.recipient?.tenantId || 'sample-tenant', + channel: { name: resolveChannelName(this.turnContext, channelName) }, }; let response = ''; diff --git a/nodejs/langchain/sample-agent/src/index.ts b/nodejs/langchain/sample-agent/src/index.ts index e92dbf77..8cb4ad96 100644 --- a/nodejs/langchain/sample-agent/src/index.ts +++ b/nodejs/langchain/sample-agent/src/index.ts @@ -20,6 +20,9 @@ useMicrosoftOpenTelemetry({ enableConsoleExporters, a365: { enabled: true, + // Registers the Agent365Exporter. Without this (or ENABLE_A365_OBSERVABILITY_EXPORTER=true) + // spans are enriched but never sent, so MAC Activity stays empty. + enableObservabilityExporter: true, // When Use_Custom_Resolver is true the sample populates a local token cache; // otherwise agent.ts refreshes tokens into AgenticTokenCacheInstance. tokenResolver: process.env.Use_Custom_Resolver === 'true' diff --git a/nodejs/langchain/sample-agent/src/observability.ts b/nodejs/langchain/sample-agent/src/observability.ts new file mode 100644 index 00000000..c89de985 --- /dev/null +++ b/nodejs/langchain/sample-agent/src/observability.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { TurnContext } from '@microsoft/agents-hosting'; +import type { AgentDetails } from '@microsoft/opentelemetry'; + +/** + * Builds the AgentDetails carried by every A365 scope in a turn. + * Returns null when the turn has no runtime agent identity — the exporter cannot + * authenticate spans emitted under a synthetic id, so the turn should run untraced. + */ +export function buildAgentDetails(turnContext: TurnContext): AgentDetails | null { + const recipient = turnContext?.activity?.recipient as any; + const agentId: string = recipient?.agenticAppId ?? ''; + if (!agentId) { + return null; + } + + return { + agentId, + agentName: process.env.agent365Observability__agentName || 'LangChainA365Agent', + agentDescription: process.env.agent365Observability__agentDescription || '', + agentAUID: recipient?.agenticUserId ?? '', + agentEmail: recipient?.agenticUserId ?? '', + // Blueprint id drives the MAC roll-up view; agentId drives the per-instance view. + agentBlueprintId: recipient?.agenticAppBlueprintId || process.env.agent365Observability__agentBlueprintId || '', + tenantId: recipient?.tenantId || process.env.agent365Observability__tenantId || '', + }; +} + +/** Channel name for MAC Activity; notification turns override it since channelId is not msteams. */ +export function resolveChannelName(turnContext: TurnContext, override?: string): string { + return override || turnContext?.activity?.channelId || 'msteams'; +}