Skip to content

Improve dialog opening and card submitting semantics#222

Merged
lilyydu merged 26 commits into
mainfrom
aamirj/dialogs
Apr 1, 2026
Merged

Improve dialog opening and card submitting semantics#222
lilyydu merged 26 commits into
mainfrom
aamirj/dialogs

Conversation

@heyitsaamir

@heyitsaamir heyitsaamir commented Nov 24, 2025

Copy link
Copy Markdown
Collaborator

This PR attempts to improve the devex for dialog opening and card submitting semantics.

For opening a dialog, before:

@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]) -> None:
    """Handle message activities and show dialog launcher card."""

    # Create the launcher adaptive card using Python objects to demonstrate SubmitActionData
    # This tests that ms_teams correctly serializes to 'msteams'
    card = AdaptiveCard(version="1.4")
    card.body = [TextBlock(text="Select the examples you want to see!", size="Large", weight="Bolder")]

-    # Use SubmitActionData with ms_teams to test serialization
-    # SubmitActionData uses extra="allow" to accept custom fields
-    simple_form_data = SubmitActionData.model_validate({"opendialogtype": "simple_form"})
-    simple_form_data.ms_teams = TaskFetchSubmitActionData().model_dump()
-    card.actions = [
-        SubmitAction(title="Simple form test").with_data(simple_form_data)
-    ]

+   # Use OpenDialogData to create dialog open actions with clean API
+    card.actions = [
+        SubmitAction(title="Simple form test").with_data(OpenDialogData("simple_form"))
+    ]

    # Send the card as an attachment
    message = MessageActivityInput(text="Enter this form").add_card(card)
    await ctx.send(message)

- @app.on_dialog_open
+ @app.on_dialog_open("simple_form")
async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]):
    """Handle dialog open events for all dialog types."""
-     data: Optional[Any] = ctx.activity.value.data
-     dialog_type = data.get("opendialogtype") if data else None

-     if dialog_type == "simple_form":
        dialog_card = AdaptiveCard.model_validate(
            {
                "type": "AdaptiveCard",
                "version": "1.4",
                "body": [
                    {"type": "TextBlock", "text": "This is a simple form", "size": "Large", "weight": "Bolder"},
                    {
                        "type": "Input.Text",
                        "id": "name",
                        "label": "Name",
                        "placeholder": "Enter your name",
                        "isRequired": True,
                    },
                ],
                "actions": [
                    {"type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "simple_form"}}
                ],
            }
        )

        return InvokeResponse(
            body=TaskModuleResponse(
                task=TaskModuleContinueResponse(
                    value=CardTaskModuleTaskInfo(
                        title="Simple Form Dialog",
                        card=card_attachment(AdaptiveCardAttachment(content=dialog_card)),
                    )
                )
            )
        )

-     # Default return for unknown dialog types
-     return TaskModuleResponse(task=TaskModuleMessageResponse(value="Unknown dialog type"))

Notice how we have the opendialogtype that's kind of hidden away in the code. Then, in your on_dialog_open you have to pull the action out, and then handle it.
The two additions are:

  1. OpenDialogData (which basically abstracts away the ugliness in favor of a simple type).
  2. And now on_dialog_open accepts a dialog identifier to know when to trigger the handler.

Now, with submitting cards, we have a similar issue.

@app.on_dialog_open("simple_form")
async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]):
    """Handle dialog open events for all dialog types."""
      dialog_card = AdaptiveCard.model_validate(
          {
              "type": "AdaptiveCard",
              "version": "1.4",
              "body": [
                  {"type": "TextBlock", "text": "This is a simple form", "size": "Large", "weight": "Bolder"},
                  {
                      "type": "Input.Text",
                      "id": "name",
                      "label": "Name",
                      "placeholder": "Enter your name",
                      "isRequired": True,
                  },
              ],
-               "actions": [
-                   {"type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "simple_form"}}
-               ],
+                 # Alternative: Use SubmitActionData for cleaner action-based routing
+                 # SubmitAction(title="Submit").with_data(SubmitActionData("submit_simple_form"))
+                 {"type": "Action.Submit", "title": "Submit", "data": {"action": "submit_simple_form"}}

          }
      )

      return InvokeResponse(
          body=TaskModuleResponse(
              task=TaskModuleContinueResponse(
                  value=CardTaskModuleTaskInfo(
                      title="Simple Form Dialog",
                      card=card_attachment(AdaptiveCardAttachment(content=dialog_card)),
                  )
              )
          )
      )

  # Default return for unknown dialog types
  return TaskModuleResponse(task=TaskModuleMessageResponse(value="Unknown dialog type"))

- @app.on_dialog_submit
+ @app.on_dialog_submit("submit_simple_form")
async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]):
    """Handle dialog submit events for all dialog types."""
    data: Optional[Any] = ctx.activity.value.data
