-
Notifications
You must be signed in to change notification settings - Fork 43
Add Devin Agent Sample #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
075b216
Add devin agent sample
walterluna 6c8a2b6
remove terms & conditions check
walterluna 1985920
remove old comments
walterluna 0e72ee5
update directory structure
walterluna 507403c
remove old references
walterluna 552a317
Improve error handling on devin client
walterluna ddb7a30
changes from code review
walterluna e6e754d
Update nodejs/devin/sample-agent/src/agent.ts
walterluna 15a0862
Update nodejs/devin/sample-agent/src/devin-client.ts
walterluna 57506ee
Update nodejs/devin/sample-agent/src/devin-client.ts
walterluna a669eec
Update nodejs/devin/sample-agent/src/agent.ts
walterluna e694d3f
Initial plan
Copilot b069d33
Add Microsoft copyright headers to all TypeScript files
Copilot 4cea614
Merge pull request #27 from microsoft/copilot/sub-pr-22
walterluna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Devin API Configuration | ||
| DEVIN_BASE_URL=https://api.devin.ai/v1 | ||
| DEVIN_API_KEY=your_devin_api_key_here | ||
|
|
||
| # Polling interval in seconds (how often to check for Devin responses) | ||
| POLLING_INTERVAL_SECONDS=10 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| TODO | ||
|
walterluna marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| { | ||
| "name": "devin-agent-sample", | ||
| "version": "1.0.0", | ||
| "main": "index.js", | ||
| "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" | ||
| }, | ||
| "keywords": [], | ||
| "license": "ISC", | ||
| "description": "", | ||
| "dependencies": { | ||
| "@microsoft/agents-a365-notifications": "*", | ||
| "@microsoft/agents-a365-observability": "*", | ||
| "@microsoft/agents-a365-runtime": "*", | ||
| "@microsoft/agents-a365-tooling": "*", | ||
| "@microsoft/agents-hosting": "^1.0.15", | ||
| "uuid": "^13.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@microsoft/m365agentsplayground": "^0.2.20", | ||
| "nodemon": "^3.1.10", | ||
| "rimraf": "^5.0.0", | ||
| "ts-node": "^10.9.2", | ||
| "typescript": "^5.9.2" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { | ||
| AgentDetails, | ||
| BaggageBuilder, | ||
| InferenceDetails, | ||
| InferenceOperationType, | ||
| InferenceScope, | ||
| InvokeAgentScope, | ||
| TenantDetails, | ||
| } from "@microsoft/agents-a365-observability"; | ||
|
walterluna marked this conversation as resolved.
|
||
| import { Activity, ActivityTypes } from "@microsoft/agents-activity"; | ||
| import { | ||
| AgentApplication, | ||
| DefaultConversationState, | ||
| TurnContext, | ||
| TurnState, | ||
| } from "@microsoft/agents-hosting"; | ||
| import { Stream } from "stream"; | ||
| import { v4 as uuidv4 } from "uuid"; | ||
| import { devinClient } from "./devin-client"; | ||
| import { getAgentDetails, getTenantDetails } from "./utils"; | ||
|
|
||
| interface ConversationState extends DefaultConversationState { | ||
| count: number; | ||
| } | ||
| type ApplicationTurnState = TurnState<ConversationState>; | ||
|
|
||
| export class A365Agent extends AgentApplication<ApplicationTurnState> { | ||
| isApplicationInstalled: boolean = false; | ||
| agentName = "Devin Agent"; | ||
|
|
||
| constructor() { | ||
| super(); | ||
|
|
||
| this.onActivity( | ||
| ActivityTypes.Message, | ||
| async (context: TurnContext, state: ApplicationTurnState) => { | ||
| // Increment count state | ||
| let count = state.conversation.count ?? 0; | ||
| state.conversation.count = ++count; | ||
|
|
||
| // Extract agent and tenant details from context | ||
| const invokeAgentDetails = getAgentDetails(context); | ||
| const tenantDetails = getTenantDetails(context); | ||
|
|
||
| // Create BaggageBuilder scope | ||
| const baggageScope = new BaggageBuilder() | ||
| .tenantId(tenantDetails.tenantId) | ||
| .agentId(invokeAgentDetails.agentId) | ||
| .correlationId(uuidv4()) | ||
| .agentName(invokeAgentDetails.agentName) | ||
| .conversationId(context.activity.conversation?.id) | ||
| .build(); | ||
|
|
||
| await baggageScope.run(async () => { | ||
| const invokeAgentScope = InvokeAgentScope.start( | ||
| invokeAgentDetails, | ||
| tenantDetails | ||
| ); | ||
|
|
||
| await invokeAgentScope.withActiveSpanAsync(async () => { | ||
| invokeAgentScope.recordInputMessages([ | ||
| context.activity.text ?? "Unknown text", | ||
| ]); | ||
|
|
||
| await context.sendActivity(Activity.fromObject({ type: "typing" })); | ||
| await this.handleAgentMessageActivity( | ||
| context, | ||
| invokeAgentScope, | ||
| invokeAgentDetails, | ||
| tenantDetails | ||
| ); | ||
| }); | ||
|
|
||
| invokeAgentScope.dispose(); | ||
| }); | ||
|
|
||
| baggageScope.dispose(); | ||
| } | ||
| ); | ||
|
|
||
| this.onActivity( | ||
| ActivityTypes.InstallationUpdate, | ||
| async (context: TurnContext, state: TurnState) => { | ||
| await this.handleInstallationUpdateActivity(context, state); | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Handles incoming user messages and sends responses. | ||
| */ | ||
| async handleAgentMessageActivity( | ||
| turnContext: TurnContext, | ||
| invokeAgentScope: InvokeAgentScope, | ||
| agentDetails: AgentDetails, | ||
| tenantDetails: TenantDetails | ||
| ): Promise<void> { | ||
| if (!this.isApplicationInstalled) { | ||
| await turnContext.sendActivity( | ||
| "Please install the application before sending messages." | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const userMessage = turnContext.activity.text?.trim() || ""; | ||
|
|
||
| if (!userMessage) { | ||
| await turnContext.sendActivity( | ||
| "Please send me a message and I'll help you!" | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const inferenceDetails: InferenceDetails = { | ||
| operationName: InferenceOperationType.CHAT, | ||
| model: "claude-3-7-sonnet-20250219", | ||
| providerName: "cognition-ai", | ||
| inputTokens: Math.ceil(userMessage.length / 4), // Rough estimate | ||
| responseId: `resp-${Date.now()}`, | ||
| outputTokens: 0, // Will be updated after response | ||
| finishReasons: undefined, | ||
| }; | ||
|
|
||
| const inferenceScope = InferenceScope.start( | ||
| inferenceDetails, | ||
| agentDetails, | ||
| tenantDetails | ||
| ); | ||
|
|
||
| let totalResponseLength = 0; | ||
| const responseStream = new Stream() | ||
| .on("data", async (chunk) => { | ||
| totalResponseLength += (chunk as string).length; | ||
| invokeAgentScope.recordOutputMessages([`LLM Response: ${chunk}`]); | ||
| inferenceScope.recordOutputMessages([`LLM Response: ${chunk}`]); | ||
| await turnContext.sendActivity(chunk); | ||
| }) | ||
| .on("error", async (error) => { | ||
| invokeAgentScope.recordOutputMessages([`Streaming error: ${error}`]); | ||
| inferenceScope.recordOutputMessages([`Streaming error: ${error}`]); | ||
| await turnContext.sendActivity(error); | ||
| }) | ||
| .on("close", () => { | ||
| inferenceScope.recordOutputTokens(Math.ceil(totalResponseLength / 4)); // Rough estimate | ||
| inferenceScope.recordFinishReasons(["stop"]); | ||
| }); | ||
|
|
||
| inferenceScope.recordInputMessages([userMessage]); | ||
|
|
||
| await devinClient.invokeAgent(userMessage, responseStream); | ||
| } catch (error) { | ||
| invokeAgentScope.recordOutputMessages([`LLM error: ${error}`]); | ||
| await turnContext.sendActivity( | ||
| "There was an error processing your request" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handles agent installation and removal events. | ||
| */ | ||
| async handleInstallationUpdateActivity( | ||
| turnContext: TurnContext, | ||
| state: TurnState | ||
| ): Promise<void> { | ||
| if (turnContext.activity.action === "add") { | ||
| this.isApplicationInstalled = true; | ||
| await turnContext.sendActivity( | ||
| "Thank you for hiring me! Looking forward to assisting you in your professional journey!" | ||
| ); | ||
| } else if (turnContext.activity.action === "remove") { | ||
| this.isApplicationInstalled = false; | ||
| await turnContext.sendActivity( | ||
| "Thank you for your time, I enjoyed working with you." | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export const agentApplication = new A365Agent(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import Stream from "stream"; | ||
| import { | ||
| DevinCreateSessionResponse, | ||
| DevinSessionResponse, | ||
| DevinSessionStatus, | ||
| } from "./types/devin-client.types"; | ||
|
walterluna marked this conversation as resolved.
|
||
|
|
||
| export interface Client { | ||
| invokeAgent(prompt: string, responseStream: Stream): Promise<void>; | ||
| } | ||
|
|
||
| /** | ||
| * DevinClient provides an interface to interact with the Devin API | ||
| * It maintains agentOptions as an instance field and exposes an invokeAgent method. | ||
| */ | ||
| export class DevinClient implements Client { | ||
| private readonly devinMessageType = "devin_message"; | ||
| private readonly devinBaseUrl: string; | ||
| private readonly devinApiKey: string; | ||
| private readonly pollingIntervalSeconds: number; | ||
| private currentSession: string | undefined; | ||
|
|
||
| constructor() { | ||
| this.devinBaseUrl = process.env.DEVIN_BASE_URL || ""; | ||
| this.devinApiKey = process.env.DEVIN_API_KEY || ""; | ||
| this.pollingIntervalSeconds = parseInt( | ||
| process.env.POLLING_INTERVAL_SECONDS || "10" | ||
| ); | ||
|
|
||
| if (!this.devinBaseUrl) { | ||
| throw new Error("DEVIN_BASE_URL environment variable is required"); | ||
| } | ||
|
|
||
| if (!this.devinApiKey) { | ||
| throw new Error("DEVIN_API_KEY environment variable is required"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Sends a user message to Devin API and returns the AI's response in a stream. | ||
| * Handles streaming results and error reporting. | ||
| * | ||
| * @param {string} prompt - The message or prompt to send to Devin. | ||
| * @param {Stream} responseStream - A stream for the client to send Devin's replies to. | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async invokeAgent(prompt: string, responseStream: Stream): Promise<void> { | ||
| const pollMs = this.pollingIntervalSeconds * 1_000 || 10_000; | ||
|
|
||
| this.currentSession = await this.promptDevin(prompt, this.currentSession); | ||
| await this.getDevinResponse(this.currentSession, pollMs, responseStream); | ||
| } | ||
|
|
||
| private async promptDevin( | ||
| prompt: string, | ||
| sessionId?: string | ||
| ): Promise<string> { | ||
| const requestUrl = sessionId | ||
| ? `${this.devinBaseUrl}/sessions/${sessionId}/message` | ||
| : `${this.devinBaseUrl}/sessions`; | ||
|
|
||
| const requestBody = sessionId ? { message: prompt } : { prompt }; | ||
|
|
||
| const response = await fetch(requestUrl, { | ||
| method: "POST", | ||
| headers: this.getReqHeaders(), | ||
| body: JSON.stringify(requestBody), | ||
| }); | ||
|
|
||
| const data = (await response.json()) as DevinCreateSessionResponse; | ||
| const rawSessionId = String(data?.session_id ?? ""); | ||
| return sessionId || rawSessionId.replace("devin-", ""); | ||
| } | ||
|
|
||
| private async getDevinResponse( | ||
| sessionId: string, | ||
| pollMs: number, | ||
| responseStream: Stream, | ||
| timeoutMs: number = 300_000 | ||
| ): Promise<void> { | ||
| const deadline = Date.now() + timeoutMs; | ||
| const sentMessages = new Set<string>(); | ||
| let latestStatus = DevinSessionStatus.new; | ||
|
|
||
| console.debug("starting poll for Devin's reply"); | ||
|
|
||
| while (Object.values(DevinSessionStatus).includes(latestStatus)) { | ||
| console.debug("calling GET session/messages"); | ||
| if (Date.now() > deadline) { | ||
| console.info("Timed out, not polling for an answer anymore"); | ||
| break; | ||
| } | ||
|
|
||
| await this.delay(pollMs); | ||
| const requestUrl = `${this.devinBaseUrl}/sessions/${sessionId}`; | ||
|
|
||
| const response = await fetch(requestUrl, { | ||
| headers: this.getReqHeaders(), | ||
| }); | ||
|
|
||
| if (response.status !== 200) { | ||
| console.error(`API call failed with status ${response.status}}`); | ||
| console.error(`Error response: ${JSON.stringify(response)}`); | ||
| responseStream.emit( | ||
| "data", | ||
| "There was an error processing your request, please try again" | ||
| ); | ||
| break; | ||
| } | ||
|
|
||
| const data = (await response.json()) as DevinSessionResponse; | ||
| latestStatus = data.status; | ||
| console.debug(`Current Devin Session status is: ${latestStatus}`); | ||
| const latestMessage = data?.messages?.pop(); | ||
| console.debug(`latest message is ${JSON.stringify(latestMessage)}`); | ||
|
|
||
| if (latestMessage && latestMessage.type === this.devinMessageType) { | ||
| if (!sentMessages.has(latestMessage.event_id)) { | ||
| const messageContent = String(latestMessage?.message); | ||
| responseStream.emit("data", messageContent); | ||
| sentMessages.add(latestMessage.event_id); | ||
| console.debug(`emit data event with content: ${messageContent}}`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.debug("emitting close event"); | ||
| responseStream.emit("close"); | ||
| } | ||
|
|
||
| private delay(ms: number): Promise<void> { | ||
| return new Promise((r) => setTimeout(r, ms)); | ||
| } | ||
|
|
||
| private getReqHeaders(): Record<string, string> { | ||
| return { | ||
| Authorization: `Bearer ${this.devinApiKey}`, | ||
| "Content-Type": "application/json", | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| export const devinClient = new DevinClient(); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.