diff --git a/nodejs/claude/sample-agent/.babelrc b/nodejs/claude/sample-agent/.babelrc deleted file mode 100644 index 1320b9a3..00000000 --- a/nodejs/claude/sample-agent/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/preset-env"] -} diff --git a/nodejs/claude/sample-agent/.env.template b/nodejs/claude/sample-agent/.env.template new file mode 100644 index 00000000..55f78fb7 --- /dev/null +++ b/nodejs/claude/sample-agent/.env.template @@ -0,0 +1,30 @@ +# Anthropic Configuration +ANTHROPIC_API_KEY= + +# MCP Tooling Configuration +TOOLS_MODE=MCPPlatform # Options: MockMCPServer | MCPPlatform +BEARER_TOKEN= +USE_ENVIRONMENT_ID=false + +# Environment Settings +NODE_ENV=development # Retrieve mcp servers from ToolingManifest + +# Telemetry and Tracing Configuration +DEBUG=agents:* + +# Use Agentic Authentication rather than OBO +USE_AGENTIC_AUTH=false + +# 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 diff --git a/nodejs/claude/sample-agent/README.md b/nodejs/claude/sample-agent/README.md index 6d80f726..e7fdf42a 100644 --- a/nodejs/claude/sample-agent/README.md +++ b/nodejs/claude/sample-agent/README.md @@ -1,247 +1,75 @@ -# Simple Claude Agent +# Sample Agent - Node.js Claude -An integration of **Claude Code SDK** with **Microsoft 365 Agents SDK** and **Agent 365 SDK** for conversational AI experiences. +This directory contains a sample agent implementation using Node.js and Claude Agent SDK. -## 🚀 Quick Start +## Demonstrates -### Prerequisites +This sample demonstrates how to build an agent using the Agent365 framework with Node.js and Claude Agent SDK. + +## Prerequisites - Node.js 18+ -- Anthropic API key from [https://console.anthropic.com/](https://console.anthropic.com/) +- Anthropic API access +- Claude Agent SDK +- Agents SDK -### Setup +## How to run this sample -1. **Install Dependencies** +1. **Setup environment variables** + ```bash + # Copy the template environment file + cp .env.template .env + ``` +2. **Install dependencies** ```bash - cd nodejs/claude/sample-agent npm install ``` -2. **Configure Claude API Key** + **Note** Be sure to create the folder `./packages/` and add the a365 packages here for the preinstall script to work. +3. **Build the project** ```bash - # 1. Get your API key from https://console.anthropic.com/ - # 2. Set your Anthropic API key in .env file - ANTHROPIC_API_KEY=your_anthropic_api_key_here + npm run build ``` -3. **Configure Environment** (optional) - +4. **Start the agent** ```bash - cp env.TEMPLATE .env - # Edit .env if needed for Azure Bot Service deployment or MCP Tooling - # the .env is already configured for connecting to the Mock MCP Server. + npm start ``` -4. **Start the Agent** - +5. **Optionally, while testing you can run in dev mode** ```bash npm run dev ``` -5. **Test with Playground** +6. **Start AgentsPlayground to chat with your agent** ```bash - npm run test-tool + agentsplayground ``` -## 💡 How It Works - -This agent demonstrates the simplest possible integration between: - -- **Claude Code SDK**: Provides AI capabilities with tool access -- **Microsoft 365 Agents SDK**: Handles conversational interface and enterprise features -- **Agents 365 SDK**: Handles MCP tooling and Agent Notification set up - -### Key Features - -- ✨ **Direct Claude Integration**: Uses `query()` API for natural conversations -- 🔧 **Tool Access**: Claude can use Read, Write, WebSearch, Bash, and Grep tools -- 🎴 **Adaptive Card Responses**: Beautiful, interactive card-based responses -- 💬 **Streaming Progress**: Real-time processing indicators -- 🏢 **Enterprise Features**: Sensitivity labels and compliance features - -## 📝 Usage Examples - -Just chat naturally with the agent: - -``` -"Use MailTools to send an email." -"Query my calendar with CalendarTools." -"Summarize a web page using NLWeb." -"Search SharePoint files with SharePointTools." -"Access my OneDrive files using OneDriveMCPServer." -``` - -## 🏗️ Architecture - -``` -User Input → M365 Agent → Claude Code SDK → AI Response → User -``` - -### Core Components - -1. `src/index.js`: Server startup and configuration -2. `src/agent.js`: Main agent orchestration and turn handling -3. `src/claudeAgent.js`: Claude agent wrapper and higher-level logic -4. `src/claudeClient.js`: Claude SDK client -5. `src/adaptiveCards.js`: Adaptive card utilities for rich responses -6. `src/telemetry.js`: Application telemetry and tracing helpers -7. `src/evals/`: Evaluation scripts and result viewers (benchmarks and test harness) - -### Simple Integration Pattern - -```javascript -import { query } from "@anthropic-ai/claude-agent-sdk"; - -for await (const message of query({ - prompt: userMessage, - options: { - allowedTools: ["Read", "Write", "WebSearch"], - maxTurns: 3, - }, -})) { - if (message.type === "result") { - // Create adaptive card response - const responseCard = createClaudeResponseCard(message.result, userMessage); - const cardAttachment = MessageFactory.attachment({ - contentType: "application/vnd.microsoft.card.adaptive", - content: responseCard, - }); - await context.sendActivity(cardAttachment); - } -} -``` - -## 🎴 Adaptive Card Features - -The agent displays Claude responses in interactive adaptive cards featuring: - -- **Rich Formatting**: Markdown rendering with styling -- **User Context**: Shows the original query for reference -- **Timestamps**: Generated response time for tracking -- **Interactive Actions**: Follow-up buttons and error recovery -- **Error Handling**: error cards with troubleshooting steps - -### Card Types - -1. **Response Cards**: Main Claude responses with formatted text -2. **Error Cards**: Friendly error handling with action buttons -3. **Thinking Cards**: Processing indicators (optional) - -## 🔧 Customization - -### Agent Notification Handling - -You can handle agent notifications (such as email, mentions, etc.) using the `OnAgentNotification` method from the Agent 365 SDK. This allows your agent to respond to custom activities and notifications. - -#### Example: Registering a Notification Handler - -```javascript -import "@microsoft/agents-a365-notifications"; -import { ClaudeAgent } from "./claudeAgent.js"; - -const claudeAgent = new ClaudeAgent(simpleClaudeAgent.authorization); - -// Route all notifications (any channel) -simpleClaudeAgent.onAgentNotification( - "*", - claudeAgent.handleAgentNotificationActivity.bind(claudeAgent) -); -``` - -**Note:** - -- The first argument to `onAgentNotification` can be a specific `channelId` (such as `'email'`, `'mention'`, etc.) to route only those notifications, or use `'*'` to route all notifications regardless of channel. - -This enables flexible notification routing for your agent, allowing you to handle all notifications or only those from specific channels as needed. - -### Add More Tools - -Tools can be added directly to the `options` passed to the query, or dynamically registered using Agent 365 SDK's `McpToolRegistrationService.addMcpToolServers()` method. - -#### Example: Registering MCP Tool Servers - -```javascript -import { McpToolRegistrationService } from "@microsoft/agents-a365-tooling-extensions-claude"; - -const toolServerService = new McpToolRegistrationService(); -const agentOptions = { - allowedTools: ["Read", "Write", "WebSearch", "Bash", "Grep"], - // ...other options -}; - -await toolServerService.addMcpToolServers( - agentOptions, - process.env.AGENTIC_USER_ID || "", // Only required outside development mode - process.env.MCP_ENVIRONMENT_ID || "", // Only required outside development mode - app.authorizaiton, - turnContext, - process.env.MCP_AUTH_TOKEN || "" // Only required if your mcp server requires this -); -``` - -This will register all MCP tool servers found in your ToolingManifest.json and make them available to the agent at runtime. - -Depending on your environment, tool servers may also be discovered dynamically from a tooling gateway (such as via the Agent 365 SDK) instead of or in addition to ToolingManifest.json. This enables flexible and environment-specific tool server registration for your agent. - -**Note:** The `allowedTools` and `mcpServers` properties in your agent options will be automatically modified by appending the tools found in the tool servers specified. This enables dynamic tool access for Claude and the agent, based on the current MCP tool server configuration. - -**Note:** This sample uses the agentic authorization flow if MCP_AUTH_TOKEN is not provided. To run agentic auth you must provide values that match your Azure app registrations and tenant: - -- `AGENT_APPLICATION_ID` — agent application (client) id -- `AGENT_CLIENT_SECRET` (optional) — if not using managed identity, provide the agent application client secret securely -- `AGENT_ID` — agent identity client id -- `USER_PRINCIPAL_NAME` — the agent's username (UPN) -- `AGENTIC_USER_ID` — agentic user id (used by some tooling flows) -- `MANAGED_IDENTITY_TOKEN` (optional, dev) — pre-acquired managed identity token used as a client_assertion fallback for local development - -### Custom System Prompt - -```javascript -appendSystemPrompt: "You are a specialized assistant for..."; -``` - -### Conversation Memory - -The agent maintains conversation context automatically through the M365 SDK. - -## 🚢 Deployment - -### Local Development - -```bash -npm start # Runs on localhost:3978 -``` - -### Azure Bot Service - -1. Create Azure Bot Service -2. Set environment variables in `.env` -3. Deploy to Azure App Service -4. Configure messaging endpoint +The agent will start and be ready to receive requests through the configured hosting mechanism. -## ⚙️ Configuration +## Documentation -### Environment Variables +For detailed information about this sample, please refer to: -- `TENANT_ID`, `CLIENT_ID`, `CLIENT_SECRET`: Azure Bot Service credentials -- `ANTHROPIC_API_KEY`: Anthropic API key for Claude authentication (required) -- `NODE_ENV`: Environment (development/production) -- `PORT`: Server port (default: 3978) +- **[AGENT-CODE-WALKTHROUGH.md](AGENT-CODE-WALKTHROUGH.md)** - Detailed code explanation and architecture walkthrough -### Claude Authentication +## 📚 Related Documentation -- Obtain an API key from [Anthropic Console](https://console.anthropic.com/) -- Set `ANTHROPIC_API_KEY` in your `.env` file -- Suitable for all deployment scenarios +- [Claude Agent SDK Documentation](https://docs.claude.com/en/docs/agent-sdk/typescript.md) +- [Microsoft Agent 365 Tooling](https://github.com/microsoft/Agent365-nodejs/tree/main/packages/agents-a365-tooling-extensions-claude) +- [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/typescript-sdk/tree/main) +- [AgentsPlayground](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/test-with-toolkit-project?tabs=windows) ## 🤝 Contributing -This is a minimal example. Extend it by [WIP] +1. Follow the existing code patterns and structure +2. Add comprehensive logging and error handling +3. Update documentation for new features +4. Test thoroughly with different authentication methods -## 📚 Learn More +## 📄 License -- [Claude Agent SDK Documentation](https://docs.anthropic.com/claude-agent-sdk) -- [Microsoft 365 Agents SDK](https://github.com/microsoft/agents) -- [Agent Playground Tool](https://www.npmjs.com/package/@microsoft/m365agentsplayground) +This project is licensed under the MIT License - see the [LICENSE](../../../LICENSE.md) file for details. diff --git a/nodejs/claude/sample-agent/ToolingManifest.json b/nodejs/claude/sample-agent/ToolingManifest.json index 74748dde..60feb6c4 100644 --- a/nodejs/claude/sample-agent/ToolingManifest.json +++ b/nodejs/claude/sample-agent/ToolingManifest.json @@ -1,19 +1,18 @@ { "mcpServers": [ { - "mcpServerName": "mcp_MailTools" + "mcpServerName": "mcp_MailTools", + "mcpServerUniqueName": "mcp_MailTools", + "url": "https://agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "scope": "McpServers.Mail.All", + "audience": "05879165-0320-489e-b644-f72b33f3edf0" }, { - "mcpServerName": "mcp_CalendarTools" - }, - { - "mcpServerName": "mcp_NLWeb" - }, - { - "mcpServerName": "mcp_SharePointTools" - }, - { - "mcpServerName": "mcp_OneDriveServer" + "mcpServerName": "mcp_WordServer", + "mcpServerUniqueName": "mcp_WordServer", + "url": "https://agent365.svc.cloud.dev.microsoft/agents/servers/mcp_WordServer", + "scope": "McpServers.Word.All", + "audience": "05879165-0320-489e-b644-f72b33f3edf0" } ] } \ No newline at end of file diff --git a/nodejs/claude/sample-agent/env.TEMPLATE b/nodejs/claude/sample-agent/env.TEMPLATE deleted file mode 100644 index 023aa599..00000000 --- a/nodejs/claude/sample-agent/env.TEMPLATE +++ /dev/null @@ -1,54 +0,0 @@ -# Simple Claude Agent Environment Configuration -# Copy this file to .env and fill in your values - -# Microsoft 365 Bot Configuration (optional - for Azure Bot Service) -# Only needed if deploying to Azure Bot Service -TENANT_ID= -CLIENT_ID= -CLIENT_SECRET= - -# Agent365 Authentication Configuration -AGENT_APPLICATION_ID= -AGENT_ID= -USER_PRINCIPAL_NAME= -AGENTIC_USER_ID= - -# Claude Code SDK Configuration -# Get your API key from https://console.anthropic.com/ -ANTHROPIC_API_KEY= - -# Development Settings -NODE_ENV=development -PORT=3978 - -# Note: Get your Anthropic API key from https://console.anthropic.com/ -# and set ANTHROPIC_API_KEY above - -# MockMCPServer Settings -# Note: MCP_ENVIRONMENT_ID is only needed for Tooling Gateway or if your MCP -# server uses an environment id in its path. -TOOLS_MODE=MockMCPServer -MCP_AUTH_TOKEN= -MCP_ENVIRONMENT_ID= - - -# Service Connection Settings -connections__service_connection__settings__clientId= -connections__service_connection__settings__clientSecret= -connections__service_connection__settings__tenantId= -connections__service_connection__settings__altBlueprintConnectionName=agentBlueprint - -# Agent Authentication Connection Settings -connections__agentBlueprint__settings__clientId= -connections__agentBlueprint__settings__clientSecret= -connections__agentBlueprint__settings__tenantId= -connections__agentBlueprint__settings__authority=https://login.microsoftonline.com - -# Set service connection as default -connectionsMap__0__serviceUrl=* -connectionsMap__0__connection=service_connection - -# AgenticAuthentication Options -agentic_altBlueprintConnectionName=agentBlueprint -agentic_scopes=https://graph.microsoft.com/.default -agentic_type=agentic \ No newline at end of file diff --git a/nodejs/claude/sample-agent/package.json b/nodejs/claude/sample-agent/package.json index 994921d1..6d4ef65b 100644 --- a/nodejs/claude/sample-agent/package.json +++ b/nodejs/claude/sample-agent/package.json @@ -1,47 +1,30 @@ { - "name": "agent365-sdk-claude-sample-agent", - "version": "0.1.0", - "description": "Sample agent integrating Claude Code SDK with Microsoft Agent 365 SDK", - "main": "src/index.js", - "type": "module", + "name": "claude-agents-sdk", + "version": "1.0.0", + "main": "index.js", "scripts": { - "preinstall": "node preinstall-local-packages.js", - "start": "node src/index.js", - "dev": "node --env-file .env --watch src/index.js", + "start": "node dist/index.js", + "dev": "nodemon --watch src/*.ts --exec ts-node src/index.ts", "test-tool": "agentsplayground", - "eval": "node --env-file .env src/evals/index.js", - "build": "babel src -d dist", "install:clean": "npm run clean && npm install", - "clean": "rimraf node_modules package-lock.json" + "clean": "rimraf dist node_modules package-lock.json", + "build": "tsc" }, - "keywords": [ - "claude-code-sdk", - "microsoft-365", - "agent", - "ai" - ], - "author": "airaamane@microsoft.com", + "keywords": [], + "author": "Microsoft", "license": "MIT", + "description": "", "dependencies": { + "@microsoft/agents-hosting": "^1.1.0-alpha.85", "@anthropic-ai/claude-agent-sdk": "^0.1.1", - "@microsoft/agents-activity": "^1.1.0-alpha.78", - "@microsoft/agents-hosting": "^1.1.0-alpha.78", - "@microsoft/agents-hosting-express": "^1.1.0-alpha.78", - "express": "^5.1.0", - "node-fetch": "^3.3.2", - "uuid": "^9.0.0" - }, - "overrides": { - "@microsoft/agents-activity": "^1.1.0-alpha.78", - "@microsoft/agents-hosting": "^1.1.0-alpha.78", - "@microsoft/agents-hosting-express": "^1.1.0-alpha.78" + "dotenv": "^17.2.2", + "express": "^5.1.0" }, "devDependencies": { - "@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.32", - "@babel/cli": "^7.28.3", - "@babel/core": "^7.28.4", - "@babel/preset-env": "^7.28.3", - "@microsoft/m365agentsplayground": "^0.2.16", - "rimraf": "^5.0.0" + "@microsoft/m365agentsplayground": "^0.2.18", + "nodemon": "^3.1.10", + "rimraf": "^5.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" } -} +} \ No newline at end of file diff --git a/nodejs/claude/sample-agent/preinstall-local-packages.js b/nodejs/claude/sample-agent/preinstall-local-packages.js deleted file mode 100644 index 7e913fd7..00000000 --- a/nodejs/claude/sample-agent/preinstall-local-packages.js +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env node - -import { readdir } from 'fs/promises'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { execSync } from 'child_process'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Look for *.tgz files two directories above -const tgzDir = join(__dirname, '../../'); - -// Define the installation order -const installOrder = [ - 'microsoft-agents-a365-runtime-', - 'microsoft-agents-a365-notifications-', - 'microsoft-agents-a365-observability-', - 'microsoft-agents-a365-tooling-', - 'microsoft-agents-a365-tooling-extensions-claude-' -]; - -async function findTgzFiles() { - try { - const files = await readdir(tgzDir); - return files.filter(file => file.endsWith('.tgz')); - } catch (error) { - console.log('No tgz directory found or no files to install'); - return []; - } -} - -function findFileForPattern(files, pattern) { - return files.find(file => file.startsWith(pattern)); -} - -async function installPackages() { - const tgzFiles = await findTgzFiles(); - - if (tgzFiles.length === 0) { - console.log('No .tgz files found in', tgzDir); - return; - } - - console.log('Found .tgz files:', tgzFiles); - - for (const pattern of installOrder) { - const file = findFileForPattern(tgzFiles, pattern); - if (file) { - const filePath = join(tgzDir, file); - console.log(`Installing ${file}...`); - try { - execSync(`npm install "${filePath}"`, { - stdio: 'inherit', - cwd: __dirname - }); - console.log(`✓ Successfully installed ${file}`); - } catch (error) { - console.error(`✗ Failed to install ${file}:`, error.message); - process.exit(1); - } - } else { - console.log(`No file found matching pattern: ${pattern}`); - } - } -} - -// Run the installation -installPackages().catch(error => { - console.error('Error during package installation:', error); - process.exit(1); -}); \ No newline at end of file diff --git a/nodejs/claude/sample-agent/src/adaptiveCards.js b/nodejs/claude/sample-agent/src/adaptiveCards.js deleted file mode 100644 index 03ea6c31..00000000 --- a/nodejs/claude/sample-agent/src/adaptiveCards.js +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Adaptive Card utilities for Claude responses - */ - -export function createClaudeResponseCard(response, userQuery) { - // Clean and format the response text - const formattedResponse = formatResponseText(response) - - return { - $schema: "http://adaptivecards.io/schemas/adaptive-card.json", - version: "1.4", - type: "AdaptiveCard", - body: [ - { - type: "Container", - style: "emphasis", - items: [ - { - type: "ColumnSet", - columns: [ - { - type: "Column", - width: "auto", - items: [ - { - type: "Image", - url: "https://cdn.jsdelivr.net/gh/microsoft/fluentui-emoji/assets/Robot/3D/robot_3d.png", - size: "Small", - style: "Person" - } - ] - }, - { - type: "Column", - width: "stretch", - items: [ - { - type: "TextBlock", - text: "Claude Assistant", - weight: "Bolder", - size: "Medium" - }, - { - type: "TextBlock", - text: `Responding to: "${truncateText(userQuery, 100)}"`, - isSubtle: true, - size: "Small", - wrap: true - } - ] - } - ] - } - ] - }, - { - type: "Container", - items: [ - { - type: "TextBlock", - text: formattedResponse, - wrap: true, - spacing: "Medium" - } - ] - }, - { - type: "Container", - separator: true, - items: [ - { - type: "ColumnSet", - columns: [ - { - type: "Column", - width: "stretch", - items: [ - { - type: "TextBlock", - text: `Generated at ${new Date().toLocaleTimeString()}`, - isSubtle: true, - size: "Small" - } - ] - }, - { - type: "Column", - width: "auto", - items: [ - { - type: "TextBlock", - text: "🤖 Powered by Claude Code SDK", - isSubtle: true, - size: "Small" - } - ] - } - ] - } - ] - } - ], - actions: [ - { - type: "Action.Submit", - title: "Ask Follow-up", - data: { - action: "followup", - context: truncateText(response, 200) - } - } - ] - } -} - -export function createErrorCard(error, userQuery) { - return { - $schema: "http://adaptivecards.io/schemas/adaptive-card.json", - version: "1.4", - type: "AdaptiveCard", - body: [ - { - type: "Container", - style: "attention", - items: [ - { - type: "ColumnSet", - columns: [ - { - type: "Column", - width: "auto", - items: [ - { - type: "TextBlock", - text: "⚠️", - size: "Large" - } - ] - }, - { - type: "Column", - width: "stretch", - items: [ - { - type: "TextBlock", - text: "Error Processing Request", - weight: "Bolder", - color: "Attention" - }, - { - type: "TextBlock", - text: error.message || "An unexpected error occurred", - wrap: true, - isSubtle: true - } - ] - } - ] - } - ] - }, - { - type: "Container", - items: [ - { - type: "TextBlock", - text: "**Troubleshooting Steps:**", - weight: "Bolder", - spacing: "Medium" - }, - { - type: "TextBlock", - text: "• Ensure ANTHROPIC_API_KEY is set in your environment", - wrap: true - }, - { - type: "TextBlock", - text: "• Get your API key from https://console.anthropic.com/", - wrap: true, - isSubtle: true - }, - { - type: "TextBlock", - text: "• Check your network connection", - wrap: true - }, - { - type: "TextBlock", - text: "• Try rephrasing your question", - wrap: true - } - ] - } - ], - actions: [ - { - type: "Action.Submit", - title: "Try Again", - data: { - action: "retry", - originalQuery: userQuery - } - } - ] - } -} - -export function createThinkingCard(query) { - return { - $schema: "http://adaptivecards.io/schemas/adaptive-card.json", - version: "1.4", - type: "AdaptiveCard", - body: [ - { - type: "Container", - style: "emphasis", - items: [ - { - type: "ColumnSet", - columns: [ - { - type: "Column", - width: "auto", - items: [ - { - type: "TextBlock", - text: "🤔", - size: "Large" - } - ] - }, - { - type: "Column", - width: "stretch", - items: [ - { - type: "TextBlock", - text: "Claude is thinking...", - weight: "Bolder" - }, - { - type: "TextBlock", - text: `Processing: "${truncateText(query, 80)}"`, - isSubtle: true, - wrap: true - } - ] - } - ] - } - ] - } - ] - } -} - -function formatResponseText(text) { - if (!text) return "No response received" - - // Basic markdown-like formatting for adaptive cards - return text - .replace(/\*\*(.*?)\*\*/g, '**$1**') // Keep bold - .replace(/\*(.*?)\*/g, '*$1*') // Keep italic - .replace(/`([^`]+)`/g, '`$1`') // Keep inline code - .replace(/^### (.*$)/gm, '**$1**') // Convert h3 to bold - .replace(/^## (.*$)/gm, '**$1**') // Convert h2 to bold - .replace(/^# (.*$)/gm, '**$1**') // Convert h1 to bold -} - -export function createCodeAnalysisCard(analysis, filePath, userQuery) { - // Parse analysis if it's a string - let analysisData - try { - analysisData = typeof analysis === 'string' ? JSON.parse(analysis) : analysis - } catch { - // If not JSON, treat as plain text analysis - analysisData = { summary: analysis } - } - - return { - $schema: "http://adaptivecards.io/schemas/adaptive-card.json", - version: "1.4", - type: "AdaptiveCard", - body: [ - { - type: "Container", - style: "emphasis", - items: [ - { - type: "ColumnSet", - columns: [ - { - type: "Column", - width: "auto", - items: [ - { - type: "TextBlock", - text: "🔍", - size: "Large" - } - ] - }, - { - type: "Column", - width: "stretch", - items: [ - { - type: "TextBlock", - text: "Code Analysis Complete", - weight: "Bolder", - size: "Medium" - }, - { - type: "TextBlock", - text: `File: ${filePath || 'Unknown'}`, - isSubtle: true, - size: "Small" - } - ] - } - ] - } - ] - }, - { - type: "Container", - items: [ - { - type: "TextBlock", - text: analysisData.summary || analysis, - wrap: true, - spacing: "Medium" - } - ] - }, - ...(analysisData.issues ? [{ - type: "Container", - separator: true, - items: [ - { - type: "TextBlock", - text: "🚨 Issues Found", - weight: "Bolder", - color: "Attention" - }, - ...analysisData.issues.slice(0, 5).map(issue => ({ - type: "TextBlock", - text: `• **${issue.type || 'Issue'}**: ${issue.description || issue}`, - wrap: true, - spacing: "Small" - })) - ] - }] : []), - ...(analysisData.recommendations ? [{ - type: "Container", - separator: true, - items: [ - { - type: "TextBlock", - text: "💡 Recommendations", - weight: "Bolder", - color: "Good" - }, - ...analysisData.recommendations.slice(0, 3).map(rec => ({ - type: "TextBlock", - text: `• ${rec}`, - wrap: true, - spacing: "Small" - })) - ] - }] : []), - { - type: "Container", - separator: true, - items: [ - { - type: "ColumnSet", - columns: [ - { - type: "Column", - width: "stretch", - items: [ - { - type: "TextBlock", - text: `Analyzed at ${new Date().toLocaleTimeString()}`, - isSubtle: true, - size: "Small" - } - ] - }, - { - type: "Column", - width: "auto", - items: [ - { - type: "TextBlock", - text: "🤖 Claude Code Analysis", - isSubtle: true, - size: "Small" - } - ] - } - ] - } - ] - } - ], - actions: [ - { - type: "Action.Submit", - title: "Analyze Another File", - data: { - action: "analyze_another", - previousFile: filePath - } - }, - { - type: "Action.Submit", - title: "Get Detailed Report", - data: { - action: "detailed_analysis", - file: filePath - } - } - ] - } -} - -function truncateText(text, maxLength) { - if (!text) return "" - if (text.length <= maxLength) return text - return text.substring(0, maxLength - 3) + "..." -} \ No newline at end of file diff --git a/nodejs/claude/sample-agent/src/agent.js b/nodejs/claude/sample-agent/src/agent.js deleted file mode 100644 index e121cbb6..00000000 --- a/nodejs/claude/sample-agent/src/agent.js +++ /dev/null @@ -1,48 +0,0 @@ -import { ActivityTypes } from '@microsoft/agents-activity' -import { AgentApplicationBuilder, MemoryStorage } from '@microsoft/agents-hosting' - -import '@microsoft/agents-a365-notifications' -import { ClaudeAgent } from './claudeAgent.js' - -const storage = new MemoryStorage(); - -export const simpleClaudeAgent = new AgentApplicationBuilder() - .withAuthorization({ - agentic: { } // We have the type and scopes set in the .env file - }) - .withStorage(storage) - .build(); - -// Create Claude Agent -// Pass the authorization from the agent application -const claudeAgent = new ClaudeAgent(simpleClaudeAgent.authorization); - -// Register notification handler -// simpleClaudeAgent.onAgentNotification("*", claudeAgent.handleAgentNotificationActivity.bind(claudeAgent)); -simpleClaudeAgent.onAgenticEmailNotification(claudeAgent.emailNotificationHandler.bind(claudeAgent)); - -simpleClaudeAgent.onAgenticWordNotification(claudeAgent.wordNotificationHandler.bind(claudeAgent)); - -// Welcome message when user joins -simpleClaudeAgent.onConversationUpdate('membersAdded', async (context, state) => { - const welcomeMessage = ` -🤖 **Simple Claude Agent** is ready! - -This agent demonstrates MCP tooling integration and notification routing. - -**Features:** -- Handles email notifications and @-mentions from Word and Excel using notification routing -- Integrates with Microsoft 365 via MCP Tooling - -**Try these commands:** - - Ask the agent to use MCP tools from tool servers - - Send mock custom activities (email, mentions) - ` - await context.sendActivity(welcomeMessage) -}) - -// Handle user messages -simpleClaudeAgent.onActivity(ActivityTypes.Message, claudeAgent.handleAgentMessageActivity.bind(claudeAgent)); - -// Handle installation updates -simpleClaudeAgent.onActivity(ActivityTypes.InstallationUpdate, claudeAgent.handleInstallationUpdateActivity.bind(claudeAgent)) \ No newline at end of file diff --git a/nodejs/claude/sample-agent/src/agent.ts b/nodejs/claude/sample-agent/src/agent.ts new file mode 100644 index 00000000..3908f9a1 --- /dev/null +++ b/nodejs/claude/sample-agent/src/agent.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TurnState, AgentApplication, TurnContext, MemoryStorage } from '@microsoft/agents-hosting'; +import { ActivityTypes } from '@microsoft/agents-activity'; + +// Notification Imports +import '@microsoft/agents-a365-notifications'; +import { AgentNotificationActivity } from '@microsoft/agents-a365-notifications'; + +import { Client, getClient } from './client'; + +export class MyAgent extends AgentApplication { + + constructor() { + super({ + startTypingTimer: true, + storage: new MemoryStorage(), + authorization: { + agentic: { + type: 'agentic', + } // scopes set in the .env file... + } + }); + + // Route agent notifications + this.onAgentNotification("agents:*", async (context: TurnContext, state: TurnState, agentNotificationActivity: AgentNotificationActivity) => { + await this.handleAgentNotificationActivity(context, state, agentNotificationActivity); + }); + + this.onActivity(ActivityTypes.Message, async (context: TurnContext, state: TurnState) => { + await this.handleAgentMessageActivity(context, state); + }); + } + + /** + * Handles incoming user messages and sends responses. + */ + async handleAgentMessageActivity(turnContext: TurnContext, state: TurnState): Promise { + const userMessage = turnContext.activity.text?.trim() || ''; + + if (!userMessage) { + await turnContext.sendActivity('Please send me a message and I\'ll help you!'); + return; + } + + try { + const client: Client = await getClient(this.authorization, turnContext); + const response = await client.invokeAgentWithScope(userMessage); + await turnContext.sendActivity(response); + } catch (error) { + console.error('LLM query error:', error); + const err = error as any; + await turnContext.sendActivity(`Error: ${err.message || err}`); + } + } + + async handleAgentNotificationActivity(context: TurnContext, state: TurnState, agentNotificationActivity: AgentNotificationActivity) { + context.sendActivity("Received an AgentNotification!"); + /* your logic here... */ + } +} + +export const agentApplication = new MyAgent(); diff --git a/nodejs/claude/sample-agent/src/claudeAgent.js b/nodejs/claude/sample-agent/src/claudeAgent.js deleted file mode 100644 index 368ec019..00000000 --- a/nodejs/claude/sample-agent/src/claudeAgent.js +++ /dev/null @@ -1,284 +0,0 @@ - -import { createClaudeResponseCard, createErrorCard } from './adaptiveCards.js' -import { MessageFactory } from "@microsoft/agents-hosting" -import { ClaudeClient } from './claudeClient.js'; -import { McpToolRegistrationService } from '@microsoft/agents-a365-tooling-extensions-claude' -import { NotificationType } from '@microsoft/agents-a365-notifications'; - -// When running in debug mode, these variables can interfere with Claude's child -// processes -const cleanEnv = { ...process.env }; -delete cleanEnv.NODE_OPTIONS; -delete cleanEnv.VSCODE_INSPECTOR_OPTIONS; - -/** - * ClaudeClient provides an interface to interact with the Claude Code SDK. - * It maintains agentOptions as an instance field and exposes an invokeAgent method. - */ -export class ClaudeAgent { - /** - * Indicates if the application is installed (installation update state). - */ - isApplicationInstalled = false; - - /** - * Indicates if the user has accepted terms and conditions. - */ - termsAndConditionsAccepted = false; - - toolServerService = new McpToolRegistrationService() - - /** - * @param {object} agentOptions - Configuration for the Claude agent (tooling, system prompt, etc). - */ - constructor(authorization) { - this.authorization = authorization - } - - /** - * Handles incoming user messages, streams progress, and sends adaptive card responses using Claude. - * Manages feedback, sensitivity labels, and error handling for conversational activities. - */ - async handleAgentMessageActivity(turnContext, state) { - // Set up streaming response - turnContext.streamingResponse.setFeedbackLoop(true) - turnContext.streamingResponse.setSensitivityLabel({ - type: 'https://schema.org/Message', - '@type': 'CreativeWork', - name: 'Internal' - }) - turnContext.streamingResponse.setGeneratedByAILabel(true) - - if (!this.isApplicationInstalled) { - await turnContext.sendActivity(MessageFactory.Text("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() || '' - - if (!userMessage) { - await turnContext.streamingResponse.queueTextChunk('Please send me a message and I\'ll help you!') - await turnContext.streamingResponse.endStream() - return - } - - try { - // Show processing indicator - await turnContext.streamingResponse.queueInformativeUpdate('🤔 Thinking with Claude...') - - const claudeClient = await this.getClaudeClient(turnContext) - - // Use Claude Code SDK to process the user's request - const claudeResponse = await claudeClient.invokeAgentWithScope(userMessage) - - // End streaming and send adaptive card response - await turnContext.streamingResponse.endStream() - - // Create and send adaptive card with Claude's response - const responseCard = createClaudeResponseCard(claudeResponse, userMessage) - - const cardAttachment = MessageFactory.attachment({ - contentType: 'application/vnd.microsoft.card.adaptive', - content: responseCard - }) - - await turnContext.sendActivity(cardAttachment) - } catch (error) { - console.error('Claude query error:', error) - - // End streaming first - await turnContext.streamingResponse.endStream() - - // Send error as adaptive card - const errorCard = createErrorCard(error, userMessage) - const errorAttachment = MessageFactory.attachment({ - contentType: 'application/vnd.microsoft.card.adaptive', - content: errorCard - }) - - await turnContext.sendActivity(errorAttachment) - } - } - - /** - * Handles agent notification activities by parsing the activity type. - * Supports: - * - Email notifications - * - @-mentions from Word and Excel - * - Agent on-boarding and off-boarding activities - * - * @param {object} turnContext - The context object for the current turn. - * @param {object} state - The state object for the current turn. - * @param {object} agentNotificationActivity - The incoming activity to handle. - */ - async handleAgentNotificationActivity(turnContext, state, agentNotificationActivity) { - try { - if (!this.isApplicationInstalled) { - await turnContext.sendActivity(MessageFactory.Text("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 - - 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); - await turnContext.sendActivity(`Error handling notification: ${error.message || error}`); - } - } - - /** - * Handles agent installation and removal events, updating internal state and prompting for terms acceptance. - * Sends a welcome or farewell message based on the activity action. - */ - async handleInstallationUpdateActivity(turnContext, state) { - 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. - * @param {object} turnContext - The context object for the current turn. - * @param {object} mentionNotificationEntity - The mention notification entity. - */ - async wordNotificationHandler(turnContext, state, wordActivity) { - await turnContext.sendActivity('Thanks for the @-mention notification! Working on a response...'); - const mentionNotificationEntity = wordActivity.wpxCommentNotification; - - if (!mentionNotificationEntity) { - await turnContext.sendActivity('I could not find the mention notification details.'); - return; - } - - // Use correct fields from mentionActivity.json - 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 claudeClient = await this.getClaudeClient(turnContext); - const commentContent = await claudeClient.invokeAgentWithScope(mentionPrompt); - - const response = await claudeClient.invokeAgentWithScope( - `You have received the following comment. Please follow any instructions in it. ${commentContent.content}` - ); - - await turnContext.sendActivity(response); - return; - } - - /** - * Handles email notification activities. - * @param {object} turnContext - The context object for the current turn. - * @param {object} emailNotificationEntity - The email notification entity. - */ - async emailNotificationHandler(turnContext, state, emailActivity) { - 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 claudeClient = await this.getClaudeClient(turnContext); - const emailContent = await claudeClient.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 claudeClient.invokeAgentWithScope( - `You have received the following email. Please follow any instructions in it. ${emailContent.content}` - ); - - await turnContext.sendActivity(response); - return; - } - - async getClaudeClient(turnContext) { - const agentOptions = { - appendSystemPrompt: `You are a helpful AI assistant integrated with Microsoft 365.`, - maxTurns: 3, - allowedTools: ['Read', 'Write', 'WebSearch', 'Bash', 'Grep'], - env: { - ...cleanEnv - }, - } - - const mcpEnvironmentId = process.env.MCP_ENVIRONMENT_ID || ''; - const agenticUserId = process.env.AGENTIC_USER_ID || ''; - const mcpAuthToken = process.env.MCP_AUTH_TOKEN || ''; - - if (mcpEnvironmentId && agenticUserId) { - try { - - await this.toolServerService.addToolServersToAgent( - agentOptions, - agenticUserId, - mcpEnvironmentId, - this.authorization, - turnContext, - mcpAuthToken - ) - } catch (error) { - console.warn('Failed to register MCP tool servers:', error.message); - } - } else { - console.log('MCP configuration not provided, using basic Claude agent functionality'); - } - - return new ClaudeClient(agentOptions) - } -} \ No newline at end of file diff --git a/nodejs/claude/sample-agent/src/claudeClient.js b/nodejs/claude/sample-agent/src/claudeClient.js deleted file mode 100644 index 16f16f2e..00000000 --- a/nodejs/claude/sample-agent/src/claudeClient.js +++ /dev/null @@ -1,116 +0,0 @@ -import { InferenceScope, InvokeAgentScope, InferenceOperationType } from '@microsoft/agents-a365-observability'; -import { query } from '@anthropic-ai/claude-agent-sdk'; - -export class ClaudeClient { - - constructor(agentOptions = {}) { - this.agentOptions = agentOptions; - this.configureAuthentication(); - } - - /** - * Configures authentication for Claude API. - * Requires API key authentication. - */ - configureAuthentication() { - // Check if API key is provided in environment - if (!process.env.ANTHROPIC_API_KEY) { - throw new Warning('ANTHROPIC_API_KEY environment variable is required. Get your API key from https://console.anthropic.com/'); - } - - // Ensure the API key is available in the environment for the Claude SDK - this.agentOptions.env = { - ...this.agentOptions.env, - ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY - }; - } - - /** - * Sends a user message to the Claude Code SDK and returns the AI's response. - * Handles streaming results and error reporting. - * - * @param {string} userMessage - The message or prompt to send to Claude. - * @returns {Promise} The response from Claude, or an error message if the query fails. - */ - async invokeAgent(userMessage) { - let claudeResponse = ""; - try { - for await (const message of query({ - prompt: userMessage, - options: this.agentOptions - })) { - if (message.type === 'result' && message.result) { - claudeResponse = message.result; - break; - } - } - if (!claudeResponse) { - return "Sorry, I couldn't get a response from Claude :("; - } - return claudeResponse; - } catch (error) { - console.error('Claude query error:', error); - return `Error: ${error.message || error}`; - } - } - - /** - * Wrapper for invokeAgent that adds tracing and span management using Agent365 SDK. - * @param prompt - The prompt to send to Claude. - */ - async invokeAgentWithScope(prompt) { - const invokeAgentDetails = { agentId: process.env.AGENT_ID || 'sample-agent' }; - const invokeAgentScope = InvokeAgentScope.start(invokeAgentDetails); - - if (!invokeAgentScope) { - // fallback: do the work without active parent span - await new Promise((resolve) => setTimeout(resolve, 200)); - return await this.invokeAgent(prompt); - } - - try { - const inferenceDetails = { - operationName: InferenceOperationType.CHAT, - model: 'gpt-4', - providerName: 'openai', - inputTokens: 45, - outputTokens: 78, - responseId: `resp-${Date.now()}`, - finishReasons: ['stop'] - }; - return await invokeAgentScope.withActiveSpanAsync(async () => { - // Create the inference (child) scope while the invoke span is active - const scope = InferenceScope.start(inferenceDetails); - - if (!scope) { - await new Promise((resolve) => setTimeout(resolve, 200)); - return await this.invokeAgent(prompt); - } - - try { - // Activate the inference span for the inference work - const result = await scope.withActiveSpanAsync(async () => { - const response = await this.invokeAgent(prompt); - scope.recordOutputMessages([{ - content: response, - responseId: `resp-${Date.now()}`, - finishReason: 'stop', - inputTokens: 45, - outputTokens: 78, - totalTokens: 123, - }]); - return response; - }); - return result; - } catch (error) { - scope.recordError(error); - throw error; - } finally { - scope.dispose(); - } - }); - } finally { - invokeAgentScope.dispose(); - } - } -} \ No newline at end of file diff --git a/nodejs/claude/sample-agent/src/client.ts b/nodejs/claude/sample-agent/src/client.ts new file mode 100644 index 00000000..d2dbdeb6 --- /dev/null +++ b/nodejs/claude/sample-agent/src/client.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { query } from '@anthropic-ai/claude-agent-sdk'; +import { TurnContext, Authorization } from '@microsoft/agents-hosting'; + +import { McpToolRegistrationService } from '@microsoft/agents-a365-tooling-extensions-claude'; + +// Observability Imports +import { + ObservabilityManager, + InferenceScope, + Builder, + InferenceOperationType, + AgentDetails, + TenantDetails, + InferenceDetails +} from '@microsoft/agents-a365-observability'; + +export interface Client { + invokeAgentWithScope(prompt: string): Promise; +} + +const sdk = ObservabilityManager.configure( + (builder: Builder) => + builder + .withService('TypeScript Claude Sample Agent', '1.0.0') +); + +sdk.start(); + +const toolService = new McpToolRegistrationService(); + +// Claude agent configuration +const agentConfig = { + maxTurns: 10, + mcpServers: {} as Record +}; + + +export async function getClient(authorization: Authorization, turnContext: TurnContext): Promise { + try { + await toolService.addToolServersToAgent( + agentConfig, + process.env.AGENTIC_USER_ID || '', + process.env.MCP_ENVIRONMENT_ID || "", + authorization, + turnContext, + process.env.MCP_AUTH_TOKEN || "", + ); + } catch (error) { + console.warn('Failed to register MCP tool servers:', error); + } + + return new ClaudeClient(agentConfig); +} + +/** + * ClaudeClient provides an interface to interact with the Claude Agent SDK. + * It maintains agentConfig as an instance field and exposes an invokeAgent method. + */ +class ClaudeClient implements Client { + config: typeof agentConfig; + + constructor(config: typeof agentConfig) { + this.config = config; + } + + /** + * Sends a user message to the Claude Agent SDK and returns the AI's response. + * Handles streaming results and error reporting. + * + * @param {string} userMessage - The message or prompt to send to Claude. + * @returns {Promise} The response from Claude, or an error message if the query fails. + */ + async invokeAgent(prompt: string): Promise { + try { + const result = query({ + prompt, + options: { + maxTurns: this.config.maxTurns, + mcpServers: this.config.mcpServers + } + }); + + let finalResponse = ''; + + // Process streaming messages + for await (const message of result) { + if (message.type === 'result') { + // Get the final output from the result message + const resultContent = message.content; + if (resultContent && resultContent.length > 0) { + for (const content of resultContent) { + if (content.type === 'text') { + finalResponse += content.text; + } + } + } + } else if (message.type === 'assistant') { + // Get assistant message content + const assistantContent = message.content; + if (assistantContent && assistantContent.length > 0) { + for (const content of assistantContent) { + if (content.type === 'text') { + finalResponse += content.text; + } + } + } + } + } + + return finalResponse || "Sorry, I couldn't get a response from Claude :("; + } catch (error) { + console.error('Claude agent error:', error); + const err = error as any; + return `Error: ${err.message || err}`; + } + } + + async invokeAgentWithScope(prompt: string) { + const inferenceDetails: InferenceDetails = { + operationName: InferenceOperationType.CHAT, + model: this.config.model, + }; + + const agentDetails: AgentDetails = { + agentId: 'claude-travel-agent', + agentName: 'Claude Travel Agent', + conversationId: 'conv-12345', + }; + + const tenantDetails: TenantDetails = { + tenantId: 'claude-sample-tenant', + }; + + const scope = InferenceScope.start(inferenceDetails, agentDetails, tenantDetails); + + const response = await this.invokeAgent(prompt); + + // Record the inference response with token usage + scope?.recordOutputMessages([response]); + scope?.recordInputMessages([prompt]); + scope?.recordResponseId(`resp-${Date.now()}`); + scope?.recordInputTokens(45); + scope?.recordOutputTokens(78); + scope?.recordFinishReasons(['stop']); + + return response; + } +} diff --git a/nodejs/claude/sample-agent/src/index.js b/nodejs/claude/sample-agent/src/index.js deleted file mode 100644 index 7ad1f7f5..00000000 --- a/nodejs/claude/sample-agent/src/index.js +++ /dev/null @@ -1,46 +0,0 @@ -import express from 'express'; -import { CloudAdapter, authorizeJWT, loadAuthConfigFromEnv } from '@microsoft/agents-hosting'; -import { simpleClaudeAgent } from './agent.js'; -import { observabilityManager } from './telemetry.js'; - -console.log('🚀 Starting Simple Claude Agent...'); -console.log(' Claude Code SDK + Microsoft 365 Agents SDK'); -console.log(' Access at: http://localhost:3978'); -console.log(''); - -const authConfig = {}; -const adapter = new CloudAdapter(); - -const app = express(); -app.use(express.json()); -app.use(authorizeJWT(authConfig)); - -observabilityManager.start(); - -app.post('/api/messages', async (req, res) => { - await adapter.process(req, res, async (context) => { - await simpleClaudeAgent.run(context); - }); -}); - -const port = process.env.PORT || 3978; -const server = app.listen(port, () => { - console.log(`Server listening to port ${port} on sdk 1.0.15 for debug ${process.env.DEBUG}`); -}); - -server.on('error', async (err) => { - console.error(err); - await observabilityManager.shutdown(); - process.exit(1); -}).on('close', async () => { - console.log('Observability Manager is shutting down...'); - await observabilityManager.shutdown(); -}); - -process.on('SIGINT', () => { - console.log('Received SIGINT. Shutting down gracefully...'); - server.close(() => { - console.log('Server closed.'); - process.exit(0); - }); -}); diff --git a/nodejs/claude/sample-agent/src/index.ts b/nodejs/claude/sample-agent/src/index.ts new file mode 100644 index 00000000..de76eed5 --- /dev/null +++ b/nodejs/claude/sample-agent/src/index.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// It is important to load environment variables before importing other modules +import { configDotenv } from 'dotenv'; + +configDotenv(); + +import { AuthConfiguration, authorizeJWT, CloudAdapter, Request } from '@microsoft/agents-hosting'; +import express, { Response } from 'express' +import { agentApplication } from './agent'; + +const authConfig: AuthConfiguration = {}; + +const server = express() +server.use(express.json()) +server.use(authorizeJWT(authConfig)) + +server.post('/api/messages', (req: Request, res: Response) => { + const adapter = agentApplication.adapter as CloudAdapter; + adapter.process(req, res, async (context) => { + await agentApplication.run(context) + }) +}) + +const port = process.env.PORT || 3978 +server.listen(port, async () => { + console.log(`\nServer listening to port ${port} for appId ${authConfig.clientId} debug ${process.env.DEBUG}`) +}).on('error', async (err) => { + console.error(err); + process.exit(1); +}).on('close', async () => { + console.log('Server closed'); + process.exit(0); +}); diff --git a/nodejs/claude/sample-agent/src/telemetry.js b/nodejs/claude/sample-agent/src/telemetry.js deleted file mode 100644 index 8872928f..00000000 --- a/nodejs/claude/sample-agent/src/telemetry.js +++ /dev/null @@ -1,11 +0,0 @@ -import { - ObservabilityManager, - Builder, -} from '@microsoft/agents-a365-observability'; - -export const observabilityManager = ObservabilityManager.configure( - (builder) => - builder - .withService('TypeScript Sample Agent', '1.0.0') -); - diff --git a/nodejs/claude/sample-agent/tsconfig.json b/nodejs/claude/sample-agent/tsconfig.json new file mode 100644 index 00000000..5fb9619c --- /dev/null +++ b/nodejs/claude/sample-agent/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "incremental": true, + "lib": ["ES2021"], + "target": "es2019", + "module": "commonjs", + "declaration": true, + "sourceMap": true, + "composite": true, + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo" + } +} diff --git a/nodejs/openai/sample-agent/package.json b/nodejs/openai/sample-agent/package.json index 3ec74516..60d6afab 100644 --- a/nodejs/openai/sample-agent/package.json +++ b/nodejs/openai/sample-agent/package.json @@ -28,4 +28,4 @@ "ts-node": "^10.9.2", "typescript": "^5.9.2" } -} +} \ No newline at end of file