重要
需要成为 Frontier 预览计划的一部分,才能获得 抢先体验Microsoft Agent 365。 Frontier将你直接连接到Microsoft最新的人工智能创新。 Frontier 预览版受客户协议中现有预览条款的约束。 由于这些功能仍在开发中,其可用性和功能可能会随时间而变化。
通过使用代理 365 SDK,代理可以处理平台活动事件,例如安装和卸载,并在单个轮次内发送多个离散消息。 本文介绍在代理处理请求时响应用户并确保用户知情的关键模式。
处理代理安装和卸载事件
当用户在 Teams 或其他代理 365 托管频道中安装或卸载代理时,平台会发送活动 InstallationUpdate (也称为 agentInstanceCreated 事件)。 代理可以处理这些事件,以在安装时发送欢迎消息,并在卸载时发送告别消息。
| Action |
说明 |
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 生产代理 365 流量,并具有可选的代理身份验证处理程序,一次用于 isAgenticOnly: false 使用 Agents Playground 或 WebChat 进行本地测试。
发送多条消息
代理 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)
此示例通过在 LLM 响应之前发送即时确认来演示 (on_message) 中的此模式。
// 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);
此示例演示了此模式(OnMessageAsyncMyAgent.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) { }
}