chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# composio-gemini
|
||||
|
||||
Adapts Composio tools to the [`google-genai`](https://pypi.org/project/google-genai/) SDK as Python callables compatible with Gemini's Automatic Function Calling.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install composio composio-gemini google-genai
|
||||
```
|
||||
|
||||
Set `COMPOSIO_API_KEY` (create one at https://dashboard.composio.dev/settings) and `GOOGLE_API_KEY` (from https://aistudio.google.com/apikey) in your environment.
|
||||
|
||||
## Quickstart
|
||||
|
||||
`GeminiProvider` wraps each Composio tool as a typed Python callable. Pass the callables to `GenerateContentConfig(tools=...)` and the `google-genai` SDK derives function declarations from their signatures and executes tool calls automatically inside the chat loop; there is no manual tool-call handling.
|
||||
|
||||
```python
|
||||
from composio import Composio
|
||||
from composio_gemini import GeminiProvider
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
composio = Composio(provider=GeminiProvider())
|
||||
client = genai.Client()
|
||||
|
||||
# Create a session for your user
|
||||
session = composio.create(user_id="user_123")
|
||||
tools = session.tools()
|
||||
|
||||
config = types.GenerateContentConfig(tools=tools)
|
||||
chat = client.chats.create(model="gemini-3-pro-preview", config=config)
|
||||
|
||||
response = chat.send_message(
|
||||
"Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'"
|
||||
)
|
||||
print(response.text)
|
||||
```
|
||||
|
||||
If you disable Automatic Function Calling and handle function calls yourself, `composio.provider.handle_response(response)` executes the function calls in a Gemini response and returns `Part` objects ready to send back.
|
||||
|
||||
## composio-gemini vs composio-google
|
||||
|
||||
This package targets the `google-genai` SDK (`from google import genai`). [`composio-google`](../google) targets the older Vertex AI SDK (`vertexai.generative_models`). For new projects, Google recommends `google-genai`, so use this package.
|
||||
|
||||
## Links
|
||||
|
||||
- Google provider docs: https://docs.composio.dev/docs/providers/google
|
||||
- Composio docs: https://docs.composio.dev
|
||||
@@ -0,0 +1,3 @@
|
||||
from .provider import GeminiProvider
|
||||
|
||||
__all__ = ("GeminiProvider",)
|
||||
@@ -0,0 +1,205 @@
|
||||
"""Gemini provider for Composio SDK.
|
||||
|
||||
Returns Python callables compatible with google-genai's Automatic Function
|
||||
Calling (AFC). The SDK can introspect the callable's signature to derive
|
||||
FunctionDeclaration schemas and auto-execute tool calls in the chat loop.
|
||||
"""
|
||||
|
||||
import types as pytypes
|
||||
import typing as t
|
||||
from inspect import Parameter, Signature
|
||||
|
||||
from composio.client.types import Tool
|
||||
from composio.core.provider import AgenticProvider
|
||||
from composio.core.provider.agentic import AgenticProviderExecuteFn
|
||||
from composio.utils.shared import (
|
||||
ToolSchemaAliases,
|
||||
alias_tool_input_schema,
|
||||
get_pydantic_signature_format_from_schema_params,
|
||||
normalize_tool_arguments,
|
||||
)
|
||||
|
||||
# google-genai is only needed for handle_response (backward compat)
|
||||
try:
|
||||
from google.genai import types as genai_types
|
||||
|
||||
HAS_GENAI = True
|
||||
except ImportError:
|
||||
genai_types = None # type: ignore
|
||||
HAS_GENAI = False
|
||||
|
||||
|
||||
def _to_serializable(value: t.Any) -> t.Any:
|
||||
"""Recursively convert Pydantic models (and other non-JSON types) to plain dicts/lists.
|
||||
|
||||
The google-genai SDK's AFC pipeline calls ``convert_if_exist_pydantic_model``
|
||||
on function arguments, turning nested dicts into dynamically-generated
|
||||
Pydantic ``GeneratedModel`` instances. These are not JSON-serializable, so
|
||||
the Composio ``execute_tool`` call fails. This helper normalises them back
|
||||
to plain Python primitives before handing off to the API.
|
||||
"""
|
||||
# Pydantic v2 BaseModel
|
||||
if hasattr(value, "model_dump"):
|
||||
return value.model_dump()
|
||||
# Pydantic v1 BaseModel
|
||||
if hasattr(value, "dict") and hasattr(value, "__fields__"):
|
||||
return value.dict()
|
||||
if isinstance(value, dict):
|
||||
return {k: _to_serializable(v) for k, v in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [_to_serializable(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def _process_execution_result(result: t.Any) -> t.Dict:
|
||||
"""Process a tool execution result into a dict suitable for Gemini function responses."""
|
||||
if not isinstance(result, dict):
|
||||
return {"result": result}
|
||||
|
||||
if result.get("successful", True) and "data" in result:
|
||||
data = result["data"]
|
||||
return data if isinstance(data, dict) else {"result": data}
|
||||
|
||||
if not result.get("successful", True):
|
||||
return {
|
||||
"error": result.get("error", "Tool execution failed"),
|
||||
"details": result,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class GeminiProvider(AgenticProvider[t.Callable, list[t.Callable]], name="gemini"):
|
||||
"""Composio toolset for Google AI Python Gemini framework.
|
||||
|
||||
Returns Python callables compatible with google-genai's Automatic Function
|
||||
Calling (AFC). Pass the result of ``wrap_tools()`` directly to
|
||||
``GenerateContentConfig(tools=...)`` and the SDK will auto-execute tool
|
||||
calls in the ``chat.send_message()`` loop.
|
||||
"""
|
||||
|
||||
__schema_skip_defaults__ = True
|
||||
|
||||
def __init__(self, **kwargs: t.Any):
|
||||
super().__init__(**kwargs)
|
||||
self._executors: t.Dict[
|
||||
str, t.Tuple[AgenticProviderExecuteFn, ToolSchemaAliases]
|
||||
] = {}
|
||||
|
||||
def wrap_tool(
|
||||
self,
|
||||
tool: Tool,
|
||||
execute_tool: AgenticProviderExecuteFn,
|
||||
) -> t.Callable:
|
||||
"""Wrap a Composio tool as a Python callable for google-genai AFC.
|
||||
|
||||
The returned function has ``__name__``, ``__doc__``, ``__signature__``
|
||||
and ``__annotations__`` set so the google-genai SDK can:
|
||||
|
||||
1. Derive a ``FunctionDeclaration`` schema via ``from_callable()``
|
||||
2. Store it in the AFC ``function_map`` for automatic execution
|
||||
"""
|
||||
aliases = alias_tool_input_schema(schema=tool.input_parameters)
|
||||
self._executors[tool.slug] = (execute_tool, aliases)
|
||||
|
||||
def function(**kwargs: t.Any) -> t.Dict:
|
||||
"""Composio tool execution wrapper."""
|
||||
kwargs = _to_serializable(kwargs)
|
||||
kwargs = aliases.restore_arguments(kwargs)
|
||||
# Normalize defensively so a stringified payload is coerced to a dict (issue #2406).
|
||||
result = execute_tool(tool.slug, normalize_tool_arguments(kwargs))
|
||||
return _process_execution_result(result)
|
||||
|
||||
# Create a real function object (passes inspect.isfunction)
|
||||
action_func = pytypes.FunctionType(
|
||||
function.__code__,
|
||||
globals=globals(),
|
||||
name=tool.slug,
|
||||
closure=function.__closure__,
|
||||
)
|
||||
|
||||
# Build typed signature from JSON schema.
|
||||
# Uses get_pydantic_signature_format_from_schema_params (not
|
||||
# get_signature_format_from_schema_params) because the pydantic variant
|
||||
# goes through json_schema_to_pydantic_type() which produces
|
||||
# parameterized generics (e.g. List[str] instead of bare List).
|
||||
# The google-genai SDK requires parameterized array types — bare List
|
||||
# generates {"type": "ARRAY"} without "items", which the API rejects.
|
||||
sig_params = get_pydantic_signature_format_from_schema_params(
|
||||
schema_params=aliases.schema,
|
||||
skip_default=True,
|
||||
)
|
||||
action_func.__signature__ = Signature(parameters=sig_params) # type: ignore
|
||||
action_func.__doc__ = tool.description or f"Execute {tool.slug}"
|
||||
|
||||
# Build __annotations__ for typing.get_type_hints() compatibility
|
||||
annotations: t.Dict[str, t.Any] = {}
|
||||
for param in sig_params:
|
||||
if param.annotation is not Parameter.empty:
|
||||
annotations[param.name] = param.annotation
|
||||
annotations["return"] = dict
|
||||
action_func.__annotations__ = annotations
|
||||
|
||||
return action_func
|
||||
|
||||
def wrap_tools(
|
||||
self,
|
||||
tools: t.Sequence[Tool],
|
||||
execute_tool: AgenticProviderExecuteFn,
|
||||
) -> list[t.Callable]:
|
||||
"""Wrap multiple Composio tools as Python callables for google-genai AFC."""
|
||||
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
||||
|
||||
# --- Backward compatibility: manual function calling ---
|
||||
|
||||
def handle_response(self, response: t.Any) -> tuple[list, bool]:
|
||||
"""Manually handle function calls in a Gemini response.
|
||||
|
||||
Provided for backward compatibility with code that uses manual function
|
||||
calling instead of AFC. For new code, pass the callables from
|
||||
``wrap_tools()`` to ``GenerateContentConfig(tools=...)`` and AFC will
|
||||
handle execution automatically.
|
||||
|
||||
Returns:
|
||||
tuple: ``(function_responses, executed)`` where *function_responses*
|
||||
are ``genai_types.Part`` objects ready to send back, and *executed*
|
||||
is ``True`` if any functions were executed.
|
||||
"""
|
||||
if not HAS_GENAI:
|
||||
return [], False
|
||||
|
||||
if not (hasattr(response, "candidates") and response.candidates):
|
||||
return [], False
|
||||
|
||||
candidate = response.candidates[0]
|
||||
if not (hasattr(candidate, "content") and candidate.content.parts):
|
||||
return [], False
|
||||
|
||||
function_responses: list = []
|
||||
executed = False
|
||||
|
||||
for part in candidate.content.parts:
|
||||
if not (hasattr(part, "function_call") and part.function_call):
|
||||
continue
|
||||
|
||||
fc = part.function_call
|
||||
if fc.name not in self._executors:
|
||||
continue
|
||||
|
||||
execute_tool, aliases = self._executors[fc.name]
|
||||
arguments = aliases.restore_arguments(dict(fc.args))
|
||||
result = execute_tool(
|
||||
slug=fc.name, arguments=normalize_tool_arguments(arguments)
|
||||
)
|
||||
processed = _process_execution_result(result)
|
||||
|
||||
function_responses.append(
|
||||
genai_types.Part(
|
||||
function_response=genai_types.FunctionResponse(
|
||||
name=fc.name, response=processed
|
||||
)
|
||||
)
|
||||
)
|
||||
executed = True
|
||||
|
||||
return function_responses, executed
|
||||
@@ -0,0 +1,26 @@
|
||||
from composio_gemini import GeminiProvider
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
from composio import Composio
|
||||
|
||||
# Create composio client
|
||||
composio = Composio(provider=GeminiProvider())
|
||||
|
||||
# Create google client
|
||||
client = genai.Client()
|
||||
|
||||
# Create genai client config
|
||||
config = types.GenerateContentConfig(
|
||||
tools=composio.tools.get(
|
||||
user_id="default",
|
||||
tools=[
|
||||
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER",
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# Use the chat interface.
|
||||
chat = client.chats.create(model="gemini-2.0-flash", config=config)
|
||||
response = chat.send_message("Can you star composiohq/composio repository on github")
|
||||
print(response.text)
|
||||
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "composio-gemini"
|
||||
version = "0.17.1"
|
||||
description = "Use Composio to get an array of tools with your Gemini agent."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<4"
|
||||
authors = [
|
||||
{ name = "Composio", email = "tech@composio.dev" }
|
||||
]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
dependencies = [
|
||||
"google-genai>=2.8.0",
|
||||
"composio",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/ComposioHQ/composio"
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Setup configuration for Composio Gemin plugin
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="composio_gemini",
|
||||
version="0.17.1",
|
||||
author="Composio",
|
||||
author_email="tech@composio.dev",
|
||||
description="Use Composio to get an array of tools with your Gemini agent.",
|
||||
long_description=(Path(__file__).parent / "README.md").read_text(encoding="utf-8"),
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/ComposioHQ/composio",
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires=">=3.10,<4",
|
||||
install_requires=[
|
||||
"google-genai>=2.8.0",
|
||||
"composio",
|
||||
],
|
||||
include_package_data=True,
|
||||
)
|
||||
Reference in New Issue
Block a user