Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
146ee97
Initial port of Dialogs from Bot Framework Python
rodrigobr-msft Apr 10, 2026
c21e8c8
Merge branch 'main' of https://github.com/microsoft/Agents-for-python…
rodrigobr-msft Apr 10, 2026
b7e8751
Cleaning up Dialogs code and preparing the Dialogs sample
rodrigobr-msft Apr 16, 2026
50c6dc4
Initial working Dialogues sample
rodrigobr-msft Apr 16, 2026
f5846df
Updating dev setup scripts to install microsoft-agents-hosting-dialog…
rodrigobr-msft Apr 16, 2026
addbc6b
Improvement to dialog test coverage
rodrigobr-msft Apr 16, 2026
f7db6c4
Reorganizing test_samples directory for ActivityHandler-based samples
rodrigobr-msft Apr 17, 2026
df94811
Ported custom dialogs sample
rodrigobr-msft Apr 17, 2026
c9bae1c
Porting complex dialogs
rodrigobr-msft Apr 17, 2026
1e19661
Replacing 'bot' with 'agent'
rodrigobr-msft Apr 17, 2026
40a3830
Adding microsoft-agents-hosting-dialogs install in azdo and github pi…
rodrigobr-msft Apr 17, 2026
6555ddd
Adding more test cases to dialogs
rodrigobr-msft Apr 17, 2026
76375dd
Improving integration test coverage of dialogs
rodrigobr-msft Apr 17, 2026
8a3af5d
Removing .infra.json file
rodrigobr-msft Apr 17, 2026
e25706f
Addressing PR comments
rodrigobr-msft Apr 17, 2026
3098e63
Updates to test samples
rodrigobr-msft Apr 17, 2026
d6ca148
Updating dependencies
rodrigobr-msft Apr 17, 2026
09a2b9f
Addressing PR comments
rodrigobr-msft Apr 17, 2026
4bd5c81
Merge branch 'main' into users/robrandao/dialogs
rodrigobr-msft Apr 17, 2026
57b3ebe
Changes to test samples
rodrigobr-msft Apr 17, 2026
eb4c79a
Small tweaks to dialog code imports
rodrigobr-msft Apr 20, 2026
f860354
Addressing final PR comments
rodrigobr-msft Apr 20, 2026
453ecc4
Formatting
rodrigobr-msft Apr 20, 2026
370d2fc
Fixing OAuthPrompt port
rodrigobr-msft Apr 21, 2026
12361be
Fixes to OAuthPrompt port
rodrigobr-msft Apr 21, 2026
d36d732
Initial work porting bot-authentication sample
rodrigobr-msft Apr 21, 2026
6d09ccd
Working auth samples for dialogs
rodrigobr-msft Apr 22, 2026
99f04e4
Updating test samples
rodrigobr-msft Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .azdo/ci-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ steps:
python -m pip install ./dist/microsoft_agents_authentication_msal*.whl
python -m pip install ./dist/microsoft_agents_copilotstudio_client*.whl
python -m pip install ./dist/microsoft_agents_hosting_aiohttp*.whl
python -m pip install ./dist/microsoft_agents_hosting_dialogs*.whl
python -m pip install ./dist/microsoft_agents_hosting_teams*.whl
python -m pip install ./dist/microsoft_agents_storage_blob*.whl
python -m pip install ./dist/microsoft_agents_storage_cosmos*.whl
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
python -m pip install ./dist/microsoft_agents_authentication_msal*.whl
python -m pip install ./dist/microsoft_agents_copilotstudio_client*.whl
python -m pip install ./dist/microsoft_agents_hosting_aiohttp*.whl
python -m pip install ./dist/microsoft_agents_hosting_dialogs*.whl
python -m pip install ./dist/microsoft_agents_hosting_teams*.whl
python -m pip install ./dist/microsoft_agents_storage_blob*.whl
python -m pip install ./dist/microsoft_agents_storage_cosmos*.whl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
AiohttpScenario,
)

from .activity_handler_scenario import (
ActivityHandlerEnvironment,
ActivityHandlerScenario,
)

