diff --git a/nodejs/n8n/sample-agent/.env.template b/nodejs/n8n/sample-agent/.env.template new file mode 100644 index 00000000..c868d47d --- /dev/null +++ b/nodejs/n8n/sample-agent/.env.template @@ -0,0 +1,33 @@ +# Agent Authentication Configuration +AGENT_ID=your-agent-id +AGENTIC_USER_ID=your-user-id + +# n8n Configuration +N8N_WEBHOOK_URL=https://example/webhook/url +# this is the value of the 'Authorization' to be passed when making the request to n8n webhook +N8N_WEBHOOK_AUTH_HEADER="Basic base64EncodedBasicAuthCredentials" + +# Development Settings +NODE_ENV=development +PORT=3978 + +# MCP Tool Server Configuration (optional) +# Set these if you want to provide Microsoft 365 tools to your n8n workflow +MCP_ENVIRONMENT_ID= +MCP_AUTH_TOKEN= +TOOLS_MODE=MCPPlatform + +# Service Connection Settings +connections__service_connection__settings__clientId= +connections__service_connection__settings__clientSecret= +connections__service_connection__settings__tenantId= + +# Set service connection as default +connectionsMap__0__serviceUrl=* +connectionsMap__0__connection=service_connection + +# AgenticAuthentication Options +agentic_type=agentic +agentic_altBlueprintConnectionName=service_connection +agentic_scopes=ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default # Prod Agentic scope +agentic_connectionName=service_connection \ No newline at end of file diff --git a/nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD b/nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD new file mode 100644 index 00000000..ff211dfe --- /dev/null +++ b/nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD @@ -0,0 +1,240 @@ + +# n8n Agent Code Walkthrough + +This document provides a detailed technical walkthrough of the n8n Sample Agent implementation, covering architecture, key components, activity handling, and integration patterns with n8n workflows. + +## 📁 File Structure Overview + +``` +sample-agent/ +├── src/ +│ ├── index.ts # 🔵 Express server entry point +│ ├── agent.ts # 🔵 Agent application and activity routing +│ ├── n8nAgent.ts # 🔵 Core business logic and lifecycle management +│ ├── n8nClient.ts # 🔵 n8n webhook client with observability +│ ├── mcpToolRegistrationService.ts # 🔵 MCP tool discovery and registration +│ └── telemetry.ts # 🔵 Observability configuration +├── ToolingManifest.json # 🔧 MCP tool server definitions +├── package.json # 📦 Dependencies and scripts +├── tsconfig.json # 🔧 TypeScript configuration +├── .env.example # ⚙️ Environment template +└── Documentation files... +``` + +## 🏗️ Architecture Overview + +### Design Principles +1. **n8n Integration**: Forwards all processing to n8n workflows via webhooks +1. **Event-Driven**: Bot Framework activity handlers for messages, notifications, and lifecycle events +1. **Stateful**: Tracks installation and terms acceptance status +1. **Observable**: Comprehensive telemetry with InvokeAgentScope and InferenceScope +1. **Tool-Enabled**: MCP tool integration for Microsoft 365 services + +### High-Level Flow +``` +┌─────────────────────────────────────────────────────────────────┐ +│ n8n Agent Architecture │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Teams/M365 → CloudAdapter → AgentApplication → N8nAgent │ +│ ↓ │ +│ Activity Router │ +│ ┌──────────┼──────────┐ │ +│ ↓ ↓ ↓ │ +│ Message Installation (Future: │ +│ Activity Activity Notifications) │ +│ ↓ ↓ │ +│ N8nClient State Updates │ +│ ↓ │ +│ MCP Tools │ +│ Discovery │ +│ ↓ │ +│ n8n Webhook (POST) │ +│ ↓ │ +│ { text, from, mcpServers } │ +│ ↓ │ +│ n8n Workflow Processing │ +│ ↓ │ +│ { output: "response" } │ +│ ↓ │ +│ Response to Teams/M365 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 🔍 Core Components Deep Dive + +### Code Overview + +#### index.ts - Express Server Entry Point +- Loads authentication configuration from environment variables +- Sets up Express server with JSON parsing and JWT authorization middleware +- Configures `/api/messages` endpoint for Bot Framework message processing +- Manages observability lifecycle (start on launch, shutdown on error/close) +- Handles graceful shutdown on SIGINT/SIGTERM signals + +#### agent.ts - Agent Application Setup +- Defines conversation state interface (`ConversationState` with message count) +- Creates `AgentApplication` with memory storage and file download support +- Registers activity handlers: + - **Message Activity**: Increments counter, delegates to `N8nAgent.handleAgentMessageActivity` + - **InstallationUpdate Activity**: Delegates to `N8nAgent.handleInstallationUpdateActivity` + +#### n8nAgent.ts - Core Business Logic +- Manages agent state: `isApplicationInstalled` and `termsAndConditionsAccepted` +- **Message Handler**: Enforces installation → terms acceptance → processing flow +- **Installation Handler**: Sets state flags and sends welcome/goodbye messages +- **Notification Handlers**: Processes email and Word @-mention notifications with two-stage flow (metadata extraction → content retrieval via n8n) +- **getN8nClient**: Discovers MCP tool servers and creates configured N8nClient + +#### n8nClient.ts - Webhook Client with Observability +- Constructs JSON payload with user message, sender name, and MCP tool metadata +- POSTs to `N8N_WEBHOOK_URL` with optional authentication header +- Parses response expecting `{ output: "text" }` format +- Wraps invocations with nested observability spans: + - **InvokeAgentScope**: Tracks entire agent invocation + - **InferenceScope**: Tracks n8n workflow execution with input/output recording + +#### mcpToolRegistrationService.ts - MCP Tool Discovery +- `getMcpServers`: Lists tool servers from configuration service, retrieves agentic token if needed +- `getTools`: Connects to each MCP server via HTTP transport, lists available tools with schemas +- Returns array of `McpServer` objects with tool metadata for n8n workflows + +#### telemetry.ts - Observability Configuration +- Configures `ObservabilityManager` with service name and version +- Can be extended with exporters (Console, OTLP, etc.) +- Lifecycle managed in `index.ts` + +--- + +## 🎯 Design Patterns and Best Practices + +### Separation of Concerns +- **index.ts**: Server infrastructure +- **agent.ts**: Activity routing +- **n8nAgent.ts**: Business logic +- **n8nClient.ts**: External integration +- **mcpToolRegistrationService.ts**: Tool management +- **telemetry.ts**: Observability configuration + +### Lifecycle State Management +```typescript +// Installation → Terms → Active → Uninstall +isApplicationInstalled: false → true → true → false +termsAndConditionsAccepted: false → false → true → false +``` + +**Flow**: +1. User installs agent → Welcome message +2. User sends "I accept" → Terms accepted +3. User sends messages → Forwarded to n8n +4. Answer is sent back to user + +### Observability-First Design +- All n8n invocations wrapped with InvokeAgentScope +- Inference operations tracked with InferenceScope +- Error recording for debugging +- Proper span disposal in finally blocks + +### Graceful Degradation +- MCP tool discovery failures don't block agent +- Empty tool array if discovery fails +- Webhook errors return user-friendly messages +- Fallback responses when n8n unavailable + +### Security Best Practices +- JWT authorization on all endpoints +- Environment-based secrets (no hardcoded credentials) +- Optional authentication header for n8n webhook +- Agentic authentication for MCP tool access + +--- + +## 🛠️ Extension Points + +### Adding Persistent State +Replace in-memory flags with persistent storage: +```typescript +// Instead of class properties +private stateService: IStateService; + +async handleAgentMessageActivity(turnContext, state) { + const userState = await this.stateService.getUserState(userId); + if (!userState.termsAccepted) { ... } +} +``` + +### Custom Notification Types +Add more notification handlers: +```typescript +agentApplication.onAgentNotification("CustomNotificationType", + async (context, state, notification) => { + await n8nAgent.handleCustomNotification(context, state, notification); + } +); +``` + +### Enhanced Observability +Add custom metrics and tags: +```typescript +scope?.recordCustomMetric('n8n_response_time_ms', duration); +scope?.addTags({ workflow_id: 'xyz', user_tier: 'premium' }); +``` + +### Webhook Response Enrichment +Process structured responses from n8n: +```typescript +interface N8nResponse { + output: string; + actions?: string[]; + metadata?: Record; +} + +// Handle actions, show adaptive cards, etc. +``` + +## 🔍 Configuration Requirements + +### Required Environment Variables +```bash +# Agent Identity +AGENT_ID=your-agent-id +AGENTIC_USER_ID=your-user-id + +# n8n Integration +N8N_WEBHOOK_URL=https://your-n8n-instance/webhook/path +N8N_WEBHOOK_AUTH_HEADER="Basic base64credentials" + +# Service Connection (Authentication) +connections__service_connection__settings__clientId= +connections__service_connection__settings__clientSecret= +connections__service_connection__settings__tenantId= + +# Agentic Authentication +agentic_type=agentic +agentic_altBlueprintConnectionName=service_connection +agentic_scopes=ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default + +# MCP Tools (Optional) +MCP_ENVIRONMENT_ID=your-environment-id +MCP_AUTH_TOKEN=optional-bearer-token +TOOLS_MODE=MCPPlatform +``` + +--- + +## **Summary** + +This n8n agent provides a stateful, observable bridge between Microsoft 365 and n8n workflows. It handles installation lifecycle, enforces terms acceptance, forwards messages and notifications to n8n with MCP tool context, and provides comprehensive telemetry for monitoring and debugging. The architecture separates concerns cleanly, enabling easy extension and maintenance while following Microsoft Agent 365 best practices. + + +### Features + +- **Message handling** from Teams chats with conversation state tracking +- **Installation/uninstallation lifecycle management** with welcome messages +- **Terms and conditions acceptance flow** before agent activation +- **Email notification support** - processes email notifications via agent notifications +- **Word document @-mention support** - handles @-mentions in Word comments via agent notifications +- **MCP Tool Server integration** - discovers and connects to Microsoft 365 MCP tool servers +- **Telemetry and observability** with Agent 365 SDK (InvokeAgentScope, InferenceScope) +- **Graceful shutdown** handling for SIGINT and SIGTERM signals diff --git a/nodejs/n8n/sample-agent/README.md b/nodejs/n8n/sample-agent/README.md new file mode 100644 index 00000000..ab5772d3 --- /dev/null +++ b/nodejs/n8n/sample-agent/README.md @@ -0,0 +1,54 @@ +# n8n Agent + +A Microsoft Agent 365 that integrates with n8n workflows for AI-powered automation. + +## Demonstrates + +This agent receives messages from Microsoft 365 (Teams, email, Word comments) and forwards them to an n8n workflow via webhook. The n8n workflow processes the request and returns a response. + +## Prerequisites + +- Node.js 18+ +- n8n instance with webhook endpoint +- Agentic Authentication registration + +## How to run this sample + +1. **Configure n8n webhook:** + - Create a workflow in n8n with a webhook trigger + - Configure the webhook to accept POST requests + - The webhook should expect a JSON body with `text`, `from`, `type`, and optional `mcpServers` fields + - Return a JSON response with an `output` field containing the response text + +1. **Set environment variables:** + Copy `.env.example` to `.env` and configure: + + ```bash + cp .env.example .env + ``` + +1. **Install dependencies** + ```bash + npm install + ``` + +1. **Build the project** + ```bash + npm run build + ``` + +1. **Start the agent** + ```bash + npm start + ``` + +1. **Optionally, while testing you can run in dev mode** + ```bash + npm run dev + ``` + +1. **Optionally, for testing you can use the Agents Playground:** + ```bash + # Launch Agents Playground for testing + npm run test-tool + ``` diff --git a/nodejs/n8n/sample-agent/ToolingManifest.json b/nodejs/n8n/sample-agent/ToolingManifest.json new file mode 100644 index 00000000..74748dde --- /dev/null +++ b/nodejs/n8n/sample-agent/ToolingManifest.json @@ -0,0 +1,19 @@ +{ + "mcpServers": [ + { + "mcpServerName": "mcp_MailTools" + }, + { + "mcpServerName": "mcp_CalendarTools" + }, + { + "mcpServerName": "mcp_NLWeb" + }, + { + "mcpServerName": "mcp_SharePointTools" + }, + { + "mcpServerName": "mcp_OneDriveServer" + } + ] +} \ No newline at end of file diff --git a/nodejs/n8n/sample-agent/package.json b/nodejs/n8n/sample-agent/package.json new file mode 100644 index 00000000..927c290b --- /dev/null +++ b/nodejs/n8n/sample-agent/package.json @@ -0,0 +1,30 @@ +{ + "name": "n8n-agent", + "version": "1.0.0", + "description": "sample agent to integrate with n8n", + "main": "src/index.ts", + "scripts": { + "start": "node ./dist/index.js", + "dev": "tsx watch ./src/index.ts", + "build": "tsc", + "test-tool": "agentsplayground" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@microsoft/agents-hosting-express": "^1.1.0-alpha.85", + "@microsoft/agents-a365-notifications": "*", + "@microsoft/agents-a365-observability": "*", + "@microsoft/agents-a365-runtime": "*", + "@microsoft/agents-a365-tooling": "*", + "@modelcontextprotocol/sdk": "^1.18.1", + "express": "^5.1.0", + "dotenv": "^17.2.2" + }, + "devDependencies": { + "tsx": "^4.20.5", + "@microsoft/m365agentsplayground": "^0.2.18", + "@types/node": "^24.5.1", + "typescript": "^5.0.0" + } +} diff --git a/nodejs/n8n/sample-agent/src/agent.ts b/nodejs/n8n/sample-agent/src/agent.ts new file mode 100644 index 00000000..949e154e --- /dev/null +++ b/nodejs/n8n/sample-agent/src/agent.ts @@ -0,0 +1,31 @@ +import { TurnState, AgentApplication, AttachmentDownloader, MemoryStorage, TurnContext } from '@microsoft/agents-hosting'; +import { ActivityTypes } from '@microsoft/agents-activity'; +import { N8nAgent } from './n8nAgent'; + +interface ConversationState { + count: number; +} +type ApplicationTurnState = TurnState + +const downloader = new AttachmentDownloader(); +const storage = new MemoryStorage(); + +export const agentApplication = new AgentApplication({ + storage, + fileDownloaders: [downloader] +}); + +const n8nAgent = new N8nAgent(undefined); + +agentApplication.onActivity(ActivityTypes.Message, async (context: TurnContext, state: ApplicationTurnState) => { + // Increment count state + let count = state.conversation.count ?? 0; + state.conversation.count = ++count; + + await n8nAgent.handleAgentMessageActivity(context, state); +}); + +agentApplication.onActivity(ActivityTypes.InstallationUpdate, async (context: TurnContext, state: ApplicationTurnState) => { + await n8nAgent.handleInstallationUpdateActivity(context, state); +}); + diff --git a/nodejs/n8n/sample-agent/src/index.ts b/nodejs/n8n/sample-agent/src/index.ts new file mode 100644 index 00000000..d9f1c83e --- /dev/null +++ b/nodejs/n8n/sample-agent/src/index.ts @@ -0,0 +1,50 @@ +import express, { Response } from 'express'; +import 'dotenv/config'; +import { AuthConfiguration, authorizeJWT, CloudAdapter, loadAuthConfigFromEnv, Request } from '@microsoft/agents-hosting'; +import { observabilityManager } from './telemetry'; +import { agentApplication } from './agent'; + +const authConfig: AuthConfiguration = loadAuthConfigFromEnv(); +const adapter = new CloudAdapter(authConfig); +const app = express(); +const port = process.env.PORT ?? 3978; + +// Middleware +app.use(express.json()); +app.use(authorizeJWT(authConfig)); + +observabilityManager.start(); + +app.post('/api/messages', async (req: Request, res: Response) => { + await adapter.process(req, res, async (context) => { + const app = agentApplication; + await app.run(context); + }); +}); + +const server = app.listen(port, () => { + console.log(`\nServer listening to port ${port} for appId ${authConfig.clientId} debug ${process.env.DEBUG}`); +}).on('error', async (err) => { + console.error(err); + await observabilityManager.shutdown(); + process.exit(1); +}).on('close', async () => { + console.log('observabilityManager is shutting down...'); + await observabilityManager.shutdown(); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + server.close(() => { + console.log('Server closed.'); + process.exit(0); + }); +}); + + +process.on('SIGTERM', () => { + server.close(() => { + console.log('Server closed.'); + process.exit(0); + }); +}); diff --git a/nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts b/nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts new file mode 100644 index 00000000..5ad5553e --- /dev/null +++ b/nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts @@ -0,0 +1,101 @@ +import { McpToolServerConfigurationService, McpClientTool, MCPServerConfig } from '@microsoft/agents-a365-tooling'; +import { AgenticAuthenticationService, Authorization } from '@microsoft/agents-a365-runtime'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { TurnContext } from '@microsoft/agents-hosting'; + +export type McpServer = MCPServerConfig & { + type: string, + requestInit: { + headers?: Record; + }, + tools: McpClientTool[] +}; + +/** + * Discover MCP servers and list tools + * Use getMcpServers to fetch server configs and getTools to enumerate tools. + */ +export class McpToolRegistrationService { + private configService: McpToolServerConfigurationService = new McpToolServerConfigurationService(); + + async getMcpServers( + agentUserId: string, + environmentId: string, + authorization: Authorization, + turnContext: TurnContext, + authToken: string + ): Promise { + if (!authToken) { + authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, turnContext); + } + + const mcpServers: McpServer[] = []; + const servers = await this.configService.listToolServers(agentUserId, environmentId, authToken); + + for (const server of servers) { + // Compose headers if values are available + const headers: Record = {}; + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + + if (environmentId) { + headers['x-ms-environment-id'] = environmentId; + } + + // Add each server to the config object + const mcpServer = { + mcpServerName: server.mcpServerName, + url: server.url, + requestInit: { + headers: headers + } + } as McpServer; + + const tools = await this.getTools(mcpServer); + mcpServer.tools = tools; + mcpServers.push(mcpServer); + } + return mcpServers; + } + + /** + * Connect to the MCP server and return tools + * Throws if the server URL is missing or the client fails to list tools. + */ + async getTools(mcpServerConfig: McpServer): Promise { + if (!mcpServerConfig) { + throw new Error('Invalid MCP Server Configuration'); + } + + if (!mcpServerConfig.url) { + throw new Error('MCP Server URL cannot be null or empty'); + } + + const transport = new StreamableHTTPClientTransport( + new URL(mcpServerConfig.url), + { + requestInit: mcpServerConfig.requestInit + } + ); + + const mcpClient = new Client({ + name: mcpServerConfig.mcpServerName, + version: '1.0', + }); + + await mcpClient.connect(transport); + const toolsObj = await mcpClient.listTools(); + await mcpClient.close(); + + const tools = toolsObj.tools.map(tool => ({ + name: mcpServerConfig.mcpServerName, + description: tool.description, + inputSchema: tool.inputSchema + })) as McpClientTool[]; + + return tools; + } +} + diff --git a/nodejs/n8n/sample-agent/src/n8nAgent.ts b/nodejs/n8n/sample-agent/src/n8nAgent.ts new file mode 100644 index 00000000..1b6e74a4 --- /dev/null +++ b/nodejs/n8n/sample-agent/src/n8nAgent.ts @@ -0,0 +1,196 @@ +import { AgentNotificationActivity, NotificationType, createAgentNotificationActivity } from '@microsoft/agents-a365-notifications'; +import { TurnContext, TurnState } from '@microsoft/agents-hosting'; +import { N8nClient } from './n8nClient'; +import { McpToolRegistrationService, McpServer } from './mcpToolRegistrationService'; + +export class N8nAgent { + isApplicationInstalled: boolean = false; + termsAndConditionsAccepted: boolean = false; + toolService: McpToolRegistrationService = new McpToolRegistrationService(); + authorization: any; + + constructor(authorization: any) { + this.authorization = authorization; + } + + /** + * Handles incoming user messages and sends responses using n8n. + */ + async handleAgentMessageActivity(turnContext: TurnContext, state: TurnState): Promise { + if (!this.isApplicationInstalled) { + await turnContext.sendActivity("Please install the application before sending messages."); + return; + } + + if (!this.termsAndConditionsAccepted) { + if (turnContext.activity.text?.trim().toLowerCase() === "i accept") { + this.termsAndConditionsAccepted = true; + await turnContext.sendActivity("Thank you for accepting the terms and conditions! How can I assist you today?"); + return; + } else { + await turnContext.sendActivity("Please accept the terms and conditions to proceed. Send 'I accept' to accept."); + return; + } + } + + const userMessage = turnContext.activity.text?.trim() || ''; + const fromUser = turnContext.activity.from?.name || ''; + + if (!userMessage) { + await turnContext.sendActivity('Please send me a message and I\'ll help you!'); + return; + } + + try { + const n8nClient = await this.getN8nClient(turnContext); + const response = await n8nClient.invokeAgentWithScope(userMessage, fromUser); + await turnContext.sendActivity(response); + } catch (error) { + console.error('n8n query error:', error); + const err = error as any; + await turnContext.sendActivity(`Error: ${err.message || err}`); + } + } + + /** + * Handles agent notification activities by parsing the activity type. + */ + async handleAgentNotificationActivity(turnContext: TurnContext, state: TurnState): Promise { + try { + const activity = turnContext.activity; + if (!activity || !Array.isArray(activity.entities)) { + await turnContext.sendActivity('No activity entities found.'); + return; + } + + if (!this.isApplicationInstalled) { + await turnContext.sendActivity("Please install the application before sending notifications."); + return; + } + + if (!this.termsAndConditionsAccepted) { + if (turnContext.activity.text?.trim().toLowerCase() === "i accept") { + this.termsAndConditionsAccepted = true; + await turnContext.sendActivity("Thank you for accepting the terms and conditions! How can I assist you today?"); + return; + } else { + await turnContext.sendActivity("Please accept the terms and conditions to proceed. Send 'I accept' to accept."); + return; + } + } + + // Find the first known notification type entity + const agentNotificationActivity = createAgentNotificationActivity(activity); + + switch (agentNotificationActivity.notificationType) { + case NotificationType.EmailNotification: + await this.emailNotificationHandler(turnContext, state, agentNotificationActivity); + break; + case NotificationType.WpxComment: + await this.wordNotificationHandler(turnContext, state, agentNotificationActivity); + break; + default: + await turnContext.sendActivity('Notification type not yet implemented.'); + } + } catch (error) { + console.error('Error handling agent notification activity:', error); + const err = error as any; + await turnContext.sendActivity(`Error handling notification: ${err.message || err}`); + } + } + + /** + * Handles agent installation and removal events. + */ + async handleInstallationUpdateActivity(turnContext: TurnContext, state: TurnState): Promise { + if (turnContext.activity.action === 'add') { + this.isApplicationInstalled = true; + this.termsAndConditionsAccepted = false; + await turnContext.sendActivity('Thank you for hiring me! Looking forward to assisting you in your professional journey! Before I begin, could you please confirm that you accept the terms and conditions? Send "I accept" to accept.'); + } else if (turnContext.activity.action === 'remove') { + this.isApplicationInstalled = false; + this.termsAndConditionsAccepted = false; + await turnContext.sendActivity('Thank you for your time, I enjoyed working with you.'); + } + } + + /** + * Handles @-mention notification activities. + */ + async wordNotificationHandler(turnContext: TurnContext, state: TurnState, mentionActivity: AgentNotificationActivity): Promise { + await turnContext.sendActivity('Thanks for the @-mention notification! Working on a response...'); + const mentionNotificationEntity = mentionActivity.wpxCommentNotification; + + if (!mentionNotificationEntity) { + await turnContext.sendActivity('I could not find the mention notification details.'); + return; + } + + const documentId = mentionNotificationEntity.documentId; + const odataId = mentionNotificationEntity["odata.id"]; + const initiatingCommentId = mentionNotificationEntity.initiatingCommentId; + const subjectCommentId = mentionNotificationEntity.subjectCommentId; + + let mentionPrompt = `You have been mentioned in a Word document. + Document ID: ${documentId || 'N/A'} + OData ID: ${odataId || 'N/A'} + Initiating Comment ID: ${initiatingCommentId || 'N/A'} + Subject Comment ID: ${subjectCommentId || 'N/A'} + Please retrieve the text of the initiating comment and return it in plain text.`; + + const n8nClient = await this.getN8nClient(turnContext); + const commentContent = await n8nClient.invokeAgentWithScope(mentionPrompt); + const response = await n8nClient.invokeAgentWithScope( + `You have received the following comment. Please follow any instructions in it. ${commentContent}` + ); + await turnContext.sendActivity(response); + } + + /** + * Handles email notification activities. + */ + async emailNotificationHandler(turnContext: TurnContext, state: TurnState, emailActivity: AgentNotificationActivity): Promise { + await turnContext.sendActivity('Thanks for the email notification! Working on a response...'); + const emailNotificationEntity = emailActivity.emailNotification; + + if (!emailNotificationEntity) { + await turnContext.sendActivity('I could not find the email notification details.'); + return; + } + + const emailNotificationId = emailNotificationEntity.id; + const emailNotificationConversationId = emailNotificationEntity.conversationId; + const emailNotificationConversationIndex = emailNotificationEntity.conversationIndex; + const emailNotificationChangeKey = emailNotificationEntity.changeKey; + + const n8nClient = await this.getN8nClient(turnContext); + const emailContent = await n8nClient.invokeAgentWithScope( + `You have a new email from ${turnContext.activity.from?.name} with id '${emailNotificationId}', + ConversationId '${emailNotificationConversationId}', ConversationIndex '${emailNotificationConversationIndex}', + and ChangeKey '${emailNotificationChangeKey}'. Please retrieve this message and return it in text format.` + ); + + const response = await n8nClient.invokeAgentWithScope( + `You have received the following email. Please follow any instructions in it. ${emailContent}` + ); + + await turnContext.sendActivity(response); + } + + async getN8nClient(turnContext: TurnContext): Promise { + const mcpServers: McpServer[] = []; + try { + mcpServers.push(...await this.toolService.getMcpServers( + process.env.AGENTIC_USER_ID || '', + process.env.MCP_ENVIRONMENT_ID || "", + this.authorization, + turnContext, + process.env.MCP_AUTH_TOKEN || "" + )); + } catch (error) { + console.warn('Failed to register MCP tool servers:', error); + } + + return new N8nClient(mcpServers); + } +} diff --git a/nodejs/n8n/sample-agent/src/n8nClient.ts b/nodejs/n8n/sample-agent/src/n8nClient.ts new file mode 100644 index 00000000..45b8312c --- /dev/null +++ b/nodejs/n8n/sample-agent/src/n8nClient.ts @@ -0,0 +1,129 @@ +import { InferenceScope, InvokeAgentScope, TenantDetails, InvokeAgentDetails, InferenceOperationType } from '@microsoft/agents-a365-observability'; +import { McpServer } from './mcpToolRegistrationService'; + +export class N8nClient { + mcpServers: McpServer[]; + + constructor(mcpServers?: McpServer[]) { + this.mcpServers = mcpServers ?? []; + } + + /** + * Generate a response based on the incoming message + */ + private async generateResponse(messageContent: string, fromUser: string = ''): Promise { + + const body = JSON.stringify( + { + "type": "message", + "text": messageContent, + "id": "b30d2fa7-f9f2-4e8f-8947-904063e4a8bd", + "from": fromUser, + "timestamp": "2025-10-02T16:10:59.882Z", + "textFormat": "plain", + "locale": "en-US", + "mcpServers": this.mcpServers + } + ); + + if (!process.env.N8N_WEBHOOK_URL) { + throw new Error('N8N_WEBHOOK_URL environment variable is not set.'); + } + const response = await fetch(process.env.N8N_WEBHOOK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(process.env.N8N_WEBHOOK_AUTH_HEADER ? { 'Authorization': process.env.N8N_WEBHOOK_AUTH_HEADER } : {}) + }, + body: body + }); + + if (!response.ok) { + console.error(`n8n webhook returned error status: ${response.status} ${response.statusText}`); + return null; + } + + let result: { output: string } | null = null; + try { + result = await response.json() as { output: string }; + } catch (err) { + console.error('Failed to parse n8n webhook response as JSON:', err); + return null; + } + + if (!result || typeof result.output !== 'string') { + console.error('n8n webhook response JSON missing expected "output" property:', result); + return null; + } + + return result.output; + } + + async invokeAgent(userMessage: string, fromUser: string = '') { + let response = ""; + try { + response = await this.generateResponse(userMessage, fromUser) || '' + if (!response) { + return "Sorry, I couldn't get a response from n8n :("; + } + return response; + } catch (error) { + console.error('Agent query error:', error); + return `Error: ${error}`; + } + } + + public async invokeAgentWithScope(userMessage: string, fromUser: string = '') { + const agentDetails = { agentId: process.env.AGENT_ID || 'sample-agent' }; + + const invokeAgentDetails: InvokeAgentDetails = { + ...agentDetails, + agentName: 'N8N Agent', + }; + + const tenantDetails: TenantDetails = { + tenantId: 'n8n-sample-tenant', + }; + + const invokeAgentScope = InvokeAgentScope.start(invokeAgentDetails, tenantDetails); + + if (!invokeAgentScope) { + // fallback: do the work without active parent span + await new Promise((resolve) => setTimeout(resolve, 200)); + return await this.invokeAgent(userMessage, fromUser); + } + + try { + return await invokeAgentScope.withActiveSpanAsync(async () => { + // Create the inference (child) scope while the invoke span is active + const scope = InferenceScope.start({ + model: 'n8n-workflow', + providerName: 'n8n', + operationName: InferenceOperationType.CHAT, + }, agentDetails, tenantDetails); + + if (!scope) { + await new Promise((resolve) => setTimeout(resolve, 200)); + return await this.invokeAgent(userMessage, fromUser); + } + + try { + // Activate the inference span for the inference work + const result = await scope.withActiveSpanAsync(async () => { + const response = await this.invokeAgent(userMessage, fromUser); + scope.recordOutputMessages([response || '']); + return response; + }); + return result; + } catch (error) { + scope.recordError(error as Error); + throw error; + } finally { + scope.dispose(); + } + }); + } finally { + invokeAgentScope.dispose(); + } + } +} diff --git a/nodejs/n8n/sample-agent/src/telemetry.ts b/nodejs/n8n/sample-agent/src/telemetry.ts new file mode 100644 index 00000000..558708f1 --- /dev/null +++ b/nodejs/n8n/sample-agent/src/telemetry.ts @@ -0,0 +1,10 @@ +import { + ObservabilityManager, +} from '@microsoft/agents-a365-observability'; + +export const observabilityManager = ObservabilityManager.configure( + (builder) => + builder + .withService('n8n Sample Agent', '1.0.0') +); + diff --git a/nodejs/n8n/sample-agent/tsconfig.json b/nodejs/n8n/sample-agent/tsconfig.json new file mode 100644 index 00000000..b51f5421 --- /dev/null +++ b/nodejs/n8n/sample-agent/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "incremental": true, + "lib": ["ES2021"], + "target": "es2019", + "module": "node16", + "declaration": true, + "sourceMap": true, + "composite": true, + "strict": true, + "moduleResolution": "node16", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo" + } +}