stash-sdk 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.
- stash_sdk-0.1.0/PKG-INFO +48 -0
- stash_sdk-0.1.0/README.md +28 -0
- stash_sdk-0.1.0/pyproject.toml +32 -0
- stash_sdk-0.1.0/setup.cfg +4 -0
- stash_sdk-0.1.0/src/stash_sdk/__init__.py +3 -0
- stash_sdk-0.1.0/src/stash_sdk/client.py +457 -0
- stash_sdk-0.1.0/src/stash_sdk/py.typed +0 -0
- stash_sdk-0.1.0/src/stash_sdk.egg-info/PKG-INFO +48 -0
- stash_sdk-0.1.0/src/stash_sdk.egg-info/SOURCES.txt +10 -0
- stash_sdk-0.1.0/src/stash_sdk.egg-info/dependency_links.txt +1 -0
- stash_sdk-0.1.0/src/stash_sdk.egg-info/requires.txt +1 -0
- stash_sdk-0.1.0/src/stash_sdk.egg-info/top_level.txt +1 -0
stash_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stash-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Stash — shared memory for AI agents
|
|
5
|
+
Author: Fergana Labs
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://joinstash.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/Fergana-Labs/stash
|
|
9
|
+
Keywords: stash,ai,agents,memory,sdk
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
|
|
21
|
+
# stash-sdk
|
|
22
|
+
|
|
23
|
+
Python client for [Stash](https://joinstash.ai) — shared memory for AI
|
|
24
|
+
agents. One dependency (httpx), fully typed.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install stash-sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from stash_sdk import Stash
|
|
32
|
+
|
|
33
|
+
stash = Stash() # reads STASH_API_KEY (and optional STASH_URL) from env
|
|
34
|
+
|
|
35
|
+
stash.push_event(
|
|
36
|
+
agent_name="my-agent",
|
|
37
|
+
event_type="user_message",
|
|
38
|
+
content="Can you rebook my Lisbon trip?",
|
|
39
|
+
session_id="sam:trip-114",
|
|
40
|
+
)
|
|
41
|
+
hits = stash.search_events("Lisbon")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Everything the API offers is a method on `Stash`: events and transcripts,
|
|
45
|
+
pages and folders, files, tables, and Skills. Errors raise `StashError`
|
|
46
|
+
with the API's status code and detail — nothing fails silently.
|
|
47
|
+
|
|
48
|
+
The full API reference lives at [joinstash.ai/docs](https://joinstash.ai/docs).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# stash-sdk
|
|
2
|
+
|
|
3
|
+
Python client for [Stash](https://joinstash.ai) — shared memory for AI
|
|
4
|
+
agents. One dependency (httpx), fully typed.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install stash-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from stash_sdk import Stash
|
|
12
|
+
|
|
13
|
+
stash = Stash() # reads STASH_API_KEY (and optional STASH_URL) from env
|
|
14
|
+
|
|
15
|
+
stash.push_event(
|
|
16
|
+
agent_name="my-agent",
|
|
17
|
+
event_type="user_message",
|
|
18
|
+
content="Can you rebook my Lisbon trip?",
|
|
19
|
+
session_id="sam:trip-114",
|
|
20
|
+
)
|
|
21
|
+
hits = stash.search_events("Lisbon")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Everything the API offers is a method on `Stash`: events and transcripts,
|
|
25
|
+
pages and folders, files, tables, and Skills. Errors raise `StashError`
|
|
26
|
+
with the API's status code and detail — nothing fails silently.
|
|
27
|
+
|
|
28
|
+
The full API reference lives at [joinstash.ai/docs](https://joinstash.ai/docs).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "stash-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for Stash — shared memory for AI agents"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Fergana Labs" }]
|
|
9
|
+
keywords = ["stash", "ai", "agents", "memory", "sdk"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.27.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://joinstash.ai"
|
|
25
|
+
Repository = "https://github.com/Fergana-Labs/stash"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=68.0"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Stash Python SDK — shared memory for AI agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.joinstash.ai"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StashError(Exception):
|
|
15
|
+
def __init__(self, status_code: int, detail: str | list):
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.detail = detail
|
|
18
|
+
super().__init__(f"[{status_code}] {detail}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Stash:
|
|
22
|
+
"""Client for the Stash API.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key: Stash API key (``st_...``). Falls back to ``STASH_API_KEY`` env var.
|
|
26
|
+
base_url: API base URL. Falls back to ``STASH_URL`` env var, then production.
|
|
27
|
+
timeout: Default request timeout in seconds.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_key: str | None = None,
|
|
33
|
+
base_url: str | None = None,
|
|
34
|
+
timeout: int = 30,
|
|
35
|
+
):
|
|
36
|
+
self.api_key = api_key or os.environ.get("STASH_API_KEY", "")
|
|
37
|
+
base = (base_url or os.environ.get("STASH_URL", "")).rstrip("/") or DEFAULT_BASE_URL
|
|
38
|
+
self._http = httpx.Client(base_url=base, timeout=timeout)
|
|
39
|
+
|
|
40
|
+
def close(self) -> None:
|
|
41
|
+
self._http.close()
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, *args):
|
|
47
|
+
self.close()
|
|
48
|
+
|
|
49
|
+
# --- internals ---
|
|
50
|
+
|
|
51
|
+
def _headers(self) -> dict[str, str]:
|
|
52
|
+
if not self.api_key:
|
|
53
|
+
return {}
|
|
54
|
+
return {"Authorization": f"Bearer {self.api_key}"}
|
|
55
|
+
|
|
56
|
+
def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
57
|
+
headers = kwargs.pop("headers", {})
|
|
58
|
+
headers.update(self._headers())
|
|
59
|
+
resp = self._http.request(method, path, headers=headers, **kwargs)
|
|
60
|
+
if not resp.is_success:
|
|
61
|
+
detail: str | list = resp.text
|
|
62
|
+
try:
|
|
63
|
+
detail = resp.json().get("detail", resp.text)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
raise StashError(resp.status_code, detail)
|
|
67
|
+
return resp
|
|
68
|
+
|
|
69
|
+
def _get(self, path: str, **params) -> dict | list:
|
|
70
|
+
return self._request("GET", path, params=params).json()
|
|
71
|
+
|
|
72
|
+
def _post(self, path: str, json=None, **kwargs) -> dict:
|
|
73
|
+
resp = self._request("POST", path, json=json, **kwargs)
|
|
74
|
+
return {} if resp.status_code == 204 else resp.json()
|
|
75
|
+
|
|
76
|
+
def _put(self, path: str, json=None) -> dict:
|
|
77
|
+
return self._request("PUT", path, json=json).json()
|
|
78
|
+
|
|
79
|
+
def _patch(self, path: str, json=None) -> dict:
|
|
80
|
+
return self._request("PATCH", path, json=json).json()
|
|
81
|
+
|
|
82
|
+
def _delete(self, path: str) -> None:
|
|
83
|
+
self._request("DELETE", path)
|
|
84
|
+
|
|
85
|
+
def _list(self, path: str, key: str, **params) -> list:
|
|
86
|
+
data = self._get(path, **params)
|
|
87
|
+
return data.get(key, data) if isinstance(data, dict) else data
|
|
88
|
+
|
|
89
|
+
def _upload(self, path: str, file_path: str | Path) -> dict:
|
|
90
|
+
p = Path(file_path)
|
|
91
|
+
content_type = mimetypes.guess_type(p.name)[0] or "application/octet-stream"
|
|
92
|
+
with open(p, "rb") as f:
|
|
93
|
+
resp = self._request(
|
|
94
|
+
"POST",
|
|
95
|
+
path,
|
|
96
|
+
files={"file": (p.name, f, content_type)},
|
|
97
|
+
timeout=300,
|
|
98
|
+
)
|
|
99
|
+
return resp.json()
|
|
100
|
+
|
|
101
|
+
# =========================================================================
|
|
102
|
+
# Auth
|
|
103
|
+
# =========================================================================
|
|
104
|
+
|
|
105
|
+
def register(self, name: str, description: str = "", password: str | None = None) -> dict:
|
|
106
|
+
body: dict = {"name": name, "description": description}
|
|
107
|
+
if password:
|
|
108
|
+
body["password"] = password
|
|
109
|
+
return self._post("/api/v1/users/register", json=body)
|
|
110
|
+
|
|
111
|
+
def login(self, name: str, password: str) -> dict:
|
|
112
|
+
return self._post("/api/v1/users/login", json={"name": name, "password": password})
|
|
113
|
+
|
|
114
|
+
def whoami(self) -> dict:
|
|
115
|
+
return self._get("/api/v1/users/me")
|
|
116
|
+
|
|
117
|
+
def list_api_keys(self) -> list:
|
|
118
|
+
return self._get("/api/v1/users/me/keys")
|
|
119
|
+
|
|
120
|
+
def revoke_api_key(self, key_id: str) -> None:
|
|
121
|
+
self._delete(f"/api/v1/users/me/keys/{key_id}")
|
|
122
|
+
|
|
123
|
+
# =========================================================================
|
|
124
|
+
# Discover (public Skills)
|
|
125
|
+
# =========================================================================
|
|
126
|
+
|
|
127
|
+
def list_discover_skills(
|
|
128
|
+
self,
|
|
129
|
+
query: str = "",
|
|
130
|
+
sort: str = "trending",
|
|
131
|
+
limit: int = 48,
|
|
132
|
+
) -> dict:
|
|
133
|
+
params: dict = {"sort": sort, "limit": limit}
|
|
134
|
+
if query:
|
|
135
|
+
params["q"] = query
|
|
136
|
+
return self._get("/api/v1/discover/skills", **params)
|
|
137
|
+
|
|
138
|
+
# =========================================================================
|
|
139
|
+
# Skills
|
|
140
|
+
# =========================================================================
|
|
141
|
+
|
|
142
|
+
def list_skills(self) -> list:
|
|
143
|
+
return self._list("/api/v1/me/skills", "skills")
|
|
144
|
+
|
|
145
|
+
def publish_skill_folder(
|
|
146
|
+
self,
|
|
147
|
+
folder_id: str,
|
|
148
|
+
title: str | None = None,
|
|
149
|
+
description: str = "",
|
|
150
|
+
discoverable: bool = False,
|
|
151
|
+
) -> dict:
|
|
152
|
+
body = {
|
|
153
|
+
"folder_id": folder_id,
|
|
154
|
+
"description": description,
|
|
155
|
+
"discoverable": discoverable,
|
|
156
|
+
}
|
|
157
|
+
if title:
|
|
158
|
+
body["title"] = title
|
|
159
|
+
return self._post("/api/v1/me/skills", json=body)
|
|
160
|
+
|
|
161
|
+
def update_skill(self, skill_id: str, **fields) -> dict:
|
|
162
|
+
return self._patch(f"/api/v1/skills/{skill_id}", json=fields)
|
|
163
|
+
|
|
164
|
+
def unpublish_skill(self, skill_id: str) -> None:
|
|
165
|
+
self._delete(f"/api/v1/skills/{skill_id}")
|
|
166
|
+
|
|
167
|
+
def get_public_skill(self, slug: str) -> dict:
|
|
168
|
+
return self._get(f"/api/v1/skills/{slug}")
|
|
169
|
+
|
|
170
|
+
def get_skill_text(self, slug: str) -> str:
|
|
171
|
+
resp = self._request("GET", f"/api/v1/skills/{slug}", params={"format": "text"})
|
|
172
|
+
return resp.text
|
|
173
|
+
|
|
174
|
+
def fork_skill(self, slug: str) -> dict:
|
|
175
|
+
return self._post(f"/api/v1/skills/{slug}/add-to-stash")
|
|
176
|
+
|
|
177
|
+
# =========================================================================
|
|
178
|
+
# Aggregate
|
|
179
|
+
# =========================================================================
|
|
180
|
+
|
|
181
|
+
def all_pages(self) -> list:
|
|
182
|
+
return self._list("/api/v1/me/pages", "pages")
|
|
183
|
+
|
|
184
|
+
def all_events(
|
|
185
|
+
self,
|
|
186
|
+
agent_name: str | None = None,
|
|
187
|
+
event_type: str | None = None,
|
|
188
|
+
limit: int = 50,
|
|
189
|
+
) -> list:
|
|
190
|
+
params: dict = {"limit": limit}
|
|
191
|
+
if agent_name:
|
|
192
|
+
params["agent_name"] = agent_name
|
|
193
|
+
if event_type:
|
|
194
|
+
params["event_type"] = event_type
|
|
195
|
+
return self._list("/api/v1/me/session-events", "events", **params)
|
|
196
|
+
|
|
197
|
+
def all_tables(self) -> list:
|
|
198
|
+
return self._list("/api/v1/me/tables", "tables")
|
|
199
|
+
|
|
200
|
+
# =========================================================================
|
|
201
|
+
# Folders (user-scoped, nestable)
|
|
202
|
+
# =========================================================================
|
|
203
|
+
|
|
204
|
+
def list_folders(self) -> list:
|
|
205
|
+
return self._list("/api/v1/me/folders", "folders")
|
|
206
|
+
|
|
207
|
+
def create_folder(
|
|
208
|
+
self,
|
|
209
|
+
name: str,
|
|
210
|
+
parent_folder_id: str | None = None,
|
|
211
|
+
) -> dict:
|
|
212
|
+
body: dict = {"name": name}
|
|
213
|
+
if parent_folder_id:
|
|
214
|
+
body["parent_folder_id"] = parent_folder_id
|
|
215
|
+
return self._post("/api/v1/me/folders", json=body)
|
|
216
|
+
|
|
217
|
+
def delete_folder(self, folder_id: str) -> None:
|
|
218
|
+
self._delete(f"/api/v1/me/folders/{folder_id}")
|
|
219
|
+
|
|
220
|
+
def get_tree(self) -> dict:
|
|
221
|
+
return self._get("/api/v1/me/tree")
|
|
222
|
+
|
|
223
|
+
# =========================================================================
|
|
224
|
+
# Pages (user-scoped)
|
|
225
|
+
# =========================================================================
|
|
226
|
+
|
|
227
|
+
def create_page(
|
|
228
|
+
self,
|
|
229
|
+
name: str,
|
|
230
|
+
content: str = "",
|
|
231
|
+
folder_id: str | None = None,
|
|
232
|
+
content_type: str = "markdown",
|
|
233
|
+
content_html: str = "",
|
|
234
|
+
) -> dict:
|
|
235
|
+
body: dict = {
|
|
236
|
+
"name": name,
|
|
237
|
+
"content": content,
|
|
238
|
+
"content_type": content_type,
|
|
239
|
+
"content_html": content_html,
|
|
240
|
+
}
|
|
241
|
+
if folder_id:
|
|
242
|
+
body["folder_id"] = folder_id
|
|
243
|
+
return self._post("/api/v1/me/pages/new", json=body)
|
|
244
|
+
|
|
245
|
+
def list_pages(self) -> list:
|
|
246
|
+
return self._list("/api/v1/me/pages", "pages")
|
|
247
|
+
|
|
248
|
+
def get_page(self, page_id: str) -> dict:
|
|
249
|
+
return self._get(f"/api/v1/pages/{page_id}")
|
|
250
|
+
|
|
251
|
+
def update_page(self, page_id: str, **kwargs) -> dict:
|
|
252
|
+
return self._patch(f"/api/v1/me/pages/{page_id}", json=kwargs)
|
|
253
|
+
|
|
254
|
+
def delete_page(self, page_id: str) -> None:
|
|
255
|
+
self._delete(f"/api/v1/me/pages/{page_id}")
|
|
256
|
+
|
|
257
|
+
# =========================================================================
|
|
258
|
+
# Session events
|
|
259
|
+
# =========================================================================
|
|
260
|
+
|
|
261
|
+
def push_event(
|
|
262
|
+
self,
|
|
263
|
+
agent_name: str,
|
|
264
|
+
event_type: str,
|
|
265
|
+
content: str,
|
|
266
|
+
session_id: str | None = None,
|
|
267
|
+
tool_name: str | None = None,
|
|
268
|
+
metadata: dict | None = None,
|
|
269
|
+
attachments: list[dict] | None = None,
|
|
270
|
+
created_at: str | None = None,
|
|
271
|
+
) -> dict:
|
|
272
|
+
body: dict = {
|
|
273
|
+
"agent_name": agent_name,
|
|
274
|
+
"event_type": event_type,
|
|
275
|
+
"content": content,
|
|
276
|
+
}
|
|
277
|
+
if session_id:
|
|
278
|
+
body["session_id"] = session_id
|
|
279
|
+
if tool_name:
|
|
280
|
+
body["tool_name"] = tool_name
|
|
281
|
+
if metadata:
|
|
282
|
+
body["metadata"] = metadata
|
|
283
|
+
if attachments:
|
|
284
|
+
body["attachments"] = attachments
|
|
285
|
+
if created_at:
|
|
286
|
+
body["created_at"] = created_at
|
|
287
|
+
return self._post("/api/v1/me/sessions/events", json=body)
|
|
288
|
+
|
|
289
|
+
def push_events_batch(self, events: list[dict]) -> list:
|
|
290
|
+
body: dict = {"events": events}
|
|
291
|
+
return self._post("/api/v1/me/sessions/events/batch", json=body)
|
|
292
|
+
|
|
293
|
+
def query_events(
|
|
294
|
+
self,
|
|
295
|
+
agent_name: str | None = None,
|
|
296
|
+
event_type: str | None = None,
|
|
297
|
+
limit: int = 50,
|
|
298
|
+
after: str | None = None,
|
|
299
|
+
) -> list:
|
|
300
|
+
params: dict = {"limit": limit}
|
|
301
|
+
if agent_name:
|
|
302
|
+
params["agent_name"] = agent_name
|
|
303
|
+
if event_type:
|
|
304
|
+
params["event_type"] = event_type
|
|
305
|
+
if after:
|
|
306
|
+
params["after"] = after
|
|
307
|
+
return self._list("/api/v1/me/sessions/events", "events", **params)
|
|
308
|
+
|
|
309
|
+
def search_events(self, query: str, limit: int = 50) -> list:
|
|
310
|
+
return self._list(
|
|
311
|
+
"/api/v1/me/sessions/events/search",
|
|
312
|
+
"events",
|
|
313
|
+
q=query,
|
|
314
|
+
limit=limit,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def list_agent_names(self) -> list:
|
|
318
|
+
data = self._get("/api/v1/me/sessions/agent-names")
|
|
319
|
+
return data.get("agent_names", []) if isinstance(data, dict) else data
|
|
320
|
+
|
|
321
|
+
def upload_transcript(
|
|
322
|
+
self,
|
|
323
|
+
session_id: str,
|
|
324
|
+
transcript_path: str | Path,
|
|
325
|
+
agent_name: str,
|
|
326
|
+
cwd: str = "",
|
|
327
|
+
replace: bool = False,
|
|
328
|
+
) -> dict:
|
|
329
|
+
import gzip as _gzip
|
|
330
|
+
|
|
331
|
+
with open(transcript_path, "rb") as f:
|
|
332
|
+
raw = f.read()
|
|
333
|
+
body = _gzip.compress(raw)
|
|
334
|
+
name = os.path.basename(str(transcript_path))
|
|
335
|
+
if not name.endswith(".gz"):
|
|
336
|
+
name += ".gz"
|
|
337
|
+
resp = self._request(
|
|
338
|
+
"POST",
|
|
339
|
+
"/api/v1/me/transcripts",
|
|
340
|
+
data={
|
|
341
|
+
"session_id": session_id,
|
|
342
|
+
"agent_name": agent_name,
|
|
343
|
+
"cwd": cwd,
|
|
344
|
+
"replace": str(replace).lower(),
|
|
345
|
+
},
|
|
346
|
+
files={"file": (name, body, "application/gzip")},
|
|
347
|
+
timeout=120,
|
|
348
|
+
)
|
|
349
|
+
return resp.json()
|
|
350
|
+
|
|
351
|
+
# =========================================================================
|
|
352
|
+
# Files
|
|
353
|
+
# =========================================================================
|
|
354
|
+
|
|
355
|
+
def upload_file(self, file_path: str | Path) -> dict:
|
|
356
|
+
return self._upload("/api/v1/me/files", file_path)
|
|
357
|
+
|
|
358
|
+
def list_files(self) -> list:
|
|
359
|
+
return self._list("/api/v1/me/files", "files")
|
|
360
|
+
|
|
361
|
+
def get_file(self, file_id: str) -> dict:
|
|
362
|
+
return self._get(f"/api/v1/me/files/{file_id}")
|
|
363
|
+
|
|
364
|
+
def delete_file(self, file_id: str) -> None:
|
|
365
|
+
self._delete(f"/api/v1/me/files/{file_id}")
|
|
366
|
+
|
|
367
|
+
def get_file_text(self, file_id: str) -> dict:
|
|
368
|
+
return self._get(f"/api/v1/me/files/{file_id}/text")
|
|
369
|
+
|
|
370
|
+
# =========================================================================
|
|
371
|
+
# Webhooks
|
|
372
|
+
# =========================================================================
|
|
373
|
+
|
|
374
|
+
def set_webhook(self, url: str, secret: str | None = None) -> dict:
|
|
375
|
+
body: dict = {"url": url}
|
|
376
|
+
if secret:
|
|
377
|
+
body["secret"] = secret
|
|
378
|
+
return self._post("/api/v1/me/webhooks", json=body)
|
|
379
|
+
|
|
380
|
+
# =========================================================================
|
|
381
|
+
# Tables
|
|
382
|
+
# =========================================================================
|
|
383
|
+
|
|
384
|
+
def create_table(
|
|
385
|
+
self,
|
|
386
|
+
name: str,
|
|
387
|
+
description: str = "",
|
|
388
|
+
columns: list | None = None,
|
|
389
|
+
) -> dict:
|
|
390
|
+
return self._post(
|
|
391
|
+
"/api/v1/me/tables",
|
|
392
|
+
json={"name": name, "description": description, "columns": columns or []},
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def list_tables(self) -> list:
|
|
396
|
+
return self._list("/api/v1/me/tables", "tables")
|
|
397
|
+
|
|
398
|
+
def get_table(self, table_id: str) -> dict:
|
|
399
|
+
return self._get(f"/api/v1/me/tables/{table_id}")
|
|
400
|
+
|
|
401
|
+
def update_table(self, table_id: str, **kwargs) -> dict:
|
|
402
|
+
return self._patch(f"/api/v1/me/tables/{table_id}", json=kwargs)
|
|
403
|
+
|
|
404
|
+
def delete_table(self, table_id: str) -> None:
|
|
405
|
+
self._delete(f"/api/v1/me/tables/{table_id}")
|
|
406
|
+
|
|
407
|
+
def list_table_rows(
|
|
408
|
+
self,
|
|
409
|
+
table_id: str,
|
|
410
|
+
limit: int = 50,
|
|
411
|
+
offset: int = 0,
|
|
412
|
+
sort_by: str = "",
|
|
413
|
+
sort_order: str = "asc",
|
|
414
|
+
filters: str = "",
|
|
415
|
+
) -> dict:
|
|
416
|
+
params: dict = {"limit": limit, "offset": offset, "sort_order": sort_order}
|
|
417
|
+
if sort_by:
|
|
418
|
+
params["sort_by"] = sort_by
|
|
419
|
+
if filters:
|
|
420
|
+
params["filters"] = filters
|
|
421
|
+
return self._get(f"/api/v1/me/tables/{table_id}/rows", **params)
|
|
422
|
+
|
|
423
|
+
def insert_table_row(self, table_id: str, data: dict) -> dict:
|
|
424
|
+
return self._post(f"/api/v1/me/tables/{table_id}/rows", json={"data": data})
|
|
425
|
+
|
|
426
|
+
def insert_table_rows_batch(self, table_id: str, rows: list[dict]) -> dict:
|
|
427
|
+
return self._post(
|
|
428
|
+
f"/api/v1/me/tables/{table_id}/rows/batch",
|
|
429
|
+
json={"rows": [{"data": r} for r in rows]},
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def update_table_row(self, table_id: str, row_id: str, data: dict) -> dict:
|
|
433
|
+
return self._patch(
|
|
434
|
+
f"/api/v1/me/tables/{table_id}/rows/{row_id}",
|
|
435
|
+
json={"data": data},
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def delete_table_row(self, table_id: str, row_id: str) -> None:
|
|
439
|
+
self._delete(f"/api/v1/me/tables/{table_id}/rows/{row_id}")
|
|
440
|
+
|
|
441
|
+
def add_table_column(
|
|
442
|
+
self,
|
|
443
|
+
table_id: str,
|
|
444
|
+
name: str,
|
|
445
|
+
col_type: str = "text",
|
|
446
|
+
options: list | None = None,
|
|
447
|
+
) -> dict:
|
|
448
|
+
body: dict = {"name": name, "type": col_type}
|
|
449
|
+
if options:
|
|
450
|
+
body["options"] = options
|
|
451
|
+
return self._post(f"/api/v1/me/tables/{table_id}/columns", json=body)
|
|
452
|
+
|
|
453
|
+
def delete_table_column(self, table_id: str, column_id: str) -> dict:
|
|
454
|
+
return self._request(
|
|
455
|
+
"DELETE",
|
|
456
|
+
f"/api/v1/me/tables/{table_id}/columns/{column_id}",
|
|
457
|
+
).json()
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stash-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Stash — shared memory for AI agents
|
|
5
|
+
Author: Fergana Labs
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://joinstash.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/Fergana-Labs/stash
|
|
9
|
+
Keywords: stash,ai,agents,memory,sdk
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
|
|
21
|
+
# stash-sdk
|
|
22
|
+
|
|
23
|
+
Python client for [Stash](https://joinstash.ai) — shared memory for AI
|
|
24
|
+
agents. One dependency (httpx), fully typed.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install stash-sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from stash_sdk import Stash
|
|
32
|
+
|
|
33
|
+
stash = Stash() # reads STASH_API_KEY (and optional STASH_URL) from env
|
|
34
|
+
|
|
35
|
+
stash.push_event(
|
|
36
|
+
agent_name="my-agent",
|
|
37
|
+
event_type="user_message",
|
|
38
|
+
content="Can you rebook my Lisbon trip?",
|
|
39
|
+
session_id="sam:trip-114",
|
|
40
|
+
)
|
|
41
|
+
hits = stash.search_events("Lisbon")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Everything the API offers is a method on `Stash`: events and transcripts,
|
|
45
|
+
pages and folders, files, tables, and Skills. Errors raise `StashError`
|
|
46
|
+
with the API's status code and detail — nothing fails silently.
|
|
47
|
+
|
|
48
|
+
The full API reference lives at [joinstash.ai/docs](https://joinstash.ai/docs).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/stash_sdk/__init__.py
|
|
4
|
+
src/stash_sdk/client.py
|
|
5
|
+
src/stash_sdk/py.typed
|
|
6
|
+
src/stash_sdk.egg-info/PKG-INFO
|
|
7
|
+
src/stash_sdk.egg-info/SOURCES.txt
|
|
8
|
+
src/stash_sdk.egg-info/dependency_links.txt
|
|
9
|
+
src/stash_sdk.egg-info/requires.txt
|
|
10
|
+
src/stash_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.27.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stash_sdk
|