chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .agentic import AgenticProvider, AgenticProviderExecuteFn
|
||||
from .base import TTool, TToolCollection
|
||||
from .none_agentic import NonAgenticProvider
|
||||
|
||||
__all__ = [
|
||||
"TTool",
|
||||
"TToolCollection",
|
||||
"AgenticProvider",
|
||||
"NonAgenticProvider",
|
||||
"AgenticProviderExecuteFn",
|
||||
]
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
OpenAI provider implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import typing as t
|
||||
|
||||
from openai import Client
|
||||
from openai.types.beta.thread import Thread
|
||||
from openai.types.beta.threads.run import Run
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
from openai.types.chat.chat_completion_message_tool_call import (
|
||||
ChatCompletionMessageToolCall,
|
||||
)
|
||||
from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam
|
||||
from openai.types.shared_params.function_definition import FunctionDefinition
|
||||
from openai.types.shared_params.function_parameters import FunctionParameters
|
||||
|
||||
from composio.core.provider import NonAgenticProvider
|
||||
from composio.types import Modifiers, Tool, ToolExecutionResponse
|
||||
from composio.utils.shared import normalize_tool_arguments
|
||||
|
||||
OpenAITool: t.TypeAlias = ChatCompletionToolParam
|
||||
OpenAIToolCollection: t.TypeAlias = t.List[OpenAITool]
|
||||
|
||||
|
||||
class OpenAIProvider(
|
||||
NonAgenticProvider[OpenAITool, OpenAIToolCollection], name="openai"
|
||||
):
|
||||
"""OpenAIProvider class definition"""
|
||||
|
||||
def wrap_tool(self, tool: Tool) -> OpenAITool:
|
||||
return ChatCompletionToolParam(
|
||||
function=FunctionDefinition(
|
||||
name=tool.slug,
|
||||
description=tool.description,
|
||||
parameters=t.cast(FunctionParameters, tool.input_parameters),
|
||||
strict=None,
|
||||
),
|
||||
type="function",
|
||||
)
|
||||
|
||||
def wrap_tools(self, tools: t.Sequence[Tool]) -> OpenAIToolCollection:
|
||||
return [self.wrap_tool(tool) for tool in tools]
|
||||
|
||||
def execute_tool_call(
|
||||
self,
|
||||
user_id: str,
|
||||
tool_call: ChatCompletionMessageToolCall,
|
||||
modifiers: t.Optional[Modifiers] = None,
|
||||
) -> ToolExecutionResponse:
|
||||
"""Execute a tool call.
|
||||
|
||||
:param tool_call: Tool call metadata.
|
||||
:param user_id: User ID to use for executing the function call.
|
||||
:return: Object containing output data from the tool call.
|
||||
"""
|
||||
# OpenAI always serializes tool arguments as a JSON string; normalize
|
||||
# tolerates empty / object-shaped payloads too (issue #2406).
|
||||
return self.execute_tool(
|
||||
slug=tool_call.function.name,
|
||||
arguments=normalize_tool_arguments(tool_call.function.arguments),
|
||||
modifiers=modifiers,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
def handle_tool_calls(
|
||||
self,
|
||||
user_id: str,
|
||||
response: ChatCompletion,
|
||||
modifiers: t.Optional[Modifiers] = None,
|
||||
) -> t.List[ToolExecutionResponse]:
|
||||
"""
|
||||
Handle tool calls from OpenAI chat completion object.
|
||||
|
||||
:param response: Chat completion object from
|
||||
openai.OpenAI.chat.completions.create function call
|
||||
:param user_id: User ID to use for executing the function call.
|
||||
:return: A list of output objects from the function calls.
|
||||
"""
|
||||
outputs = []
|
||||
# Only the first choice is actionable: its tool results feed back into a
|
||||
# single assistant turn. With n > 1, iterating every choice would run each
|
||||
# tool call once per choice and orphan the tool_call_ids belonging to the
|
||||
# alternative completions.
|
||||
choice = response.choices[0] if response.choices else None
|
||||
# A single assistant message can carry several tool calls (parallel tool
|
||||
# calls, on by default); each one needs its own tool result.
|
||||
if choice is not None and choice.message.tool_calls is not None:
|
||||
for tool_call in choice.message.tool_calls:
|
||||
outputs.append(
|
||||
self.execute_tool_call(
|
||||
user_id=user_id,
|
||||
tool_call=t.cast(ChatCompletionMessageToolCall, tool_call),
|
||||
modifiers=modifiers,
|
||||
)
|
||||
)
|
||||
return outputs
|
||||
|
||||
def handle_assistant_tool_calls(
|
||||
self,
|
||||
user_id: str,
|
||||
run: Run,
|
||||
) -> t.List:
|
||||
"""Wait and handle assistant function calls"""
|
||||
tool_outputs: list[dict] = []
|
||||
if run.required_action is None:
|
||||
return tool_outputs
|
||||
|
||||
for tool_call in run.required_action.submit_tool_outputs.tool_calls:
|
||||
tool_outputs.append(
|
||||
{
|
||||
"tool_call_id": tool_call.id,
|
||||
"output": json.dumps(
|
||||
self.execute_tool_call(
|
||||
tool_call=t.cast(ChatCompletionMessageToolCall, tool_call),
|
||||
user_id=user_id,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
return tool_outputs
|
||||
|
||||
def wait_and_handle_assistant_tool_calls(
|
||||
self,
|
||||
user_id: str,
|
||||
client: Client,
|
||||
run: Run,
|
||||
thread: Thread,
|
||||
) -> Run:
|
||||
"""Wait and handle assistant function calls"""
|
||||
while run.status in ("queued", "in_progress", "requires_action"):
|
||||
if run.status != "requires_action":
|
||||
run = client.beta.threads.runs.retrieve(
|
||||
thread_id=thread.id,
|
||||
run_id=run.id,
|
||||
)
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
run = client.beta.threads.runs.submit_tool_outputs(
|
||||
thread_id=thread.id,
|
||||
run_id=run.id,
|
||||
tool_outputs=self.handle_assistant_tool_calls(
|
||||
run=run,
|
||||
user_id=user_id,
|
||||
),
|
||||
)
|
||||
return run
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
OpenAI Responses API provider implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from openai.types.responses.response import Response
|
||||
from openai.types.responses.response_output_item import ResponseFunctionToolCall
|
||||
|
||||
from composio.core.provider import NonAgenticProvider
|
||||
from composio.types import Modifiers, Tool, ToolExecutionResponse
|
||||
from composio.utils.shared import normalize_tool_arguments
|
||||
|
||||
# Responses API uses a flattened tool structure
|
||||
ResponsesTool = t.Dict[str, t.Any]
|
||||
ResponsesToolCollection = t.List[ResponsesTool]
|
||||
|
||||
|
||||
class OpenAIResponsesProvider(
|
||||
NonAgenticProvider[ResponsesTool, ResponsesToolCollection], name="openai_responses"
|
||||
):
|
||||
"""OpenAI Responses API Provider class definition."""
|
||||
|
||||
def wrap_tool(self, tool: Tool) -> ResponsesTool:
|
||||
"""Wrap a tool for the Responses API format."""
|
||||
return {
|
||||
"type": "function",
|
||||
"name": tool.slug,
|
||||
"description": tool.description,
|
||||
"parameters": tool.input_parameters,
|
||||
}
|
||||
|
||||
def wrap_tools(self, tools: t.Sequence[Tool]) -> ResponsesToolCollection:
|
||||
"""Wrap multiple tools for the Responses API format."""
|
||||
return [self.wrap_tool(tool) for tool in tools]
|
||||
|
||||
def execute_tool_call(
|
||||
self,
|
||||
user_id: str,
|
||||
tool_call: t.Union[ResponseFunctionToolCall],
|
||||
modifiers: t.Optional[Modifiers] = None,
|
||||
) -> ToolExecutionResponse:
|
||||
"""Execute a tool call from the Responses API.
|
||||
|
||||
:param tool_call: Tool call metadata from Responses API.
|
||||
:param user_id: User ID to use for executing the function call.
|
||||
:param modifiers: Optional modifiers for tool execution.
|
||||
:return: Object containing output data from the tool call.
|
||||
"""
|
||||
# OpenAI always serializes tool arguments as a JSON string; normalize
|
||||
# tolerates empty / object-shaped payloads too (issue #2406).
|
||||
slug = tool_call.name
|
||||
arguments = normalize_tool_arguments(tool_call.arguments)
|
||||
|
||||
return self.execute_tool(
|
||||
slug=slug,
|
||||
arguments=arguments,
|
||||
modifiers=modifiers,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
def handle_tool_calls(
|
||||
self,
|
||||
user_id: str,
|
||||
response: Response,
|
||||
modifiers: t.Optional[Modifiers] = None,
|
||||
) -> t.List[ToolExecutionResponse]:
|
||||
"""
|
||||
Handle tool calls from OpenAI Responses API.
|
||||
|
||||
:param response: Response object from openai.OpenAI.beta.responses.create
|
||||
:param user_id: User ID to use for executing the function call.
|
||||
:param modifiers: Optional modifiers for tool execution
|
||||
:return: List[ToolExecutionResponse] with tool execution results
|
||||
"""
|
||||
outputs = []
|
||||
|
||||
if response.output:
|
||||
for item in response.output:
|
||||
if isinstance(item, ResponseFunctionToolCall):
|
||||
result = self.execute_tool_call(
|
||||
user_id=user_id,
|
||||
tool_call=item,
|
||||
modifiers=modifiers,
|
||||
)
|
||||
outputs.append(result)
|
||||
|
||||
return outputs
|
||||
@@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from composio.client.types import Tool
|
||||
from composio.core.provider.base import BaseProvider, TTool, TToolCollection
|
||||
|
||||
|
||||
class AgenticProviderExecuteFn(t.Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
slug: str,
|
||||
arguments: t.Dict,
|
||||
) -> t.Dict:
|
||||
"""
|
||||
Execute a wrapped tool by slug, passing an arbitrary input dict.
|
||||
Returns a dict with the following keys:
|
||||
- data: The data returned by the tool.
|
||||
- error: The error returned by the tool.
|
||||
- successful: Whether the tool was successful.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class AgenticProvider(BaseProvider[TTool, TToolCollection]):
|
||||
"""
|
||||
Base class for all agentic providers. This class is not meant to be used
|
||||
directly but rather to be extended by concrete provider implementations.
|
||||
"""
|
||||
|
||||
def __init_subclass__(cls, name: str) -> None:
|
||||
cls.name = name
|
||||
|
||||
def wrap_tool(
|
||||
self,
|
||||
tool: Tool,
|
||||
execute_tool: AgenticProviderExecuteFn,
|
||||
) -> TTool:
|
||||
"""Wrap a tool in the provider-specific format"""
|
||||
raise NotImplementedError
|
||||
|
||||
def wrap_tools(
|
||||
self,
|
||||
tools: t.Sequence[Tool],
|
||||
execute_tool: AgenticProviderExecuteFn,
|
||||
) -> TToolCollection:
|
||||
"""Wrap a list of tools in the provider-specific format"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
BaseProvider module
|
||||
|
||||
Defines the barebones provider metaclass that needs to be subclassed for every provider.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
import typing_extensions as te
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from composio.core.models.tools import Modifiers, ToolExecutionResponse
|
||||
|
||||
TTool = t.TypeVar("TTool")
|
||||
TToolCollection = t.TypeVar("TToolCollection")
|
||||
|
||||
|
||||
class ExecuteToolFn(t.Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
slug: str,
|
||||
arguments: t.Dict,
|
||||
*,
|
||||
modifiers: t.Optional[Modifiers] = None,
|
||||
user_id: t.Optional[str] = None,
|
||||
) -> ToolExecutionResponse:
|
||||
"""
|
||||
Execute a wrapped tool by slug, passing an arbitrary input dict.
|
||||
This function is used by the providers to execute tools for the helper methods.
|
||||
Returns a dict with the following keys:
|
||||
- data: The data returned by the tool.
|
||||
- error: The error returned by the tool.
|
||||
- successful: Whether the tool was successful.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class SchemaConfig(te.TypedDict):
|
||||
skip_defaults: te.NotRequired[bool]
|
||||
|
||||
|
||||
class BaseProviderConfig(te.TypedDict):
|
||||
schema_config: te.NotRequired[SchemaConfig]
|
||||
|
||||
|
||||
class BaseProvider(t.Generic[TTool, TToolCollection]):
|
||||
"""
|
||||
BaseProvider class
|
||||
|
||||
All providers should inherit from this class and implement `wrap_tools` so that
|
||||
they can be used with the core Composio class.
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""Name of the provider"""
|
||||
|
||||
__schema_skip_defaults__ = False
|
||||
|
||||
execute_tool: ExecuteToolFn
|
||||
"""
|
||||
The function to execute a tool for the provider's helper methods.
|
||||
This is automatically injected by the core SDK.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs: t.Unpack[BaseProviderConfig]) -> None:
|
||||
self.skip_default = kwargs.get("schema_config", {}).get(
|
||||
"skip_defaults", self.__schema_skip_defaults__
|
||||
)
|
||||
|
||||
def set_execute_tool_fn(self, execute_tool_fn: ExecuteToolFn) -> None:
|
||||
self.execute_tool = execute_tool_fn
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from composio.client.types import Tool
|
||||
from composio.core.provider.base import BaseProvider, TTool, TToolCollection
|
||||
|
||||
|
||||
class NonAgenticProvider(BaseProvider[TTool, TToolCollection]):
|
||||
"""
|
||||
Base class for all non-agentic providers, such as `openai` This class is not
|
||||
meant to be used directly, but rather to be extended by concrete implementations
|
||||
This version doesn't have the execute_tool_fn for `wrap_tool` and `wrap_tools`
|
||||
"""
|
||||
|
||||
def __init_subclass__(cls, name: str) -> None:
|
||||
cls.name = name
|
||||
|
||||
def wrap_tool(
|
||||
self,
|
||||
tool: Tool,
|
||||
) -> TTool:
|
||||
"""Wrap a tool in the provider-specific format"""
|
||||
raise NotImplementedError
|
||||
|
||||
def wrap_tools(
|
||||
self,
|
||||
tools: t.Sequence[Tool],
|
||||
) -> TToolCollection:
|
||||
"""Wrap a list of tools in the provider-specific format"""
|
||||
raise NotImplementedError
|
||||
Reference in New Issue
Block a user