Skip to content

Commit 983e2f7

Browse files
authored
feat: LangGraph integration helpers and example (#33)
* 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) and prereqs - examples now document installing langgraph and langchain-openai explicitly * fix: update langgraph integration to use ToolNode instead of deprecated ToolExecutor - Replace ToolExecutor import with ToolNode in TYPE_CHECKING block - Update to_tool_executor function to return ToolNode instead of deprecated ToolExecutor - Add mypy override to ignore missing imports for langgraph modules - Fix linting issue with blank line whitespace in docstring * fix: mcp mypy error * fix: resolve mypy type errors for langgraph ToolNode fallback - Use class-based fallback instead of assignment for ToolNode in TYPE_CHECKING block - Add type: ignore for no-redef to handle the intentional redefinition - This fixes compatibility with Python 3.9 mypy type checking * fix: update CI to conditionally exclude server.py for Python 3.9 - Run mypy on server.py only for Python 3.10+ where MCP dependencies are available - Exclude server.py for Python 3.9 to avoid MCP import errors - This ensures type checking works correctly across all supported Python versions * fix: configure mypy to handle MCP library syntax errors - Add comprehensive mypy overrides for mcp module - Configure mypy to install types automatically in CI - Exclude .venv directory from type checking - This resolves pattern matching syntax errors from mcp library dependencies * fix: pin python 3.10 version - Remove unused type: ignore comment for try block - Add type: ignore for MCP decorator functions to handle untyped decorators - Set mypy python_version to 3.10 to properly handle pattern matching syntax - All mypy checks now pass successfully
1 parent 370699e commit 983e2f7

File tree

8 files changed

+403
-48
lines changed

8 files changed

+403
-48
lines changed

.github/workflows/lint.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@ jobs:
3434
args: check .
3535

3636
- name: Run Mypy
37-
run: uv run mypy stackone_ai --exclude stackone_ai/server.py
37+
run: |
38+
if [[ "${{ matrix.python-version }}" == "3.9" ]]; then
39+
uv run mypy stackone_ai --exclude stackone_ai/server.py
40+
else
41+
uv run mypy stackone_ai
42+
fi

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,58 @@ 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+
Prerequisites:
119+
120+
```bash
121+
pip install langgraph langchain-openai
122+
```
123+
124+
```python
125+
from langchain_openai import ChatOpenAI
126+
from typing import Annotated
127+
from typing_extensions import TypedDict
128+
129+
from langgraph.graph import StateGraph, START, END
130+
from langgraph.graph.message import add_messages
131+
from langgraph.prebuilt import tools_condition
132+
133+
from stackone_ai import StackOneToolSet
134+
from stackone_ai.integrations.langgraph import to_tool_node, bind_model_with_tools
135+
136+
# Prepare tools
137+
toolset = StackOneToolSet()
138+
tools = toolset.get_tools("hris_*", account_id="your-account-id")
139+
langchain_tools = tools.to_langchain()
140+
141+
class State(TypedDict):
142+
messages: Annotated[list, add_messages]
143+
144+
# Build a small agent loop: LLM -> maybe tools -> back to LLM
145+
graph = StateGraph(State)
146+
graph.add_node("tools", to_tool_node(langchain_tools))
147+
148+
def call_llm(state: dict):
149+
llm = ChatOpenAI(model="gpt-4o-mini")
150+
llm = bind_model_with_tools(llm, langchain_tools)
151+
resp = llm.invoke(state["messages"]) # returns AIMessage with optional tool_calls
152+
return {"messages": state["messages"] + [resp]}
153+
154+
graph.add_node("llm", call_llm)
155+
graph.add_edge(START, "llm")
156+
graph.add_conditional_edges("llm", tools_condition)
157+
graph.add_edge("tools", "llm")
158+
app = graph.compile()
159+
160+
_ = app.invoke({"messages": [("user", "Get employee with id emp123") ]})
161+
```
162+
163+
</details>
164+
113165
<details>
114166
<summary>CrewAI Integration (Python 3.10+)</summary>
115167

examples/langgraph_tool_node.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

pyproject.toml

Lines changed: 14 additions & 1 deletion
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
]
@@ -105,7 +106,7 @@ select = [
105106
]
106107

107108
[tool.mypy]
108-
python_version = "3.9"
109+
python_version = "3.10"
109110
disallow_untyped_defs = true
110111
disallow_incomplete_defs = true
111112
check_untyped_defs = true
@@ -115,7 +116,19 @@ warn_redundant_casts = true
115116
warn_unused_ignores = true
116117
warn_return_any = true
117118
warn_unreachable = true
119+
exclude = [
120+
"^.venv/",
121+
]
118122

