Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions nodejs/devin/sample-agent/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
PORT=3978
POLLING_INTERVAL_SECONDS=10 # Polling interval in seconds (how often to check for Devin responses)

# Observability
ENABLE_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY_EXPORTER=true
CLUSTER_CATEGORY=dev # Options: 'local', 'dev', 'test', 'preprod', 'firstrelease', 'prod', 'gov', 'high', 'dod', 'mooncake', 'ex', 'rx'
# Devin API Configuration
DEVIN_BASE_URL=https://api.devin.ai/v1
DEVIN_API_KEY=your_devin_api_key_here
DEVIN_API_KEY=<YOUR_DEVIN_API_KEY>

# Auth
connections__serviceConnection__settings__clientId=blueprint_id
connections__serviceConnection__settings__clientSecret=blueprint_secret
connections__serviceConnection__settings__tenantId=tenant_id

# Polling interval in seconds (how often to check for Devin responses)
POLLING_INTERVAL_SECONDS=10
connectionsMap__0__connection=serviceConnection
connectionsMap__0__serviceUrl=*

agentic_type=agentic
agentic_scopes=https://graph.microsoft.com/.default
agentic_connectionName=serviceConnection
65 changes: 64 additions & 1 deletion nodejs/devin/sample-agent/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
TODO
# Sample Agent - Node.js Devin

This directory contains a sample agent implementation using Node.js and Devin API.

## Demonstrates

This sample demonstrates how to build an agent using the Microsoft Agent 365 SDK with Node.js and Devin API.

## Prerequisites

- Node.js 24+
- Devin API access
- Agents SDK

## How to run this sample

1. **Setup environment variables**

```bash
# Copy the template environment file
cp .env.example .env
```

2. **Install dependencies**

```bash
npm install
```

3. **Build the project**

```bash
npm run build
```

4. **Start the agent**

```bash
npm run start
```

5. **Start AgentsPlayground to chat with your agent**
```bash
npm run test-tool
```

The agent will start and be ready to receive requests through the configured hosting mechanism.

## 📚 Related Documentation

- [Devin API Documentation](https://docs.devin.ai/api-reference/overview)
- [Microsoft Agent 365 SDK](https://github.com/microsoft/Agents-for-js)
- [AgentsPlayground](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/test-with-toolkit-project?tabs=windows)

## 🤝 Contributing

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

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](../../../LICENSE.md) file for details.
10 changes: 4 additions & 6 deletions nodejs/devin/sample-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
"scripts": {
"build": "tsc",
"start": "node --env-file=.env dist/index.js",
"test-tool": "agentsplayground",
"install:clean": "npm run clean && npm install",
"clean": "rimraf dist node_modules package-lock.json"
"test-tool": "agentsplayground"
},
"engines": {
"node": ">=24.0.0"
},
"keywords": [],
"license": "ISC",
Expand All @@ -22,9 +23,6 @@
},
"devDependencies": {
"@microsoft/m365agentsplayground": "^0.2.20",
"nodemon": "^3.1.10",
"rimraf": "^5.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
}
}
65 changes: 62 additions & 3 deletions nodejs/devin/sample-agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@ import {
InferenceOperationType,
InferenceScope,
InvokeAgentScope,
ObservabilityManager,
TenantDetails,
} from "@microsoft/agents-a365-observability";
import { ClusterCategory } from "@microsoft/agents-a365-runtime";
import { Activity, ActivityTypes } from "@microsoft/agents-activity";
import {
AgentApplication,
AgentApplicationOptions,
DefaultConversationState,
MemoryStorage,
TurnContext,
TurnState,
} from "@microsoft/agents-hosting";
import { Stream } from "stream";
import { v4 as uuidv4 } from "uuid";
import { devinClient } from "./devin-client";
import tokenCache from "./token-cache";
import { getAgentDetails, getTenantDetails } from "./utils";

