-
Notifications
You must be signed in to change notification settings - Fork 49
Expand file tree
/
Copy pathagent.ts
More file actions
216 lines (185 loc) · 9.86 KB
/
Copy pathagent.ts
File metadata and controls
216 lines (185 loc) · 9.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// IMPORTANT: Load environment variables FIRST before any other imports
// This ensures NODE_ENV and other config is available when AgentApplication initializes
import { configDotenv } from 'dotenv';
configDotenv();
import { TurnState, AgentApplication, TurnContext, MemoryStorage } from '@microsoft/agents-hosting';
import { Activity, ActivityTypes } from '@microsoft/agents-activity';
import { BaggageBuilder } from '@microsoft/agents-a365-observability';
import {AgenticTokenCacheInstance, BaggageBuilderUtils} from '@microsoft/agents-a365-observability-hosting'
import { getObservabilityAuthenticationScope } from '@microsoft/agents-a365-runtime';
// Notification Imports
import '@microsoft/agents-a365-notifications';
import { AgentNotificationActivity, NotificationType, createEmailResponseActivity } from '@microsoft/agents-a365-notifications';
import { Client, getClient } from './client';
import tokenCache, { createAgenticTokenCacheKey } from './token-cache';
export class MyAgent extends AgentApplication<TurnState> {
static authHandlerName: string = 'agentic';
constructor() {
super({
storage: new MemoryStorage(),
authorization: {
agentic: {
type: 'agentic',
} // scopes set in the .env file...
}
});
// Route agent notifications
this.onAgentNotification("agents:*", async (context: TurnContext, state: TurnState, agentNotificationActivity: AgentNotificationActivity) => {
await this.handleAgentNotificationActivity(context, state, agentNotificationActivity);
}, 1, [MyAgent.authHandlerName]);
this.onActivity(ActivityTypes.Message, async (context: TurnContext, state: TurnState) => {
await this.handleAgentMessageActivity(context, state);
}, [MyAgent.authHandlerName]);
// Handle agent install / uninstall events (agentInstanceCreated / InstallationUpdate)
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, state: TurnState): Promise<void> {
const userMessage = turnContext.activity.text?.trim() || '';
const from = turnContext.activity?.from;
console.log(`Turn received from user — DisplayName: '${from?.name ?? "(unknown)"}', UserId: '${from?.id ?? "(unknown)"}', AadObjectId: '${from?.aadObjectId ?? "(none)"}'`);
const displayName = from?.name ?? 'unknown';
if (!userMessage) {
await turnContext.sendActivity('Please send me a message and I\'ll help you!');
return;
}
// Multiple messages pattern: send an immediate acknowledgment before the LLM work begins.
// Each sendActivity call produces a discrete Teams message.
// NOTE: For Teams agentic identities, streaming is buffered into a single message by the SDK;
// use sendActivity for any messages that must arrive immediately.
await turnContext.sendActivity('Got it — working on it…');
// Send typing indicator immediately (awaited so it arrives before the LLM call starts).
await turnContext.sendActivity({ type: 'typing' } as Activity);
// Background loop refreshes the "..." animation every ~4s (it times out after ~5s).
// Only visible in 1:1 and small group chats.
let typingInterval: ReturnType<typeof setInterval> | undefined;
const startTypingLoop = () => {
typingInterval = setInterval(() => {
turnContext.sendActivity({ type: 'typing' } as Activity).catch(() => {
// Typing indicator failed — non-critical, continue
});
}, 4000);
};
const stopTypingLoop = () => { clearInterval(typingInterval); };
startTypingLoop();
// Populate baggage consistently from TurnContext using hosting utilities
const baggageScope = BaggageBuilderUtils.fromTurnContext(
new BaggageBuilder(),
turnContext
).sessionDescription('Initial onboarding session')
.build();
// Preloads or refreshes the Observability token used by the Agent 365 Observability exporter.
await this.preloadObservabilityToken(turnContext);
try {
await baggageScope.run(async () => {
const client: Client = await getClient(this.authorization, MyAgent.authHandlerName, turnContext, displayName);
const response = await client.invokeAgentWithScope(userMessage);
// Message 2: the LLM response
await turnContext.sendActivity(response);
});
} catch (error) {
console.error('LLM query error:', error);
const err = error as any;
await turnContext.sendActivity(`Error: ${err.message || err}`);
} finally {
stopTypingLoop();
baggageScope.dispose();
}
}
/**
* Preloads or refreshes the Observability token used by the Agent 365 Observability exporter.
*
* Behavior:
* - If the environment variable `Use_Custom_Resolver` is set to `true`, this method exchanges an
* AAU token using the agent's authorization and stores it in the local `tokenCache`, keyed by
* `agentId`/`tenantId` via `createAgenticTokenCacheKey`.
* - Otherwise, it refreshes the built-in `AgenticTokenCacheInstance` by invoking
* `RefreshObservabilityToken`, which is used by the default token resolver configured in the client.
*
* Notes:
* - Token acquisition failures are non-fatal for this sample and should not block the user flow.
* - `agentId` and `tenantId` are derived from the current `TurnContext` activity recipient.
* - Uses `getObservabilityAuthenticationScope()` to obtain the exporter auth scopes.
*
* @param turnContext The current turn context containing activity and identity metadata.
*/
private async preloadObservabilityToken(turnContext: TurnContext): Promise<void> {
const agentId = turnContext?.activity?.recipient?.agenticAppId ?? '';
const tenantId = turnContext?.activity?.recipient?.tenantId ?? '';
// Set Use_Custom_Resolver === 'true' to use a custom token resolver and a custom token cache (see token-cache.ts).
// Otherwise: use the default AgenticTokenCache via RefreshObservabilityToken.
if (process.env.Use_Custom_Resolver === 'true') {
const aauToken = await this.authorization.exchangeToken(turnContext, 'agentic', {
scopes: getObservabilityAuthenticationScope()
});
console.log(`Preloaded Observability token for agentId=${agentId}, tenantId=${tenantId} token=${aauToken?.token?.substring(0, 10)}...`);
const cacheKey = createAgenticTokenCacheKey(agentId, tenantId);
tokenCache.set(cacheKey, aauToken?.token || '');
} else {
// Preload/refresh the observability token into the built-in AgenticTokenCache.
// We don't immediately need the token here, and if acquisition fails we continue (non-fatal for this demo sample).
await AgenticTokenCacheInstance.RefreshObservabilityToken(
agentId,
tenantId,
turnContext,
this.authorization,
getObservabilityAuthenticationScope()
);
}
}
async handleAgentNotificationActivity(context: TurnContext, state: TurnState, agentNotificationActivity: AgentNotificationActivity) {
switch (agentNotificationActivity.notificationType) {
case NotificationType.EmailNotification:
await this.handleEmailNotification(context, state, agentNotificationActivity);
break;
default:
await context.sendActivity(`Received notification of type: ${agentNotificationActivity.notificationType}`);
}
}
private async handleEmailNotification(context: TurnContext, state: TurnState, activity: AgentNotificationActivity): Promise<void> {
const emailNotification = activity.emailNotification;
if (!emailNotification) {
const errorResponse = createEmailResponseActivity('I could not find the email notification details.');
await context.sendActivity(errorResponse);
return;
}
try {
const client: Client = await getClient(this.authorization, MyAgent.authHandlerName, context);
// First, retrieve the email content
const emailContent = await client.invokeAgentWithScope(
`You have a new email from ${context.activity.from?.name} with id '${emailNotification.id}', ` +
`ConversationId '${emailNotification.conversationId}'. Please retrieve this message and return it in text format.`
);
// Then process the email
const response = await client.invokeAgentWithScope(
`You have received the following email. Please follow any instructions in it. ${emailContent}`
);
const emailResponseActivity = createEmailResponseActivity(response || 'I have processed your email but do not have a response at this time.');
await context.sendActivity(emailResponseActivity);
} catch (error) {
console.error('Email notification error:', error);
const errorResponse = createEmailResponseActivity('Unable to process your email at this time.');
await context.sendActivity(errorResponse);
}
}
/**
* Handles agent install and uninstall events (agentInstanceCreated / InstallationUpdate).
* Sends a welcome message on install and a farewell on uninstall.
*/
async handleInstallationUpdateActivity(context: TurnContext, state: TurnState): Promise<void> {
const from = context.activity?.from;
console.log(`InstallationUpdate received — Action: '${context.activity.action ?? "(none)"}', DisplayName: '${from?.name ?? "(unknown)"}', UserId: '${from?.id ?? "(unknown)"}'`);
if (context.activity.action === 'add') {
await context.sendActivity('Thank you for hiring me! Looking forward to assisting you in your professional journey!');
} else if (context.activity.action === 'remove') {
await context.sendActivity('Thank you for your time, I enjoyed working with you.');
}
}
}
export const agentApplication = new MyAgent();