119123
[[tool.mypy.overrides]]
120124
module = "bm25s"
121125
ignore_missing_imports = true
126+
127+
[[tool.mypy.overrides]]
128+
module = "langgraph.*"
129+
ignore_missing_imports = true
130+
131+
[[tool.mypy.overrides]]
132+
module = "mcp.*"
133+
ignore_missing_imports = true
134+
ignore_errors = true
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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 ToolNode
26+
except Exception: # pragma: no cover
27+
28+
class ToolNode: # type: ignore[no-redef]
29+
pass
30+
31+
32+
def _ensure_langgraph() -> None:
33+
try:
34+
from langgraph import prebuilt as _ # noqa: F401
35+
except Exception as e: # pragma: no cover
36+
raise ImportError(
37+
"LangGraph is not installed. Install with `pip install langgraph` or "
38+
"`pip install 'stackone-ai[examples]'`"
39+
) from e
40+
41+
42+
def _to_langchain_tools(tools: Tools | Sequence[BaseTool]) -> Sequence[BaseTool]:
43+
if isinstance(tools, Tools):
44+
return tools.to_langchain()
45+
return tools
46+
47+
48+
def to_tool_node(tools: Tools | Sequence[BaseTool], **kwargs: Any) -> Any:
49+
"""Create a LangGraph `ToolNode` from StackOne tools or LangChain tools.
50+
51+
Accepts either a `Tools` collection from this SDK or an existing sequence of
52+
LangChain `BaseTool` instances and returns a LangGraph `ToolNode` suitable
53+
for inclusion in a graph.
54+
"""
55+
_ensure_langgraph()
56+
from langgraph.prebuilt import ToolNode # local import with helpful error
57+
58+
langchain_tools = _to_langchain_tools(tools)
59+
return ToolNode(langchain_tools, **kwargs)
60+
61+
62+
def to_tool_executor(tools: Tools | Sequence[BaseTool], **kwargs: Any) -> Any:
63+
"""Create a LangGraph `ToolNode` from StackOne tools or LangChain tools.
64+
65+
Note: ToolExecutor has been deprecated in favor of ToolNode.
66+
This function now returns a ToolNode for compatibility.
67+
"""
68+
_ensure_langgraph()
69+
from langgraph.prebuilt import ToolNode # local import with helpful error
70+
71+
langchain_tools = _to_langchain_tools(tools)
72+
return ToolNode(langchain_tools, **kwargs)
73+
74+
75+
def bind_model_with_tools(model: Any, tools: Tools | Sequence[BaseTool]) -> Any:
76+
"""Bind tools to an LLM that supports LangChain's `.bind_tools()` API.
77+
78+
This is a tiny helper that converts a `Tools` collection to LangChain tools
79+
and calls `model.bind_tools(...)`.
80+
"""
81+
langchain_tools = _to_langchain_tools(tools)
82+
return model.bind_tools(langchain_tools)
83+
84+
85+
def create_react_agent(llm: Any, tools: Tools | Sequence[BaseTool], **kwargs: Any) -> Any:
86+
"""Create a LangGraph ReAct agent using StackOne tools.
87+
88+
Thin wrapper around `langgraph.prebuilt.create_react_agent` that accepts a
89+
`Tools` collection from this SDK.
90+
"""
91+
_ensure_langgraph()
92+
from langgraph.prebuilt import create_react_agent as _create
93+
94+
return _create(llm, _to_langchain_tools(tools), **kwargs)

stackone_ai/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
)
1818

19-
try: # type: ignore[unreachable]
19+
try:
2020
import mcp.types as types
2121
from mcp.server import NotificationOptions, Server
2222
from mcp.server.models import InitializationOptions
@@ -56,7 +56,7 @@ def tool_needs_account_id(tool_name: str) -> bool:
5656
return True
5757

5858

59-
@app.list_tools()
59+
@app.list_tools() # type: ignore[misc]
6060
async def list_tools() -> list[Tool]:
6161
"""List all available StackOne tools as MCP tools."""
6262
if not toolset:
@@ -114,7 +114,7 @@ async def list_tools() -> list[Tool]:
114114
) from e
115115

116116

117-
@app.call_tool()
117+
@app.call_tool() # type: ignore[misc]
118118
async def call_tool(
119119
name: str, arguments: dict[str, Any]
120120
) -> list[TextContent | ImageContent | EmbeddedResource]:

0 commit comments

Comments
 (0)