interface ConversationState extends DefaultConversationState {
Expand All @@ -31,9 +36,47 @@ export class A365Agent extends AgentApplication<ApplicationTurnState> {
isApplicationInstalled: boolean = false;
agentName = "Devin Agent";

constructor() {
super();
constructor(
options?: Partial<AgentApplicationOptions<ApplicationTurnState>> | undefined
) {
super(options);
const clusterCategory: ClusterCategory =
(process.env.CLUSTER_CATEGORY as ClusterCategory) || "dev";

// Initialize Observability SDK
const observabilitySDK = ObservabilityManager.configure((builder) =>
builder
.withService("claude-travel-agent", "1.0.0")
.withTokenResolver(async (agentId, tenantId) => {
// Token resolver for authentication with Agent 365 observability
console.log(
"🔑 Token resolver called for agent:",
agentId,
"tenant:",
tenantId
);

// Retrieve the cached agentic token
const cacheKey = this.createAgenticTokenCacheKey(agentId, tenantId);
const cachedToken = tokenCache.get(cacheKey);

if (cachedToken) {
console.log("🔑 Token retrieved from cache successfully");
return cachedToken;
}

console.log(
"⚠️ No cached token found - token should be cached during agent invocation"
);
return null;
})
.withClusterCategory(clusterCategory)
);

// Start the observability SDK
observabilitySDK.start();

// Handle messages
this.onActivity(
ActivityTypes.Message,
async (context: TurnContext, state: ApplicationTurnState) => {
Expand Down Expand Up @@ -81,6 +124,7 @@ export class A365Agent extends AgentApplication<ApplicationTurnState> {
}
);

// Handle installation activities
this.onActivity(
ActivityTypes.InstallationUpdate,
async (context: TurnContext, state: TurnState) => {
Expand Down Expand Up @@ -179,6 +223,21 @@ export class A365Agent extends AgentApplication<ApplicationTurnState> {
);
}
}

/**
* Create a cache key for the agentic token
*/
private createAgenticTokenCacheKey(
agentId: string,
tenantId: string
): string {
return tenantId
? `agentic-token-${agentId}-${tenantId}`
: `agentic-token-${agentId}`;
}
}

export const agentApplication = new A365Agent();
export const agentApplication = new A365Agent({
storage: new MemoryStorage(),
authorization: { agentic: {} }, // Type and scopes set in .env
});
31 changes: 25 additions & 6 deletions nodejs/devin/sample-agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ObservabilityManager } from "@microsoft/agents-a365-observability";
import {
AuthConfiguration,
authorizeJWT,
Expand Down Expand Up @@ -40,10 +41,28 @@ const server = app
console.log("Server is shutting down...");
});

process.on("SIGINT", () => {
console.log("Received SIGINT. Shutting down gracefully...");
server.close(() => {
console.log("Server closed.");
process.exit(0);
process
.on("SIGINT", async () => {
console.log("\n🛑 Shutting down agent...");
try {
server.close();
await ObservabilityManager.shutdown();
console.log("🔭 Observability SDK shut down gracefully");
process.exit(0);
} catch (err) {
console.error("Error during shutdown:", err);
process.exit(1);
}
})
.on("SIGTERM", async () => {
console.log("\n🛑 Shutting down agent...");
try {
server.close();
await ObservabilityManager.shutdown();
console.log("🔭 Observability SDK shut down gracefully");
process.exit(0);
} catch (err) {
console.error("Error during shutdown:", err);
process.exit(1);
}
});
});
55 changes: 55 additions & 0 deletions nodejs/devin/sample-agent/src/token-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Simple in-memory token cache
* In production, use a more robust caching solution like Redis
*/
class TokenCache {
private readonly cache: Map<string, string>;

constructor() {
this.cache = new Map<string, string>();
}

/**
* Store a token with key
*/
set(key: string, token: string): void {
this.cache.set(key, token);
console.log("🔐 Token cached succesfully");
}

/**
* Retrieve a token
*/
get(key: string): string | null {
const entry = this.cache.get(key);

if (!entry) {
console.log("🔍 Token cache miss");
return null;
}

return entry;
}

/**
* Check if a token exists
*/
has(key: string): boolean {
return this.cache.has(key);
}

/**
* Clear a token from cache
*/
delete(key: string): boolean {
return this.cache.delete(key);
}
}

// Create a singleton instance for the application
const tokenCache = new TokenCache();

export default tokenCache;
Loading