Writing Tool Providers¶
A tool provider resolves ToolSpec records into Python callables. By writing a provider you can back lackpy tools with any data source — REST APIs, databases, language servers, or anything else — without modifying lackpy itself.
Protocol¶
A provider is any object with these three attributes:
class MyProvider:
@property
def name(self) -> str:
"""Unique provider identifier. Matches ToolSpec.provider."""
...
def available(self) -> bool:
"""Return True if the provider can currently serve requests."""
...
def resolve(self, tool_spec: ToolSpec) -> Callable[..., Any]:
"""Return a callable for the given ToolSpec."""
...
There is no abstract base class — Python duck typing is used. available() is not called by the current toolbox implementation, but it is good practice to implement it in case a future version adds lazy provider activation.
Complete example — REST API provider¶
This provider wraps a hypothetical REST API. Each tool is configured with a URL and HTTP method in provider_config.
# my_project/providers/rest_provider.py
from __future__ import annotations
from typing import Any, Callable
import urllib.request
import json
from lackpy.kit.toolbox import ToolSpec
class RestProvider:
@property
def name(self) -> str:
return "rest"
def available(self) -> bool:
return True # or check connectivity
def resolve(self, tool_spec: ToolSpec) -> Callable[..., Any]:
url = tool_spec.provider_config.get("url")
method = tool_spec.provider_config.get("method", "GET").upper()
if not url:
raise ValueError(
f"RestProvider requires 'url' in provider_config for tool '{tool_spec.name}'"
)
def _call(**kwargs: Any) -> Any:
if method == "GET":
# Append kwargs as query parameters
params = "&".join(f"{k}={v}" for k, v in kwargs.items())
full_url = f"{url}?{params}" if params else url
with urllib.request.urlopen(full_url) as resp: # noqa: S310
return json.loads(resp.read())
elif method == "POST":
data = json.dumps(kwargs).encode()
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as resp: # noqa: S310
return json.loads(resp.read())
else:
raise ValueError(f"Unsupported HTTP method: {method}")
# Rename to match the tool for cleaner tracebacks
_call.__name__ = tool_spec.name
return _call
Registration¶
import asyncio
from lackpy import LackpyService
from lackpy.kit.toolbox import ToolSpec, ArgSpec
from my_project.providers.rest_provider import RestProvider
async def main():
svc = LackpyService()
# Register the provider
svc.toolbox.register_provider(RestProvider())
# Register tools that use it
svc.toolbox.register_tool(ToolSpec(
name="search_issues",
provider="rest",
provider_config={
"url": "https://api.example.com/issues",
"method": "GET",
},
description="Search issues by query string",
args=[ArgSpec(name="query", type="str", description="Search query")],
returns="list[dict]",
grade_w=1,
effects_ceiling=0,
))
result = await svc.delegate(
"search for open bugs",
kit=["search_issues"],
)
print(result["output"])
asyncio.run(main())
Built-in providers reference¶
BuiltinProvider (name: "builtin")¶
Implements four filesystem primitives. The tool name must match exactly:
| Tool name | Signature | Description |
|---|---|---|
read_file |
read_file(path: str) -> str |
Read file contents |
find_files |
find_files(pattern: str) -> list[str] |
Glob from current directory |
write_file |
write_file(path: str, content: str) -> bool |
Write file (creates if missing) |
edit_file |
edit_file(path: str, old_str: str, new_str: str) -> bool |
Replace first occurrence |
PythonProvider (name: "python")¶
Wraps any importable Python function. Requires module and function in provider_config:
ToolSpec(
name="word_count",
provider="python",
provider_config={
"module": "my_tools.text",
"function": "word_count",
},
...
)
The module is imported lazily when resolve() is first called.