-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Make FastMCPToolset work with DBOS #3540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
174be7d
bump dbos version in uv.lock
qianl15 a6d16a6
initial dbos fastmcp test
qianl15 fdb73b1
DBOS-ify FastMCPToolset
qianl15 856f23b
fix tests
qianl15 ac05955
fix MCPServer, update tests
qianl15 09c21bd
coverage
qianl15 621cb3a
Merge branch 'main' into qian/dbos-fastmcp-toolset
qianl15 f333a65
Merge branch 'main' into qian/dbos-fastmcp-toolset
qianl15 20787ec
Merge branch 'main' into qian/dbos-fastmcp-toolset
DouweM 61a9c1f
Merge branch 'main' into qian/dbos-fastmcp-toolset
qianl15 0257c4c
Address review comments
qianl15 32a8412
refactor common mcp implementation
qianl15 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
pydantic_ai_slim/pydantic_ai/durable_exec/dbos/_fastmcp_toolset.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from abc import ABC | ||
| from collections.abc import Callable | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from dbos import DBOS | ||
| from typing_extensions import Self | ||
|
|
||
| from pydantic_ai import AbstractToolset, ToolsetTool, WrapperToolset | ||
| from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition | ||
| from pydantic_ai.toolsets.fastmcp import FastMCPToolset | ||
|
|
||
| from ._utils import StepConfig | ||
|
|
||
| if TYPE_CHECKING: | ||
| from pydantic_ai.mcp import ToolResult | ||
|
|
||
|
|
||
| class DBOSFastMCPToolset(WrapperToolset[AgentDepsT], ABC): | ||
qianl15 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """A wrapper for FastMCPToolset that integrates with DBOS, turning call_tool and get_tools to DBOS steps.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| wrapped: FastMCPToolset[AgentDepsT], | ||
| *, | ||
| step_name_prefix: str, | ||
| step_config: StepConfig, | ||
| ): | ||
| super().__init__(wrapped) | ||
| self._step_config = step_config or {} | ||
| self._step_name_prefix = step_name_prefix | ||
| id_suffix = f'__{wrapped.id}' if wrapped.id else '' | ||
| self._name = f'{step_name_prefix}__fastmcp_toolset{id_suffix}' | ||
qianl15 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Wrap get_tools in a DBOS step. | ||
| @DBOS.step( | ||
| name=f'{self._name}.get_tools', | ||
| **self._step_config, | ||
| ) | ||
| async def wrapped_get_tools_step( | ||
| ctx: RunContext[AgentDepsT], | ||
| ) -> dict[str, ToolDefinition]: | ||
| # Need to return a serializable dict, so we cannot return ToolsetTool directly. | ||
| tools = await super(DBOSFastMCPToolset, self).get_tools(ctx) | ||
| # ToolsetTool is not serializable as it holds a SchemaValidator (which is also the same for every MCP tool so unnecessary to pass along the wire every time), | ||
| # so we just return the ToolDefinitions and wrap them in ToolsetTool outside of the activity. | ||
| return {name: tool.tool_def for name, tool in tools.items()} | ||
|
|
||
| self._dbos_wrapped_get_tools_step = wrapped_get_tools_step | ||
|
|
||
| # Wrap call_tool in a DBOS step. | ||
| @DBOS.step( | ||
| name=f'{self._name}.call_tool', | ||
| **self._step_config, | ||
| ) | ||
| async def wrapped_call_tool_step( | ||
| name: str, | ||
| tool_args: dict[str, Any], | ||
| ctx: RunContext[AgentDepsT], | ||
| tool: ToolsetTool[AgentDepsT], | ||
| ) -> ToolResult: | ||
| return await super(DBOSFastMCPToolset, self).call_tool(name, tool_args, ctx, tool) | ||
|
|
||
| self._dbos_wrapped_call_tool_step = wrapped_call_tool_step | ||
|
|
||
| @property | ||
| def id(self) -> str | None: # pragma: lax no cover | ||
| return self.wrapped.id | ||
|
|
||
| async def __aenter__(self) -> Self: | ||
| # The wrapped FastMCPToolset enters itself around listing and calling tools | ||
| # so we don't need to enter it here (nor could we because we're not inside a DBOS step). | ||
| return self | ||
|
|
||
| async def __aexit__(self, *args: Any) -> bool | None: | ||
| return None | ||
|
|
||
| def visit_and_replace( | ||
| self, visitor: Callable[[AbstractToolset[AgentDepsT]], AbstractToolset[AgentDepsT]] | ||
| ) -> AbstractToolset[AgentDepsT]: | ||
| # DBOS-ified toolsets cannot be swapped out after the fact. | ||
| return self | ||
|
|
||
| async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: | ||
| tool_defs = await self._dbos_wrapped_get_tools_step(ctx) | ||
| return {name: self.tool_for_tool_def(tool_def) for name, tool_def in tool_defs.items()} | ||
|
|
||
| async def call_tool( | ||
| self, | ||
| name: str, | ||
| tool_args: dict[str, Any], | ||
| ctx: RunContext[AgentDepsT], | ||
| tool: ToolsetTool[AgentDepsT], | ||
| ) -> ToolResult: | ||
| return await self._dbos_wrapped_call_tool_step(name, tool_args, ctx, tool) | ||
|
|
||
| def tool_for_tool_def(self, tool_def: ToolDefinition) -> ToolsetTool[AgentDepsT]: | ||
| assert isinstance(self.wrapped, FastMCPToolset) | ||
| return self.wrapped.tool_for_tool_def(tool_def) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.