ragleap-tools 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.
- ragleap_tools-0.1.0/.gitignore +32 -0
- ragleap_tools-0.1.0/CHANGELOG.md +42 -0
- ragleap_tools-0.1.0/PKG-INFO +131 -0
- ragleap_tools-0.1.0/README.md +105 -0
- ragleap_tools-0.1.0/pyproject.toml +40 -0
- ragleap_tools-0.1.0/src/ragleap_tools/__init__.py +74 -0
- ragleap_tools-0.1.0/src/ragleap_tools/base.py +79 -0
- ragleap_tools-0.1.0/src/ragleap_tools/calculator.py +139 -0
- ragleap_tools-0.1.0/src/ragleap_tools/datetime_tools.py +89 -0
- ragleap_tools-0.1.0/src/ragleap_tools/file_ops.py +156 -0
- ragleap_tools-0.1.0/src/ragleap_tools/ingest.py +76 -0
- ragleap_tools-0.1.0/src/ragleap_tools/json_csv.py +64 -0
- ragleap_tools-0.1.0/src/ragleap_tools/text_utils.py +92 -0
- ragleap_tools-0.1.0/src/ragleap_tools/unit_conversion.py +106 -0
- ragleap_tools-0.1.0/tests/test_calculator.py +73 -0
- ragleap_tools-0.1.0/tests/test_datetime_tools.py +30 -0
- ragleap_tools-0.1.0/tests/test_file_ops.py +106 -0
- ragleap_tools-0.1.0/tests/test_json_csv.py +25 -0
- ragleap_tools-0.1.0/tests/test_public_api.py +28 -0
- ragleap_tools-0.1.0/tests/test_text_utils.py +30 -0
- ragleap_tools-0.1.0/tests/test_unit_conversion.py +35 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Environment
|
|
2
|
+
.env
|
|
3
|
+
*.env.local
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
venv/
|
|
10
|
+
.venv/
|
|
11
|
+
|
|
12
|
+
# Node
|
|
13
|
+
node_modules/
|
|
14
|
+
|
|
15
|
+
# Docker
|
|
16
|
+
docker-compose.override.yml
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
|
|
22
|
+
# OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
25
|
+
|
|
26
|
+
# Logs
|
|
27
|
+
*.log
|
|
28
|
+
.env
|
|
29
|
+
|
|
30
|
+
# Package build artifacts
|
|
31
|
+
dist/
|
|
32
|
+
build/
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `ragleap-tools` are documented here. Format
|
|
4
|
+
loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release. `Tool`/`ToolResult` base abstraction with OpenAI
|
|
13
|
+
and Gemini function-calling schema generation. Deliberately does not
|
|
14
|
+
own a tool-calling execution loop - that's `ragleap-agents`' scope,
|
|
15
|
+
per the project roadmap's own split.
|
|
16
|
+
- 12 stateless tools: calculator (safe AST-based expression
|
|
17
|
+
evaluation, never `eval()`/`exec()`), `get_current_datetime`,
|
|
18
|
+
`add_to_date`, `date_difference`, `convert_length`,
|
|
19
|
+
`convert_weight`, `convert_temperature`, `parse_json`, `parse_csv`,
|
|
20
|
+
`regex_extract`, `word_count`, `text_case_transform`.
|
|
21
|
+
- 3 sandboxed file-op tools (`read_file`, `write_file`, `list_files`)
|
|
22
|
+
via `FileOpsConfig(root_dir=...)` - confined entirely to a
|
|
23
|
+
caller-specified root directory. Path resolution follows symlinks
|
|
24
|
+
before checking containment, so symlink-based sandbox escapes are
|
|
25
|
+
rejected, not just naive `../` string checking.
|
|
26
|
+
- 1 optional ingestion tool (`make_ingest_tool`, needs the `ingest`
|
|
27
|
+
extra: `pip install ragleap-tools[ingest]`) wrapping `ragleap-rag`'s
|
|
28
|
+
already-tested `ingest_text()` - no new ingestion logic.
|
|
29
|
+
- Explicitly out of scope for this release: code execution, web
|
|
30
|
+
search, HTTP fetch, and database/business-system connectors - each
|
|
31
|
+
documented in the README as needing its own security-focused design
|
|
32
|
+
pass rather than a rushed inclusion.
|
|
33
|
+
|
|
34
|
+
### Verified
|
|
35
|
+
|
|
36
|
+
- 51 tests, all passing. Includes real security verification, not
|
|
37
|
+
just documentation: the calculator's AST whitelist is tested against
|
|
38
|
+
real code-injection attempts (`__import__`, attribute access, list
|
|
39
|
+
comprehensions, multi-statement injection - all correctly rejected),
|
|
40
|
+
and file ops' sandboxing is tested against real path-traversal
|
|
41
|
+
attempts AND a real symlink-escape attempt (a file outside the
|
|
42
|
+
sandbox, symlinked from inside it - correctly rejected).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ragleap-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone, dependency-light tool implementations (calculator, file ops, datetime, unit conversion, JSON/CSV, text utilities) exposing OpenAI/Gemini-style function-calling schemas. No tool-calling execution loop - wire these into your own LLM tool-calling code, or into ragleap-agents when that ships. BYOK, no vendor lock-in.
|
|
5
|
+
Project-URL: Homepage, https://github.com/antonyrag/ragleap-core
|
|
6
|
+
Project-URL: Repository, https://github.com/antonyrag/ragleap-core
|
|
7
|
+
Project-URL: Documentation, https://packages.ragleap.com/docs/ragleap-tools.html
|
|
8
|
+
Project-URL: Issues, https://github.com/antonyrag/ragleap-core/issues
|
|
9
|
+
Author-email: Antony <antony@ragleap.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: agents,byok,calculator,file-ops,function-calling,llm,self-hosted,tool-calling
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Provides-Extra: ingest
|
|
22
|
+
Requires-Dist: ragleap-rag>=0.12.0; extra == 'ingest'
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ragleap-tools
|
|
28
|
+
|
|
29
|
+
Standalone, dependency-light tool implementations for LLM tool-calling.
|
|
30
|
+
Exposes OpenAI/Gemini-style function-calling schemas.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ragleap-tools
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What this is (and isn't)
|
|
37
|
+
|
|
38
|
+
`ragleap-tools` provides `Tool` objects - a name, a description, a JSON
|
|
39
|
+
Schema for parameters, and a safe handler function. It does **not** own
|
|
40
|
+
a tool-calling execution loop (deciding when to call a tool, running
|
|
41
|
+
it, feeding the result back to the model) - that's `ragleap-agents`'
|
|
42
|
+
job, per the project roadmap's own split. Wire these tools into your
|
|
43
|
+
own tool-calling code, or into `ragleap-agents` once that ships.
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from ragleap_tools import STATELESS_TOOLS, CALCULATOR_TOOL
|
|
49
|
+
|
|
50
|
+
# Give these to your LLM provider's tools= parameter:
|
|
51
|
+
openai_tools = [t.to_openai_schema() for t in STATELESS_TOOLS]
|
|
52
|
+
gemini_tools = [t.to_gemini_schema() for t in STATELESS_TOOLS]
|
|
53
|
+
|
|
54
|
+
# When the model calls one, invoke the handler yourself:
|
|
55
|
+
result = CALCULATOR_TOOL.call(expression="2 + 2 * sqrt(16)")
|
|
56
|
+
print(result.success, result.result) # True 10.0
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The 12 stateless tools
|
|
60
|
+
|
|
61
|
+
No configuration needed - import and use directly.
|
|
62
|
+
|
|
63
|
+
- `CALCULATOR_TOOL` - safe arithmetic (AST-based whitelist, never `eval()`)
|
|
64
|
+
- `CURRENT_DATETIME_TOOL`, `ADD_TO_DATE_TOOL`, `DATE_DIFFERENCE_TOOL` - date/time math
|
|
65
|
+
- `CONVERT_LENGTH_TOOL`, `CONVERT_WEIGHT_TOOL`, `CONVERT_TEMPERATURE_TOOL` - unit conversion
|
|
66
|
+
- `PARSE_JSON_TOOL`, `PARSE_CSV_TOOL` - structured data parsing
|
|
67
|
+
- `REGEX_EXTRACT_TOOL`, `WORD_COUNT_TOOL`, `TEXT_CASE_TRANSFORM_TOOL` - text utilities
|
|
68
|
+
|
|
69
|
+
## File operations (sandboxed, needs configuration)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from ragleap_tools import FileOpsConfig, make_file_tools
|
|
73
|
+
|
|
74
|
+
config = FileOpsConfig(root_dir="/path/to/a/safe/directory")
|
|
75
|
+
read_tool, write_tool, list_tool = make_file_tools(config)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Every operation is confined to `root_dir` - both `../` path traversal
|
|
79
|
+
and symlink-based escapes are rejected (verified via real security
|
|
80
|
+
tests, not just documented), not just naive string-prefix checking.
|
|
81
|
+
There is no unsandboxed mode.
|
|
82
|
+
|
|
83
|
+
## Document ingestion (optional, needs ragleap-rag)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install ragleap-tools[ingest]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from ragleap import RagLeap, ProviderConfig, EmbeddingConfig
|
|
91
|
+
from ragleap_tools import IngestConfig, make_ingest_tool
|
|
92
|
+
|
|
93
|
+
rag = RagLeap(database_url="...", primary=ProviderConfig(...), embedder=EmbeddingConfig(...))
|
|
94
|
+
ingest_tool = make_ingest_tool(IngestConfig(rag=rag))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Wraps `ragleap-rag`'s already-tested `ingest_text()` - no new ingestion
|
|
98
|
+
logic, just a tool schema on top of the real 28-format-capable
|
|
99
|
+
pipeline. `ragleap-rag` owns the actual ingestion; this is a thin
|
|
100
|
+
adapter, same pattern `ragleap-graph` uses for its own optional
|
|
101
|
+
`ragleap-rag` dependency.
|
|
102
|
+
|
|
103
|
+
## Deliberately out of scope for v0.1.0
|
|
104
|
+
|
|
105
|
+
Each of these needs its own security-focused design pass, not a
|
|
106
|
+
rushed inclusion here:
|
|
107
|
+
|
|
108
|
+
- **Code execution** - a real sandboxing/resource-limit design
|
|
109
|
+
decision, not something to bolt on alongside a calculator.
|
|
110
|
+
- **Web search** - needs a pluggable-provider decision (this
|
|
111
|
+
ecosystem's BYOK philosophy applies here too), not a single
|
|
112
|
+
hardcoded provider.
|
|
113
|
+
- **HTTP fetch** - letting an LLM request arbitrary URLs carries real
|
|
114
|
+
SSRF risk, same care level as code execution.
|
|
115
|
+
- **Database/business-system connectors** (SQL, CRM, payment
|
|
116
|
+
processors, etc.) - some of what this ecosystem already has
|
|
117
|
+
elsewhere (e.g. a live payment processor) would be a materially
|
|
118
|
+
different risk if exposed to LLM tool-calling without deliberate
|
|
119
|
+
guardrails (dry-run modes, confirmation steps, scoped permissions).
|
|
120
|
+
|
|
121
|
+
## Status
|
|
122
|
+
|
|
123
|
+
v0.1.0. 51 tests, all passing, including real security verification
|
|
124
|
+
for the two risk-sensitive tools (calculator's code-injection
|
|
125
|
+
rejection, file ops' path-traversal and symlink-escape rejection) -
|
|
126
|
+
not just documented as safe, actually tested against real attack
|
|
127
|
+
vectors.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ragleap-tools
|
|
2
|
+
|
|
3
|
+
Standalone, dependency-light tool implementations for LLM tool-calling.
|
|
4
|
+
Exposes OpenAI/Gemini-style function-calling schemas.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install ragleap-tools
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## What this is (and isn't)
|
|
11
|
+
|
|
12
|
+
`ragleap-tools` provides `Tool` objects - a name, a description, a JSON
|
|
13
|
+
Schema for parameters, and a safe handler function. It does **not** own
|
|
14
|
+
a tool-calling execution loop (deciding when to call a tool, running
|
|
15
|
+
it, feeding the result back to the model) - that's `ragleap-agents`'
|
|
16
|
+
job, per the project roadmap's own split. Wire these tools into your
|
|
17
|
+
own tool-calling code, or into `ragleap-agents` once that ships.
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from ragleap_tools import STATELESS_TOOLS, CALCULATOR_TOOL
|
|
23
|
+
|
|
24
|
+
# Give these to your LLM provider's tools= parameter:
|
|
25
|
+
openai_tools = [t.to_openai_schema() for t in STATELESS_TOOLS]
|
|
26
|
+
gemini_tools = [t.to_gemini_schema() for t in STATELESS_TOOLS]
|
|
27
|
+
|
|
28
|
+
# When the model calls one, invoke the handler yourself:
|
|
29
|
+
result = CALCULATOR_TOOL.call(expression="2 + 2 * sqrt(16)")
|
|
30
|
+
print(result.success, result.result) # True 10.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The 12 stateless tools
|
|
34
|
+
|
|
35
|
+
No configuration needed - import and use directly.
|
|
36
|
+
|
|
37
|
+
- `CALCULATOR_TOOL` - safe arithmetic (AST-based whitelist, never `eval()`)
|
|
38
|
+
- `CURRENT_DATETIME_TOOL`, `ADD_TO_DATE_TOOL`, `DATE_DIFFERENCE_TOOL` - date/time math
|
|
39
|
+
- `CONVERT_LENGTH_TOOL`, `CONVERT_WEIGHT_TOOL`, `CONVERT_TEMPERATURE_TOOL` - unit conversion
|
|
40
|
+
- `PARSE_JSON_TOOL`, `PARSE_CSV_TOOL` - structured data parsing
|
|
41
|
+
- `REGEX_EXTRACT_TOOL`, `WORD_COUNT_TOOL`, `TEXT_CASE_TRANSFORM_TOOL` - text utilities
|
|
42
|
+
|
|
43
|
+
## File operations (sandboxed, needs configuration)
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from ragleap_tools import FileOpsConfig, make_file_tools
|
|
47
|
+
|
|
48
|
+
config = FileOpsConfig(root_dir="/path/to/a/safe/directory")
|
|
49
|
+
read_tool, write_tool, list_tool = make_file_tools(config)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every operation is confined to `root_dir` - both `../` path traversal
|
|
53
|
+
and symlink-based escapes are rejected (verified via real security
|
|
54
|
+
tests, not just documented), not just naive string-prefix checking.
|
|
55
|
+
There is no unsandboxed mode.
|
|
56
|
+
|
|
57
|
+
## Document ingestion (optional, needs ragleap-rag)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install ragleap-tools[ingest]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from ragleap import RagLeap, ProviderConfig, EmbeddingConfig
|
|
65
|
+
from ragleap_tools import IngestConfig, make_ingest_tool
|
|
66
|
+
|
|
67
|
+
rag = RagLeap(database_url="...", primary=ProviderConfig(...), embedder=EmbeddingConfig(...))
|
|
68
|
+
ingest_tool = make_ingest_tool(IngestConfig(rag=rag))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Wraps `ragleap-rag`'s already-tested `ingest_text()` - no new ingestion
|
|
72
|
+
logic, just a tool schema on top of the real 28-format-capable
|
|
73
|
+
pipeline. `ragleap-rag` owns the actual ingestion; this is a thin
|
|
74
|
+
adapter, same pattern `ragleap-graph` uses for its own optional
|
|
75
|
+
`ragleap-rag` dependency.
|
|
76
|
+
|
|
77
|
+
## Deliberately out of scope for v0.1.0
|
|
78
|
+
|
|
79
|
+
Each of these needs its own security-focused design pass, not a
|
|
80
|
+
rushed inclusion here:
|
|
81
|
+
|
|
82
|
+
- **Code execution** - a real sandboxing/resource-limit design
|
|
83
|
+
decision, not something to bolt on alongside a calculator.
|
|
84
|
+
- **Web search** - needs a pluggable-provider decision (this
|
|
85
|
+
ecosystem's BYOK philosophy applies here too), not a single
|
|
86
|
+
hardcoded provider.
|
|
87
|
+
- **HTTP fetch** - letting an LLM request arbitrary URLs carries real
|
|
88
|
+
SSRF risk, same care level as code execution.
|
|
89
|
+
- **Database/business-system connectors** (SQL, CRM, payment
|
|
90
|
+
processors, etc.) - some of what this ecosystem already has
|
|
91
|
+
elsewhere (e.g. a live payment processor) would be a materially
|
|
92
|
+
different risk if exposed to LLM tool-calling without deliberate
|
|
93
|
+
guardrails (dry-run modes, confirmation steps, scoped permissions).
|
|
94
|
+
|
|
95
|
+
## Status
|
|
96
|
+
|
|
97
|
+
v0.1.0. 51 tests, all passing, including real security verification
|
|
98
|
+
for the two risk-sensitive tools (calculator's code-injection
|
|
99
|
+
rejection, file ops' path-traversal and symlink-escape rejection) -
|
|
100
|
+
not just documented as safe, actually tested against real attack
|
|
101
|
+
vectors.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ragleap-tools"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Standalone, dependency-light tool implementations (calculator, file ops, datetime, unit conversion, JSON/CSV, text utilities) exposing OpenAI/Gemini-style function-calling schemas. No tool-calling execution loop - wire these into your own LLM tool-calling code, or into ragleap-agents when that ships. BYOK, no vendor lock-in."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Antony", email = "antony@ragleap.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["llm", "tool-calling", "function-calling", "agents", "calculator", "file-ops", "self-hosted", "byok"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=8.0.0"]
|
|
31
|
+
ingest = ["ragleap-rag>=0.12.0"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/antonyrag/ragleap-core"
|
|
35
|
+
Repository = "https://github.com/antonyrag/ragleap-core"
|
|
36
|
+
Documentation = "https://packages.ragleap.com/docs/ragleap-tools.html"
|
|
37
|
+
Issues = "https://github.com/antonyrag/ragleap-core/issues"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/ragleap_tools"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ragleap_tools - standalone, dependency-light tool implementations for
|
|
3
|
+
LLM tool-calling. Exposes OpenAI/Gemini-style function-calling schemas.
|
|
4
|
+
|
|
5
|
+
Deliberately does NOT own a tool-calling execution loop - that's
|
|
6
|
+
ragleap-agents' job (per the project roadmap's own split). This package
|
|
7
|
+
is just the tools: name, description, JSON Schema parameters, and a
|
|
8
|
+
safe handler function you invoke yourself once your LLM picks a tool.
|
|
9
|
+
|
|
10
|
+
v0.1.0 - 7 tools:
|
|
11
|
+
calculator, file ops (read/write/list), get_current_datetime,
|
|
12
|
+
add_to_date, date_difference, convert_length, convert_weight,
|
|
13
|
+
convert_temperature, parse_json, parse_csv, regex_extract,
|
|
14
|
+
word_count, text_case_transform, ingest_document (needs the
|
|
15
|
+
optional ragleap-rag dependency: pip install ragleap-tools[ingest])
|
|
16
|
+
|
|
17
|
+
Deliberately out of scope for now (each needs its own security-focused
|
|
18
|
+
design pass, not a rushed inclusion here): code execution, web search,
|
|
19
|
+
HTTP fetch, and database/business-system connectors (SQL, CRM,
|
|
20
|
+
payment processors, etc.) - the last of those specifically because
|
|
21
|
+
some of the connectors this ecosystem already has elsewhere (e.g. a
|
|
22
|
+
live payment processor) would be a materially different risk if
|
|
23
|
+
exposed to LLM tool-calling without deliberate guardrails.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from ragleap_tools.base import Tool, ToolResult
|
|
27
|
+
from ragleap_tools.calculator import CALCULATOR_TOOL
|
|
28
|
+
from ragleap_tools.datetime_tools import ADD_TO_DATE_TOOL, CURRENT_DATETIME_TOOL, DATE_DIFFERENCE_TOOL
|
|
29
|
+
from ragleap_tools.file_ops import FileOpsConfig, make_file_tools
|
|
30
|
+
from ragleap_tools.ingest import IngestConfig, make_ingest_tool
|
|
31
|
+
from ragleap_tools.json_csv import PARSE_CSV_TOOL, PARSE_JSON_TOOL
|
|
32
|
+
from ragleap_tools.text_utils import REGEX_EXTRACT_TOOL, TEXT_CASE_TRANSFORM_TOOL, WORD_COUNT_TOOL
|
|
33
|
+
from ragleap_tools.unit_conversion import CONVERT_LENGTH_TOOL, CONVERT_TEMPERATURE_TOOL, CONVERT_WEIGHT_TOOL
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
# Tools with no config/state needed - usable directly.
|
|
38
|
+
STATELESS_TOOLS = [
|
|
39
|
+
CALCULATOR_TOOL,
|
|
40
|
+
CURRENT_DATETIME_TOOL,
|
|
41
|
+
ADD_TO_DATE_TOOL,
|
|
42
|
+
DATE_DIFFERENCE_TOOL,
|
|
43
|
+
CONVERT_LENGTH_TOOL,
|
|
44
|
+
CONVERT_WEIGHT_TOOL,
|
|
45
|
+
CONVERT_TEMPERATURE_TOOL,
|
|
46
|
+
PARSE_JSON_TOOL,
|
|
47
|
+
PARSE_CSV_TOOL,
|
|
48
|
+
REGEX_EXTRACT_TOOL,
|
|
49
|
+
WORD_COUNT_TOOL,
|
|
50
|
+
TEXT_CASE_TRANSFORM_TOOL,
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"Tool",
|
|
55
|
+
"ToolResult",
|
|
56
|
+
"STATELESS_TOOLS",
|
|
57
|
+
"CALCULATOR_TOOL",
|
|
58
|
+
"CURRENT_DATETIME_TOOL",
|
|
59
|
+
"ADD_TO_DATE_TOOL",
|
|
60
|
+
"DATE_DIFFERENCE_TOOL",
|
|
61
|
+
"CONVERT_LENGTH_TOOL",
|
|
62
|
+
"CONVERT_WEIGHT_TOOL",
|
|
63
|
+
"CONVERT_TEMPERATURE_TOOL",
|
|
64
|
+
"PARSE_JSON_TOOL",
|
|
65
|
+
"PARSE_CSV_TOOL",
|
|
66
|
+
"REGEX_EXTRACT_TOOL",
|
|
67
|
+
"WORD_COUNT_TOOL",
|
|
68
|
+
"TEXT_CASE_TRANSFORM_TOOL",
|
|
69
|
+
"FileOpsConfig",
|
|
70
|
+
"make_file_tools",
|
|
71
|
+
"IngestConfig",
|
|
72
|
+
"make_ingest_tool",
|
|
73
|
+
"__version__",
|
|
74
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ragleap_tools.base
|
|
3
|
+
|
|
4
|
+
Core Tool/ToolResult abstraction every tool implementation in this
|
|
5
|
+
package uses. Deliberately minimal - a name, a description, a JSON
|
|
6
|
+
Schema for parameters (the OpenAI/Gemini function-calling convention,
|
|
7
|
+
so no adapter layer is needed for either provider), and a handler
|
|
8
|
+
callable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Callable, Dict, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ToolResult:
|
|
19
|
+
"""Result of calling a tool's handler.
|
|
20
|
+
|
|
21
|
+
success=False + error=<message> on any failure - handlers should
|
|
22
|
+
never raise for expected failure modes (bad input, file not found,
|
|
23
|
+
etc.); they should catch and return a ToolResult so a caller's
|
|
24
|
+
tool-calling loop can feed a clean error back to the LLM instead of
|
|
25
|
+
crashing. Handlers MAY still raise for genuinely unexpected
|
|
26
|
+
programming errors - that's a real bug, not user input to explain
|
|
27
|
+
to the model.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
success: bool
|
|
31
|
+
result: Any = None
|
|
32
|
+
error: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Tool:
|
|
37
|
+
"""A single callable tool with an OpenAI/Gemini-compatible schema.
|
|
38
|
+
|
|
39
|
+
parameters: a JSON Schema object (the same shape both OpenAI's
|
|
40
|
+
`tools=[{"type": "function", "function": {...}}]` and Gemini's
|
|
41
|
+
`FunctionDeclaration` expect for their own `parameters` field) -
|
|
42
|
+
e.g. {"type": "object", "properties": {...}, "required": [...]}.
|
|
43
|
+
|
|
44
|
+
handler: called with the tool call's arguments as keyword
|
|
45
|
+
arguments (already parsed from whatever JSON the model returned -
|
|
46
|
+
parsing that JSON is the caller's responsibility, not this
|
|
47
|
+
package's). Must return a ToolResult, never raise for expected
|
|
48
|
+
failure modes (see ToolResult's docstring).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str
|
|
52
|
+
description: str
|
|
53
|
+
parameters: Dict[str, Any]
|
|
54
|
+
handler: Callable[..., ToolResult]
|
|
55
|
+
|
|
56
|
+
def to_openai_schema(self) -> Dict[str, Any]:
|
|
57
|
+
"""Returns the {"type": "function", "function": {...}} shape
|
|
58
|
+
OpenAI's `tools=` parameter expects."""
|
|
59
|
+
return {
|
|
60
|
+
"type": "function",
|
|
61
|
+
"function": {
|
|
62
|
+
"name": self.name,
|
|
63
|
+
"description": self.description,
|
|
64
|
+
"parameters": self.parameters,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def to_gemini_schema(self) -> Dict[str, Any]:
|
|
69
|
+
"""Returns the shape google-genai's FunctionDeclaration expects
|
|
70
|
+
(name/description/parameters, same JSON Schema convention)."""
|
|
71
|
+
return {
|
|
72
|
+
"name": self.name,
|
|
73
|
+
"description": self.description,
|
|
74
|
+
"parameters": self.parameters,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def call(self, **kwargs: Any) -> ToolResult:
|
|
78
|
+
"""Invokes the handler with the given keyword arguments."""
|
|
79
|
+
return self.handler(**kwargs)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ragleap_tools.calculator
|
|
3
|
+
|
|
4
|
+
Safe arithmetic expression evaluation - deliberately NOT using eval()
|
|
5
|
+
or exec(). Parses the expression into a real Python AST, then walks it
|
|
6
|
+
recursively, rejecting anything that isn't an explicitly whitelisted
|
|
7
|
+
node type (numbers, basic binary/unary math operators, and a small
|
|
8
|
+
fixed set of math functions/constants). No name lookups beyond that
|
|
9
|
+
fixed set, no attribute access, no function calls beyond the
|
|
10
|
+
whitelist, no comprehensions, no imports - none of eval()'s real
|
|
11
|
+
attack surface exists here at all.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import ast
|
|
17
|
+
import math
|
|
18
|
+
import operator
|
|
19
|
+
from typing import Any, Dict
|
|
20
|
+
|
|
21
|
+
from ragleap_tools.base import Tool, ToolResult
|
|
22
|
+
|
|
23
|
+
_ALLOWED_BINOPS = {
|
|
24
|
+
ast.Add: operator.add,
|
|
25
|
+
ast.Sub: operator.sub,
|
|
26
|
+
ast.Mult: operator.mul,
|
|
27
|
+
ast.Div: operator.truediv,
|
|
28
|
+
ast.FloorDiv: operator.floordiv,
|
|
29
|
+
ast.Mod: operator.mod,
|
|
30
|
+
ast.Pow: operator.pow,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_ALLOWED_UNARYOPS = {
|
|
34
|
+
ast.UAdd: operator.pos,
|
|
35
|
+
ast.USub: operator.neg,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_ALLOWED_FUNCTIONS = {
|
|
39
|
+
"abs": abs,
|
|
40
|
+
"round": round,
|
|
41
|
+
"min": min,
|
|
42
|
+
"max": max,
|
|
43
|
+
"sqrt": math.sqrt,
|
|
44
|
+
"sin": math.sin,
|
|
45
|
+
"cos": math.cos,
|
|
46
|
+
"tan": math.tan,
|
|
47
|
+
"log": math.log,
|
|
48
|
+
"log10": math.log10,
|
|
49
|
+
"log2": math.log2,
|
|
50
|
+
"exp": math.exp,
|
|
51
|
+
"floor": math.floor,
|
|
52
|
+
"ceil": math.ceil,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_ALLOWED_CONSTANTS = {
|
|
56
|
+
"pi": math.pi,
|
|
57
|
+
"e": math.e,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UnsafeExpressionError(ValueError):
|
|
62
|
+
"""Raised when the expression contains anything outside the
|
|
63
|
+
explicit whitelist - not a math error, a rejected-input error."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _eval_node(node: ast.AST) -> Any:
|
|
67
|
+
if isinstance(node, ast.Expression):
|
|
68
|
+
return _eval_node(node.body)
|
|
69
|
+
if isinstance(node, ast.Constant):
|
|
70
|
+
if isinstance(node.value, (int, float)):
|
|
71
|
+
return node.value
|
|
72
|
+
raise UnsafeExpressionError(f"Constant type not allowed: {type(node.value).__name__}")
|
|
73
|
+
if isinstance(node, ast.BinOp):
|
|
74
|
+
op_type = type(node.op)
|
|
75
|
+
if op_type not in _ALLOWED_BINOPS:
|
|
76
|
+
raise UnsafeExpressionError(f"Operator not allowed: {op_type.__name__}")
|
|
77
|
+
return _ALLOWED_BINOPS[op_type](_eval_node(node.left), _eval_node(node.right))
|
|
78
|
+
if isinstance(node, ast.UnaryOp):
|
|
79
|
+
op_type = type(node.op)
|
|
80
|
+
if op_type not in _ALLOWED_UNARYOPS:
|
|
81
|
+
raise UnsafeExpressionError(f"Unary operator not allowed: {op_type.__name__}")
|
|
82
|
+
return _ALLOWED_UNARYOPS[op_type](_eval_node(node.operand))
|
|
83
|
+
if isinstance(node, ast.Name):
|
|
84
|
+
if node.id in _ALLOWED_CONSTANTS:
|
|
85
|
+
return _ALLOWED_CONSTANTS[node.id]
|
|
86
|
+
raise UnsafeExpressionError(f"Name not allowed: {node.id!r}")
|
|
87
|
+
if isinstance(node, ast.Call):
|
|
88
|
+
if not isinstance(node.func, ast.Name) or node.func.id not in _ALLOWED_FUNCTIONS:
|
|
89
|
+
raise UnsafeExpressionError("Only whitelisted functions may be called")
|
|
90
|
+
if node.keywords:
|
|
91
|
+
raise UnsafeExpressionError("Keyword arguments are not allowed")
|
|
92
|
+
args = [_eval_node(a) for a in node.args]
|
|
93
|
+
return _ALLOWED_FUNCTIONS[node.func.id](*args)
|
|
94
|
+
raise UnsafeExpressionError(f"Expression node type not allowed: {type(node).__name__}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def safe_eval_expression(expression: str) -> float:
|
|
98
|
+
"""Parses and evaluates a math expression using only the
|
|
99
|
+
whitelisted grammar above. Raises UnsafeExpressionError for
|
|
100
|
+
anything outside that whitelist, SyntaxError for invalid syntax,
|
|
101
|
+
ZeroDivisionError/ValueError/OverflowError for real math errors -
|
|
102
|
+
the caller (calculate()) catches all of these and returns a
|
|
103
|
+
ToolResult rather than letting them propagate."""
|
|
104
|
+
if not expression or not expression.strip():
|
|
105
|
+
raise ValueError("Expression is empty")
|
|
106
|
+
if len(expression) > 200:
|
|
107
|
+
raise UnsafeExpressionError("Expression too long (max 200 characters)")
|
|
108
|
+
parsed = ast.parse(expression, mode="eval")
|
|
109
|
+
return _eval_node(parsed)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def calculate(expression: str) -> ToolResult:
|
|
113
|
+
try:
|
|
114
|
+
result = safe_eval_expression(expression)
|
|
115
|
+
return ToolResult(success=True, result=result)
|
|
116
|
+
except (UnsafeExpressionError, SyntaxError, ValueError, ZeroDivisionError, OverflowError, TypeError) as e:
|
|
117
|
+
return ToolResult(success=False, error=f"{type(e).__name__}: {e}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
CALCULATOR_TOOL = Tool(
|
|
121
|
+
name="calculator",
|
|
122
|
+
description=(
|
|
123
|
+
"Evaluate a mathematical expression. Supports +, -, *, /, //, %, ** "
|
|
124
|
+
"and the functions abs, round, min, max, sqrt, sin, cos, tan, log, "
|
|
125
|
+
"log10, log2, exp, floor, ceil, plus the constants pi and e. "
|
|
126
|
+
"No variables, no string operations, no other Python syntax."
|
|
127
|
+
),
|
|
128
|
+
parameters={
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"expression": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "A math expression, e.g. '2 + 2 * sqrt(16)'",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
"required": ["expression"],
|
|
137
|
+
},
|
|
138
|
+
handler=calculate,
|
|
139
|
+
)
|