Files
2026-07-13 12:38:34 +08:00

144 lines
5.5 KiB
Python

import asyncio
import copy
import json
import typing as t
import pydantic
from agents import FunctionTool
from composio.core.provider import AgenticProvider
from composio.core.provider.agentic import AgenticProviderExecuteFn
from composio.types import Tool
from composio.utils.pydantic import parse_pydantic_error
from composio.utils.shared import normalize_tool_arguments
# Recursively remove 'examples' keys from the schema properties
def _remove_examples_from_schema(schema_obj: t.Dict[str, t.Any]) -> None:
"""
Remove 'examples', 'pattern', and 'default' keys from all properties in the
schema, including nested ones. Also ensure that any 'items' object has a 'type' key.
"""
# Handle properties directly
if "properties" in schema_obj and isinstance(schema_obj["properties"], dict):
for _, prop_value in schema_obj["properties"].items():
if isinstance(prop_value, dict):
# Remove examples, pattern, and default from this property
if "examples" in prop_value:
del prop_value["examples"]
if "pattern" in prop_value:
del prop_value["pattern"]
if "default" in prop_value:
del prop_value["default"]
# Ensure 'items' has a 'type' key
if "items" in prop_value and isinstance(prop_value["items"], dict):
if "type" not in prop_value["items"]:
# Default to string type for items if not specified
prop_value["items"]["type"] = "string"
# Recursively process nested properties
_remove_examples_from_schema(prop_value)
# Handle array items
if "items" in schema_obj and isinstance(schema_obj["items"], dict):
if "examples" in schema_obj["items"]:
del schema_obj["items"]["examples"]
if "pattern" in schema_obj["items"]:
del schema_obj["items"]["pattern"]
if "default" in schema_obj["items"]:
del schema_obj["items"]["default"]
# Ensure items has a type
if "type" not in schema_obj["items"]:
schema_obj["items"]["type"] = "string"
_remove_examples_from_schema(schema_obj["items"])
# Handle any other nested object properties
for _, value in schema_obj.items():
if isinstance(value, dict):
_remove_examples_from_schema(value)
elif isinstance(value, list):
for item in value:
if isinstance(item, dict):
_remove_examples_from_schema(item)
class OpenAIAgentsProvider(
AgenticProvider[FunctionTool, list[FunctionTool]],
name="openai_agents",
):
"""
Composio toolset for OpenAI Agents framework.
"""
def wrap_tool(
self,
tool: Tool,
execute_tool: AgenticProviderExecuteFn,
) -> FunctionTool:
"""Wrap a tool as a FunctionTool."""
# Create a function that accepts explicit JSON string for parameters
# This avoids the issue with **kwargs in schema validation
async def execute_tool_wrapper(_ctx, payload):
"""Execute Composio action with the given arguments."""
try:
return json.dumps(
obj=(
await asyncio.to_thread( # Running a thread since `execute_tool` is not async
execute_tool,
slug=tool.slug,
# Models occasionally emit arguments as a JSON string (issue #2406).
arguments=normalize_tool_arguments(payload),
)
)
)
except pydantic.ValidationError as e:
return json.dumps(
{
"successful": False,
"error": parse_pydantic_error(e),
"data": None,
}
)
except Exception as e:
return json.dumps(
{
"successful": False,
"error": str(e),
"data": None,
}
)
# Ensure the schema has additionalProperties set to false
# this is required by OpenAI's function validation.
# Deep-copy so the in-place edits below (additionalProperties + the
# examples/pattern/default removal) never mutate the caller's
# Tool.input_parameters, matching the deepcopy contract in
# composio.utils.shared.
modified_schema = copy.deepcopy(tool.input_parameters)
modified_schema["additionalProperties"] = False
# Apply the example removal function. This is done to optimize the
# schema as much as possible for Responses API
_remove_examples_from_schema(modified_schema)
# Create a custom FunctionTool with the appropriate schema
return FunctionTool(
name=tool.slug,
description=tool.description,
params_json_schema=modified_schema,
on_invoke_tool=execute_tool_wrapper,
# To avoid schema errors due to required flags. Composio tools
# already process to have optimal schemas.
strict_json_schema=False,
)
def wrap_tools(
self,
tools: t.Sequence[Tool],
execute_tool: AgenticProviderExecuteFn,
) -> list[FunctionTool]:
"""Wrap a list of tools as a list of FunctionTools."""
return [self.wrap_tool(tool, execute_tool) for tool in tools]