Skip to content

Conversation

@heyitsaamir
Copy link
Collaborator

This is a python adaptation of microsoft/teams.ts#424.

Separate activity sending from HTTP transport layer

The previous architecture tightly coupled HTTP transport concerns with activity sending logic:

Previous Architecture:

HttpPlugin (transport) → implements ISender (sending)
                      → has send() method (creates new Client per call)
                      → has createStream() method
                      → knows about Activity protocol details

ActivityContext → depends on ISender plugin
               → cannot work without transport plugin
               → conflates transport and sending concerns

There are a few issues with this:

  • HttpPlugin created NEW Client instances on every send() call. So there's really no benefit of this logic being in the "httpclient" plugin.
  • Transport plugins (HttpPlugin) were forced to implement send/createStream. This makes it more cumbersome to build your own HttpPlugin with your own servier.
  • Users couldn't "bring their own server" without implementing ISender
  • ActivityContext was tightly coupled to plugin architecture. ("Sender" was coupled with an activity, without any necessary benefits.)

New Architecture

HttpPlugin (transport) → only handles HTTP server/routing/auth
                      → emits CoreActivity (minimal protocol knowledge)
                      → just passes body payload to app

ActivitySender (NEW)  → dedicated class for sending activities
                     → receives injected, reusable Client
                     → handles all send/stream logic
                     → private to App class

ActivityContext       → uses ActivitySender now, which is not a plugin

In this PR, I am mainly decoupling responsibilities of HttpPlugin from being BOTH a listener AND a sender, to being just a listener. The sender bit is now separated to a different ActivitySender class. Other than better code organization, the main thing this lets us do is not require the app to run to be able to send proactive messages. This is a huge plus point because now the App can be used in scenarios where it doesn't necessarily need to listen to incoming messages (like agentic notifications!)

Copilot AI review requested due to automatic review settings January 26, 2026 16:26
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 pull request simplifies the HttpPlugin by separating activity sending from HTTP transport concerns. The changes adapt the TypeScript PR #424 to Python, introducing a cleaner architectural separation.

Changes:

  • Introduces a new ActivitySender class dedicated to sending activities, removing this responsibility from HttpPlugin
  • Removes the Sender plugin interface and related coupling between transport and sending logic
  • Updates ActivityContext and other components to use ActivitySender instead of depending on plugins
  • Adds app.initialize() method to enable proactive messaging without starting an HTTP server
  • Includes a new proactive-messaging example demonstrating serverless message sending

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
uv.lock Adds proactive-messaging example package to workspace
packages/devtools/src/microsoft_teams/devtools/devtools_plugin.py Updates to use PluginBase instead of Sender, converts Activity to CoreActivity
packages/botbuilder/src/microsoft_teams/botbuilder/botbuilder_plugin.py Updates signature to accept CoreActivity parameter, calls parent with new signature
packages/apps/src/microsoft_teams/apps/routing/activity_context.py Replaces Sender dependency with ActivitySender
packages/apps/src/microsoft_teams/apps/plugins/sender.py Removes the Sender abstract class entirely
packages/apps/src/microsoft_teams/apps/plugins/plugin_error_event.py Removes sender field from error events
packages/apps/src/microsoft_teams/apps/plugins/plugin_base.py Removes send() and create_stream() methods from base plugin
packages/apps/src/microsoft_teams/apps/plugins/plugin_activity_sent_event.py Removes sender field from activity sent events
packages/apps/src/microsoft_teams/apps/plugins/plugin_activity_response_event.py Removes sender field from activity response events
packages/apps/src/microsoft_teams/apps/plugins/plugin_activity_event.py Removes sender field from plugin activity events
packages/apps/src/microsoft_teams/apps/plugins/init.py Removes Sender from exports
packages/apps/src/microsoft_teams/apps/http_plugin.py Removes sending logic, updates to work with CoreActivity, changes to PluginBase
packages/apps/src/microsoft_teams/apps/events/types.py Adds CoreActivity model, removes sender from event dataclasses
packages/apps/src/microsoft_teams/apps/events/init.py Exports CoreActivity
packages/apps/src/microsoft_teams/apps/contexts/function_context.py Uses ActivitySender instead of HttpPlugin for sending
packages/apps/src/microsoft_teams/apps/app_process.py Updates to work without Sender, converts CoreActivity back to Activity
packages/apps/src/microsoft_teams/apps/app_plugins.py Removes Sender dependency from plugin injection
packages/apps/src/microsoft_teams/apps/app_events.py Updates event management to work without sender parameter
packages/apps/src/microsoft_teams/apps/app.py Adds ActivitySender, implements initialize() method, updates send() to use ActivitySender
packages/apps/src/microsoft_teams/apps/activity_sender.py New class handling activity sending and streaming
examples/proactive-messaging/src/main.py Example demonstrating proactive messaging without server
examples/proactive-messaging/pyproject.toml Package configuration for proactive messaging example
examples/proactive-messaging/README.md Documentation for proactive messaging example
Comments suppressed due to low confidence (1)

