Skip to content

Conversation

@heyitsaamir
Copy link
Collaborator

@heyitsaamir heyitsaamir commented Nov 24, 2025

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
Copy link
Contributor

Copilot AI left a comment

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

Copy link
Collaborator

@lilyydu lilyydu left a comment

Choose a reason for hiding this comment

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

Beautiful! 🔥

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.

3 participants