tool-prune 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tool-prune
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Calibrated tool selection and schema pruning for AI agents. Zero dependencies.
|
|
5
|
+
Keywords: tool-prune,tool-selection,mcp,agents,typesafe,routing,function-calling,llm
|
|
6
|
+
Author-email: Hemanth HM <hemanth.hm@gmail.com>
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Project-URL: Homepage, https://github.com/hemanth/tool-prune
|
|
20
|
+
Project-URL: Repository, https://github.com/hemanth/tool-prune
|
|
21
|
+
|
|
22
|
+
# tool-prune
|
|
23
|
+
|
|
24
|
+
Calibrated tool selection and schema pruning for AI agents. Zero dependencies.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install tool-prune
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from tool_prune import prune
|
|
34
|
+
|
|
35
|
+
tools = {
|
|
36
|
+
"read_file": "Read raw text from local filesystem path",
|
|
37
|
+
"run_query": "Execute SQL queries against database",
|
|
38
|
+
"web_search": "Search public web for documentation or articles"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
match = prune("what tables exist in the db?", tools)
|
|
42
|
+
print(match.tool) # 'run_query'
|
|
43
|
+
print(match.confidence) # 1.0
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`prune()` narrows schemas to Top-K candidates or identifies direct matches in ~140ms. `dispatch()` runs immediate tool execution if confidence clears the threshold. That's the whole API.
|
|
47
|
+
|
|
48
|
+
## Schema pruning for LLMs
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from tool_prune import ToolPrune
|
|
52
|
+
|
|
53
|
+
router = ToolPrune(tools)
|
|
54
|
+
top_tools = router.filter(user_prompt, k=3)
|
|
55
|
+
|
|
56
|
+
response = llm.chat(
|
|
57
|
+
tools=top_tools,
|
|
58
|
+
messages=[{"role": "user", "content": user_prompt}]
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Drops prompt tokens by 75-85% and eliminates context distraction without losing tools.
|
|
63
|
+
|
|
64
|
+
## Fast-path direct dispatch
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
result = router.dispatch("read ./pyproject.toml", {
|
|
68
|
+
"read_file": lambda q, m: open("pyproject.toml").read(),
|
|
69
|
+
"run_query": lambda q, m: db.query(q),
|
|
70
|
+
"fallback": lambda q, m: call_llm(q)
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Runs deterministic handlers in under 160ms with zero token cost.
|
|
75
|
+
|
|
76
|
+
## Demo
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python examples/quickstart.py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Runs live quickstart routing with your `TYPESAFE_API_KEY`.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT © [Hemanth.HM](https://h3manth.com)
|
|
87
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# tool-prune
|
|
2
|
+
|
|
3
|
+
Calibrated tool selection and schema pruning for AI agents. Zero dependencies.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install tool-prune
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from tool_prune import prune
|
|
13
|
+
|
|
14
|
+
tools = {
|
|
15
|
+
"read_file": "Read raw text from local filesystem path",
|
|
16
|
+
"run_query": "Execute SQL queries against database",
|
|
17
|
+
"web_search": "Search public web for documentation or articles"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
match = prune("what tables exist in the db?", tools)
|
|
21
|
+
print(match.tool) # 'run_query'
|
|
22
|
+
print(match.confidence) # 1.0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`prune()` narrows schemas to Top-K candidates or identifies direct matches in ~140ms. `dispatch()` runs immediate tool execution if confidence clears the threshold. That's the whole API.
|
|
26
|
+
|
|
27
|
+
## Schema pruning for LLMs
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from tool_prune import ToolPrune
|
|
31
|
+
|
|
32
|
+
router = ToolPrune(tools)
|
|
33
|
+
top_tools = router.filter(user_prompt, k=3)
|
|
34
|
+
|
|
35
|
+
response = llm.chat(
|
|
36
|
+
tools=top_tools,
|
|
37
|
+
messages=[{"role": "user", "content": user_prompt}]
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Drops prompt tokens by 75-85% and eliminates context distraction without losing tools.
|
|
42
|
+
|
|
43
|
+
## Fast-path direct dispatch
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
result = router.dispatch("read ./pyproject.toml", {
|
|
47
|
+
"read_file": lambda q, m: open("pyproject.toml").read(),
|
|
48
|
+
"run_query": lambda q, m: db.query(q),
|
|
49
|
+
"fallback": lambda q, m: call_llm(q)
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Runs deterministic handlers in under 160ms with zero token cost.
|
|
54
|
+
|
|
55
|
+
## Demo
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python examples/quickstart.py
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Runs live quickstart routing with your `TYPESAFE_API_KEY`.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT © [Hemanth.HM](https://h3manth.com)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tool-prune"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Calibrated tool selection and schema pruning for AI agents. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Hemanth HM", email = "hemanth.hm@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["tool-prune", "tool-selection", "mcp", "agents", "typesafe", "routing", "function-calling", "llm"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/hemanth/tool-prune"
|
|
31
|
+
Repository = "https://github.com/hemanth/tool-prune"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CandidateTool:
|
|
11
|
+
name: str
|
|
12
|
+
probability: float
|
|
13
|
+
tool: Any = None
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SelectionResult:
|
|
17
|
+
tool: str
|
|
18
|
+
confidence: float
|
|
19
|
+
probability: float
|
|
20
|
+
top_k: List[CandidateTool]
|
|
21
|
+
requires_generation: float
|
|
22
|
+
latency_ms: float
|
|
23
|
+
usage: Optional[Dict[str, int]] = None
|
|
24
|
+
raw: Optional[Dict[str, Any]] = None
|
|
25
|
+
|
|
26
|
+
def _normalize_tools(tools: Union[Dict[str, Any], List[Dict[str, Any]]]):
|
|
27
|
+
criteria: Dict[str, str] = {}
|
|
28
|
+
registry: Dict[str, Any] = {}
|
|
29
|
+
|
|
30
|
+
if isinstance(tools, list):
|
|
31
|
+
for t in tools:
|
|
32
|
+
if not isinstance(t, dict):
|
|
33
|
+
continue
|
|
34
|
+
name = t.get("name") or t.get("id")
|
|
35
|
+
if not name:
|
|
36
|
+
continue
|
|
37
|
+
desc = t.get("criteria") or t.get("description") or t.get("summary") or name
|
|
38
|
+
criteria[name] = str(desc)
|
|
39
|
+
registry[name] = t
|
|
40
|
+
elif isinstance(tools, dict):
|
|
41
|
+
for name, val in tools.items():
|
|
42
|
+
if isinstance(val, str):
|
|
43
|
+
criteria[name] = val
|
|
44
|
+
registry[name] = {"name": name, "description": val}
|
|
45
|
+
elif isinstance(val, dict):
|
|
46
|
+
desc = val.get("criteria") or val.get("description") or val.get("summary") or name
|
|
47
|
+
criteria[name] = str(desc)
|
|
48
|
+
registry[name] = {"name": name, **val}
|
|
49
|
+
|
|
50
|
+
return criteria, registry
|
|
51
|
+
|
|
52
|
+
class ToolPrune:
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
tools: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
56
|
+
api_key: Optional[str] = None,
|
|
57
|
+
model: str = "jev-latest",
|
|
58
|
+
endpoint: str = "https://api.typesafe.ai/v1/systemone",
|
|
59
|
+
threshold: float = 0.85,
|
|
60
|
+
top_k: int = 3,
|
|
61
|
+
):
|
|
62
|
+
self.criteria, self.registry = _normalize_tools(tools)
|
|
63
|
+
self.api_key = api_key or os.getenv("TYPESAFE_API_KEY")
|
|
64
|
+
self.model = model
|
|
65
|
+
self.endpoint = endpoint
|
|
66
|
+
self.threshold = threshold
|
|
67
|
+
self.top_k = top_k
|
|
68
|
+
|
|
69
|
+
def _prepare_payload(self, query: Union[str, Dict[str, Any]], options: Optional[Dict[str, Any]] = None) -> bytes:
|
|
70
|
+
if not self.api_key:
|
|
71
|
+
raise ValueError("TypeSafe API key required. Set TYPESAFE_API_KEY environment variable or pass api_key.")
|
|
72
|
+
|
|
73
|
+
state = {"intent": query} if isinstance(query, str) else query
|
|
74
|
+
opts = options or {}
|
|
75
|
+
body = {
|
|
76
|
+
"model": opts.get("model", self.model),
|
|
77
|
+
"state": state,
|
|
78
|
+
"questions": {
|
|
79
|
+
"tool": {
|
|
80
|
+
"type": "choice",
|
|
81
|
+
"instructions": opts.get("instructions", "Which specific tool is required to satisfy this intent?"),
|
|
82
|
+
"criteria": self.criteria,
|
|
83
|
+
},
|
|
84
|
+
"requires_generation": {
|
|
85
|
+
"type": "noul",
|
|
86
|
+
"instructions": "Does fulfilling this request require open-ended creative text or arbitrary code generation rather than a deterministic tool execution?",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
return json.dumps(body).encode("utf-8")
|
|
91
|
+
|
|
92
|
+
def _process_response(self, data: Dict[str, Any], latency_ms: float, top_k_count: Optional[int] = None) -> SelectionResult:
|
|
93
|
+
tool_answer = data.get("answers", {}).get("tool", {})
|
|
94
|
+
probs = tool_answer.get("probabilities", {})
|
|
95
|
+
sorted_probs = sorted(probs.items(), key=lambda kv: kv[1], reverse=True)
|
|
96
|
+
|
|
97
|
+
k = top_k_count or self.top_k
|
|
98
|
+
top_candidates = [
|
|
99
|
+
CandidateTool(name=name, probability=p, tool=self.registry.get(name))
|
|
100
|
+
for name, p in sorted_probs[:k]
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
selected_name = tool_answer.get("choice", "")
|
|
104
|
+
probability = probs.get(selected_name, 0.0)
|
|
105
|
+
confidence = tool_answer.get("confidence", probability)
|
|
106
|
+
requires_gen = data.get("answers", {}).get("requires_generation", {}).get("probability", 0.0)
|
|
107
|
+
|
|
108
|
+
return SelectionResult(
|
|
109
|
+
tool=selected_name,
|
|
110
|
+
confidence=confidence,
|
|
111
|
+
probability=probability,
|
|
112
|
+
top_k=top_candidates,
|
|
113
|
+
requires_generation=requires_gen,
|
|
114
|
+
latency_ms=latency_ms,
|
|
115
|
+
usage=data.get("usage"),
|
|
116
|
+
raw=data,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def select(self, query: Union[str, Dict[str, Any]], **kwargs) -> SelectionResult:
|
|
120
|
+
payload = self._prepare_payload(query, kwargs)
|
|
121
|
+
req = urllib.request.Request(
|
|
122
|
+
self.endpoint,
|
|
123
|
+
data=payload,
|
|
124
|
+
headers={
|
|
125
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
},
|
|
128
|
+
method="POST",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
start = time.perf_counter()
|
|
132
|
+
try:
|
|
133
|
+
with urllib.request.urlopen(req) as resp:
|
|
134
|
+
raw_data = resp.read()
|
|
135
|
+
except urllib.error.HTTPError as e:
|
|
136
|
+
err = e.read().decode("utf-8", errors="replace")
|
|
137
|
+
raise RuntimeError(f"tool-prune API error ({e.code}): {err}") from e
|
|
138
|
+
|
|
139
|
+
latency_ms = (time.perf_counter() - start) * 1000.0
|
|
140
|
+
data = json.loads(raw_data.decode("utf-8"))
|
|
141
|
+
return self._process_response(data, latency_ms, kwargs.get("top_k"))
|
|
142
|
+
|
|
143
|
+
def filter(self, query: Union[str, Dict[str, Any]], k: Optional[int] = None, **kwargs) -> List[Any]:
|
|
144
|
+
result = self.select(query, top_k=k or self.top_k, **kwargs)
|
|
145
|
+
target_k = k or self.top_k
|
|
146
|
+
return [c.tool if c.tool is not None else c.name for c in result.top_k[:target_k]]
|
|
147
|
+
|
|
148
|
+
def dispatch(
|
|
149
|
+
self,
|
|
150
|
+
query: Union[str, Dict[str, Any]],
|
|
151
|
+
handlers: Dict[str, Callable[..., Any]],
|
|
152
|
+
threshold: Optional[float] = None,
|
|
153
|
+
**kwargs,
|
|
154
|
+
) -> Any:
|
|
155
|
+
selection = self.select(query, **kwargs)
|
|
156
|
+
t = threshold if threshold is not None else self.threshold
|
|
157
|
+
|
|
158
|
+
if selection.confidence >= t and selection.tool in handlers:
|
|
159
|
+
return handlers[selection.tool](query, selection)
|
|
160
|
+
|
|
161
|
+
if "fallback" in handlers:
|
|
162
|
+
return handlers["fallback"](query, selection)
|
|
163
|
+
|
|
164
|
+
return selection
|
|
165
|
+
|
|
166
|
+
def prune(
|
|
167
|
+
query: Union[str, Dict[str, Any]],
|
|
168
|
+
tools: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
169
|
+
**kwargs,
|
|
170
|
+
) -> SelectionResult:
|
|
171
|
+
"""One-shot tool pruning and selection."""
|
|
172
|
+
return ToolPrune(tools, **kwargs).select(query)
|
|
173
|
+
|