-     dialog_type = data.get("submissiondialogtype") if data else None

-     if dialog_type == "simple_form":
        name = data.get("name") if data else None
        await ctx.send(f"Hi {name}, thanks for submitting the form!")
        return TaskModuleResponse(task=TaskModuleMessageResponse(value="Form was submitted"))


-     return TaskModuleResponse(task=TaskModuleMessageResponse(value="Unknown submission type"))

We reserve a keyword "action" to indicate what handler needs to be called when the form is submitted. The keyword action was used to align with web standards.
This also extends to general card actions too:

    profile_card = AdaptiveCard(
        schema="http://adaptivecards.io/schemas/adaptive-card.json",
        body=[
            TextBlock(text="User Profile", weight="Bolder", size="Large"),
            TextInput(id="name").with_label("Name").with_value("John Doe"),
            TextInput(id="email", label="Email", value="[email protected]"),
            ToggleInput(title="Subscribe to newsletter").with_id("subscribe").with_value("false"),
            ActionSet(
                actions=[
                    ExecuteAction(title="Save")
+                     .with_data(SubmitActionData("save_profile", {"entity_id": "12345"}))
                    .with_associated_inputs("auto"),
                    OpenUrlAction(url="https://adaptivecards.microsoft.com").with_title("Learn More"),
                ]
            ),
        ],
    )

