From 66e9ff0e0f19470a59937f083e1a53b9a4ac46bb Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Thu, 13 Nov 2025 10:25:52 +0000 Subject: [PATCH 01/10] Introducing playground notification handling in Perplexity agent --- nodejs/perplexity/sample-agent/README.md | 1 + nodejs/perplexity/sample-agent/src/agent.ts | 219 ++++++++++++++++-- .../src/playgroundActivityTypes.ts | 66 ++++++ 3 files changed, 261 insertions(+), 25 deletions(-) create mode 100644 nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts diff --git a/nodejs/perplexity/sample-agent/README.md b/nodejs/perplexity/sample-agent/README.md index 860d0ac7..f5418185 100644 --- a/nodejs/perplexity/sample-agent/README.md +++ b/nodejs/perplexity/sample-agent/README.md @@ -9,6 +9,7 @@ This sample demonstrates how to build an agent using the Agent 365 framework wit ## Features - ✅ **Chat with Perplexity** - Natural language conversations using Perplexity's Sonar models. +- ✅ **Playground notification handling** - Responds to notifications triggered in the playground UI (@mention in word documents, emails, custom, etc.) ## Prerequisites diff --git a/nodejs/perplexity/sample-agent/src/agent.ts b/nodejs/perplexity/sample-agent/src/agent.ts index ad149a36..2a649b17 100644 --- a/nodejs/perplexity/sample-agent/src/agent.ts +++ b/nodejs/perplexity/sample-agent/src/agent.ts @@ -1,36 +1,205 @@ -import { TurnState, AgentApplication, AttachmentDownloader, MemoryStorage, TurnContext } from '@microsoft/agents-hosting'; -import { ActivityTypes } from '@microsoft/agents-activity'; -import { AgentNotificationActivity } from '@microsoft/agents-a365-notifications'; -import { PerplexityAgent } from './perplexityAgent.js'; +import { + TurnState, + AgentApplication, + AttachmentDownloader, + MemoryStorage, + TurnContext, +} from "@microsoft/agents-hosting"; +import { ActivityTypes } from "@microsoft/agents-activity"; +import { AgentNotificationActivity } from "@microsoft/agents-a365-notifications"; +import { PerplexityAgent } from "./perplexityAgent.js"; +import { + MentionInWordValue, + PlaygroundActivityTypes, + SendEmailActivity, +} from "./playgroundActivityTypes.js"; +/** + * Conversation state interface for tracking message count. + */ interface ConversationState { count: number; } -type ApplicationTurnState = TurnState -const downloader = new AttachmentDownloader(); -const storage = new MemoryStorage(); +/** + * ApplicationTurnState combines TurnState with our ConversationState. + */ +type ApplicationTurnState = TurnState; -export const agentApplication = new AgentApplication({ - storage, - fileDownloaders: [downloader] -}); +/** + * Instantiate the AttachmentDownloader. + */ +const downloader: AttachmentDownloader = new AttachmentDownloader(); -const perplexityAgent = new PerplexityAgent(undefined); +/** + * Instantiate the MemoryStorage. + */ +const storage: MemoryStorage = new MemoryStorage(); -// Route agent notifications -agentApplication.onAgentNotification("*", async (context: TurnContext, state: ApplicationTurnState, activity: AgentNotificationActivity) => { - await perplexityAgent.handleAgentNotificationActivity(context, state, activity); -}); +/** + * Create the Agent Application instance with typed state. + */ +export const agentApplication: AgentApplication = + new AgentApplication({ + storage, + fileDownloaders: [downloader], + }); -agentApplication.onActivity(ActivityTypes.Message, async (context: TurnContext, state: ApplicationTurnState) => { - // Increment count state - let count = state.conversation.count ?? 0; - state.conversation.count = ++count; +/** + * Instantiate the PerplexityAgent. + */ +const perplexityAgent: PerplexityAgent = new PerplexityAgent(undefined); - await perplexityAgent.handleAgentMessageActivity(context, state); -}); +/* -------------------------------------------------------------------- + * ✅ Real Notification Events (Production) + * These handlers process structured AgentNotificationActivity objects + * sent by Microsoft 365 workloads (Word, Outlook, etc.) in production. + * -------------------------------------------------------------------- */ -agentApplication.onActivity(ActivityTypes.InstallationUpdate, async (context: TurnContext, state: ApplicationTurnState) => { - await perplexityAgent.handleInstallationUpdateActivity(context, state); -}); +/** + * Handles ALL real notification events from any workload. + * Fires when an AgentNotificationActivity is received. + * Use this for generic notification handling logic. + */ +agentApplication.onAgentNotification( + "*", + async ( + context: TurnContext, + state: ApplicationTurnState, + activity: AgentNotificationActivity + ): Promise => { + await perplexityAgent.handleAgentNotificationActivity( + context, + state, + activity + ); + } +); + +/** + * Handles Word-specific notifications (e.g., comments, mentions in Word). + * Fires only for AgentNotificationActivity originating from Word. + */ +agentApplication.onAgenticWordNotification( + async ( + context: TurnContext, + state: ApplicationTurnState, + activity: AgentNotificationActivity + ): Promise => { + await perplexityAgent.handleAgentNotificationActivity( + context, + state, + activity + ); + } +); + +/** + * Handles Email-specific notifications (e.g., new mail, flagged items). + * Fires only for AgentNotificationActivity originating from Outlook/Email. + */ +agentApplication.onAgenticEmailNotification( + async ( + context: TurnContext, + state: ApplicationTurnState, + activity: AgentNotificationActivity + ): Promise => { + await perplexityAgent.handleAgentNotificationActivity( + context, + state, + activity + ); + } +); + +/* -------------------------------------------------------------------- + * ✅ Playground Events (Simulated for Testing) + * These handlers process custom activityType strings sent via sendActivity() + * from the Playground UI. They DO NOT trigger real notification handlers. + * -------------------------------------------------------------------- */ + +/** + * Handles simulated Word mention notifications. + * activityType: "mentionInWord" + * Useful for testing Word-related scenarios without real notifications. + */ +agentApplication.onActivity( + PlaygroundActivityTypes.MentionInWord, + async (context: TurnContext, _state: ApplicationTurnState): Promise => { + const value: MentionInWordValue = context.activity + .value as MentionInWordValue; + const docName: string = value.mention.displayName; + const docUrl: string = value.docUrl; + const userName: string = value.mention.userPrincipalName; + const contextSnippet: string = value.context + ? `Context: ${value.context}` + : ""; + const message: string = `✅ You were mentioned in **${docName}** by ${userName}\n📄 ${docUrl}\n${contextSnippet}`; + await context.sendActivity(message); + } +); + +/** + * Handles simulated Email notifications. + * activityType: "sendEmail" + * Useful for testing email scenarios without real notifications. + */ +agentApplication.onActivity( + PlaygroundActivityTypes.SendEmail, + async (context: TurnContext, _state: ApplicationTurnState): Promise => { + const activity: SendEmailActivity = context.activity as SendEmailActivity; + const email = activity.value; + + const message: string = `📧 Email Notification: + From: ${email.from} + To: ${email.to.join(", ")} + Subject: ${email.subject} + Body: ${email.body}`; + + await context.sendActivity(message); + } +); + +/** + * Handles a generic custom notification. + * Custom activityType: "custom" + * ✅ To add more custom activities: + * - Define a new handler using agentApplication.onActivity("", ...) + * - Implement logic similar to this block. + */ +agentApplication.onActivity( + PlaygroundActivityTypes.Custom, + async (context: TurnContext, _state: ApplicationTurnState): Promise => { + await context.sendActivity("this is a custom activity handler"); + } +); + +/* -------------------------------------------------------------------- + * ✅ Generic Activity Handlers + * These handle standard activity types like messages or installation updates. + * -------------------------------------------------------------------- */ + +/** + * Handles standard message activities (ActivityTypes.Message). + * Increments conversation count and delegates to PerplexityAgent. + */ +agentApplication.onActivity( + ActivityTypes.Message, + async (context: TurnContext, state: ApplicationTurnState): Promise => { + let count: number = state.conversation.count ?? 0; + state.conversation.count = ++count; + + await perplexityAgent.handleAgentMessageActivity(context, state); + } +); + +/** + * Handles installation update activities (ActivityTypes.InstallationUpdate). + * Useful for responding to app installation or update events. + */ +agentApplication.onActivity( + ActivityTypes.InstallationUpdate, + async (context: TurnContext, state: ApplicationTurnState): Promise => { + await perplexityAgent.handleInstallationUpdateActivity(context, state); + } +); diff --git a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts new file mode 100644 index 00000000..86f1828a --- /dev/null +++ b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts @@ -0,0 +1,66 @@ +/** + * Represents the payload for a simulated Word mention activity in the Playground. + * Includes document URL, user mention details, and optional context snippet. + */ +export interface MentionInWordValue { + docUrl: string; // URL of the Word document where the mention occurred + mention: { + displayName: string; // Display name of the mentioned user + userPrincipalName: string; // UPN (email) of the mentioned user + }; + context?: string; // Optional text snippet around the mention +} + +/** + * Represents the payload for a simulated email activity in the Playground. + * Includes sender, recipients, subject, and body content. + */ +export interface SendEmailActivityValue { + from: string; // Sender email address + to: string[]; // Recipient email addresses + subject: string; // Email subject line + body: string; // Email body content +} + +/** + * Full structure of a simulated "sendEmail" activity, triggered by the Playground for testing. + */ +export interface SendEmailActivity { + type: "sendEmail"; // Activity type identifier for Playground + id: string; // Unique activity ID + channelId: string; // Channel identifier (e.g., "microsoft365") + from: { + id: string; // Sender ID + aadObjectId: string; // Azure AD object ID of sender + }; + timestamp: string; // ISO timestamp when activity was created + serviceUrl: string; // Service URL for the activity + conversation: { + conversationType: string; // Type of conversation (e.g., "personal") + tenantId: string; // Tenant ID for the conversation + id: string; // Conversation ID + }; + recipient: { + id: string; // Recipient ID + name: string; // Recipient display name + }; + value: SendEmailActivityValue; // Email details payload +} + +/** + * ✅ PlaygroundActivityTypes + * Enum of custom activity types used ONLY in the Agents Playground for simulation. + * These do NOT represent real Microsoft 365 notifications. + * + * - MentionInWord: Simulates a Word mention event (custom payload). + * - SendEmail: Simulates an email notification event (custom payload). + * - Custom: Generic placeholder for any other simulated activity. + * + * Real notifications use AgentNotificationActivity and trigger + * onAgentNotification/onAgenticWordNotification handlers instead. + */ +export enum PlaygroundActivityTypes { + MentionInWord = "mentionInWord", // Triggered when simulating a Word mention + SendEmail = "sendEmail", // Triggered when simulating an email notification + Custom = "custom", // Triggered for any custom test activity +} From 9ec07e04148d20e68b41723ae1bc541cfda405c1 Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Thu, 13 Nov 2025 10:42:54 +0000 Subject: [PATCH 02/10] Introducing playground Teams messaging notification handling in Perplexity agent --- nodejs/perplexity/sample-agent/src/agent.ts | 15 ++++++++ .../src/playgroundActivityTypes.ts | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/nodejs/perplexity/sample-agent/src/agent.ts b/nodejs/perplexity/sample-agent/src/agent.ts index 2a649b17..125ec46b 100644 --- a/nodejs/perplexity/sample-agent/src/agent.ts +++ b/nodejs/perplexity/sample-agent/src/agent.ts @@ -12,6 +12,7 @@ import { MentionInWordValue, PlaygroundActivityTypes, SendEmailActivity, + SendTeamsMessageActivity, } from "./playgroundActivityTypes.js"; /** @@ -160,6 +161,20 @@ agentApplication.onActivity( } ); +/** + * Handles simulated Teams message notifications. + * activityType: "sendTeamsMessage" + * Useful for testing Teams messaging scenarios without real notifications. + */ +agentApplication.onActivity( + PlaygroundActivityTypes.SendTeamsMessage, + async (context: TurnContext, _state: ApplicationTurnState): Promise => { + const activity = context.activity as SendTeamsMessageActivity; + const message = `💬 Teams Message: ${activity.value.text} (Scope: ${activity.value.destination.scope})`; + await context.sendActivity(message); + } +); + /** * Handles a generic custom notification. * Custom activityType: "custom" diff --git a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts index 86f1828a..5dc4a687 100644 --- a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts +++ b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts @@ -47,6 +47,39 @@ export interface SendEmailActivity { value: SendEmailActivityValue; // Email details payload } +/** + * Full structure of a simulated "sendTeamsMessage" activity, triggered by the Playground for testing. + */ +export interface SendTeamsMessageActivity { + type: "sendTeamsMessage"; // Activity type identifier + id: string; // Unique activity ID (GUID) + channelId: "msteams"; // Always Microsoft Teams + from: { + id: string; // Sender ID + aadObjectId: string; // Azure AD Object ID of the sender + }; + timestamp: string; // ISO timestamp + serviceUrl: string; // Connector service URL + conversation: { + conversationType: "personal" | "channel" | "groupChat"; // Teams conversation type + tenantId: string; // Tenant ID + id: string; // Conversation ID + }; + recipient: { + id: string; // Bot ID + name: string; // Bot display name + }; + value: { + text: string; // Message text + destination: { + scope: "personal" | "channel" | "team"; // Destination scope + chatId: string; // Optional chat ID + teamId: string; // Optional team ID + channelId: string; // Optional channel ID + }; + }; +} + /** * ✅ PlaygroundActivityTypes * Enum of custom activity types used ONLY in the Agents Playground for simulation. @@ -62,5 +95,6 @@ export interface SendEmailActivity { export enum PlaygroundActivityTypes { MentionInWord = "mentionInWord", // Triggered when simulating a Word mention SendEmail = "sendEmail", // Triggered when simulating an email notification + SendTeamsMessage = "sendTeamsMessage", // Triggered when simulating a Teams message Custom = "custom", // Triggered for any custom test activity } From 7cea376ff2d0d991d9e76ecc164931c33d7e3523 Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Thu, 13 Nov 2025 11:55:55 +0000 Subject: [PATCH 03/10] applying changes from code review --- nodejs/perplexity/sample-agent/src/agent.ts | 9 +- nodejs/perplexity/sample-agent/src/index.ts | 3 + .../sample-agent/src/perplexityAgent.ts | 144 +++++++++++++----- .../sample-agent/src/perplexityClient.ts | 3 + .../src/playgroundActivityTypes.ts | 3 + .../perplexity/sample-agent/src/telemetry.ts | 3 + 6 files changed, 125 insertions(+), 40 deletions(-) diff --git a/nodejs/perplexity/sample-agent/src/agent.ts b/nodejs/perplexity/sample-agent/src/agent.ts index 125ec46b..f5f46519 100644 --- a/nodejs/perplexity/sample-agent/src/agent.ts +++ b/nodejs/perplexity/sample-agent/src/agent.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { TurnState, AgentApplication, @@ -5,7 +8,7 @@ import { MemoryStorage, TurnContext, } from "@microsoft/agents-hosting"; -import { ActivityTypes } from "@microsoft/agents-activity"; +import { Activity, ActivityTypes } from "@microsoft/agents-activity"; import { AgentNotificationActivity } from "@microsoft/agents-a365-notifications"; import { PerplexityAgent } from "./perplexityAgent.js"; import { @@ -204,6 +207,10 @@ agentApplication.onActivity( let count: number = state.conversation.count ?? 0; state.conversation.count = ++count; + await context.sendActivity( + Activity.fromObject({ type: ActivityTypes.Typing }) + ); + await perplexityAgent.handleAgentMessageActivity(context, state); } ); diff --git a/nodejs/perplexity/sample-agent/src/index.ts b/nodejs/perplexity/sample-agent/src/index.ts index 22bc2c83..d628ea98 100644 --- a/nodejs/perplexity/sample-agent/src/index.ts +++ b/nodejs/perplexity/sample-agent/src/index.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + // It is important to load environment variables before importing other modules import { configDotenv } from "dotenv"; diff --git a/nodejs/perplexity/sample-agent/src/perplexityAgent.ts b/nodejs/perplexity/sample-agent/src/perplexityAgent.ts index ee926bf8..a5fbebdf 100644 --- a/nodejs/perplexity/sample-agent/src/perplexityAgent.ts +++ b/nodejs/perplexity/sample-agent/src/perplexityAgent.ts @@ -1,6 +1,12 @@ -import { TurnContext, TurnState } from '@microsoft/agents-hosting'; -import { PerplexityClient } from './perplexityClient.js'; -import { AgentNotificationActivity, NotificationType } from '@microsoft/agents-a365-notifications'; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TurnContext, TurnState } from "@microsoft/agents-hosting"; +import { PerplexityClient } from "./perplexityClient.js"; +import { + AgentNotificationActivity, + NotificationType, +} from "@microsoft/agents-a365-notifications"; export class PerplexityAgent { isApplicationInstalled: boolean = false; @@ -14,27 +20,38 @@ export class PerplexityAgent { /** * Handles incoming user messages and sends responses using Perplexity. */ - async handleAgentMessageActivity(turnContext: TurnContext, state: TurnState): Promise { + async handleAgentMessageActivity( + turnContext: TurnContext, + state: TurnState + ): Promise { if (!this.isApplicationInstalled) { - await turnContext.sendActivity("Please install the application before sending messages."); + 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?"); + 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."); + await turnContext.sendActivity( + "Please accept the terms and conditions to proceed. Send 'I accept' to accept." + ); return; } } - const userMessage = turnContext.activity.text?.trim() || ''; + const userMessage = turnContext.activity.text?.trim() || ""; if (!userMessage) { - await turnContext.sendActivity('Please send me a message and I\'ll help you!'); + await turnContext.sendActivity( + "Please send me a message and I'll help you!" + ); return; } @@ -43,7 +60,7 @@ export class PerplexityAgent { const response = await perplexityClient.invokeAgentWithScope(userMessage); await turnContext.sendActivity(response); } catch (error) { - console.error('Perplexity query error:', error); + console.error("Perplexity query error:", error); const err = error as any; await turnContext.sendActivity(`Error: ${err.message || err}`); } @@ -52,20 +69,30 @@ export class PerplexityAgent { /** * Handles agent notification activities by parsing the activity type. */ - async handleAgentNotificationActivity(turnContext: TurnContext, state: TurnState, agentNotificationActivity: AgentNotificationActivity): Promise { + async handleAgentNotificationActivity( + turnContext: TurnContext, + state: TurnState, + agentNotificationActivity: AgentNotificationActivity + ): Promise { try { if (!this.isApplicationInstalled) { - await turnContext.sendActivity("Please install the application before sending notifications."); + 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?"); + 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."); + await turnContext.sendActivity( + "Please accept the terms and conditions to proceed. Send 'I accept' to accept." + ); return; } } @@ -73,45 +100,72 @@ export class PerplexityAgent { // Find the first known notification type entity switch (agentNotificationActivity.notificationType) { case NotificationType.EmailNotification: - await this.emailNotificationHandler(turnContext, state, agentNotificationActivity); + await this.emailNotificationHandler( + turnContext, + state, + agentNotificationActivity + ); break; case NotificationType.WpxComment: - await this.wordNotificationHandler(turnContext, state, agentNotificationActivity); + await this.wordNotificationHandler( + turnContext, + state, + agentNotificationActivity + ); break; default: - await turnContext.sendActivity('Notification type not yet implemented.'); + await turnContext.sendActivity( + "Notification type not yet implemented." + ); } } catch (error) { - console.error('Error handling agent notification activity:', error); + console.error("Error handling agent notification activity:", error); const err = error as any; - await turnContext.sendActivity(`Error handling notification: ${err.message || err}`); + 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') { + 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 with Perplexity AI! 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') { + await turnContext.sendActivity( + 'Thank you for hiring me! Looking forward to assisting you with Perplexity AI! 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.'); + 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...'); + 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.'); + await turnContext.sendActivity( + "I could not find the mention notification details." + ); return; } @@ -121,14 +175,16 @@ export class PerplexityAgent { 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'} + 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 perplexityClient = this.getPerplexityClient(); - const commentContent = await perplexityClient.invokeAgentWithScope(mentionPrompt); + const commentContent = await perplexityClient.invokeAgentWithScope( + mentionPrompt + ); const response = await perplexityClient.invokeAgentWithScope( `You have received the following comment. Please follow any instructions in it. ${commentContent}` ); @@ -138,18 +194,28 @@ export class PerplexityAgent { /** * 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...'); + 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.'); + 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 emailNotificationConversationId = + emailNotificationEntity.conversationId; + const emailNotificationConversationIndex = + emailNotificationEntity.conversationIndex; const emailNotificationChangeKey = emailNotificationEntity.changeKey; const perplexityClient = this.getPerplexityClient(); @@ -172,10 +238,10 @@ export class PerplexityAgent { private getPerplexityClient(): PerplexityClient { const apiKey = process.env.PERPLEXITY_API_KEY; if (!apiKey) { - throw new Error('PERPLEXITY_API_KEY environment variable is not set'); + throw new Error("PERPLEXITY_API_KEY environment variable is not set"); } - const model = process.env.PERPLEXITY_MODEL || 'sonar'; + const model = process.env.PERPLEXITY_MODEL || "sonar"; return new PerplexityClient(apiKey, model); } } diff --git a/nodejs/perplexity/sample-agent/src/perplexityClient.ts b/nodejs/perplexity/sample-agent/src/perplexityClient.ts index ea93e61d..b7975797 100644 --- a/nodejs/perplexity/sample-agent/src/perplexityClient.ts +++ b/nodejs/perplexity/sample-agent/src/perplexityClient.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { Perplexity } from "@perplexity-ai/perplexity_ai"; import { InferenceScope, diff --git a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts index 5dc4a687..69129c55 100644 --- a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts +++ b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + /** * Represents the payload for a simulated Word mention activity in the Playground. * Includes document URL, user mention details, and optional context snippet. diff --git a/nodejs/perplexity/sample-agent/src/telemetry.ts b/nodejs/perplexity/sample-agent/src/telemetry.ts index 50c5a5fb..6b5e724f 100644 --- a/nodejs/perplexity/sample-agent/src/telemetry.ts +++ b/nodejs/perplexity/sample-agent/src/telemetry.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { ObservabilityManager, Builder, From 3fddabb539832243cd75a65932f6a46b5c9ac06f Mon Sep 17 00:00:00 2001 From: Aubrey Quinn <80953505+aubreyquinn@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:56:35 +0000 Subject: [PATCH 04/10] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../perplexity/sample-agent/src/playgroundActivityTypes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts index 69129c55..e950a804 100644 --- a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts +++ b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts @@ -76,9 +76,9 @@ export interface SendTeamsMessageActivity { text: string; // Message text destination: { scope: "personal" | "channel" | "team"; // Destination scope - chatId: string; // Optional chat ID - teamId: string; // Optional team ID - channelId: string; // Optional channel ID + chatId?: string; // Optional chat ID + teamId?: string; // Optional team ID + channelId?: string; // Optional channel ID }; }; } From 813ced462c0034e89ed740ae7abf5336bfe6f09c Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Thu, 13 Nov 2025 12:03:27 +0000 Subject: [PATCH 05/10] applying changes from code review --- nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts index 69129c55..e9efe4f4 100644 --- a/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts +++ b/nodejs/perplexity/sample-agent/src/playgroundActivityTypes.ts @@ -8,8 +8,8 @@ export interface MentionInWordValue { docUrl: string; // URL of the Word document where the mention occurred mention: { - displayName: string; // Display name of the mentioned user - userPrincipalName: string; // UPN (email) of the mentioned user + displayName: string; // Display name of the document + userPrincipalName: string; // UPN (name) of the user mentioning the agent in the document }; context?: string; // Optional text snippet around the mention } From bdbeab3ed050ea83a137e48bf8c695f0a6ed37d3 Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Thu, 13 Nov 2025 12:19:56 +0000 Subject: [PATCH 06/10] Introducing loading indicator in Perplexity chat --- .../sample-agent/src/perplexityAgent.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/nodejs/perplexity/sample-agent/src/perplexityAgent.ts b/nodejs/perplexity/sample-agent/src/perplexityAgent.ts index a5fbebdf..8ae7045e 100644 --- a/nodejs/perplexity/sample-agent/src/perplexityAgent.ts +++ b/nodejs/perplexity/sample-agent/src/perplexityAgent.ts @@ -55,14 +55,47 @@ export class PerplexityAgent { return; } + // Grab streamingResponse if this surface supports it + const streamingResponse = (turnContext as any).streamingResponse; + try { + // Show temporary "I'm working" message with spinner (Playground, and any streaming-enabled client) + if (streamingResponse) { + streamingResponse.queueInformativeUpdate( + "I'm working on your request..." + ); + } + const perplexityClient = this.getPerplexityClient(); const response = await perplexityClient.invokeAgentWithScope(userMessage); - await turnContext.sendActivity(response); + + if (streamingResponse) { + // Send the final response as a streamed chunk + streamingResponse.queueTextChunk(response); + + // If you ever change PerplexityClient to stream multiple chunks: + // for (const chunk of responseChunks) { + // streamingResponse.queueTextChunk(chunk); + // } + + // Close the stream when done + await streamingResponse.endStream(); + } else { + // Fallback for channels that don't support streaming + await turnContext.sendActivity(response); + } } catch (error) { console.error("Perplexity query error:", error); const err = error as any; - await turnContext.sendActivity(`Error: ${err.message || err}`); + const errorMessage = `Error: ${err.message || err}`; + + if (streamingResponse) { + // Surface the error through the stream and close it + streamingResponse.queueTextChunk(errorMessage); + await streamingResponse.endStream(); + } else { + await turnContext.sendActivity(errorMessage); + } } } From 6c88a3b48783e0565d01c8e340dc56ac4cf764c4 Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Thu, 13 Nov 2025 12:25:13 +0000 Subject: [PATCH 07/10] applying changes from code review --- nodejs/perplexity/sample-agent/src/agent.ts | 4 ---- nodejs/perplexity/sample-agent/src/perplexityAgent.ts | 6 ------ 2 files changed, 10 deletions(-) diff --git a/nodejs/perplexity/sample-agent/src/agent.ts b/nodejs/perplexity/sample-agent/src/agent.ts index f5f46519..bd979089 100644 --- a/nodejs/perplexity/sample-agent/src/agent.ts +++ b/nodejs/perplexity/sample-agent/src/agent.ts @@ -207,10 +207,6 @@ agentApplication.onActivity( let count: number = state.conversation.count ?? 0; state.conversation.count = ++count; - await context.sendActivity( - Activity.fromObject({ type: ActivityTypes.Typing }) - ); - await perplexityAgent.handleAgentMessageActivity(context, state); } ); diff --git a/nodejs/perplexity/sample-agent/src/perplexityAgent.ts b/nodejs/perplexity/sample-agent/src/perplexityAgent.ts index 8ae7045e..5d228627 100644 --- a/nodejs/perplexity/sample-agent/src/perplexityAgent.ts +++ b/nodejs/perplexity/sample-agent/src/perplexityAgent.ts @@ -72,12 +72,6 @@ export class PerplexityAgent { if (streamingResponse) { // Send the final response as a streamed chunk streamingResponse.queueTextChunk(response); - - // If you ever change PerplexityClient to stream multiple chunks: - // for (const chunk of responseChunks) { - // streamingResponse.queueTextChunk(chunk); - // } - // Close the stream when done await streamingResponse.endStream(); } else { From 55979d63d882c8e8658f9ebe01bafec6a30e189d Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Fri, 14 Nov 2025 11:03:25 +0000 Subject: [PATCH 08/10] applying changes from code review --- nodejs/perplexity/sample-agent/src/agent.ts | 65 +-------------------- 1 file changed, 1 insertion(+), 64 deletions(-) diff --git a/nodejs/perplexity/sample-agent/src/agent.ts b/nodejs/perplexity/sample-agent/src/agent.ts index 95259f04..0ce1f973 100644 --- a/nodejs/perplexity/sample-agent/src/agent.ts +++ b/nodejs/perplexity/sample-agent/src/agent.ts @@ -8,30 +8,7 @@ import { MemoryStorage, TurnContext, } from "@microsoft/agents-hosting"; -import { Activity, ActivityTypes } from "@microsoft/agents-activity"; -import { AgentNotificationActivity } from "@microsoft/agents-a365-notifications"; -import { PerplexityAgent } from "./perplexityAgent.js"; -import { - MentionInWordValue, - PlaygroundActivityTypes, - SendEmailActivity, - SendTeamsMessageActivity, -} from "./playgroundActivityTypes.js"; - -/** - * Conversation state interface for tracking message count. - */ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { - TurnState, - AgentApplication, - AttachmentDownloader, - MemoryStorage, - TurnContext, -} from "@microsoft/agents-hosting"; -import { Activity, ActivityTypes } from "@microsoft/agents-activity"; +import { ActivityTypes } from "@microsoft/agents-activity"; import { AgentNotificationActivity } from "@microsoft/agents-a365-notifications"; import { PerplexityAgent } from "./perplexityAgent.js"; import { @@ -63,29 +40,6 @@ const downloader: AttachmentDownloader = new AttachmentDownloader(); */ const storage: MemoryStorage = new MemoryStorage(); -/** - * ApplicationTurnState combines TurnState with our ConversationState. - */ -type ApplicationTurnState = TurnState; - -/** - * Instantiate the AttachmentDownloader. - */ -const downloader: AttachmentDownloader = new AttachmentDownloader(); - -/** - * Instantiate the MemoryStorage. - */ -const storage: MemoryStorage = new MemoryStorage(); - -/** - * Create the Agent Application instance with typed state. - */ -export const agentApplication: AgentApplication = - new AgentApplication({ - storage, - fileDownloaders: [downloader], - }); /** * Create the Agent Application instance with typed state. */ @@ -99,10 +53,6 @@ export const agentApplication: AgentApplication = * Instantiate the PerplexityAgent. */ const perplexityAgent: PerplexityAgent = new PerplexityAgent(undefined); -/** - * Instantiate the PerplexityAgent. - */ -const perplexityAgent: PerplexityAgent = new PerplexityAgent(undefined); /* -------------------------------------------------------------------- * ✅ Real Notification Events (Production) @@ -260,9 +210,6 @@ agentApplication.onActivity( await perplexityAgent.handleAgentMessageActivity(context, state); } ); - await perplexityAgent.handleAgentMessageActivity(context, state); - } -); /** * Handles installation update activities (ActivityTypes.InstallationUpdate). @@ -274,13 +221,3 @@ agentApplication.onActivity( await perplexityAgent.handleInstallationUpdateActivity(context, state); } ); -/** - * Handles installation update activities (ActivityTypes.InstallationUpdate). - * Useful for responding to app installation or update events. - */ -agentApplication.onActivity( - ActivityTypes.InstallationUpdate, - async (context: TurnContext, state: ApplicationTurnState): Promise => { - await perplexityAgent.handleInstallationUpdateActivity(context, state); - } -); From e1882adb0ab2de9a41cbadc6079b4f7bd9f8207c Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Fri, 14 Nov 2025 12:53:35 +0000 Subject: [PATCH 09/10] Introducing telemetry markers to Perplexity agent --- nodejs/perplexity/sample-agent/src/agent.ts | 311 +++++++++++++----- .../sample-agent/src/perplexityClient.ts | 88 ++--- .../sample-agent/src/telemetryHelpers.ts | 43 +++ 3 files changed, 310 insertions(+), 132 deletions(-) create mode 100644 nodejs/perplexity/sample-agent/src/telemetryHelpers.ts diff --git a/nodejs/perplexity/sample-agent/src/agent.ts b/nodejs/perplexity/sample-agent/src/agent.ts index 0ce1f973..637631d2 100644 --- a/nodejs/perplexity/sample-agent/src/agent.ts +++ b/nodejs/perplexity/sample-agent/src/agent.ts @@ -18,6 +18,18 @@ import { SendTeamsMessageActivity, } from "./playgroundActivityTypes.js"; +import { + BaggageBuilder, + InvokeAgentDetails, + InvokeAgentScope, + ExecutionType, + ServiceEndpoint, +} from "@microsoft/agents-a365-observability"; +import { + extractAgentDetailsFromTurnContext, + extractTenantDetailsFromTurnContext, +} from "./telemetryHelpers.js"; + /** * Conversation state interface for tracking message count. */ @@ -55,15 +67,87 @@ export const agentApplication: AgentApplication = const perplexityAgent: PerplexityAgent = new PerplexityAgent(undefined); /* -------------------------------------------------------------------- - * ✅ Real Notification Events (Production) - * These handlers process structured AgentNotificationActivity objects - * sent by Microsoft 365 workloads (Word, Outlook, etc.) in production. + * 🔧 Shared telemetry helper + * -------------------------------------------------------------------- */ + +async function runWithTelemetry( + context: TurnContext, + _state: ApplicationTurnState, + options: { + operationName: string; + executionType: ExecutionType; + requestContent?: string; + }, + handler: () => Promise +): Promise { + const agentInfo = extractAgentDetailsFromTurnContext(context); + const tenantInfo = extractTenantDetailsFromTurnContext(context); + + const requestContent = + options.requestContent ?? + context.activity.text ?? + options.operationName ?? + "Unknown request"; + + const baggageScope = new BaggageBuilder() + .tenantId(tenantInfo.tenantId) + .agentId(agentInfo.agentId) + .agentName(agentInfo.agentName) + .conversationId(context.activity.conversation?.id) + .callerId((context.activity.from as any)?.aadObjectId) + .callerUpn(context.activity.from?.id) + .correlationId(context.activity.id ?? `corr-${Date.now()}`) + .build(); + + await baggageScope.run(async () => { + const invokeDetails: InvokeAgentDetails = { + ...agentInfo, + conversationId: context.activity.conversation?.id, + request: { + content: requestContent, + executionType: options.executionType, + sessionId: context.activity.conversation?.id, + }, + endpoint: { + host: context.activity.serviceUrl ?? "unknown", + port: 0, + } as ServiceEndpoint, + }; + + const invokeScope = InvokeAgentScope.start( + invokeDetails, + tenantInfo, + agentInfo + ); + + // If observability isn't configured, just run the handler + if (!invokeScope) { + await handler(); + return; + } + + try { + await invokeScope.withActiveSpanAsync(async () => { + invokeScope.recordInputMessages([requestContent]); + + await handler(); + + invokeScope.recordOutputMessages([ + `${options.operationName} handled by PerplexityAgent`, + ]); + }); + } finally { + invokeScope.dispose(); + } + }); +} + +/* -------------------------------------------------------------------- + * ✅ Real Notification Events (Production) + telemetry * -------------------------------------------------------------------- */ /** * Handles ALL real notification events from any workload. - * Fires when an AgentNotificationActivity is received. - * Use this for generic notification handling logic. */ agentApplication.onAgentNotification( "*", @@ -72,17 +156,27 @@ agentApplication.onAgentNotification( state: ApplicationTurnState, activity: AgentNotificationActivity ): Promise => { - await perplexityAgent.handleAgentNotificationActivity( + await runWithTelemetry( context, state, - activity + { + operationName: "AgentNotification_*", + executionType: ExecutionType.EventToAgent, + requestContent: `NotificationType=${activity.notificationType}`, + }, + async () => { + await perplexityAgent.handleAgentNotificationActivity( + context, + state, + activity + ); + } ); } ); /** - * Handles Word-specific notifications (e.g., comments, mentions in Word). - * Fires only for AgentNotificationActivity originating from Word. + * Word-specific notifications. */ agentApplication.onAgenticWordNotification( async ( @@ -90,17 +184,27 @@ agentApplication.onAgenticWordNotification( state: ApplicationTurnState, activity: AgentNotificationActivity ): Promise => { - await perplexityAgent.handleAgentNotificationActivity( + await runWithTelemetry( context, state, - activity + { + operationName: "AgentNotification_Word", + executionType: ExecutionType.EventToAgent, + requestContent: `WordNotificationType=${activity.notificationType}`, + }, + async () => { + await perplexityAgent.handleAgentNotificationActivity( + context, + state, + activity + ); + } ); } ); /** - * Handles Email-specific notifications (e.g., new mail, flagged items). - * Fires only for AgentNotificationActivity originating from Outlook/Email. + * Email-specific notifications. */ agentApplication.onAgenticEmailNotification( async ( @@ -108,116 +212,167 @@ agentApplication.onAgenticEmailNotification( state: ApplicationTurnState, activity: AgentNotificationActivity ): Promise => { - await perplexityAgent.handleAgentNotificationActivity( + await runWithTelemetry( context, state, - activity + { + operationName: "AgentNotification_Email", + executionType: ExecutionType.EventToAgent, + requestContent: `EmailNotificationType=${activity.notificationType}`, + }, + async () => { + await perplexityAgent.handleAgentNotificationActivity( + context, + state, + activity + ); + } ); } ); /* -------------------------------------------------------------------- - * ✅ Playground Events (Simulated for Testing) - * These handlers process custom activityType strings sent via sendActivity() - * from the Playground UI. They DO NOT trigger real notification handlers. + * ✅ Playground Events (Simulated) + telemetry * -------------------------------------------------------------------- */ -/** - * Handles simulated Word mention notifications. - * activityType: "mentionInWord" - * Useful for testing Word-related scenarios without real notifications. - */ agentApplication.onActivity( PlaygroundActivityTypes.MentionInWord, - async (context: TurnContext, _state: ApplicationTurnState): Promise => { - const value: MentionInWordValue = context.activity - .value as MentionInWordValue; - const docName: string = value.mention.displayName; - const docUrl: string = value.docUrl; - const userName: string = value.mention.userPrincipalName; - const contextSnippet: string = value.context - ? `Context: ${value.context}` - : ""; - const message: string = `✅ You were mentioned in **${docName}** by ${userName}\n📄 ${docUrl}\n${contextSnippet}`; - await context.sendActivity(message); + async (context: TurnContext, state: ApplicationTurnState): Promise => { + await runWithTelemetry( + context, + state, + { + operationName: "Playground_MentionInWord", + executionType: ExecutionType.HumanToAgent, + requestContent: JSON.stringify(context.activity.value ?? {}), + }, + async () => { + const value: MentionInWordValue = context.activity + .value as MentionInWordValue; + const docName: string = value.mention.displayName; + const docUrl: string = value.docUrl; + const userName: string = value.mention.userPrincipalName; + const contextSnippet: string = value.context + ? `Context: ${value.context}` + : ""; + const message: string = `✅ You were mentioned in **${docName}** by ${userName}\n📄 ${docUrl}\n${contextSnippet}`; + await context.sendActivity(message); + } + ); } ); -/** - * Handles simulated Email notifications. - * activityType: "sendEmail" - * Useful for testing email scenarios without real notifications. - */ agentApplication.onActivity( PlaygroundActivityTypes.SendEmail, - async (context: TurnContext, _state: ApplicationTurnState): Promise => { - const activity: SendEmailActivity = context.activity as SendEmailActivity; - const email = activity.value; + async (context: TurnContext, state: ApplicationTurnState): Promise => { + await runWithTelemetry( + context, + state, + { + operationName: "Playground_SendEmail", + executionType: ExecutionType.HumanToAgent, + requestContent: JSON.stringify(context.activity.value ?? {}), + }, + async () => { + const activity = context.activity as SendEmailActivity; + const email = activity.value; - const message: string = `📧 Email Notification: - From: ${email.from} - To: ${email.to.join(", ")} - Subject: ${email.subject} - Body: ${email.body}`; + const message: string = `📧 Email Notification: + From: ${email.from} + To: ${email.to.join(", ")} + Subject: ${email.subject} + Body: ${email.body}`; - await context.sendActivity(message); + await context.sendActivity(message); + } + ); } ); -/** - * Handles simulated Teams message notifications. - * activityType: "sendTeamsMessage" - * Useful for testing Teams messaging scenarios without real notifications. - */ agentApplication.onActivity( PlaygroundActivityTypes.SendTeamsMessage, - async (context: TurnContext, _state: ApplicationTurnState): Promise => { - const activity = context.activity as SendTeamsMessageActivity; - const message = `💬 Teams Message: ${activity.value.text} (Scope: ${activity.value.destination.scope})`; - await context.sendActivity(message); + async (context: TurnContext, state: ApplicationTurnState): Promise => { + await runWithTelemetry( + context, + state, + { + operationName: "Playground_SendTeamsMessage", + executionType: ExecutionType.HumanToAgent, + requestContent: JSON.stringify(context.activity.value ?? {}), + }, + async () => { + const activity = context.activity as SendTeamsMessageActivity; + const message = `💬 Teams Message: ${activity.value.text} (Scope: ${activity.value.destination.scope})`; + await context.sendActivity(message); + } + ); } ); -/** - * Handles a generic custom notification. - * Custom activityType: "custom" - * ✅ To add more custom activities: - * - Define a new handler using agentApplication.onActivity("", ...) - * - Implement logic similar to this block. - */ agentApplication.onActivity( PlaygroundActivityTypes.Custom, - async (context: TurnContext, _state: ApplicationTurnState): Promise => { - await context.sendActivity("this is a custom activity handler"); + async (context: TurnContext, state: ApplicationTurnState): Promise => { + await runWithTelemetry( + context, + state, + { + operationName: "Playground_Custom", + executionType: ExecutionType.HumanToAgent, + requestContent: "custom", + }, + async () => { + await context.sendActivity("this is a custom activity handler"); + } + ); } ); /* -------------------------------------------------------------------- - * ✅ Generic Activity Handlers - * These handle standard activity types like messages or installation updates. + * ✅ Message Activities + telemetry * -------------------------------------------------------------------- */ -/** - * Handles standard message activities (ActivityTypes.Message). - * Increments conversation count and delegates to PerplexityAgent. - */ agentApplication.onActivity( ActivityTypes.Message, async (context: TurnContext, state: ApplicationTurnState): Promise => { + // Increment count state let count: number = state.conversation.count ?? 0; state.conversation.count = ++count; - await perplexityAgent.handleAgentMessageActivity(context, state); + await runWithTelemetry( + context, + state, + { + operationName: "Message", + executionType: ExecutionType.HumanToAgent, + requestContent: context.activity.text || "Unknown text", + }, + async () => { + await perplexityAgent.handleAgentMessageActivity(context, state); + } + ); } ); -/** - * Handles installation update activities (ActivityTypes.InstallationUpdate). - * Useful for responding to app installation or update events. - */ +/* -------------------------------------------------------------------- + * ✅ Installation Updates (add/remove) + telemetry + * -------------------------------------------------------------------- */ + agentApplication.onActivity( ActivityTypes.InstallationUpdate, async (context: TurnContext, state: ApplicationTurnState): Promise => { - await perplexityAgent.handleInstallationUpdateActivity(context, state); + const action = (context.activity as any).action ?? "unknown"; + + await runWithTelemetry( + context, + state, + { + operationName: "InstallationUpdate", + executionType: ExecutionType.EventToAgent, + requestContent: `InstallationUpdate action=${action}`, + }, + async () => { + await perplexityAgent.handleInstallationUpdateActivity(context, state); + } + ); } ); diff --git a/nodejs/perplexity/sample-agent/src/perplexityClient.ts b/nodejs/perplexity/sample-agent/src/perplexityClient.ts index 926ebaf2..5888545b 100644 --- a/nodejs/perplexity/sample-agent/src/perplexityClient.ts +++ b/nodejs/perplexity/sample-agent/src/perplexityClient.ts @@ -4,8 +4,6 @@ import { Perplexity } from "@perplexity-ai/perplexity_ai"; import { InferenceScope, - InvokeAgentScope, - InvokeAgentDetails, AgentDetails, TenantDetails, InferenceDetails, @@ -47,9 +45,6 @@ export class PerplexityClient { /** * Sends a user message to the Perplexity SDK and returns the AI's response. - * - * @param {string} userMessage - The message or prompt to send to Perplexity. - * @returns {Promise} The response from Perplexity, or an error message if the query fails. */ async invokeAgent(userMessage: string): Promise { try { @@ -81,73 +76,58 @@ export class PerplexityClient { } /** - * Wrapper for invokeAgent that adds tracing and span management using Microsoft Agent 365 SDK. + * Wrapper for invokeAgent that adds tracing and span management using + * Microsoft Agent 365 SDK (InferenceScope only). + * + * The outer InvokeAgentScope is created in agent.ts around the activity handler. */ async invokeAgentWithScope(prompt: string): Promise { - const invokeAgentDetails: InvokeAgentDetails = { + const agentDetails: AgentDetails = { agentId: process.env.AGENT_ID || "perplexity-agent", + agentName: process.env.AGENT_NAME || "Perplexity Agent", }; - const agentDetails: AgentDetails = { - agentId: "perplexity-agent", - agentName: "Perplexity Agent", + const tenantDetails: TenantDetails = { + tenantId: process.env.TENANT_ID || "perplexity-sample-tenant", }; - const tenantDetails: TenantDetails = { - tenantId: "perplexity-sample-tenant", + const inferenceDetails: InferenceDetails = { + operationName: InferenceOperationType.CHAT, + model: this.model, + providerName: "perplexity", }; - const invokeAgentScope = InvokeAgentScope.start( - invokeAgentDetails, - tenantDetails, - agentDetails + const scope = InferenceScope.start( + inferenceDetails, + agentDetails, + tenantDetails ); - if (!invokeAgentScope) { + // If observability isn't configured, just run the call + if (!scope) { await new Promise((resolve) => setTimeout(resolve, 200)); return await this.invokeAgent(prompt); } try { - return await invokeAgentScope.withActiveSpanAsync(async () => { - const inferenceDetails: InferenceDetails = { - operationName: InferenceOperationType.CHAT, - model: this.model, - providerName: "perplexity", - }; - - const scope = InferenceScope.start( - inferenceDetails, - agentDetails, - tenantDetails - ); - - if (!scope) { - await new Promise((resolve) => setTimeout(resolve, 200)); - return await this.invokeAgent(prompt); - } - - try { - const result = await scope.withActiveSpanAsync(async () => { - const response = await this.invokeAgent(prompt); - - scope?.recordOutputMessages([response]); - scope?.recordResponseId(`resp-${Date.now()}`); - scope?.recordFinishReasons(["stop"]); - - return response; - }); - - return result; - } catch (error) { - scope.recordError(error as Error); - throw error; - } finally { - scope.dispose(); - } + const result = await scope.withActiveSpanAsync(async () => { + scope.recordInputMessages([prompt]); + + const response = await this.invokeAgent(prompt); + + scope.recordOutputMessages([response]); + scope.recordResponseId(`resp-${Date.now()}`); + scope.recordFinishReasons(["stop"]); + + return response; }); + + return result; + } catch (error) { + scope.recordError(error as Error); + throw error; } finally { - invokeAgentScope.dispose(); + scope.dispose(); } } } diff --git a/nodejs/perplexity/sample-agent/src/telemetryHelpers.ts b/nodejs/perplexity/sample-agent/src/telemetryHelpers.ts new file mode 100644 index 00000000..25258266 --- /dev/null +++ b/nodejs/perplexity/sample-agent/src/telemetryHelpers.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { EnhancedAgentDetails } from "@microsoft/agents-a365-observability"; +import { TurnContext } from "@microsoft/agents-hosting"; + +/** + * This function extracts agent details from the TurnContext. + * @param context The TurnContext from which to extract agent details. + * @returns An object containing enhanced agent details. + */ +export function extractAgentDetailsFromTurnContext( + context: TurnContext +): EnhancedAgentDetails { + const recipient: any = context.activity.recipient || {}; + const agentId = + recipient.agenticAppId || process.env.AGENT_ID || "sample-agent"; + + return { + agentId, + agentName: recipient.name || process.env.AGENT_NAME || "Basic Agent Sample", + agentAUID: recipient.agenticUserId, + agentUPN: recipient.id, + conversationId: context.activity.conversation?.id, + } as EnhancedAgentDetails; +} + +/** + * This function extracts tenant details from the TurnContext. + * @param context The TurnContext from which to extract tenant details. + * @returns An object containing tenant details. + */ +export function extractTenantDetailsFromTurnContext(context: TurnContext): { + tenantId: string; +} { + const recipient: any = context.activity.recipient || {}; + const tenantId = + recipient.tenantId || + process.env.connections__serviceConnection__settings__tenantId || + "sample-tenant"; + + return { tenantId }; +} From 8f8c459e9583df5f0330e492ba4268d0d949b59e Mon Sep 17 00:00:00 2001 From: aubreyquinn Date: Fri, 14 Nov 2025 16:06:32 +0000 Subject: [PATCH 10/10] updated readme file --- nodejs/perplexity/sample-agent/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/nodejs/perplexity/sample-agent/README.md b/nodejs/perplexity/sample-agent/README.md index f5418185..7ebcff15 100644 --- a/nodejs/perplexity/sample-agent/README.md +++ b/nodejs/perplexity/sample-agent/README.md @@ -10,6 +10,7 @@ This sample demonstrates how to build an agent using the Agent 365 framework wit - ✅ **Chat with Perplexity** - Natural language conversations using Perplexity's Sonar models. - ✅ **Playground notification handling** - Responds to notifications triggered in the playground UI (@mention in word documents, emails, custom, etc.) +- ✅ **Playground observability** - Telemetry output visible in the terminal ## Prerequisites