通过使用 Agent 365 SDK,您的智能体可以处理安装和卸载等平台活动事件,并在单个对话轮次中发送多条独立消息。 本文介绍了在智能体处理请求时,向用户做出响应并保持其知情的关键模式。
处理智能体安装和卸载事件
当用户在 Teams 或其他由 Agent 365 托管的频道中安装或卸载您的智能体时,平台会发送一个 InstallationUpdate 活动(也称为 agentInstanceCreated 事件)。 您的智能体可以处理这些事件,在安装时发送欢迎消息,在卸载时发送告别消息。
| 操作 |
描述 |
add |
用户安装智能体 |
remove |
用户卸载智能体 |
与通知处理程序不同,InstallationUpdate 处理程序不需要身份验证,因为安装或卸载事件是在用户拥有活动会话之前或之后触发的。
注册安装和卸载处理程序
在智能体的初始化过程中,为 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 是一个字符串,在安装智能体时设置为 "add",在卸载智能体时设置为 "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 是一个字符串,在安装智能体时设置为 'add',在卸载智能体时设置为 '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 智能体可以针对单个用户提示发送多条独立消息。 要实现这一点,请在单个对话轮次中多次调用 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 处理请求期间,智能体会每四秒左右循环发送输入指示器,以保持 ... 动画的持续显示:
# 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) { }
}