+ @app.on_card_execute_action("save_profile")
async def handle_save_profile(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse:
    """Handle profile save."""
    data = ctx.activity.value.action.data
    print("Received save_profile action data:", data)
    entity_id = data.get("entity_id")
    name = data.get("name", "Unknown")
    email = data.get("email", "No email")
    subscribe = data.get("subscribe", "false")
    age = data.get("age")
    location = data.get("location", "Not specified")

    response_text = f"Profile saved!\nName: {name}\nEmail: {email}\nSubscribed: {subscribe}"
    if entity_id:
        response_text += f"\nEntity ID: {entity_id}"
    if age:
        response_text += f"\nAge: {age}"
    if location != "Not specified":
        response_text += f"\nLocation: {location}"

    await ctx.send(response_text)
    return AdaptiveCardActionMessageResponse(
        status_code=200,
        type="application/vnd.microsoft.activity.message",
        value="Action processed successfully",
    )

PR Dependency Tree

This tree was auto-generated by Charcoal

heyitsaamir added a commit that referenced this pull request Nov 24, 2025
We wrap our AI functions such that we can call the AI plugins etc. 
But if there are no parameters, we call the function like `foo()`.
However, the wrapped function always has 1 positional argument. So
python throws. This fixes that bug.


#### PR Dependency Tree


* **PR #221** 👈
  * **PR #222**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
Base automatically changed from aamirj/fixWrapped to main November 24, 2025 17:46
Copilot AI review requested due to automatic review settings November 24, 2025 18:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces cleaner semantics for dialog opening and card action routing by implementing action-based routing patterns. Instead of manually extracting action identifiers from data dictionaries in handler logic, developers can now specify routing identifiers directly in the decorator, improving code clarity and maintainability.

Key Changes

  • Introduces OpenDialogData and SubmitActionData (aliased from EnhancedSubmitActionData) utility classes that abstract away the complexity of setting reserved keywords for routing
  • Enhances on_dialog_open, on_dialog_submit, and on_card_action decorators to accept optional routing identifiers, enabling specific handler matching
  • Fixes function handler wrapping in ChatPrompt to properly support functions with no parameters when used with plugins

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/cards/src/microsoft/teams/cards/utilities/open_dialog.py New utility class for creating dialog open action data with dialog_id routing
packages/cards/src/microsoft/teams/cards/utilities/submit_dialog.py New utility class for creating card submit action data with action routing keyword
packages/cards/src/microsoft/teams/cards/utilities/__init__.py Exports utility classes with convenient naming
packages/cards/src/microsoft/teams/cards/__init__.py Integrates utility classes into main cards package exports
packages/apps/src/microsoft/teams/apps/routing/activity_handlers.py Implements manual handler methods with routing support for dialogs and card actions
packages/apps/src/microsoft/teams/apps/routing/generated_handlers.py Removes auto-generated handlers that are now manually implemented
packages/apps/src/microsoft/teams/apps/routing/activity_route_configs.py Comments out route configs for manually implemented handlers
packages/apps/tests/test_dialog_routing.py Comprehensive tests for dialog routing with and without identifiers
packages/apps/tests/test_card_action_routing.py Comprehensive tests for card action routing with and without identifiers
packages/ai/src/microsoft/teams/ai/chat_prompt.py Fixes function handler wrapping to support parameter-less functions with plugins
packages/ai/tests/test_chat_prompt.py Adds test coverage for parameter-less functions with plugins
examples/dialogs/src/main.py Refactored to use new routing patterns and utility classes
examples/cards/src/main.py Refactored to use action-based routing with separate handlers
packages/apps/src/microsoft/teams/apps/http_plugin.py Minor documentation fix removing unused parameter from example

Comment thread packages/cards/src/microsoft/teams/cards/utilities/submit_dialog.py Outdated
Comment thread packages/cards/src/microsoft/teams/cards/utilities/open_dialog.py Outdated
Comment thread examples/cards/src/main.py Outdated

@lilyydu lilyydu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful! 🔥

Comment thread packages/apps/src/microsoft_teams/apps/routing/activity_handlers.py Outdated
Comment thread examples/cards/src/main.py
Comment thread examples/cards/src/main.py
Comment thread packages/cards/tests/test_core_serialization.py Outdated
Comment thread packages/cards/src/microsoft_teams/cards/utilities/__init__.py Outdated
Comment thread examples/cards/src/main.py
Comment thread packages/apps/src/microsoft_teams/apps/routing/activity_handlers.py
Comment thread packages/cards/src/microsoft_teams/cards/utilities/submit_action_data.py Outdated
Comment thread packages/cards/src/microsoft_teams/cards/utilities/__init__.py Outdated

@heyitsaamir heyitsaamir left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@corinagum corinagum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@lilyydu
lilyydu merged commit d981cca into main Apr 1, 2026
7 checks passed
@lilyydu
lilyydu deleted the aamirj/dialogs branch April 1, 2026 23:21
corinagum added a commit to microsoft/teams.ts that referenced this pull request Apr 7, 2026
## Summary

- Adds `OpenDialogData` class that extends `SubmitActionData` and
abstracts away the `msteams: { type: 'task/fetch' }` protocol detail for
opening dialogs. Takes a `dialogId` and optional extra data.
- Adds `SubmitData` class that extends `SubmitActionData` with a
convenience constructor accepting an `action` identifier for handler
routing. Aligns with [web form `action`
attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#action).
- Above matches Python SDK implementation (microsoft/teams.py#222).
- Updates dialogs and cards examples to use the new classes.
- Updates dialogs example handler to read `dialog_id` (the reserved
field set by `OpenDialogData`) instead of `opendialogtype`.
- Regenerated core.ts from latest codegen: non-required hero constructor
params now use `?` only when type-safe (arrays, `any`, or types that
include `undefined`). Required hero params stay required.
- New generated types: CollabStageInvokeDataValue, OpenUrlDialogAction,
PopoverAction, ProgressBar, ProgressRing, Resources, StringResource,
TabInfo.


**Note:** Identifier-based handler routing (e.g. `on('dialog.open',
'simple_form')`) is not included in this PR and will be scoped
separately.

## Test plan

- [x] 0 TypeScript compilation errors
- [x] 18/18 cards package tests pass
- [x] E2E: dialogs example tested against Teams (dialog open, form
submit, multi-step)
- [x] E2E: cards example tested against Teams (submit_basic,
save_profile, submit_feedback)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
corinagum added a commit to microsoft/teams.net that referenced this pull request Apr 7, 2026
#410)

## Summary

- Adds `OpenDialogData` class that extends `SubmitActionData` and
abstracts away the `msteams: { type: 'task/fetch' }` protocol detail for
opening dialogs. Takes a `dialogId` and optional extra data.
- Adds `SubmitData` class that extends `SubmitActionData` with a
convenience constructor accepting an `action` identifier for handler
routing. Aligns with [web form `action`
attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#action).
- Updates dialogs and cards samples to use the new classes.
- Updates dialogs sample handler to read `dialog_id` (the reserved field
set by `OpenDialogData`) instead of `opendialogtype`.
- Regenerated Core.cs from latest codegen: `params IList<T>` replaced
with `params T[]` for .NET 8 compatibility, `IList<T>` overloads added
for backward compat, parameterless constructors added for
System.Text.Json deserialization.
- New generated types: CollabStageInvokeDataValue, OpenUrlDialogAction,
PopoverAction, ProgressBar, ProgressRing, Resources, StringResource,
TabInfo.
- Matches Python SDK implementation (microsoft/teams.py#222).

**Note:** Identifier-based handler routing is not included in this PR
and will be scoped separately.

## Test plan

- [x] Build succeeds on .NET 8 and .NET 10 (0 errors)
- [x] 588/588 tests pass on .NET 10
- [x] 11/11 cards tests pass on .NET 8
- [x] E2E: dialogs sample tested against Teams on .NET 8
- [x] E2E: cards sample tested against Teams on .NET 8

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants