chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:34 +08:00
commit 0549b088a4
2405 changed files with 810255 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
# composio-langgraph
Adapts Composio tools to LangChain's `StructuredTool` format for use in LangGraph agents and graph workflows, giving them access to 1000+ apps through a single Composio session.
## Installation
```bash
pip install composio composio-langgraph langgraph langchain langchain-openai
```
Set `COMPOSIO_API_KEY` (get one from [dashboard.composio.dev/settings](https://dashboard.composio.dev/settings)) and `OPENAI_API_KEY` in your environment:
```bash
export COMPOSIO_API_KEY=xxxxxxxxx
export OPENAI_API_KEY=xxxxxxxxx
```
## Quickstart
Create a session for your user, fetch its tools, and hand them to your agent. The wrapped tools also work anywhere LangGraph accepts LangChain tools, such as a `ToolNode` in a custom graph.
```python
from composio import Composio
from composio_langgraph import LanggraphProvider
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
composio = Composio(provider=LanggraphProvider())
llm = ChatOpenAI(model="gpt-5.2")
# Each session is scoped to one of your users
session = composio.create(user_id="user_123")
tools = session.tools()
agent = create_agent(tools=tools, model=llm)
result = agent.invoke(
{
"messages": [
(
"user",
"Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'",
)
]
}
)
print(result["messages"][-1].content)
```
## Error handling
Each wrapped tool builds its `args_schema` from the Composio tool's input schema. When argument validation fails, the tool does not raise; it returns a structured result:
```python
{"successful": False, "error": "<validation message>", "data": None}
```
Check `successful` in tool output instead of wrapping calls in `try`/`except`.
## Links
- [LangChain provider docs](https://docs.composio.dev/docs/providers/langchain) (covers LangGraph)
- [Composio documentation](https://docs.composio.dev)
@@ -0,0 +1,3 @@
from .provider import LanggraphProvider
__all__ = ("LanggraphProvider",)
@@ -0,0 +1,106 @@
"""ComposioLangChain class definition"""
import types
import typing as t
from inspect import Signature
import pydantic
from langchain_core.tools import StructuredTool as BaseStructuredTool
from composio.core.provider import AgenticProvider, AgenticProviderExecuteFn
from composio.types import Tool
from composio.utils.pydantic import parse_pydantic_error
from composio.utils.shared import (
get_signature_format_from_schema_params,
json_schema_to_model,
normalize_tool_arguments,
reinstate_reserved_python_keywords,
substitute_reserved_python_keywords,
)
class StructuredTool(BaseStructuredTool): # type: ignore[misc]
def run(self, *args, **kwargs):
try:
return super().run(*args, **kwargs)
except pydantic.ValidationError as e:
return {"successful": False, "error": parse_pydantic_error(e), "data": None}
class LanggraphProvider(
AgenticProvider[StructuredTool, t.List[StructuredTool]],
name="langgraph",
):
"""
Composio toolset for Langchain framework.
"""
def _wrap_action(
self,
tool: str,
description: str,
schema_params: t.Dict,
keywords: t.Dict,
execute_tool: AgenticProviderExecuteFn,
):
def function(**kwargs: t.Any) -> t.Dict:
"""Wrapper function for composio action."""
kwargs = reinstate_reserved_python_keywords(
request=kwargs, keywords=keywords
)
# Normalize defensively so a stringified payload is coerced to a dict (issue #2406).
return execute_tool(tool, normalize_tool_arguments(kwargs))
action_func = types.FunctionType(
function.__code__,
globals=globals(),
name=tool,
closure=function.__closure__,
)
action_func.__signature__ = Signature( # type: ignore
parameters=get_signature_format_from_schema_params(
schema_params=schema_params,
skip_default=self.skip_default,
)
)
action_func.__doc__ = description
return action_func
def wrap_tool(
self, tool: Tool, execute_tool: AgenticProviderExecuteFn
) -> StructuredTool:
"""Wraps composio tool as Langchain StructuredTool object."""
schema_params, keywords = substitute_reserved_python_keywords(
schema=tool.input_parameters
)
return t.cast(
StructuredTool,
StructuredTool.from_function(
name=tool.slug,
description=tool.description,
args_schema=json_schema_to_model(
json_schema=schema_params,
skip_default=self.skip_default,
),
return_schema=True,
func=self._wrap_action(
tool=tool.slug,
description=tool.description,
schema_params=schema_params,
keywords=keywords,
execute_tool=execute_tool,
),
handle_tool_error=True,
handle_validation_error=True,
),
)
def wrap_tools(
self,
tools: t.Sequence[Tool],
execute_tool: AgenticProviderExecuteFn,
) -> t.List[StructuredTool]:
"""
Get composio tools wrapped as Langchain StructuredTool objects.
"""
return [self.wrap_tool(tool=tool, execute_tool=execute_tool) for tool in tools]
@@ -0,0 +1,97 @@
import json
import operator
from typing import Annotated, Sequence, TypedDict
from composio_langgraph import LanggraphProvider
from langchain_core.messages import BaseMessage, FunctionMessage, HumanMessage
from langchain_core.utils.function_calling import convert_to_openai_function
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph
from composio import Composio
composio = Composio(provider=LanggraphProvider())
tools = composio.tools.get(
user_id="default",
tools=[
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER",
"GITHUB_GET_THE_AUTHENTICATED_USER",
],
)
functions = [convert_to_openai_function(t) for t in tools]
model = ChatOpenAI(temperature=0, streaming=True).bind_functions(functions)
def function_1(state):
messages = state["messages"]
response = model.invoke(messages)
return {"messages": [response]}
def function_2(state):
messages = state["messages"]
last_message = messages[-1]
parsed_function_call = last_message.additional_kwargs["function_call"]
# Find the correct tool to use from the provided list of tools.
tool_to_use = None
for t in tools:
if t.name == parsed_function_call["name"]:
tool_to_use = t
break
if tool_to_use is None:
raise ValueError(f"Tool with name {parsed_function_call['name']} not found.")
response = tool_to_use.invoke(json.loads(parsed_function_call["arguments"]))
function_message = FunctionMessage(
content=str(response), name=parsed_function_call["name"]
)
return {"messages": [function_message]}
def where_to_go(state):
messages = state["messages"]
last_message = messages[-1]
if "function_call" in last_message.additional_kwargs:
return "continue"
return "end"
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
workflow = StateGraph(AgentState)
workflow.add_node("agent", function_1)
workflow.add_node("tool", function_2)
workflow.add_conditional_edges(
"agent",
where_to_go,
{
# If return is "continue" then we call the tool node.
"continue": "tool",
# Otherwise we finish. END is a special node marking
# that the graph should finish.
"end": END,
},
)
workflow.add_edge("tool", "agent")
workflow.set_entry_point("agent")
app = workflow.compile()
inputs = {
"messages": [
HumanMessage(content="Star a repo composiohq/composio on GitHub"),
]
}
for output in app.stream(inputs): # type: ignore
for key, value in output.items():
print(f"Output from node '{key}':")
print("---")
print(value)
print("\n---\n")
@@ -0,0 +1,62 @@
from typing import Literal
from composio_langgraph import LanggraphProvider
from langchain_openai import ChatOpenAI
from langgraph.graph import MessagesState, StateGraph
from langgraph.prebuilt import ToolNode
from composio import Composio
composio = Composio(provider=LanggraphProvider())
tools = composio.tools.get(
user_id="default",
tools=[
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER",
"GITHUB_GET_THE_AUTHENTICATED_USER",
],
)
tool_node = ToolNode(tools)
model_with_tools = ChatOpenAI(temperature=0, streaming=True).bind_tools(tools)
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls: # type: ignore
return "tools"
return "__end__"
def call_model(state: MessagesState):
messages = state["messages"]
response = model_with_tools.invoke(messages)
return {"messages": [response]}
workflow = StateGraph(MessagesState)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
workflow.add_edge("__start__", "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
)
workflow.add_edge("tools", "agent")
app = workflow.compile()
for chunk in app.stream(
{
"messages": [
( # type: ignore
"human",
"Star the Github Repository composiohq/composio",
)
]
},
stream_mode="values",
):
chunk["messages"][-1].pretty_print()
+21
View File
@@ -0,0 +1,21 @@
[project]
name = "composio-langgraph"
version = "0.17.1"
description = "Use Composio to get array of tools with LangGraph Agent Workflows."
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 = [
"langgraph>=1.2.5",
"composio",
]
[project.urls]
Homepage = "https://github.com/ComposioHQ/composio"
+26
View File
@@ -0,0 +1,26 @@
"""
Setup configuration for Composio LangGraph plugin
"""
from pathlib import Path
from setuptools import setup
setup(
name="composio_langgraph",
version="0.17.1",
author="composio",
author_email="tech@composio.dev",
description="Use Composio to get array of tools with LangGraph Agent Workflows",
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=["langgraph>=1.2.5", "composio"],
include_package_data=True,
)