Skip to content

Commit 82f08cd

Browse files
committed
feat(langgraph): add integration helpers, aligned example, and docs
- Add stackone_ai/integrations/langgraph with: - to_tool_node, to_tool_executor - bind_model_with_tools, create_react_agent - Add examples/langgraph_tool_node.py matching README snippet (English comments) - Update README with a minimal LangGraph agent loop (tools_condition) - Require langgraph[openai] in the examples extra
1 parent 3c5d8fb commit 82f08cd

File tree

6 files changed

+412
-25
lines changed

6 files changed

+412
-25
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,52 @@ for tool_call in response.tool_calls:
110110

111111
</details>
112112

113+
<details>
114+
<summary>LangGraph Integration</summary>
115+
116+
StackOne tools convert to LangChain tools, which LangGraph consumes via its prebuilt nodes:
117+
118+
```python
119+
from langchain_openai import ChatOpenAI
120+
from typing import Annotated
121+
from typing_extensions import TypedDict
122+
123+
from langgraph.graph import StateGraph, START, END
124+
from langgraph.graph.message import add_messages
125+
from langgraph.prebuilt import tools_condition
126+
127+
from stackone_ai import StackOneToolSet
128+
from stackone_ai.integrations.langgraph import to_tool_node, bind_model_with_tools
129+
130+
# Prepare tools
131+
toolset = StackOneToolSet()
132+
tools = toolset.get_tools("hris_*", account_id="your-account-id")
133+
langchain_tools = tools.to_langchain()
134+
135+
class State(TypedDict):
136+
messages: Annotated[list, add_messages]
137+
138+
# Build a small agent loop: LLM -> maybe tools -> back to LLM
139+
graph = StateGraph(State)
140+
graph.add_node("tools", to_tool_node(langchain_tools))
141+
142+
def call_llm(state: dict):
143+
llm = ChatOpenAI(model="gpt-4o-mini")
144+
llm = bind_model_with_tools(llm, langchain_tools)
145+
resp = llm.invoke(state["messages"]) # returns AIMessage with optional tool_calls
146+
return {"messages": state["messages"] + [resp]}
147+
148+
graph.add_node("llm", call_llm)
149+
graph.add_edge(START, "llm")
150+
graph.add_conditional_edges("llm", tools_condition)
151+
graph.add_edge("tools", "llm")
152+
app = graph.compile()
153+
154+
_ = app.invoke({"messages": [("user", "Get employee with id emp123") ]})
155+
```
156+
157+
</details>
158+
113159
<details>
114160
<summary>CrewAI Integration (Python 3.10+)</summary>
115161

examples/langgraph_tool_node.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,60 @@
11
"""
2-
TODO!!
2+
Minimal LangGraph example identical to the README snippet.
33
4-
This example demonstrates how to use StackOne tools with LangGraph.
4+
Run:
5+
uv run examples/langgraph_tool_node.py
56
6-
```bash
7-
uv run examples/langgraph_tool_node.py
8-
```
7+
Prerequisites:
8+
- `pip install 'stackone-ai[examples]'` (includes `langgraph[openai]`)
9+
- `STACKONE_API_KEY` (required) and `OPENAI_API_KEY` (required)
10+
- Optionally set `STACKONE_ACCOUNT_ID` (required by some tools)
911
"""
1012

13+
import os
14+
from typing import Annotated
15+
1116
from dotenv import load_dotenv
17+
from langchain_openai import ChatOpenAI
18+
from langgraph.graph import START, StateGraph
19+
from langgraph.graph.message import add_messages
20+
from langgraph.prebuilt import tools_condition
21+
from typing_extensions import TypedDict
1222

1323
from stackone_ai import StackOneToolSet
24+
from stackone_ai.integrations.langgraph import bind_model_with_tools, to_tool_node
1425

15-
load_dotenv()
16-
17-
account_id = "45072196112816593343"
18-
employee_id = "c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA"
1926

27+
def main() -> None:
28+
load_dotenv()
2029

21-
def langgraph_tool_node() -> None:
22-
"""Demonstrate basic LangGraph integration with StackOne tools."""
30+
# Prepare tools
31+
account_id = os.getenv("STACKONE_ACCOUNT_ID") # Set if your tools require it
2332
toolset = StackOneToolSet()
2433
tools = toolset.get_tools("hris_*", account_id=account_id)
34+
langchain_tools = tools.to_langchain()
2535

26-
# Verify we have the tools we need
27-
assert len(tools) > 0, "Expected at least one HRIS tool"
28-
employee_tool = tools.get_tool("hris_get_employee")
29-
assert employee_tool is not None, "Expected hris_get_employee tool"
36+
class State(TypedDict):
37+
messages: Annotated[list, add_messages]
3038

