-
Notifications
You must be signed in to change notification settings - Fork 4
Google adk support #98
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
Changes from all commits
98e4233
ba42b52
80a99b3
b6d504b
ee61abc
886ba88
f68e263
2618ffb
94b34dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
3.11 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,4 +9,4 @@ pydantic~=2.11.2 | |
PyJWT~=2.9.0 | ||
cryptography~=45.0.6 | ||
setuptools~=80.9.0 | ||
langchain-core~=0.3.74 | ||
mcp~=1.12.4 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from typing import Optional, Any, Dict, List, Callable | ||
from scalekit.tools import ToolsClient | ||
from scalekit.v1.tools.tools_pb2 import Filter, ScopedToolFilter | ||
from scalekit.actions.frameworks.types.google_adk_tool import ( | ||
ScalekitGoogleAdkTool, | ||
) | ||
from scalekit.actions.frameworks.util import build_mcp_tool_from_spec, struct_to_dict | ||
|
||
|
||
class GoogleADK: | ||
def __init__(self, tools_client: ToolsClient, execute_callback: Callable): | ||
if not execute_callback: | ||
raise ValueError("execute_callback is required. GoogleADK must be initialized with ActionClient's execute_tool method.") | ||
|
||
self.tools = tools_client | ||
self.execute_callback = execute_callback | ||
|
||
|
||
def get_tools( | ||
self, | ||
identifier: str, | ||
providers: Optional[List[str]] = None, | ||
tool_names: Optional[List[str]] = None, | ||
connection_names: Optional[List[str]] = None, | ||
page_size: Optional[int] = None, | ||
page_token: Optional[str] = None | ||
) -> List[ScalekitGoogleAdkTool]: | ||
""" | ||
Get scoped tools from Scalekit and convert them to Google ADK compatible tools | ||
:param identifier: Identifier to scope the tools list | ||
:param providers: List of provider names to filter by | ||
:param tool_names: List of tool names to filter by | ||
:param connection_names: List of connection names to filter by | ||
:param page_size: Maximum number of tools to return per page | ||
:param page_token: Token from a previous response for pagination | ||
:returns: List of Google ADK compatible tools | ||
:raises ImportError: If Google ADK dependencies are not installed | ||
""" | ||
if identifier is None or identifier == "": | ||
raise ValueError("Identifier must be provided to get tools") | ||
|
||
|
||
# Create ScopedToolFilter if any filter parameters are provided | ||
scoped_filter = None | ||
if providers or tool_names or connection_names: | ||
scoped_filter = ScopedToolFilter( | ||
providers=providers or [], | ||
tool_names=tool_names or [], | ||
connection_names=connection_names or [] | ||
) | ||
|
||
# Call list_scoped_tools which returns (response, metadata) tuple | ||
result_tuple = self.tools.list_scoped_tools(identifier, scoped_filter, page_size, page_token) | ||
|
||
# Extract the response[0] (the actual ListScopedToolsResponse proto object) | ||
response = result_tuple[0] | ||
|
||
google_adk_tools = [] | ||
for scoped_tool in response.tools: | ||
google_adk_tool = self._convert_tool_to_google_adk_tool( | ||
scoped_tool.tool, | ||
scoped_tool.connected_account_id | ||
) | ||
google_adk_tools.append(google_adk_tool) | ||
|
||
return google_adk_tools | ||
|
||
def _convert_tool_to_google_adk_tool(self, tool, connected_account_id: str): | ||
"""Convert a Scalekit Tool to Google ADK compatible tool""" | ||
|
||
|
||
spec = struct_to_dict(tool) | ||
mcp_tool = build_mcp_tool_from_spec(spec) | ||
|
||
return ScalekitGoogleAdkTool( | ||
mcp_tool=mcp_tool, | ||
connected_account_id=connected_account_id, | ||
execute_callback=self.execute_callback | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from .google_adk_tool import ( | ||
ScalekitGoogleAdkTool, | ||
) | ||
|
||
__all__ = [ | ||
'ScalekitGoogleAdkTool', | ||
] |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,77 @@ | ||||
from typing import Callable | ||||
from mcp.types import Tool as McpBaseTool | ||||
|
||||
# Dynamic imports with helpful error messages | ||||
def _import_google_adk(): | ||||
"""Import Google ADK with helpful error message if not available""" | ||||
try: | ||||
from google.adk.tools.mcp_tool.mcp_tool import McpTool | ||||
from google.adk.tools.tool_context import ToolContext | ||||
from google.adk.auth.auth_credential import AuthCredential | ||||
return McpTool, AuthCredential, ToolContext | ||||
except ImportError as e: | ||||
raise ImportError( | ||||
"Google ADK not found. To use Google ADK integration, please install:\n" | ||||
"pip install google-adk\n\n" | ||||
"For more information, see: https://google.github.io/adk-docs/\n" | ||||
f"Original error: {e}" | ||||
) | ||||
|
||||
McpTool,AuthCredential,ToolContext = _import_google_adk() | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add spaces after commas in variable assignment. Should be 'McpTool, AuthCredential, ToolContext = _import_google_adk()'. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||
|
||||
|
||||
class ScalekitGoogleAdkTool(McpTool): | ||||
"""Google ADK Tool wrapper for Scalekit tools inheriting from Google BaseTool""" | ||||
|
||||
def __init__( | ||||
self, | ||||
mcp_tool: McpBaseTool, | ||||
connected_account_id: str, | ||||
execute_callback: Callable | ||||
): | ||||
""" | ||||
Initialize ScalekitGoogleAdkTool | ||||
:param connected_account_id: Connected account ID for execution | ||||
:param execute_callback: Callback function for tool execution (ActionClient.execute_tool) | ||||
""" | ||||
|
||||
super().__init__( | ||||
mcp_tool=mcp_tool, | ||||
mcp_session_manager=None, | ||||
auth_scheme=None, | ||||
auth_credential=None, | ||||
) | ||||
|
||||
self.connected_account_id = connected_account_id | ||||
self.execute_callback = execute_callback | ||||
self.name = mcp_tool.name | ||||
self.description = mcp_tool.description | ||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Comment on lines
+51
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove excessive blank lines. Multiple consecutive empty lines reduce code readability.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||
async def _run_async_impl(self, *, args, **kwargs) -> str: | ||||
|
||||
try: | ||||
# Call connect.execute_tool via callback (includes modifiers and enhanced handling) | ||||
response = self.execute_callback( | ||||
tool_input=args, | ||||
tool_name=self.name, | ||||
connected_account_id=self.connected_account_id | ||||
) | ||||
|
||||
result_data = response.data if hasattr(response, 'data') else {} | ||||
|
||||
execution_id = response.execution_id if hasattr(response, 'execution_id') else None | ||||
|
||||
# Format the response | ||||
result_dict = dict(result_data) if result_data else {} | ||||
if execution_id: | ||||
result_dict['execution_id'] = execution_id | ||||
|
||||
return str(result_dict) if result_dict else f"Tool {self.name} executed successfully" | ||||
|
||||
except Exception as e: | ||||
return f"Error executing tool {self.name}: {str(e)}" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from typing import Dict, Any | ||
from mcp.types import Tool as McpBaseTool, ToolAnnotations | ||
|
||
|
||
def struct_to_dict(struct) -> Dict[str, Any]: | ||
""" | ||
Convert protobuf Struct to Python dict | ||
|
||
:param struct: Protobuf Struct object | ||
:returns: Python dictionary representation | ||
""" | ||
from google.protobuf.json_format import MessageToDict | ||
return MessageToDict(struct) | ||
|
||
|
||
def extract_tool_metadata(tool, default_provider: str = 'unknown'): | ||
""" | ||
Extract common tool metadata from Scalekit tool definition | ||
|
||
:param tool: Scalekit tool object | ||
:param default_provider: Default provider name if not found | ||
:returns: Tuple of (tool_name, tool_description, definition_dict) | ||
""" | ||
definition_dict = struct_to_dict(tool.definition) if hasattr(tool, 'definition') and tool.definition else {} | ||
|
||
tool_name = definition_dict.get('name', getattr(tool, 'provider', default_provider) + '_tool') | ||
tool_description = definition_dict.get('description', 'Scalekit tool') | ||
|
||
return tool_name, tool_description, definition_dict | ||
|
||
|
||
def convert_to_mcp_input_schema(definition_dict: Dict[str, Any]) -> Dict[str, Any]: | ||
""" | ||
Convert Scalekit tool definition to MCP input schema format | ||
|
||
:param definition_dict: Scalekit tool definition dictionary | ||
:returns: MCP-compatible input schema | ||
""" | ||
input_schema = definition_dict.get("input_schema", {}) | ||
|
||
return { | ||
"type": "object", | ||
"properties": input_schema.get("properties", {}), | ||
"required": input_schema.get("required", []) | ||
} | ||
|
||
def build_mcp_tool_from_spec(spec: Dict[str, Any]) -> McpBaseTool: | ||
"""Converts the raw spec dict into an MCP Tool instance. | ||
|
||
Mapping performed: | ||
definition.input_schema -> inputSchema | ||
definition.annotations.* snake_case -> ToolAnnotations camelCase | ||
""" | ||
definition = spec["definition"] | ||
ann_raw = definition.get("annotations", {}) | ||
ann_map = { | ||
"title": ann_raw.get("title"), | ||
"readOnlyHint": ann_raw.get("read_only_hint"), | ||
"destructiveHint": ann_raw.get("destructive_hint"), | ||
"idempotentHint": ann_raw.get("idempotent_hint"), | ||
"openWorldHint": ann_raw.get("open_world_hint"), | ||
} | ||
# Filter out None values | ||
ann_clean = {k: v for k, v in ann_map.items() if v is not None} | ||
annotations = ToolAnnotations(**ann_clean) if ann_clean else None | ||
|
||
mcp_tool = McpBaseTool( | ||
name=definition["name"], | ||
description=definition.get("description"), | ||
inputSchema=definition["input_schema"], | ||
annotations=annotations, | ||
) | ||
return mcp_tool |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,8 +20,8 @@ | |
"deprecation>=2.1.0", | ||
"python-dotenv>=1.1.0", | ||
"Faker~=25.8.0", | ||
"pydantic~=2.11.2", | ||
"langchain-core>=0.3.36,<0.4", | ||
"pydantic>=2.10.6", | ||
"mcp>= 1.15.0", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove extra space before version constraint. Should be 'mcp>=1.15.0'. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
], | ||
url="https://github.com/scalekit-inc/scalekit-sdk-python", | ||
license="MIT", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove extra space before 'build_mcp_tool_from_spec'. Should have single space after 'import'.
Copilot uses AI. Check for mistakes.