Revert "remove devtools (#381)"#409
Conversation
This reverts commit 2192aad.
There was a problem hiding this comment.
Pull request overview
Reverts the prior removal of the DevTools package, restoring it as a workspace member and re-integrating it into examples and tooling so developers can run a local DevTools UI/API alongside sample apps.
Changes:
- Re-add
microsoft-teams-devtoolsto the workspace (packaging, lockfile, pyright paths). - Restore DevTools plugin implementation (FastAPI + websocket + Bot Framework-style
/v3/...routes) and include its web UI assets. - Update examples and repo docs/tooling to reference and use DevTools.
Reviewed changes
Copilot reviewed 36 out of 52 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Adds devtools as a workspace member and dependency; reverts several pinned versions as part of the revert. |
pyrightconfig.json |
Adds packages/devtools/src to Pyright extraPaths. |
pyproject.toml |
Re-adds microsoft-teams-devtools as a workspace dependency. |
packages/devtools/tests/test_devtools_plugin.py |
Adds initial tests for DevToolsPlugin env guard and basic API (add_page, callbacks). |
packages/devtools/src/microsoft_teams/devtools/web/index.html |
DevTools UI entrypoint HTML referencing bundled assets. |
packages/devtools/src/microsoft_teams/devtools/web/icon.png |
DevTools UI icon asset. |
packages/devtools/src/microsoft_teams/devtools/web/assets/index-DUmBwYhV.css |
Bundled CSS asset for DevTools UI. |
packages/devtools/src/microsoft_teams/devtools/web/assets/FluentSystemIcons-Light-eu0dDZrh.woff2 |
Bundled font asset for DevTools UI. |
packages/devtools/src/microsoft_teams/devtools/web/assets/FluentSystemIcons-Light-DzHdd4FE.woff |
Bundled font asset for DevTools UI. |
packages/devtools/src/microsoft_teams/devtools/routes/v3/router.py |
Adds v3 route root router composition. |
packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/router.py |
Adds conversations router composition under v3. |
packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/router.py |
Adds activities router with POST endpoint. |
packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/create.py |
Implements activity creation endpoint + forwarding into app processing. |
packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/__init__.py |
Exports activities route helpers. |
packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/__init__.py |
Exports conversations router. |
packages/devtools/src/microsoft_teams/devtools/routes/v3/__init__.py |
Exports v3 router and submodule exports. |
packages/devtools/src/microsoft_teams/devtools/routes/router.py |
Adds top-level router composition under /v3. |
packages/devtools/src/microsoft_teams/devtools/routes/context.py |
Introduces RouteContext dataclass passed through routers/handlers. |
packages/devtools/src/microsoft_teams/devtools/routes/__init__.py |
Exports routes API and combines submodule exports. |
packages/devtools/src/microsoft_teams/devtools/page.py |
Adds Page dataclass for custom DevTools pages. |
packages/devtools/src/microsoft_teams/devtools/event.py |
Adds DevTools websocket event models and discriminated union. |
packages/devtools/src/microsoft_teams/devtools/devtools_plugin.py |
Core DevTools plugin (FastAPI server, websocket broadcast, route wiring). |
packages/devtools/src/microsoft_teams/devtools/__init__.py |
Package exports (DevToolsPlugin, Page) and logging NullHandler. |
packages/devtools/src/microsoft/teams/devtools/__init__.py |
Back-compat shim for deprecated microsoft.teams.devtools imports. |
packages/devtools/pyproject.toml |
DevTools package metadata and dependencies. |
packages/devtools/README.md |
DevTools package documentation and basic usage snippet. |
examples/tab/src/main.py |
Enables DevToolsPlugin by default for the tab example. |
examples/tab/pyproject.toml |
Adds microsoft-teams-devtools dependency to tab example. |
examples/echo/src/main.py |
Enables DevToolsPlugin by default for the echo example. |
examples/echo/pyproject.toml |
Adds microsoft-teams-devtools dependency to echo example. |
examples/botbuilder/src/main.py |
Adds DevToolsPlugin to the botbuilder example plugin list. |
examples/botbuilder/pyproject.toml |
Adds devtools dependency + workspace source for botbuilder example. |
examples/a2a-test/src/main.py |
Adds DevToolsPlugin to the A2A test example’s plugin list. |
examples/a2a-test/README.md |
Updates usage text to reference DevTools for the client. |
RELEASE.md |
Notes devtools exclusion from publishing (documentation update). |
README.md |
Adds devtools to the package list. |
CLAUDE.md |
Documents devtools as a package in the monorepo. |
.github/scripts/analyze_issue.py |
Adds devtools to the package list used by issue tooling. |
.github/copilot-instructions.md |
Updates validation instructions to include devtools port/UI endpoint. |
.azdo/publish.yml |
Installs devtools editable in the publish pipeline environment setup. |
| logger.error(err) | ||
| raise HTTPException(status_code=500, detail=str(err)) from err |
There was a problem hiding this comment.
The exception handler logs only logger.error(err) (no stack trace) and then returns a 500 with detail=str(err), which can leak internal exception text to the client. Consider using logger.exception(...) and returning a generic error message (or omit detail) while keeping the detailed info in logs.
| logger.error(err) | |
| raise HTTPException(status_code=500, detail=str(err)) from err | |
| logger.exception("Failed to create activity") | |
| raise HTTPException(status_code=500, detail="Internal server error") from err |
| DevToolsActivityReceivedEvent | ||
| | DevToolsActivitySendingEvent | ||
| | DevToolsActivitySentEvent | ||
| | DevToolsActivityErrorEvent |
There was a problem hiding this comment.
The discriminated union is declared as Union[ A | B | C ], which is inconsistent with the repo’s other Pydantic discriminated unions (they use Union[A, B, C]). Using | inside Union[...] is redundant and can confuse type checkers / tooling; consider switching to the comma-separated Union[...] form for consistency.
| DevToolsActivityReceivedEvent | |
| | DevToolsActivitySendingEvent | |
| | DevToolsActivitySentEvent | |
| | DevToolsActivityErrorEvent | |
| DevToolsActivityReceivedEvent, | |
| DevToolsActivitySendingEvent, | |
| DevToolsActivitySentEvent, | |
| DevToolsActivityErrorEvent, |
| > **Note:** The `devtools` package is excluded from publishing. The pipeline filters out packages matching the `ExcludePackageFolders` variable. Prerelease versions are tagged `next` on PyPI; stable versions are tagged `latest`. | ||
|
|
There was a problem hiding this comment.
This note says the devtools package is excluded from publishing, but .azdo/publish.yml defaults ExcludePackageFolders to empty (no exclusions) and this PR also adds packages/devtools to the build environment. If devtools must be excluded, consider enforcing it in the pipeline config (e.g., set ExcludePackageFolders to include devtools by default) or clarify where that exclusion is configured.
| async def emit_activity_to_sockets(self, event: DevToolsActivityEvent): | ||
| data = event.model_dump(mode="json", exclude_none=True) | ||
| for socket_id, websocket in self.sockets.items(): | ||
| try: | ||
| await websocket.send_json(data) | ||
| except WebSocketDisconnect: | ||
| logger.debug(f"WebSocket {socket_id} disconnected") | ||
| del self.sockets[socket_id] | ||
|
|
There was a problem hiding this comment.
emit_activity_to_sockets mutates self.sockets (deletes entries) while iterating over self.sockets.items(), which can raise RuntimeError: dictionary changed size during iteration and also risks KeyError elsewhere (e.g., on_socket_connection finally block). Consider iterating over a snapshot (e.g., list(self.sockets.items())) and collecting disconnected socket IDs to remove after the loop (or use pop(..., None)).
| async def process(token: TokenProtocol, activity: Activity): | ||
| response_future = asyncio.get_event_loop().create_future() | ||
| if activity.id: | ||
| self.pending[activity.id] = response_future |
There was a problem hiding this comment.
process() always awaits response_future, but only stores the future in self.pending when activity.id is truthy. If an incoming activity has a missing/empty id, nothing will ever resolve the future (since on_activity_response also keys off event.activity.id), causing the request to hang indefinitely. Consider ensuring an ID is always present (generate one when missing) or falling back to returning await self.on_activity_event(...) directly.
| core_activity = CoreActivity.model_validate(activity_dict) | ||
| result = self.on_activity_event(ActivityEvent(body=core_activity, token=token)) | ||
| # If the handler is a coroutine, schedule it | ||
| if asyncio.iscoroutine(result): | ||
| asyncio.create_task(result) | ||
| except Exception as error: |
There was a problem hiding this comment.
self.on_activity_event(...) is scheduled via asyncio.create_task but never awaited; if the task raises, the exception won't propagate here and response_future may never be completed, leaving the HTTP request hanging. Also, asyncio.get_event_loop() is discouraged in async code; prefer asyncio.get_running_loop() for creating futures/tasks.
| router.include_router(get_router(RouteContext(port=self._port, process=process))) | ||
| self.app.include_router(router) | ||
|
|
||
| config = uvicorn.Config(app=self.app, host="0.0.0.0", port=self._port, log_level="info") |
There was a problem hiding this comment.
DevTools is explicitly marked as insecure, but the server is configured to listen on 0.0.0.0, which exposes the DevTools UI + websocket/API to the local network by default. Consider defaulting to 127.0.0.1 (or making the host configurable with a safe default) to reduce accidental exposure.
| config = uvicorn.Config(app=self.app, host="0.0.0.0", port=self._port, log_level="info") | |
| devtools_host = os.getenv("MICROSOFT_TEAMS_DEVTOOLS_HOST", "127.0.0.1").strip() or "127.0.0.1" | |
| config = uvicorn.Config(app=self.app, host=devtools_host, port=self._port, log_level="info") |
| body = await request.json() | ||
| channel_data = body.get("channelData", {}) | ||
| id = channel_data.get("streamId", str(uuid4())) | ||
|
|
There was a problem hiding this comment.
channel_data.get("streamId", ...) and body.get("id", ...) will return None if the key exists but is explicitly set to null, which can lead to returning {id: null} and can also trigger hangs upstream if activity.id ends up unset. Prefer using an or fallback (e.g., channel_data.get("streamId") or ...) and avoid naming the variable id (shadows the built-in).
This reverts commit 2192aad.