packages/apps/src/microsoft_teams/apps/http_plugin.py:317

  • The route handler signature has changed from parsing the request body internally to expecting core_activity: CoreActivity as a parameter. While FastAPI can automatically parse Pydantic models from request bodies, this pattern differs from the manual parsing approach used elsewhere in the codebase (e.g., in the func decorator where await r.json() is used).

Please verify that FastAPI correctly injects the CoreActivity parameter when the route is called. If this doesn't work as expected at runtime, consider either:

  1. Manually parsing the body: body = await request.json(); core_activity = CoreActivity.model_validate(body)
  2. Using FastAPI's Body annotation: core_activity: CoreActivity = Body(...)

Note that existing tests likely need updates to match this new signature.

    async def on_activity_request(self, core_activity: CoreActivity, request: Request, response: Response) -> Any:
        """Handle incoming Teams activity."""
        # Get validated token from middleware (if present - will be missing if skip_auth is True)
        if hasattr(request.state, "validated_token") and request.state.validated_token:
            token = request.state.validated_token
        else:
            token = cast(
                TokenProtocol,
                SimpleNamespace(
                    app_id="",
                    app_display_name="",
                    tenant_id="",
                    service_url=core_activity.service_url or "",
                    from_="azure",
                    from_id="",
                    is_expired=lambda: False,
                ),
            )

        activity_type = core_activity.type or "unknown"
        activity_id = core_activity.id or "unknown"

        self.logger.debug(f"Received activity: {activity_type} (ID: {activity_id})")
        self.logger.debug(f"Processing activity {activity_id} via handler...")

        # Process the activity
        result = await self._process_activity(core_activity, token)
        return self._handle_activity_response(response, result)

    def _setup_routes(self) -> None:
        """Setup FastAPI routes."""

        self.app.post("/api/messages")(self.on_activity_request)

Comment on lines 15 to 17
__all__ = [
"PluginBase",
"Sender",
"StreamerProtocol",
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The removal of Sender from the exports represents a breaking API change. External consumers importing Sender from microsoft_teams.apps will encounter import errors.

Additionally, the removal of the sender field from various event types (PluginErrorEvent, PluginActivitySentEvent, PluginActivityResponseEvent, etc.) and the removal of send() and create_stream() methods from PluginBase are breaking changes.

While these changes align with the architectural goals of separating concerns, consider documenting these breaking changes in release notes or providing a migration guide for users upgrading to this version.

Copilot uses AI. Check for mistakes.
from microsoft_teams.api.activities import SentActivity
from microsoft_teams.api.models import ConversationReference

if TYPE_CHECKING:
Copy link
Collaborator

Choose a reason for hiding this comment

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

nice! we get to remove these too lol

),
)

activity_type = core_activity.type or "unknown"
Copy link
Collaborator

Choose a reason for hiding this comment

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

if we hit one of these lines, should we be logging an error instead?

recipient=mock_account,
),
)
from microsoft_teams.apps.events import CoreActivity
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we add this to top level?

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