31-
# TODO: Add LangGraph specific integration
32-
# For now, just verify the tools are properly configured
33-
langchain_tools = tools.to_langchain()
34-
assert len(langchain_tools) > 0, "Expected LangChain tools"
35-
assert all(hasattr(tool, "_run") for tool in langchain_tools), "Expected all tools to have _run method"
39+
# Build a small agent loop: LLM -> maybe tools -> back to LLM
40+
graph = StateGraph(State)
41+
graph.add_node("tools", to_tool_node(langchain_tools))
42+
43+
def call_llm(state: dict):
44+
llm = ChatOpenAI(model="gpt-4o-mini")
45+
llm = bind_model_with_tools(llm, langchain_tools)
46+
resp = llm.invoke(state["messages"]) # returns AIMessage with optional tool_calls
47+
return {"messages": state["messages"] + [resp]}
48+
49+
graph.add_node("llm", call_llm)
50+
graph.add_edge(START, "llm")
51+
graph.add_conditional_edges("llm", tools_condition)
52+
graph.add_edge("tools", "llm")
53+
app = graph.compile()
54+
55+
# Kick off with a simple instruction; replace IDs as needed
56+
_ = app.invoke({"messages": [("user", "Get employee with id emp123")]})
3657

3758

3859
if __name__ == "__main__":
39-
langgraph_tool_node()
60+
main()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mcp = [
5353
examples = [
5454
"crewai>=0.102.0; python_version>='3.10'",
5555
"langchain-openai>=0.3.6",
56+
"langgraph>=0.2.0",
5657
"openai>=1.63.2",
5758
"python-dotenv>=1.0.1",
5859
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Integration helpers for external frameworks.
2+
3+
Currently includes:
4+
5+
- LangGraph helpers to turn StackOne tools into a `ToolNode` or `ToolExecutor`.
6+
"""
7+
8+
from .langgraph import (
9+
bind_model_with_tools,
10+
create_react_agent,
11+
to_tool_executor,
12+
to_tool_node,
13+
)
14+
15+
__all__ = [
16+
"to_tool_node",
17+
"to_tool_executor",
18+
"bind_model_with_tools",
19+
"create_react_agent",
20+
]
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""LangGraph integration helpers.
2+
3+
These utilities convert StackOne tools into LangGraph prebuilt components.
4+
5+
Usage:
6+
from stackone_ai import StackOneToolSet
7+
from stackone_ai.integrations.langgraph import to_tool_node
8+
9+
toolset = StackOneToolSet()
10+
tools = toolset.get_tools("hris_*", account_id="...")
11+
node = to_tool_node(tools) # langgraph.prebuilt.ToolNode
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from collections.abc import Sequence
17+
from typing import TYPE_CHECKING, Any
18+
19+
from langchain_core.tools import BaseTool
20+
21+
from stackone_ai.models import Tools
22+
23+
if TYPE_CHECKING: # pragma: no cover - only for typing
24+
try:
25+
from langgraph.prebuilt import ToolExecutor, ToolNode
26+
except Exception: # pragma: no cover
27+
ToolExecutor = Any
28+
ToolNode = Any
29+
30+
31+
def _ensure_langgraph() -> None:
32+
try:
33+
from langgraph import prebuilt as _ # noqa: F401
34+
except Exception as e: # pragma: no cover
35+
raise ImportError(
36+
"LangGraph is not installed. Install with `pip install langgraph` or "
37+
"`pip install 'stackone-ai[examples]'`"
38+
) from e
39+
40+
41+
def _to_langchain_tools(tools: Tools | Sequence[BaseTool]) -> Sequence[BaseTool]:
42+
if isinstance(tools, Tools):
43+
return tools.to_langchain()
44+
return tools
45+
46+
47+
def to_tool_node(tools: Tools | Sequence[BaseTool], **kwargs: Any) -> Any:
48+
"""Create a LangGraph `ToolNode` from StackOne tools or LangChain tools.
49+
50+
Accepts either a `Tools` collection from this SDK or an existing sequence of
51+
LangChain `BaseTool` instances and returns a LangGraph `ToolNode` suitable
52+
for inclusion in a graph.
53+
"""
54+
_ensure_langgraph()
55+
from langgraph.prebuilt import ToolNode # local import with helpful error
56+
57+
langchain_tools = _to_langchain_tools(tools)
58+
return ToolNode(langchain_tools, **kwargs)
59+
60+
61+
def to_tool_executor(tools: Tools | Sequence[BaseTool], **kwargs: Any) -> Any:
62+
"""Create a LangGraph `ToolExecutor` from StackOne tools or LangChain tools."""
63+
_ensure_langgraph()
64+
from langgraph.prebuilt import ToolExecutor # local import with helpful error
65+
66+
langchain_tools = _to_langchain_tools(tools)
67+
return ToolExecutor(langchain_tools, **kwargs)
68+
69+
70+
def bind_model_with_tools(model: Any, tools: Tools | Sequence[BaseTool]) -> Any:
71+
"""Bind tools to an LLM that supports LangChain's `.bind_tools()` API.
72+
73+
This is a tiny helper that converts a `Tools` collection to LangChain tools
74+
and calls `model.bind_tools(...)`.
75+
"""
76+
langchain_tools = _to_langchain_tools(tools)
77+
return model.bind_tools(langchain_tools)
78+
79+
80+
def create_react_agent(llm: Any, tools: Tools | Sequence[BaseTool], **kwargs: Any) -> Any:
81+
"""Create a LangGraph ReAct agent using StackOne tools.
82+
83+
Thin wrapper around `langgraph.prebuilt.create_react_agent` that accepts a
84+
`Tools` collection from this SDK.
85+
"""
86+
_ensure_langgraph()
87+
from langgraph.prebuilt import create_react_agent as _create
88+
89+
return _create(llm, _to_langchain_tools(tools), **kwargs)

0 commit comments

Comments
 (0)