Skip to content

Commit fa324d2

Browse files
feat(helpers): Tools + Secrets managers for the reusable-tools registry (#109)
* feat(helpers): Tools + Secrets managers for the reusable-tools registry Thin standalone wrappers (like Campaign/Audience) over the /tool and /secret endpoints. Tools: list/create/update/delete/duplicate. Secrets: list/create/delete (write-only vault). Live-verified against the API. Files are .fernignore-preserved so they survive regeneration. * test(helpers): add Tools + Secrets unit tests (satisfy verify.py 'every helper has a test') The compile/verify gate requires each public helper to have a test in tests/custom. Adds monkeypatched requests tests asserting Tools (list/create/update/duplicate/delete) and Secrets (list/create/delete) build the right URL + bearer auth.
1 parent 54f52ce commit fa324d2

4 files changed

Lines changed: 214 additions & 1 deletion

File tree

‎src/smallestai/atoms/helpers/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from smallestai.atoms.helpers.call import Call, CallAnalytics
77
from smallestai.atoms.helpers.campaign import Campaign
88
from smallestai.atoms.helpers.kb import KB
9+
from smallestai.atoms.helpers.secrets import Secrets
10+
from smallestai.atoms.helpers.tools import Tools
911
from smallestai.atoms.helpers.versioning import (
1012
BaseRevisionUnavailableError,
1113
DraftConflictError,
@@ -29,6 +31,8 @@
2931
"Call",
3032
"Campaign",
3133
"KB",
34+
"Secrets",
35+
"Tools",
3236
"Page",
3337
"as_page",
3438
"require_id",
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
Secrets vault.
3+
4+
The org-level, write-only Secrets vault: store API keys and tokens encrypted and
5+
reference them by name from a tool's ``auth`` block. Values are never returned —
6+
``list`` and ``create`` surface the name only.
7+
8+
Usage:
9+
from smallestai.atoms.helpers import Secrets
10+
11+
secrets = Secrets()
12+
secrets.create(name="ORDER_API_TOKEN", value="sk_live_...")
13+
secrets.list() # names only
14+
secrets.delete(secret_id)
15+
"""
16+
17+
import os
18+
from typing import Any, Dict, Optional
19+
20+
import requests
21+
22+
DEFAULT_BASE_URL = "https://api.smallest.ai/atoms/v1"
23+
24+
25+
class Secrets:
26+
"""Manager for the org Secrets vault (``/secret``).
27+
28+
Standalone:
29+
secrets = Secrets()
30+
secrets.list()
31+
"""
32+
33+
def __init__(
34+
self,
35+
base_url: Optional[str] = None,
36+
api_key: Optional[str] = None,
37+
):
38+
"""
39+
Args:
40+
base_url: API base URL (default: api.smallest.ai/atoms/v1)
41+
api_key: API key (default: SMALLEST_API_KEY env var)
42+
"""
43+
self.base_url = base_url or os.environ.get("SMALLEST_BASE_URL", DEFAULT_BASE_URL)
44+
self.api_key = api_key or os.environ.get("SMALLEST_API_KEY", "")
45+
46+
def _get_headers(self) -> Dict[str, str]:
47+
return {
48+
"Authorization": f"Bearer {self.api_key}",
49+
"Content-Type": "application/json",
50+
}
51+
52+
def list(self) -> Dict[str, Any]:
53+
"""List the org's secret names (values are never returned)."""
54+
url = f"{self.base_url}/secret"
55+
response = requests.get(url, headers=self._get_headers())
56+
response.raise_for_status()
57+
return response.json() # type: ignore[no-any-return]
58+
59+
def create(self, name: str, value: str) -> Dict[str, Any]:
60+
"""Create a secret. The value is encrypted at rest; the response holds the name only.
61+
62+
Args:
63+
name: secret name (letters, numbers, underscores) — referenced from a tool's ``auth``.
64+
value: the secret value (write-only).
65+
"""
66+
url = f"{self.base_url}/secret"
67+
response = requests.post(url, headers=self._get_headers(), json={"name": name, "value": value})
68+
response.raise_for_status()
69+
return response.json() # type: ignore[no-any-return]
70+
71+
def delete(self, secret_id: str) -> Dict[str, Any]:
72+
"""Delete a secret by its id."""
73+
url = f"{self.base_url}/secret/{secret_id}"
74+
response = requests.delete(url, headers=self._get_headers())
75+
response.raise_for_status()
76+
return response.json() # type: ignore[no-any-return]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Reusable Tools library.
3+
4+
The org-level Tools registry: create a tool once and reference it from any number
5+
of agents by its ``toolId``. Two types are registry tools — ``api_call`` and
6+
``client_tool``. System tools (transfer, end-call, knowledge-base search) are
7+
configured per-agent, not here.
8+
9+
Usage:
10+
from smallestai.atoms.helpers import Tools
11+
12+
tools = Tools()
13+
created = tools.create({
14+
"type": "api_call",
15+
"name": "get_order_status",
16+
"description": "Look up an order by id.",
17+
"method": "GET",
18+
"url": "https://api.example.com/orders/{{order_id}}",
19+
"llmParameters": [
20+
{"name": "order_id", "type": "text", "description": "The order id", "required": True}
21+
],
22+
})
23+
tool_id = created["data"]["toolId"]
24+
tools.list()
25+
tools.update(tool_id, {...})
26+
tools.duplicate(tool_id)
27+
tools.delete(tool_id)
28+
"""
29+
30+
import os
31+
from typing import Any, Dict, Optional
32+
33+
import requests
34+
35+
DEFAULT_BASE_URL = "https://api.smallest.ai/atoms/v1"
36+
37+
38+
class Tools:
39+
"""Manager for the org Tools library (``/tool``).
40+
41+
Standalone:
42+
tools = Tools()
43+
tools.list()
44+
"""
45+
46+
def __init__(
47+
self,
48+
base_url: Optional[str] = None,
49+
api_key: Optional[str] = None,
50+
):
51+
"""
52+
Args:
53+
base_url: API base URL (default: api.smallest.ai/atoms/v1)
54+
api_key: API key (default: SMALLEST_API_KEY env var)
55+
"""
56+
self.base_url = base_url or os.environ.get("SMALLEST_BASE_URL", DEFAULT_BASE_URL)
57+
self.api_key = api_key or os.environ.get("SMALLEST_API_KEY", "")
58+
59+
def _get_headers(self) -> Dict[str, str]:
60+
return {
61+
"Authorization": f"Bearer {self.api_key}",
62+
"Content-Type": "application/json",
63+
}
64+
65+
def list(self) -> Dict[str, Any]:
66+
"""List the org's tools."""
67+
url = f"{self.base_url}/tool"
68+
response = requests.get(url, headers=self._get_headers())
69+
response.raise_for_status()
70+
return response.json() # type: ignore[no-any-return]
71+
72+
def create(self, definition: Dict[str, Any]) -> Dict[str, Any]:
73+
"""Create a tool.
74+
75+
Args:
76+
definition: the tool body (``type`` ``api_call`` or ``client_tool``,
77+
plus ``name``, ``description``, and the fields for that type).
78+
"""
79+
url = f"{self.base_url}/tool"
80+
response = requests.post(url, headers=self._get_headers(), json={"definition": definition})
81+
response.raise_for_status()
82+
return response.json() # type: ignore[no-any-return]
83+
84+
def update(self, tool_id: str, definition: Dict[str, Any]) -> Dict[str, Any]:
85+
"""Update a tool. Propagates to every agent that references it."""
86+
url = f"{self.base_url}/tool/{tool_id}"
87+
response = requests.patch(url, headers=self._get_headers(), json={"definition": definition})
88+
response.raise_for_status()
89+
return response.json() # type: ignore[no-any-return]
90+
91+
def duplicate(self, tool_id: str) -> Dict[str, Any]:
92+
"""Duplicate a tool into a new library tool with a new ``toolId``."""
93+
url = f"{self.base_url}/tool/{tool_id}/duplicate"
94+
response = requests.post(url, headers=self._get_headers())
95+
response.raise_for_status()
96+
return response.json() # type: ignore[no-any-return]
97+
98+
def delete(self, tool_id: str) -> Dict[str, Any]:
99+
"""Delete a tool. Blocked (400) while any agent still references it."""
100+
url = f"{self.base_url}/tool/{tool_id}"
101+
response = requests.delete(url, headers=self._get_headers())
102+
response.raise_for_status()
103+
return response.json() # type: ignore[no-any-return]

‎tests/custom/test_helpers_rest.py‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import smallestai.atoms.helpers.audience as audience_mod
99
import smallestai.atoms.helpers.campaign as campaign_mod
1010
import smallestai.atoms.helpers.kb as kb_mod
11-
from smallestai.atoms.helpers import KB, Audience, Campaign, Page
11+
import smallestai.atoms.helpers.secrets as secrets_mod
12+
import smallestai.atoms.helpers.tools as tools_mod
13+
from smallestai.atoms.helpers import KB, Audience, Campaign, Page, Secrets, Tools
1214

1315

1416
class _FakeResp:
@@ -77,3 +79,31 @@ def test_campaign_list_builds_url_with_auth(monkeypatch):
7779
method, url, headers = fake.last
7880
assert method == "get" and url.startswith("https://api.example/atoms/v1")
7981
assert headers.get("Authorization") == "Bearer sk_test"
82+
83+
84+
def test_tools_crud_build_urls_with_auth(monkeypatch):
85+
fake = _patch(monkeypatch, tools_mod)
86+
tools = Tools(base_url="https://api.example/atoms/v1", api_key="sk_test")
87+
tools.list()
88+
assert fake.last[0] == "get" and fake.last[1].endswith("/tool")
89+
tools.create({"type": "api_call", "name": "t", "description": "d"})
90+
assert fake.last[0] == "post" and fake.last[1].endswith("/tool")
91+
tools.update("tool_abc", {"type": "api_call", "name": "t", "description": "d"})
92+
assert fake.last[0] == "patch" and fake.last[1].endswith("/tool/tool_abc")
93+
tools.duplicate("tool_abc")
94+
assert fake.last[0] == "post" and fake.last[1].endswith("/tool/tool_abc/duplicate")
95+
tools.delete("tool_abc")
96+
assert fake.last[0] == "delete" and fake.last[1].endswith("/tool/tool_abc")
97+
assert fake.last[2].get("Authorization") == "Bearer sk_test"
98+
99+
100+
def test_secrets_crud_build_urls_with_auth(monkeypatch):
101+
fake = _patch(monkeypatch, secrets_mod)
102+
secrets = Secrets(base_url="https://api.example/atoms/v1", api_key="sk_test")
103+
secrets.list()
104+
assert fake.last[0] == "get" and fake.last[1].endswith("/secret")
105+
secrets.create(name="MY_SECRET", value="v")
106+
assert fake.last[0] == "post" and fake.last[1].endswith("/secret")
107+
secrets.delete("secret_id_1")
108+
assert fake.last[0] == "delete" and fake.last[1].endswith("/secret/secret_id_1")
109+
assert fake.last[2].get("Authorization") == "Bearer sk_test"

0 commit comments

Comments
 (0)