RagLeap Packages
Looking for the self-hosted AI employee system? This page documents the open-source pip install ragleap-tools library. To install and run ragleap-core (AI employees, channels, Docker), see docs.ragleap.com; the project home is ragleap.com.
362 tests passing in CI — including real security verification, not just documented as safe

ragleap-tools

Standalone, dependency-light tool implementations for LLM tool-calling. Exposes OpenAI-, Gemini- and Anthropic-style function-calling schemas — ragleap-tools provides the Tool objects, not the execution loop.

0.5.1
PyPI version
362
Tests passing
12
Stateless tools included
0
Uses of eval()/exec()

Install

No configuration needed for the 12 stateless tools. File operations, document ingestion and search, web search, GitHub search and image description are opt-in and each need their own config object.

pip install ragleap-tools
# or, with uv
uv add ragleap-tools

# optional, for the ragleap-rag-backed ingestion tool:
pip install ragleap-tools[ingest]

What this is (and isn't)

ragleap-tools provides Tool objects — a name, a description, a JSON Schema for parameters, and a safe handler function. It does not own a tool-calling execution loop (deciding when to call a tool, running it, feeding the result back to the model) — that's ragleap-agents's job, per the roadmap's own split. Wire these tools into your own tool-calling code, or into ragleap-agents once that ships.

Quick start

from ragleap_tools import STATELESS_TOOLS, CALCULATOR_TOOL

# Give these to your LLM provider's tools= parameter:
openai_tools = [t.to_openai_schema() for t in STATELESS_TOOLS]
gemini_tools = [t.to_gemini_schema() for t in STATELESS_TOOLS]

# When the model calls one, invoke the handler yourself:
result = CALCULATOR_TOOL.call(expression="2 + 2 * sqrt(16)")
print(result.success, result.result)  # True 10.0

Full public API

Stateless tools (v0.1.0+) — no configuration needed

CALCULATOR_TOOL
Safe arithmetic via an AST-based whitelist evaluator — never eval()/exec().
CURRENT_DATETIME_TOOL, ADD_TO_DATE_TOOL, DATE_DIFFERENCE_TOOL
Date/time math.
CONVERT_LENGTH_TOOL, CONVERT_WEIGHT_TOOL, CONVERT_TEMPERATURE_TOOL
Unit conversion.
PARSE_JSON_TOOL, PARSE_CSV_TOOL
Structured data parsing.
REGEX_EXTRACT_TOOL, WORD_COUNT_TOOL, TEXT_CASE_TRANSFORM_TOOL
Text utilities.

File operations (v0.1.0+) — sandboxed, needs configuration

from ragleap_tools import FileOpsConfig, make_file_tools

config = FileOpsConfig(root_dir="/path/to/a/safe/directory")
read_tool, write_tool, list_tool = make_file_tools(config)

Every operation is confined to root_dir. Paths are resolved via Path.resolve() (which follows symlinks) before containment is checked with is_relative_to() — so both ../ traversal and symlink-based escapes are rejected, not just naive string-prefix checking. There is no unsandboxed mode.

Document ingestion and search (v0.1.0+) — optional, needs ragleap-rag

from ragleap import RagLeap, ProviderConfig, EmbeddingConfig
from ragleap_tools import IngestConfig, make_ingest_tool

rag = RagLeap(database_url="...", primary=ProviderConfig(...), embedder=EmbeddingConfig(...))
ingest_tool = make_ingest_tool(IngestConfig(rag=rag))

Wraps ragleap-rag's already-tested ingest_text() — no new ingestion logic, just a tool schema on top of the real 28-format-capable pipeline. Same optional-dependency pattern ragleap-graph already uses for its own optional ragleap-rag link.

Document search (v0.1.1+) — optional, needs ragleap-rag

from ragleap_tools import SearchConfig, make_search_tool

search_tool = make_search_tool(SearchConfig(rag=rag))
result = search_tool.call(query="what was the Q3 revenue?", filename="q3-report.pdf")

Wraps ragleap-rag's retrieve() for hybrid vector+keyword search. Chunk dicts are returned unmodified. The optional filename= scopes the search to one document previously ingested via ingest_document, which stores {"filename": filename} as metadata since v0.1.1.

Web search (v0.2.0+) — bring-your-own-key, no extra dependencies

from ragleap_tools import WebSearchConfig, TavilySearchProvider, make_web_search_tool

provider = TavilySearchProvider(api_key="...")  # or SerperSearchProvider(api_key="...")
search_tool = make_web_search_tool(WebSearchConfig(provider=provider))
result = search_tool.call(query="latest pgvector release", num_results=5)

No default provider and no environment-variable fallback. num_results is model-controlled and costs your API quota, so it is clamped to 1-20. Neither provider has been called against a live account yet. Results are untrusted third-party text and are not screened for prompt injection.

GitHub repository search (v0.3.0+) — token optional, no extra dependencies

from ragleap_tools import GitHubSearchConfig, make_github_search_tool

tool = make_github_search_tool(GitHubSearchConfig())  # or GitHubSearchConfig(token="...")
result = tool.call(query="language:python topic:llm", num_results=5, sort="stars")

Repository search only, via GitHub's REST API. Unauthenticated search works at a lower limit (10 requests for the search resource in a live check on 2026-10-01); authenticated requests have not been live-verified.

Image description (v0.4.0+) — bring-your-own-key, no extra dependencies

from ragleap_tools import FileOpsConfig, GeminiVisionProvider, VisionConfig, make_vision_tool

provider = GeminiVisionProvider(api_key="...", model="...")  # you choose the model
tool = make_vision_tool(VisionConfig(
    provider=provider,
    sandbox=FileOpsConfig(root_dir="/path/to/images"),
))
result = tool.call(path="chart.png", prompt="What does this chart show?")

api_key and model are both required. Images are read only from inside the sandbox directory (same path-escape and symlink protection as the file tools); URLs are deliberately not accepted. The image type is detected from the file's bytes. GeminiVisionProvider was live-checked once on 2026-10-03 (PNG input); AnthropicVisionProvider has not been called against a live account. Descriptions are untrusted text derived from an image and are not screened for prompt injection.

Network limits (v0.4.1+) — every HTTP provider

from ragleap_tools import TavilySearchProvider

provider = TavilySearchProvider(
    api_key="...",
    max_response_bytes=2_000_000,  # default 1,048,576
    total_timeout=30,              # seconds; default 20 (90 for the vision providers)
)

Tavily, Serper, GitHub search and the Gemini and Anthropic vision providers enforce a response-size cap and a hard total deadline; both are optional, validated fields on the provider (or GitHubSearchConfig). An oversize response or an exceeded deadline returns a failed ToolResult instead of hanging your loop. Per-operation timeouts are unchanged (15 seconds; 60 for vision). Python cannot kill a thread, so an abandoned request is stopped on a best-effort basis, and DNS resolution is not covered by the per-operation timeout.

Write your own tool and dispatch calls (v0.5.0+) — no configuration, no extra dependencies

from typing import Literal
from ragleap_tools import tool, ToolError, Toolkit, default_toolkit

@tool
def lookup_order(order_id: str, status: Literal["open", "closed"] = "open") -> dict:
    """Look up an order by id.

    Args:
        order_id: The order id, for example "A-1001".
        status: Which orders to search.
    """
    if order_id not in ORDERS:
        raise ToolError("order not found")   # safe to show the model
    return ORDERS[order_id]

kit = default_toolkit().add(lookup_order)       # the 12 stateless tools + yours
provider_tools = kit.to_openai_schema()          # also to_gemini_schema() / to_anthropic_schema()
result = kit.call(name_from_model, arguments_from_model)   # never raises
@tool, @tool(name=, description=, metadata=, expose_errors=)
Turns a type-annotated function into a Tool. The schema comes from the annotations (str, int, float, bool, Optional[X] with a default, Literal[...], list[X], dict, Any) and the Google-style docstring. The model's arguments are validated before the function runs: a missing, unknown or mistyped argument returns a failed ToolResult and the function is not called, and null for an optional argument means omitted. An unsupported shape raises ToolDefinitionError when the function is decorated. Sync functions only.
Toolkit(tools, max_arguments_chars=100000)
add, get, names, select(include, exclude, where), to_openai_schema(), to_gemini_schema(), to_anthropic_schema() and call(name, arguments). call takes the provider's JSON string or a dict and never raises: an unknown tool, oversize or invalid JSON, arguments that do not match the schema, a ToolError and any exception from the handler all return a failed ToolResult.
ToolError
Raise it inside a tool for an expected failure; its message reaches the model, so write one that is safe to show. Any other exception reaches the model only as its type name (the detail goes to the ragleap_tools logger).
default_toolkit(), validate_arguments(schema, arguments), Tool.metadata, Tool.to_anthropic_schema()
A new toolkit of the 12 stateless tools; the argument checker behind @tool and Toolkit; a free-form dict for your own policy code (never sent to a provider); and the Anthropic tool shape.

Version history

VersionWhat shippedReal live test performed
v0.1.0Initial release: 12 stateless tools, sandboxed file ops (FileOpsConfig, make_file_tools), optional ingestion tool (IngestConfig, make_ingest_tool)51 tests incl. real security tests: calculator rejects __import__/attribute access/list comprehensions/multi-statement injection; file ops blocked against a real on-disk symlink pointing outside the sandbox, not just a path-string check
v0.1.1Added search_documents (SearchConfig, make_search_tool) wrapping ragleap-rag's retrieve() for hybrid vector+keyword search, with an optional filename= to scope search to one document. Fixed ingest_document to pass metadata={"filename": filename} to ingest_text() - v0.1.0 passed no metadata at all, silently making every ingested document unfilterable by metadata_filter, which filename= depends on.69 tests (was 51). New: 8 tests for search_documents asserting on the fake's actually-recorded metadata_filter value, not just call-succeeded. 6 tests backfilled for ingest_document - v0.1.0 shipped it with zero direct test coverage, exactly the kind of gap that hid the metadata bug.
v0.2.0Added search_web (WebSearchConfig, make_web_search_tool) - a pluggable, bring-your-own-key web search tool. A SearchProvider abstract base with two reference implementations, TavilySearchProvider and SerperSearchProvider; api_key required at construction, no env-var fallback, no default provider. Standard library only (urllib.request) - no new dependency. num_results is model-controlled and costs the caller's API quota, so it's clamped to 1-20 regardless of provider.85 tests (was 69). Provider request-building tests mock urllib.request.urlopen so each provider's real payload/headers are exercised, not bypassed. Neither provider has been called against a live account - request shapes checked against current public docs only; an earlier draft's Tavily auth style (key in the request body) did not match current docs and was corrected to a Bearer header before release. Documented as best-effort until confirmed live, same standard as ragleap-rag's unverified providers.
v0.3.0Added search_github_repositories (GitHubSearchConfig, make_github_search_tool) via the real GitHub REST API (/search/repositories). Token optional (unauthenticated search works at a lower rate limit). Sets the User-Agent header GitHub requires. Standard library only - no new dependency. num_results is clamped to 1-20; values of 0 or below return 1 result rather than an error.100 tests passing in CI (was 85); the 15 new request-building tests mock urllib.request.urlopen. Live-checked on 2026-10-01 against the real GitHub API, unauthenticated, using the published 0.3.0 wheel: a normal query, the 1-20 clamp, sort=stars, and the 422 error path (returns success=False, does not raise). Not live-verified: authenticated requests (the Bearer header path is covered by mocked tests only). Correction: the 0.3.0 GitHubSearchConfig docstring says 60 requests/hour for search; a live /rate_limit check showed search at 10 (core at 60). The docstring was fixed and shipped in v0.4.0.
v0.4.0Added describe_image (VisionConfig, make_vision_tool) - a pluggable, bring-your-own-key image description tool. A VisionProvider abstract base with two reference implementations, GeminiVisionProvider and AnthropicVisionProvider; api_key and model are both required, no env-var fallback, no default provider or model. Reads images only from the existing file-ops sandbox (same path-escape and symlink protection); URLs are deliberately not accepted (SSRF risk). Image type is detected from the file's bytes, not its extension; size is capped at 5,000,000 bytes and the prompt at 2,000 characters by default. Standard library only - no new dependency. Descriptions are untrusted text (not screened for prompt injection).133 tests (was 100). Provider tests mock urllib.request.urlopen and assert on each provider's real URL, headers and body; sandbox tests include a real on-disk symlink escape, and rejected inputs are asserted never to reach the provider. Live-checked on 2026-10-03 against the real Gemini API (model gemini-3.6-flash): one call on a generated 64x64 PNG returned the expected answer, confirming the header, request body and response parsing for PNG input. Not live-verified: AnthropicVisionProvider; Gemini JPEG/WebP input and error responses. Also ships the corrected GitHubSearchConfig docstring (the search resource limit is 10 unauthenticated, not 60).
v0.4.1Bounded HTTP for every network provider: a response-size cap (max_response_bytes, default 1 MiB) and a hard total deadline (total_timeout, default 20 s; 90 s for vision) on TavilySearchProvider, SerperSearchProvider, GitHubSearchConfig, GeminiVisionProvider and AnthropicVisionProvider. The new fields are optional and validated; an oversize response or an exceeded deadline returns a failed ToolResult. GitHub error bodies are capped at 16 KiB and still surfaced. Fixes Tool.call raising TypeError for a tool argument named self. CI now runs Python 3.10, 3.11 and 3.12.200 tests (133 existing unchanged, 67 new against a real local HTTP server: slow-drip, stalled, never-answering, oversize, chunked, truncated and huge-error-body responses); mutation-tested, 38 of 38 deliberate breaks caught. Live-checked on the new transport: Gemini vision on 2026-10-09 (model gemini-3.6-flash, expected answer returned) and unauthenticated GitHub search, including the size cap, the deadline and a real HTTP 422 error body. The published wheel and sdist were verified against the tested files. Not live-verified: AnthropicVisionProvider, Tavily, Serper, authenticated GitHub.
v0.5.0Published to PyPI on 2026-10-18. Easy to extend, safe to dispatch: a @tool decorator (a type-annotated function becomes a Tool; the schema comes from the annotations and docstring), a Toolkit (named collection, three provider schema exports, and a call() that never raises: size-limited JSON or a dict in, arguments validated against the schema before the handler runs, exceptions contained), default_toolkit(), ToolError, validate_arguments(), Tool.metadata and Tool.to_anthropic_schema(). Standard library only; nothing that existed behaves differently.362 tests (200 existing unchanged, 162 new), passing on Python 3.10.12 and in CI on 3.10, 3.11 and 3.12; each of the 12 built-in tools exercised through the toolkit with one valid and one invalid call; mutation-tested, 89 of 89 deliberate breaks caught. After release the published wheel and sdist were checked: the TestPyPI and PyPI files had identical hashes, all 17 source files were byte-identical to the tag, and the sdist's 362 tests pass against the installed wheel (Python 3.10.12). Not live-verified: that OpenAI, Gemini or Anthropic accept the generated schemas in a live tool-calling request. The 0.5.0 project page on PyPI still shows the old repository owner (fixed in 0.5.1).
v0.5.1Published to PyPI on 2026-10-19. Metadata only, no code change: the project links (Homepage, Repository, Issues) now point at the ragleap organization repository, so the PyPI project page shows the new owner.The same 362 tests. The TestPyPI files were checked: the project links show the new owner, all 17 source files were byte-identical to main, and a smoke test of the new API passed (Python 3.10.12). The release-tag run then skipped the existing files, confirming that trusted publishing works under the new owner.

Real design decisions & scope boundaries this session

Not fabricated wins — each of these was a deliberate call made before writing code, not an afterthought.

  • ✓ shipped
    No execution-loop ownership. ragleap-tools provides Tool objects only. The roadmap already assigns tool-calling execution + HITL approval gates to ragleap-agents — avoiding two competing execution-loop implementations in the ecosystem.
  • ✓ shipped
    AST-based calculator, never eval()/exec(). Parses the expression, walks the tree, rejects anything outside an explicit allowlist of node types/functions. Verified against real code-injection payloads, not just documented as safe.
  • ✓ shipped
    Symlink-aware file sandboxing. Path.resolve() before containment check, not naive string-prefix matching — closes a real escape class that string-based sandboxing misses. Verified with a real on-disk symlink pointing outside the sandbox, not just a synthetic path string.
  • deliberately deferred
    Code execution, HTTP fetch, and business-system connectors remain out of scope. Each needs its own security-focused design pass. Web search was also out of scope for v0.1.0 and shipped in v0.2.0 only after its own design doc; GitHub search (v0.3.0) and image description (v0.4.0) followed the same way.
  • ✓ shipped
    A model's arguments are checked before any handler runs. Toolkit.call and every @tool function validate the arguments against the tool's schema first, so a missing, unknown or mistyped argument becomes a failed result instead of a TypeError inside the caller's loop; the JSON text is size-limited, and an exception from a handler reaches the model only as its type name. Covered by 162 new tests and 89 of 89 deliberate breaks caught.
  • not live-verified
    Generated schemas against real providers. The schemas @tool generates use the same shape as the built-in tools plus default and integer enum; no provider has been called with one.

Known limitations

  • No tool-calling execution loop is provided — the caller (or ragleap-agents, once it ships) must decide when to call a tool and feed results back to the model.
  • File ops require an explicit root_dir — there is no unsandboxed mode, and no default directory is assumed.
  • Code execution, HTTP fetch, and database/business-system connectors are not included.
  • Not live-verified: the Tavily and Serper web search providers, AnthropicVisionProvider, and authenticated GitHub requests. Request shapes were checked against current public documentation only.
  • Search results and image descriptions are untrusted text that flows into a model's context; neither is screened for prompt injection.
  • Network calls are bounded (v0.4.1), but Python cannot kill a thread: after the deadline an abandoned request is stopped on a best-effort basis (control still returns on time), DNS resolution is not covered by the per-operation timeout, and a response larger than the cap (1 MiB by default) now fails instead of succeeding.
  • @tool supports sync functions only and a small set of annotations, and validates against a small JSON Schema subset (dict values and keywords such as pattern are not checked, so a handler must still validate what the schema cannot express). Toolkit.call bounds the argument JSON but not how long a handler runs or how large its result is. The generated schemas have not been sent to any provider.