Skip to content

Commit 08a8600

Browse files
SonAIengineclaude
andcommitted
refactor: filter_tools/GraphToolkit 최상위 export — LangChain 의존 없이 from graph_tool_call import filter_tools
- toolkit.py를 graph_tool_call/ 최상위로 이동 (langchain 패키지 의존 제거) - `from graph_tool_call import filter_tools, GraphToolkit` 가능 - 기존 `from graph_tool_call.langchain import ...` 경로도 호환 유지 - README: "Wrap Existing Tools" 독립 섹션 분리, LangChain 전용은 별도로 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5e6b41f commit 08a8600

File tree

5 files changed

+259
-36
lines changed

5 files changed

+259
-36
lines changed

README.md

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -489,54 +489,59 @@ from graph_tool_call.middleware import patch_anthropic
489489
patch_anthropic(client, graph=tg, top_k=5)
490490
```
491491

492-
### LangChain Integration
493-
494-
```bash
495-
pip install graph-tool-call[langchain]
496-
```
492+
### Wrap Existing Tools (any format)
497493

498-
**Wrap existing tools** — filter any tool list down to relevant ones:
494+
Already have a tool list? Wrap it with `filter_tools`**no extra dependencies**, works with any format:
499495

500496
```python
501-
from graph_tool_call.langchain import filter_tools
497+
from graph_tool_call import filter_tools
502498

503-
# Works with any tool format:
504-
# - LangChain BaseTool (@tool, StructuredTool, etc.)
505-
# - OpenAI function dicts ({"type": "function", "function": {...}})
506-
# - MCP tool dicts ({"name": ..., "inputSchema": ...})
507-
# - Python functions with type hints
499+
# Accepts any tool format:
500+
# LangChain BaseTool, OpenAI function dicts, MCP tool dicts,
501+
# Anthropic tool dicts, or plain Python functions
508502

509503
filtered = filter_tools(all_tools, "send an email to John", top_k=5)
510-
511-
agent = create_react_agent(llm, filtered)
512-
agent.invoke({"input": "send an email to John"})
504+
# → only the 5 most relevant tools, original objects preserved
513505
```
514506

515507
**Reusable toolkit** — build the graph once, filter per query:
516508

517509
```python
518-
from graph_tool_call.langchain import GraphToolkit
510+
from graph_tool_call import GraphToolkit
519511

520512
toolkit = GraphToolkit(tools=all_tools, top_k=5)
521513

522-
# Each call returns only relevant tools — original objects preserved
523514
tools_a = toolkit.get_tools("cancel my order")
524515
tools_b = toolkit.get_tools("check the weather")
525516

526517
# Access the underlying ToolGraph for advanced config
527518
toolkit.graph.enable_embedding("ollama/qwen3-embedding:0.6b")
528519
```
529520

521+
### LangChain Integration
522+
523+
```bash
524+
pip install graph-tool-call[langchain]
525+
```
526+
527+
`filter_tools` / `GraphToolkit` work directly with LangChain agents:
528+
529+
```python
530+
from graph_tool_call import filter_tools
531+
532+
filtered = filter_tools(langchain_tools, "cancel order", top_k=5)
533+
agent = create_react_agent(llm, filtered)
534+
```
535+
530536
<details>
531-
<summary>Retriever (returns Documents instead of tools)</summary>
537+
<summary>LangChain Retriever (returns Documents instead of tools)</summary>
532538

533539
```python
534540
from graph_tool_call import ToolGraph
535541
from graph_tool_call.langchain import GraphToolRetriever
536542

537543
tg = ToolGraph.from_url("https://api.example.com/openapi.json")
538544

539-
# Use as a LangChain retriever — compatible with any chain/agent
540545
retriever = GraphToolRetriever(tool_graph=tg, top_k=5)
541546
docs = retriever.invoke("cancel an order")
542547

graph_tool_call/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"CategorySummary",
99
"DuplicatePair",
1010
"GraphAnalysisReport",
11+
"GraphToolkit",
1112
"MCPAnnotations",
1213
"MergeStrategy",
1314
"NodeType",
@@ -20,6 +21,7 @@
2021
"ToolCallPolicy",
2122
"ToolGraph",
2223
"ToolSchema",
24+
"filter_tools",
2325
"parse_tool",
2426
]
2527

