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,5 @@
1
+ from .router import ToolPrune, CandidateTool, SelectionResult, prune
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["ToolPrune", "CandidateTool", "SelectionResult", "prune"]
5
+
@@ -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
+