from .transcript_formatter import (
DetailLevel,
ConversationTranscriptFormatter,
Expand Down Expand Up @@ -88,11 +93,13 @@
"Unset",
"AgentEnvironment",
"AiohttpScenario",
"ActivityHandlerEnvironment",
"ActivityHandlerScenario",
"ScenarioEntry",
"scenario_registry",
"load_scenarios",
"DetailLevel",
"ConversationTranscriptFormatter",
"ActivityTranscriptFormatter",
"TranscriptFormatter",
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""ActivityHandlerScenario - In-process testing for ActivityHandler-based agents.

Provides a scenario that hosts an ActivityHandler-based agent within the test
process using aiohttp, enabling integration testing of dialog-heavy agents.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Awaitable
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from aiohttp.web import Application, Request, Response, middleware
from aiohttp.test_utils import TestServer

from microsoft_agents.hosting.core import (
ActivityHandler,
ConversationState,
UserState,
MemoryStorage,
Storage,
)
from microsoft_agents.hosting.core.authorization import ClaimsIdentity
from microsoft_agents.hosting.aiohttp import CloudAdapter

from .core import (
AiohttpCallbackServer,
_AiohttpClientFactory,
ClientFactory,
Scenario,
ScenarioConfig,
)


@dataclass
class ActivityHandlerEnvironment:
"""Components available when an ActivityHandler-based agent is running.

Attributes:
storage: In-memory state storage shared by all state objects.
conversation_state: Conversation-scoped state accessor.
user_state: User-scoped state accessor.
adapter: CloudAdapter instance (anonymous auth, no real credentials).
handler: The ActivityHandler instance under test.
"""

storage: Storage
conversation_state: ConversationState
user_state: UserState
adapter: CloudAdapter
handler: ActivityHandler


class ActivityHandlerScenario(Scenario):
"""Test scenario for ActivityHandler-based agents.

Use this scenario when your agent extends ``ActivityHandler`` rather than
``AgentApplication``. The scenario creates ``MemoryStorage``,
``ConversationState``, ``UserState``, and a ``CloudAdapter`` (no auth), then
wires them up and hosts the handler on an ephemeral aiohttp test server.

Example::

def create_agent(conv_state, user_state, storage):
dialog = UserProfileDialog(user_state)
return DialogAgent(conv_state, user_state, dialog)

scenario = ActivityHandlerScenario(create_agent)
async with scenario.client() as client:
await client.send("hello", wait=1.0)
client.expect().that_for_any(text="~Please enter")

:param create_handler: A callable that receives ``(conv_state, user_state,
storage)`` and returns an ``ActivityHandler`` (sync or async factory).
:param config: Optional scenario configuration.
"""

def __init__(
self,
create_handler: Callable[
[ConversationState, UserState, Storage],
ActivityHandler | Awaitable[ActivityHandler],
],
config: ScenarioConfig | None = None,
) -> None:
super().__init__(config)
if not create_handler:
raise ValueError("create_handler must be provided.")
self._create_handler = create_handler
self._env: ActivityHandlerEnvironment | None = None

@property
def environment(self) -> ActivityHandlerEnvironment:
"""Get the agent environment (only valid while the scenario is running)."""
if self._env is None:
raise RuntimeError(
"Agent environment not available. Is the scenario running?"
)
return self._env

async def _setup(self) -> None:
"""Create storage, state objects, adapter, and handler."""
storage = MemoryStorage()
conv_state = ConversationState(storage)
user_state = UserState(storage)
adapter = CloudAdapter()

result = self._create_handler(conv_state, user_state, storage)
if hasattr(result, "__await__"):
handler = await result # type: ignore[misc]
else:
handler = result # type: ignore[assignment]

self._env = ActivityHandlerEnvironment(
storage=storage,
conversation_state=conv_state,
user_state=user_state,
adapter=adapter,
handler=handler,
)

def _build_app(self) -> Application:
"""Build the aiohttp Application with an /api/messages POST route.

An anonymous-identity middleware is applied so that the adapter's
auth pipeline succeeds without real credentials. The middleware sets
``request["claims_identity"]`` to an unauthenticated anonymous
``ClaimsIdentity``, which causes the adapter to skip token acquisition
and use an empty bearer token for outbound calls (acceptable for
in-process integration tests where ``serviceUrl`` is the local
callback server).
"""
assert self._env is not None
agent = self._env.handler
adapter = self._env.adapter

_anonymous = ClaimsIdentity({}, False, authentication_type="Anonymous")

@middleware
async def anonymous_auth(request: Request, handler):
request["claims_identity"] = _anonymous
return await handler(request)

app = Application(middlewares=[anonymous_auth])

async def entry_point(request: Request) -> Response:
return await adapter.process(request, agent)

app.router.add_post("/api/messages", entry_point)
return app

@asynccontextmanager
async def run(self) -> AsyncIterator[ClientFactory]:
"""Start the scenario and yield a client factory.

The agent server binds to an ephemeral port; the callback server
(which receives ``send_activity`` calls from the handler) uses the
port from ``ScenarioConfig.callback_server_port`` (default 9378).
"""
await self._setup()
app = self._build_app()

callback_server = AiohttpCallbackServer(self._config.callback_server_port)

async with callback_server.listen() as transcript:
# port=None → aiohttp picks an available ephemeral port
async with TestServer(app, port=None) as server:
agent_endpoint = f"http://127.0.0.1:{server.port}/api/messages"

factory = _AiohttpClientFactory(
agent_endpoint=agent_endpoint,
response_endpoint=callback_server.service_endpoint,
sdk_config={},
default_config=self._config.client_config,
transcript=transcript,
)

try:
yield factory
finally:
await factory.cleanup()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""BookingDialog — collects origin, destination, and passenger count for a flight booking.

Steps:
1. origin_step — TextPrompt: departure city
2. destination_step — TextPrompt: arrival city
3. passengers_step — NumberPrompt (validator: 1 <= n <= 9) with retry
4. confirm_step — ConfirmPrompt: shows summary, asks to confirm
5. finalize_step — confirms or cancels the booking
"""

from microsoft_agents.hosting.core import MessageFactory
from microsoft_agents.hosting.dialogs import (
ComponentDialog,
DialogTurnResult,
WaterfallDialog,
WaterfallStepContext,
)
from microsoft_agents.hosting.dialogs.prompts import (
ConfirmPrompt,
NumberPrompt,
PromptOptions,
PromptValidatorContext,
TextPrompt,
)


class BookingDialog(ComponentDialog):
"""ComponentDialog that collects a simple flight booking."""

def __init__(self) -> None:
super().__init__(BookingDialog.__name__)

self.add_dialog(
WaterfallDialog(
WaterfallDialog.__name__,
[
self._origin_step,
self._destination_step,
self._passengers_step,
self._confirm_step,
self._finalize_step,
],
)
)
self.add_dialog(TextPrompt(TextPrompt.__name__))
self.add_dialog(
NumberPrompt(NumberPrompt.__name__, self._passengers_validator)
)
self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))

self.initial_dialog_id = WaterfallDialog.__name__

# ------------------------------------------------------------------
# Steps
# ------------------------------------------------------------------

async def _origin_step(self, step: WaterfallStepContext) -> DialogTurnResult:
return await step.prompt(
TextPrompt.__name__,
PromptOptions(prompt=MessageFactory.text("Where are you flying from?")),
)

async def _destination_step(self, step: WaterfallStepContext) -> DialogTurnResult:
step.values["origin"] = step.result
return await step.prompt(
TextPrompt.__name__,
PromptOptions(prompt=MessageFactory.text("Where are you flying to?")),
)

async def _passengers_step(self, step: WaterfallStepContext) -> DialogTurnResult:
step.values["destination"] = step.result
return await step.prompt(
NumberPrompt.__name__,
PromptOptions(
prompt=MessageFactory.text("How many passengers? (1-9)"),
retry_prompt=MessageFactory.text(
"Please enter a number between 1 and 9."
),
),
)

async def _confirm_step(self, step: WaterfallStepContext) -> DialogTurnResult:
step.values["passengers"] = int(step.result)
origin = step.values["origin"]
dest = step.values["destination"]
pax = step.values["passengers"]
summary = f"Route: {origin} → {dest} for {pax} passenger(s). Confirm?"
return await step.prompt(
ConfirmPrompt.__name__,
PromptOptions(prompt=MessageFactory.text(summary)),
)

async def _finalize_step(self, step: WaterfallStepContext) -> DialogTurnResult:
if step.result:
origin = step.values["origin"]
dest = step.values["destination"]
pax = step.values["passengers"]
await step.context.send_activity(
MessageFactory.text(
f"Booking confirmed: {origin} to {dest} for {pax} passenger(s)."
)
)
else:
await step.context.send_activity(
MessageFactory.text("Booking cancelled.")
)
return await step.end_dialog()

# ------------------------------------------------------------------
# Validators
# ------------------------------------------------------------------

@staticmethod
async def _passengers_validator(pc: PromptValidatorContext) -> bool:
return pc.recognized.succeeded and 1 <= int(pc.recognized.value) <= 9
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""DialogAgent — generic ActivityHandler that drives a single root Dialog."""

from microsoft_agents.hosting.core import (
ActivityHandler,
ConversationState,
TurnContext,
UserState,
)
from microsoft_agents.hosting.dialogs import Dialog, DialogSet, DialogTurnStatus


class DialogAgent(ActivityHandler):
"""ActivityHandler that routes every message turn through a dialog."""

def __init__(
self,
conversation_state: ConversationState,
user_state: UserState,
dialog: Dialog,
) -> None:
super().__init__()
if conversation_state is None:
raise TypeError("conversation_state is required")
if user_state is None:
raise TypeError("user_state is required")
if dialog is None:
raise TypeError("dialog is required")

self._conversation_state = conversation_state
self._user_state = user_state
self._dialog = dialog
self._dialog_state = conversation_state.create_property("DialogState")

async def on_turn(self, turn_context: TurnContext) -> None:
await super().on_turn(turn_context)
await self._conversation_state.save(turn_context)
await self._user_state.save(turn_context)

async def on_message_activity(self, turn_context: TurnContext) -> None:
dialog_set = DialogSet(self._dialog_state)
dialog_set.add(self._dialog)

dc = await dialog_set.create_context(turn_context)
results = await dc.continue_dialog()
if results.status == DialogTurnStatus.Empty:
await dc.begin_dialog(self._dialog.id)
Loading
Loading