@@ -36,6 +38,8 @@
3638
"ToolCallPolicy": ("graph_tool_call.assist.policy", "ToolCallPolicy"),
3739
"RetrievalResult": ("graph_tool_call.retrieval.engine", "RetrievalResult"),
3840
"SearchMode": ("graph_tool_call.retrieval.engine", "SearchMode"),
41+
"filter_tools": ("graph_tool_call.toolkit", "filter_tools"),
42+
"GraphToolkit": ("graph_tool_call.toolkit", "GraphToolkit"),
3943
}
4044

4145

graph_tool_call/langchain/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
1212
"GraphToolRetriever": ("graph_tool_call.langchain.retriever", "GraphToolRetriever"),
13-
"GraphToolkit": ("graph_tool_call.langchain.toolkit", "GraphToolkit"),
14-
"filter_tools": ("graph_tool_call.langchain.toolkit", "filter_tools"),
13+
"GraphToolkit": ("graph_tool_call.toolkit", "GraphToolkit"),
14+
"filter_tools": ("graph_tool_call.toolkit", "filter_tools"),
1515
"langchain_tools_to_schemas": ("graph_tool_call.langchain.tools", "langchain_tools_to_schemas"),
1616
"tool_schema_to_openai_function": (
1717
"graph_tool_call.langchain.tools",

graph_tool_call/toolkit.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""Toolkit: wrap existing tools with graph-based filtering.
2+
3+
Provides :func:`filter_tools` for one-shot filtering and
4+
:class:`GraphToolkit` for reusable tool management with retrieval.
5+
6+
Accepts any tool format:
7+
- LangChain ``BaseTool`` (``@tool``, ``StructuredTool``, etc.)
8+
- OpenAI function dict (``{"type": "function", "function": {"name": ...}}``)
9+
- Anthropic tool dict (``{"name": ..., "input_schema": ...}``)
10+
- MCP tool dict (``{"name": ..., "inputSchema": ...}``)
11+
- Python callable with type hints
12+
13+
Usage::
14+
15+
from graph_tool_call.langchain import filter_tools, GraphToolkit
16+
17+
# One-shot: filter tools by query
18+
filtered = filter_tools(all_tools, "cancel order", top_k=5)
19+
20+
# Reusable: wrap once, filter many times
21+
toolkit = GraphToolkit(tools=all_tools, top_k=5)
22+
filtered = toolkit.get_tools("cancel order")
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import logging
28+
from typing import Any
29+
30+
logger = logging.getLogger("graph-tool-call.langchain")
31+
32+
33+
def _extract_name(tool: Any) -> str:
34+
"""Extract tool name from any supported format."""
35+
# Object with .name attribute (LangChain BaseTool, ToolSchema, etc.)
36+
if hasattr(tool, "name"):
37+
return tool.name
38+
39+
# Dict formats
40+
if isinstance(tool, dict):
41+
# OpenAI: {"type": "function", "function": {"name": ...}}
42+
if "function" in tool:
43+
return tool["function"].get("name", "")
44+
# MCP / Anthropic: {"name": ...}
45+
if "name" in tool:
46+
return tool["name"]
47+
48+
# Callable (Python function)
49+
if callable(tool):
50+
return getattr(tool, "__name__", "")
51+
52+
return ""
53+
54+
55+
def _ingest_tools(graph: Any, tools: list[Any]) -> None:
56+
"""Ingest tools into a ToolGraph, auto-detecting format."""
57+
from graph_tool_call.core.tool import parse_tool
58+
59+
callables = []
60+
for tool in tools:
61+
if callable(tool) and not hasattr(tool, "name") and not isinstance(tool, dict):
62+
callables.append(tool)
63+
else:
64+
graph.add_tool(parse_tool(tool))
65+
66+
if callables:
67+
graph.ingest_functions(callables)
68+
69+
70+
def filter_tools(
71+
tools: list[Any],
72+
query: str,
73+
*,
74+
top_k: int = 5,
75+
graph: Any | None = None,
76+
) -> list[Any]:
77+
"""Filter tools by relevance to *query*.
78+
79+
Parameters
80+
----------
81+
tools:
82+
List of tools in any format — LangChain ``BaseTool``, OpenAI function
83+
dicts, MCP tool dicts, Anthropic tool dicts, or Python callables.
84+
query:
85+
Natural-language query to match tools against.
86+
top_k:
87+
Maximum number of tools to return (default: 5).
88+
graph:
89+
Optional pre-built ``ToolGraph``. If *None*, a temporary graph is
90+
built from *tools* on the fly.
91+
92+
Returns
93+
-------
94+
list
95+
Subset of *tools* ranked by relevance. Original tool objects are
96+
preserved (not copies), so they remain callable by the agent.
97+
"""
98+
from graph_tool_call import ToolGraph
99+
100+
if graph is None:
101+
graph = ToolGraph()
102+
103+
# Index by name for fast lookup
104+
tool_map: dict[str, Any] = {}
105+
for t in tools:
106+
name = _extract_name(t)
107+
if name:
108+
tool_map[name] = t
109+
110+
# Ingest if not already present
111+
existing = set(graph.tools.keys())
112+
if not existing.intersection(tool_map.keys()):
113+
_ingest_tools(graph, tools)
114+
115+
results = graph.retrieve(query, top_k=top_k)
116+
result_names = [r.name for r in results]
117+
118+
filtered = [tool_map[name] for name in result_names if name in tool_map]
119+
120+
if filtered:
121+
logger.debug(
122+
"Filtered %d → %d tools for query: %s",
123+
len(tools),
124+
len(filtered),
125+
query[:50],
126+
)
127+
return filtered
128+
129+
logger.debug("Retrieval returned no matches, returning all %d tools", len(tools))
130+
return list(tools)
131+
132+
133+
class GraphToolkit:
134+
"""Wraps a list of tools with graph-based retrieval.
135+
136+
Build once from existing tools, then call :meth:`get_tools` per query.
137+
138+
Parameters
139+
----------
140+
tools:
141+
List of tools in any format — LangChain ``BaseTool``, OpenAI function
142+
dicts, MCP tool dicts, Anthropic tool dicts, or Python callables.
143+
top_k:
144+
Default number of tools to return per query.
145+
graph:
146+
Optional pre-built ``ToolGraph``. If *None*, one is built from *tools*.
147+
"""
148+
149+
def __init__(
150+
self,
151+
tools: list[Any],
152+
*,
153+
top_k: int = 5,
154+
graph: Any | None = None,
155+
) -> None:
156+
from graph_tool_call import ToolGraph
157+
158+
self._tools: dict[str, Any] = {}
159+
for t in tools:
160+
name = _extract_name(t)
161+
if name:
162+
self._tools[name] = t
163+
164+
self._top_k = top_k
165+
166+
if graph is not None:
167+
self._graph: ToolGraph = graph
168+
else:
169+
self._graph = ToolGraph()
170+
171+
# Ingest tools into graph
172+
existing = set(self._graph.tools.keys())
173+
if not existing.intersection(self._tools.keys()):
174+
_ingest_tools(self._graph, tools)
175+
176+
@property
177+
def graph(self) -> Any:
178+
"""Underlying ``ToolGraph`` instance."""
179+
return self._graph
180+
181+
@property
182+
def all_tools(self) -> list[Any]:
183+
"""All registered tools."""
184+
return list(self._tools.values())
185+
186+
def get_tools(self, query: str, *, top_k: int | None = None) -> list[Any]:
187+
"""Return tools relevant to *query*.
188+
189+
Parameters
190+
----------
191+
query:
192+
Natural-language query.
193+
top_k:
194+
Override the default top_k for this call.
195+
196+
Returns
197+
-------
198+
list
199+
Filtered tools, ordered by relevance. Original objects preserved.
200+
"""
201+
k = top_k if top_k is not None else self._top_k
202+
results = self._graph.retrieve(query, top_k=k)
203+
result_names = [r.name for r in results]
204+
205+
filtered = [self._tools[name] for name in result_names if name in self._tools]
206+
return filtered if filtered else self.all_tools

0 commit comments

Comments
 (0)