chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user