diff --git a/nodejs/perplexity/sample-agent/README.md b/nodejs/perplexity/sample-agent/README.md index d0777e91..3e9e9961 100644 --- a/nodejs/perplexity/sample-agent/README.md +++ b/nodejs/perplexity/sample-agent/README.md @@ -46,7 +46,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope ## Trademarks -*Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653.* +_Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653._ ## License 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 }; +}