使用 Agent 365 SDK,您的 Agent 可處理平台活動事件 (例如安裝與解除安裝),並在單一回合內傳送多則個別訊息。 本文說明在 Agent 處理要求期間,用於回應使用者並讓使用者隨時掌握狀態的主要模式。
處理 Agent 安裝與解除安裝事件
當使用者在 Teams 或其他 Agent 365 代管的通道中安裝或解除安裝您的 Agent 時,平台會傳送 InstallationUpdate 活動 (也稱為 agentInstanceCreated 事件)。 您的 Agent 可處理這些事件,在安裝時傳送歡迎訊息,並在解除安裝時傳送告別訊息。
| 動作 |
Description |
add |
使用者安裝 Agent |
remove |
使用者解除安裝 Agent |
與通知處理常式不同,InstallationUpdate 處理常式不需要驗證,因為安裝或解除安裝事件會在使用者擁有作用中工作階段之前或之後觸發。
註冊安裝與解除安裝處理常式
在 Agent 的初始化程序中,為 InstallationUpdate 活動類型註冊活動處理常式:
@agent_app.activity("installationUpdate")
async def on_installation_update(context: TurnContext, state: TurnState):
action = context.activity.action
from_prop = context.activity.from_property
logger.info(
"InstallationUpdate received — Action: '%s', DisplayName: '%s', UserId: '%s'",
action or "(none)",
getattr(from_prop, "name", "(unknown)") if from_prop else "(unknown)",
getattr(from_prop, "id", "(unknown)") if from_prop else "(unknown)",
)
if action == "add":
await context.send_activity("Thank you for hiring me! Looking forward to assisting you in your professional journey!")
elif action == "remove":
await context.send_activity("Thank you for your time, I enjoyed working with you.")
Activity.action 是一個字串,安裝 Agent 時設為"add",解除安裝 Agent 時則設為"remove"。
Activity.from_property 是包含使用者身分識別的 ChannelAccount 執行個體。
// In your agent class constructor:
this.onActivity(ActivityTypes.InstallationUpdate, async (context: TurnContext, state: TurnState) => {
await this.handleInstallationUpdateActivity(context, state);
});
// Handler method:
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.');
}
}
ActivityTypes 是從@microsoft/agents-activity 匯入的活動類型常數列舉。
Activity.action 是一個字串,安裝 Agent 時設為'add',解除安裝 Agent 時則設為'remove'。
// In your agent class constructor:
OnActivity(ActivityTypes.InstallationUpdate, OnInstallationUpdateAsync, isAgenticOnly: true, autoSignInHandlers: agenticInstallHandlers);
OnActivity(ActivityTypes.InstallationUpdate, OnInstallationUpdateAsync, isAgenticOnly: false);
// Handler method:
protected async Task OnInstallationUpdateAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
_logger?.LogInformation(
"InstallationUpdate received — Action: '{Action}', DisplayName: '{Name}', UserId: '{Id}'",
turnContext.Activity.Action ?? "(none)",
turnContext.Activity.From?.Name ?? "(unknown)",
turnContext.Activity.From?.Id ?? "(unknown)");
if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Thank you for hiring me! Looking forward to assisting you in your professional journey!"), cancellationToken);
}
else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Thank you for your time, I enjoyed working with you."), cancellationToken);
}
}
ActivityTypes 是活動類型常數的列舉。
InstallationUpdateActionTypes 提供 Add 及 Remove 常數,用於比較活動動作。
注意
針對 .NET,請註冊處理常式兩次:一次搭配 isAgenticOnly: true,用於具有選用代理型驗證處理常式的正式 Agent 365 流量;另一次搭配 isAgenticOnly: false,用於透過 Agents Playground 或 WebChat 進行本機測試。
傳送多則訊息
Agent 365 Agent 可針對單一使用者提示,傳送多則個別訊息以回應。 若要這麼做,請在單一回合內多次呼叫 SendActivityAsync(.NET)、send_activity(Python) 或 sendActivity(JavaScript)。
重要
Teams 不支援代理型身分識別的串流回應。 SDK 會偵測代理型身分識別,並將串流緩衝為單一訊息。 直接使用 SendActivityAsync、send_activity 或 sendActivity,即可立即向使用者傳送個別訊息。
下列範例示範在 LLM 回應之前先傳送即時確認訊息的模式:
@agent_app.activity("message")
async def on_message(context: TurnContext, state: TurnState):
# Message 1: immediate ack — reaches the user right away
await context.send_activity("Got it — working on it…")
# ... LLM processing ...
# Message 2: the LLM response
await context.send_activity(response)
此範例會在 on_message(host_agent_server.py) 中示範此模式,於 LLM 回應之前先傳送即時確認訊息。
// Message 1: immediate ack — reaches the user right away
await context.sendActivity('Got it — working on it…');
// ... LLM processing ...
// Message 2: the LLM response
await context.sendActivity(modelResponse);
此範例會在訊息活動處理常式 (agent.ts) 中示範此模式。
// Message 1: immediate ack — reaches the user right away
await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken);
// ... LLM processing ...
// Message 2: the LLM response (via StreamingResponse, buffered into one message for Teams agentic)
await turnContext.StreamingResponse.EndStreamAsync(cancellationToken);
此範例會在 OnMessageAsync(MyAgent.cs) 中示範此模式。
每次呼叫 sendActivity、send_activity 或 SendActivityAsync,都會建立個別的訊息。 您可視需要多次呼叫,以傳送進度更新、部分結果或最終答案。
輸入指示器
打字指示器會在 Teams 中顯示...進度動畫:
- 其內建的可見逾時時間約為 5 秒,必須以迴圈每隔約 4 秒重新整理一次。
- 這些指示器僅在一對一聊天及小型群組聊天中可見,通道中則不會顯示。
在 LLM 處理要求期間,Agent 會以迴圈每隔約 4 秒傳送打字指示器,以維持...動畫顯示:
# Message 1: immediate ack — reaches the user right away
await context.send_activity("Got it — working on it…")
# Send typing indicator immediately (awaited so it arrives before the LLM call starts).
await context.send_activity(Activity(type="typing"))
# Background loop refreshes the "..." animation every ~4s (it times out after ~5s).
async def _typing_loop():
try:
while True:
await asyncio.sleep(4)
await context.send_activity(Activity(type="typing"))
except asyncio.CancelledError:
pass # Expected on cancel.
typing_task = asyncio.create_task(_typing_loop())
try:
response = await agent.process_user_message(...)
await context.send_activity(response)
finally:
typing_task.cancel()
try:
await typing_task
except asyncio.CancelledError:
pass
let typingInterval: ReturnType<typeof setInterval> | undefined;
const startTypingLoop = () => {
typingInterval = setInterval(async () => {
await context.sendActivity(Activity.fromObject({ type: ActivityTypes.Typing }));
}, 4000);
};
const stopTypingLoop = () => { clearInterval(typingInterval); };
startTypingLoop();
try {
// ... LLM processing ...
} finally {
stopTypingLoop();
}
// Typing indicator loop — refreshes every ~4s for long-running operations.
using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var typingTask = Task.Run(async () =>
{
try
{
while (!typingCts.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token);
await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token);
}
}
catch (OperationCanceledException) { /* expected on cancel */ }
}, typingCts.Token);
try { /* ... do work ... */ }
finally
{
typingCts.Cancel();
try { await typingTask; } catch (OperationCanceledException) { }
}