tinyhumansai 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.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinyhumansai
3
+ Version: 0.1.0
4
+ Summary: Python SDK for TinyHumans API - ingest and delete memory
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx>=0.25
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest; extra == "dev"
11
+ Requires-Dist: pytest-asyncio; extra == "dev"
12
+ Requires-Dist: mypy; extra == "dev"
13
+ Requires-Dist: ruff; extra == "dev"
14
+
15
+ # tinyhuman
16
+
17
+ Python SDK for the [TinyHumans API](https://tinyhumans.ai).
18
+
19
+ ## Requirements
20
+
21
+ - Python ≥ 3.9
22
+ - `httpx >= 0.25`
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install tinyhuman
28
+ ```
29
+
30
+ ## Quick start (synchronous)
31
+
32
+ ```python
33
+ from tinyhumansai import (
34
+ TinyHumanMemoryClient,
35
+ TinyHumanConfig,
36
+ IngestMemoryRequest,
37
+ ReadMemoryRequest,
38
+ DeleteMemoryRequest,
39
+ MemoryItem,
40
+ )
41
+
42
+ client = TinyHumanMemoryClient(TinyHumanConfig(token="your-api-key"))
43
+
44
+ # Ingest (upsert) memory
45
+ result = client.ingest_memory(
46
+ IngestMemoryRequest(
47
+ items=[
48
+ MemoryItem(
49
+ key="user-preference-theme",
50
+ content="User prefers dark mode",
51
+ namespace="preferences",
52
+ metadata={"source": "onboarding"},
53
+ )
54
+ ]
55
+ )
56
+ )
57
+ print(result) # IngestMemoryResponse(ingested=1, updated=0, errors=0)
58
+
59
+ # Read memory
60
+ items = client.read_memory(ReadMemoryRequest(namespace="preferences"))
61
+ print(items.count, items.items)
62
+
63
+ # Delete by key
64
+ client.delete_memory(DeleteMemoryRequest(key="user-preference-theme", namespace="preferences"))
65
+
66
+ # Delete all user memory
67
+ client.delete_memory(DeleteMemoryRequest(delete_all=True))
68
+ ```
69
+
70
+ The client implements the context-manager protocol for deterministic cleanup:
71
+
72
+ ```python
73
+ with TinyHumanMemoryClient(TinyHumanConfig(token="your-api-key")) as client:
74
+ result = client.read_memory()
75
+ ```
76
+
77
+ ## Async usage
78
+
79
+ Use `AsyncTinyHumanMemoryClient` inside `async` code to avoid blocking the
80
+ event loop (e.g. FastAPI, LangGraph async pipelines):
81
+
82
+ ```python
83
+ import asyncio
84
+ from tinyhumansai import AsyncTinyHumanMemoryClient, TinyHumanConfig, ReadMemoryRequest
85
+
86
+ async def main():
87
+ async with AsyncTinyHumanMemoryClient(TinyHumanConfig(token="your-api-key")) as client:
88
+ result = await client.read_memory()
89
+ print(result.items)
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ## API reference
95
+
96
+ ### `TinyHumanMemoryClient(config)` / `AsyncTinyHumanMemoryClient(config)`
97
+
98
+ | Param | Type | Required | Description |
99
+ |-------|------|----------|-------------|
100
+ | `config.token` | `str` | ✓ | JWT or API key |
101
+ | `config.base_url` | `str` | | Override API URL. If not set, uses `ALPHAHUMAN_BASE_URL` from env (e.g. `.env`) or default `https://staging-api.alphahuman.xyz` |
102
+
103
+ ### `client.ingest_memory(request)`
104
+
105
+ Upserts memory items. Items are deduplicated by `(namespace, key)`.
106
+
107
+ Returns `IngestMemoryResponse(ingested, updated, errors)`.
108
+
109
+ ### `client.read_memory(request?)`
110
+
111
+ Read memory items, optionally filtered by `key`, `keys`, or `namespace`.
112
+
113
+ Returns `ReadMemoryResponse(items, count)`.
114
+
115
+ ### `client.delete_memory(request)`
116
+
117
+ Delete memory by `key`, `keys`, or `delete_all=True`, optionally scoped by `namespace`.
118
+
119
+ Returns `DeleteMemoryResponse(deleted)`.
120
+
121
+ ## Error handling
122
+
123
+ ```python
124
+ from tinyhumansai import TinyHumanError
125
+
126
+ try:
127
+ client.read_memory()
128
+ except TinyHumanError as e:
129
+ print(e.status, str(e))
130
+ ```
131
+
132
+ `TinyHumanError` carries `.status` (HTTP status code) and `.body` (parsed
133
+ response or raw text for non-JSON responses such as gateway errors).
@@ -0,0 +1,119 @@
1
+ # tinyhuman
2
+
3
+ Python SDK for the [TinyHumans API](https://tinyhumans.ai).
4
+
5
+ ## Requirements
6
+
7
+ - Python ≥ 3.9
8
+ - `httpx >= 0.25`
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install tinyhuman
14
+ ```
15
+
16
+ ## Quick start (synchronous)
17
+
18
+ ```python
19
+ from tinyhumansai import (
20
+ TinyHumanMemoryClient,
21
+ TinyHumanConfig,
22
+ IngestMemoryRequest,
23
+ ReadMemoryRequest,
24
+ DeleteMemoryRequest,
25
+ MemoryItem,
26
+ )
27
+
28
+ client = TinyHumanMemoryClient(TinyHumanConfig(token="your-api-key"))
29
+
30
+ # Ingest (upsert) memory
31
+ result = client.ingest_memory(
32
+ IngestMemoryRequest(
33
+ items=[
34
+ MemoryItem(
35
+ key="user-preference-theme",
36
+ content="User prefers dark mode",
37
+ namespace="preferences",
38
+ metadata={"source": "onboarding"},
39
+ )
40
+ ]
41
+ )
42
+ )
43
+ print(result) # IngestMemoryResponse(ingested=1, updated=0, errors=0)
44
+
45
+ # Read memory
46
+ items = client.read_memory(ReadMemoryRequest(namespace="preferences"))
47
+ print(items.count, items.items)
48
+
49
+ # Delete by key
50
+ client.delete_memory(DeleteMemoryRequest(key="user-preference-theme", namespace="preferences"))
51
+
52
+ # Delete all user memory
53
+ client.delete_memory(DeleteMemoryRequest(delete_all=True))
54
+ ```
55
+
56
+ The client implements the context-manager protocol for deterministic cleanup:
57
+
58
+ ```python
59
+ with TinyHumanMemoryClient(TinyHumanConfig(token="your-api-key")) as client:
60
+ result = client.read_memory()
61
+ ```
62
+
63
+ ## Async usage
64
+
65
+ Use `AsyncTinyHumanMemoryClient` inside `async` code to avoid blocking the
66
+ event loop (e.g. FastAPI, LangGraph async pipelines):
67
+
68
+ ```python
69
+ import asyncio
70
+ from tinyhumansai import AsyncTinyHumanMemoryClient, TinyHumanConfig, ReadMemoryRequest
71
+
72
+ async def main():
73
+ async with AsyncTinyHumanMemoryClient(TinyHumanConfig(token="your-api-key")) as client:
74
+ result = await client.read_memory()
75
+ print(result.items)
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ ## API reference
81
+
82
+ ### `TinyHumanMemoryClient(config)` / `AsyncTinyHumanMemoryClient(config)`
83
+
84
+ | Param | Type | Required | Description |
85
+ |-------|------|----------|-------------|
86
+ | `config.token` | `str` | ✓ | JWT or API key |
87
+ | `config.base_url` | `str` | | Override API URL. If not set, uses `ALPHAHUMAN_BASE_URL` from env (e.g. `.env`) or default `https://staging-api.alphahuman.xyz` |
88
+
89
+ ### `client.ingest_memory(request)`
90
+
91
+ Upserts memory items. Items are deduplicated by `(namespace, key)`.
92
+
93
+ Returns `IngestMemoryResponse(ingested, updated, errors)`.
94
+
95
+ ### `client.read_memory(request?)`
96
+
97
+ Read memory items, optionally filtered by `key`, `keys`, or `namespace`.
98
+
99
+ Returns `ReadMemoryResponse(items, count)`.
100
+
101
+ ### `client.delete_memory(request)`
102
+
103
+ Delete memory by `key`, `keys`, or `delete_all=True`, optionally scoped by `namespace`.
104
+
105
+ Returns `DeleteMemoryResponse(deleted)`.
106
+
107
+ ## Error handling
108
+
109
+ ```python
110
+ from tinyhumansai import TinyHumanError
111
+
112
+ try:
113
+ client.read_memory()
114
+ except TinyHumanError as e:
115
+ print(e.status, str(e))
116
+ ```
117
+
118
+ `TinyHumanError` carries `.status` (HTTP status code) and `.body` (parsed
119
+ response or raw text for non-JSON responses such as gateway errors).
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tinyhumansai"
7
+ version = "0.1.0"
8
+ description = "Python SDK for TinyHumans API - ingest and delete memory"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ dependencies = ["httpx>=0.25"]
13
+
14
+ [project.optional-dependencies]
15
+ dev = ["pytest", "pytest-asyncio", "mypy", "ruff"]
16
+
17
+ [project.scripts]
18
+ run-act-publish = "scripts.run_act_publish:main"
19
+
20
+ [tool.setuptools.packages.find]
21
+ include = ["tinyhumansai*", "scripts*"]
22
+
23
+ [tool.ruff]
24
+ line-length = 88
25
+ target-version = "py39"
26
+ src = ["tinyhumansai", "alphahuman_memory", "scripts"]
@@ -0,0 +1 @@
1
+ # Dev script entry points (run-act-publish, etc.)
@@ -0,0 +1,49 @@
1
+ """Run act to test .github/workflows/publish.yml locally with secrets from .act-secrets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+
13
+ def _repo_root() -> Path:
14
+ root = Path(__file__).resolve().parent.parent
15
+ if not (root / ".github" / "workflows").exists():
16
+ raise SystemExit("run_act_publish: must run from repo root (has .github/workflows)")
17
+ return root
18
+
19
+
20
+ def main() -> None:
21
+ root = _repo_root()
22
+ secrets_file = root / ".act-secrets"
23
+ if not secrets_file.exists():
24
+ print("Missing .act-secrets. Copy act-secrets.example to .act-secrets and add PYPI_API_TOKEN.", file=sys.stderr)
25
+ sys.exit(1)
26
+
27
+ tag = os.environ.get("ACT_TAG", "v0.1.0")
28
+ event = {"ref": f"refs/tags/{tag}"}
29
+
30
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
31
+ json.dump(event, f)
32
+ event_path = f.name
33
+
34
+ try:
35
+ workflow = root / ".github" / "workflows" / "publish.yml"
36
+ cmd = [
37
+ "act",
38
+ "push",
39
+ "-W", str(workflow),
40
+ "--secret-file", str(secrets_file),
41
+ "-e", event_path,
42
+ ]
43
+ sys.exit(subprocess.run(cmd, cwd=root).returncode)
44
+ finally:
45
+ os.unlink(event_path, dir_fd=None)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,36 @@
1
+ """TinyHumans Python SDK."""
2
+
3
+ from .async_client import AsyncTinyHumanMemoryClient, AsyncAlphahumanMemoryClient
4
+ from .client import TinyHumanMemoryClient, AlphahumanMemoryClient
5
+ from .types import (
6
+ TinyHumanConfig,
7
+ TinyHumanError,
8
+ DeleteMemoryRequest,
9
+ DeleteMemoryResponse,
10
+ IngestMemoryRequest,
11
+ IngestMemoryResponse,
12
+ MemoryItem,
13
+ ReadMemoryItem,
14
+ ReadMemoryRequest,
15
+ ReadMemoryResponse,
16
+ )
17
+
18
+ __all__ = [
19
+ # Preferred TinyHuman names
20
+ "TinyHumanMemoryClient",
21
+ "AsyncTinyHumanMemoryClient",
22
+ "TinyHumanConfig",
23
+ "TinyHumanError",
24
+ # Backwards-compatible Alphahuman names
25
+ "AlphahumanMemoryClient",
26
+ "AsyncAlphahumanMemoryClient",
27
+ # Shared types
28
+ "DeleteMemoryRequest",
29
+ "DeleteMemoryResponse",
30
+ "IngestMemoryRequest",
31
+ "IngestMemoryResponse",
32
+ "MemoryItem",
33
+ "ReadMemoryItem",
34
+ "ReadMemoryRequest",
35
+ "ReadMemoryResponse",
36
+ ]
@@ -0,0 +1,197 @@
1
+ """Async TinyHumans memory client for Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Optional
7
+
8
+ import httpx
9
+
10
+ from .types import (
11
+ TinyHumanConfig,
12
+ TinyHumanError,
13
+ BASE_URL_ENV,
14
+ DEFAULT_BASE_URL,
15
+ DeleteMemoryRequest,
16
+ DeleteMemoryResponse,
17
+ IngestMemoryRequest,
18
+ IngestMemoryResponse,
19
+ ReadMemoryItem,
20
+ ReadMemoryRequest,
21
+ ReadMemoryResponse,
22
+ )
23
+
24
+
25
+ class AsyncTinyHumanMemoryClient:
26
+ """Async client for the TinyHumans memory API.
27
+
28
+ Prefer this in async runtimes (e.g. FastAPI, LangGraph async pipelines)
29
+ to avoid blocking the event loop.
30
+
31
+ Args:
32
+ config: Connection configuration (token required, base_url optional).
33
+
34
+ Example::
35
+
36
+ async with AsyncTinyHumanMemoryClient(TinyHumanConfig(token="...")) as client:
37
+ result = await client.ingest_memory(IngestMemoryRequest(items=[...]))
38
+ """
39
+
40
+ def __init__(self, config: TinyHumanConfig) -> None:
41
+ if not config.token or not config.token.strip():
42
+ raise ValueError("token is required")
43
+ base_url = config.base_url or os.environ.get(BASE_URL_ENV) or DEFAULT_BASE_URL
44
+ self._base_url = base_url.rstrip("/")
45
+ self._token = config.token
46
+ self._http = httpx.AsyncClient(
47
+ base_url=self._base_url,
48
+ headers={"Authorization": f"Bearer {self._token}"},
49
+ timeout=30,
50
+ )
51
+
52
+ async def close(self) -> None:
53
+ """Close the underlying HTTP client and release connections."""
54
+ await self._http.aclose()
55
+
56
+ async def __aenter__(self) -> "AsyncAlphahumanMemoryClient":
57
+ return self
58
+
59
+ async def __aexit__(self, *_: object) -> None:
60
+ await self.close()
61
+
62
+ async def ingest_memory(self, request: IngestMemoryRequest) -> IngestMemoryResponse:
63
+ """Ingest (upsert) one or more memory items asynchronously.
64
+
65
+ Args:
66
+ request: The ingestion request containing items to upsert.
67
+
68
+ Returns:
69
+ Counts of ingested, updated, and errored items.
70
+
71
+ Raises:
72
+ ValueError: If items list is empty.
73
+ TinyHumanError: On API errors.
74
+ """
75
+ if not request.items:
76
+ raise ValueError("items must be a non-empty list")
77
+
78
+ body = {
79
+ "items": [
80
+ {
81
+ "key": item.key,
82
+ "content": item.content,
83
+ "namespace": item.namespace,
84
+ "metadata": item.metadata,
85
+ }
86
+ for item in request.items
87
+ ]
88
+ }
89
+ data = await self._send("POST", "/v1/memory", body)
90
+ return IngestMemoryResponse(
91
+ ingested=data["ingested"],
92
+ updated=data["updated"],
93
+ errors=data["errors"],
94
+ )
95
+
96
+ async def read_memory(
97
+ self, request: Optional[ReadMemoryRequest] = None
98
+ ) -> ReadMemoryResponse:
99
+ """Read memory items by key, keys, or namespace asynchronously.
100
+
101
+ Returns all user memory if no filters are provided.
102
+
103
+ Args:
104
+ request: Optional filters for the read.
105
+
106
+ Returns:
107
+ List of matching memory items and count.
108
+
109
+ Raises:
110
+ TinyHumanError: On API errors.
111
+ """
112
+ params: list[tuple[str, str]] = []
113
+ if request:
114
+ if request.key:
115
+ params.append(("key", request.key))
116
+ if request.keys:
117
+ for k in request.keys:
118
+ params.append(("keys[]", k))
119
+ if request.namespace:
120
+ params.append(("namespace", request.namespace))
121
+
122
+ data = await self._get("/v1/memory", params)
123
+ items = [
124
+ ReadMemoryItem(
125
+ key=item["key"],
126
+ content=item["content"],
127
+ namespace=item["namespace"],
128
+ metadata=item.get("metadata", {}),
129
+ created_at=item.get("createdAt", ""),
130
+ updated_at=item.get("updatedAt", ""),
131
+ )
132
+ for item in data["items"]
133
+ ]
134
+ return ReadMemoryResponse(items=items, count=data["count"])
135
+
136
+ async def delete_memory(self, request: DeleteMemoryRequest) -> DeleteMemoryResponse:
137
+ """Delete memory items by key, keys, or delete all asynchronously.
138
+
139
+ Args:
140
+ request: The deletion request specifying what to delete.
141
+
142
+ Returns:
143
+ Count of deleted items.
144
+
145
+ Raises:
146
+ ValueError: If no deletion target is specified.
147
+ TinyHumanError: On API errors.
148
+ """
149
+ has_key = isinstance(request.key, str) and len(request.key) > 0
150
+ has_keys = isinstance(request.keys, list) and len(request.keys) > 0
151
+ if not has_key and not has_keys and not request.delete_all:
152
+ raise ValueError('Provide "key", "keys", or set delete_all=True')
153
+
154
+ body: dict[str, Any] = {}
155
+ if request.key is not None:
156
+ body["key"] = request.key
157
+ if request.keys is not None:
158
+ body["keys"] = request.keys
159
+ if request.namespace is not None:
160
+ body["namespace"] = request.namespace
161
+ if request.delete_all:
162
+ body["deleteAll"] = True
163
+
164
+ data = await self._send("DELETE", "/v1/memory", body)
165
+ return DeleteMemoryResponse(deleted=data["deleted"])
166
+
167
+ # ------------------------------------------------------------------
168
+ # Internal helpers
169
+ # ------------------------------------------------------------------
170
+
171
+ async def _get(self, path: str, params: list[tuple[str, str]]) -> dict[str, Any]:
172
+ response = await self._http.get(path, params=params)
173
+ return self._parse_response(response)
174
+
175
+ async def _send(
176
+ self, method: str, path: str, body: dict[str, Any]
177
+ ) -> dict[str, Any]:
178
+ response = await self._http.request(method, path, json=body)
179
+ return self._parse_response(response)
180
+
181
+ def _parse_response(self, response: httpx.Response) -> dict[str, Any]:
182
+ try:
183
+ payload = response.json()
184
+ except Exception:
185
+ raise TinyHumanError(
186
+ f"HTTP {response.status_code}: non-JSON response",
187
+ response.status_code,
188
+ response.text,
189
+ )
190
+ if not response.is_success:
191
+ message = payload.get("error", f"HTTP {response.status_code}")
192
+ raise TinyHumanError(message, response.status_code, payload)
193
+ return payload["data"]
194
+
195
+
196
+ # Backwards-compatible alias
197
+ AsyncAlphahumanMemoryClient = AsyncTinyHumanMemoryClient
@@ -0,0 +1,191 @@
1
+ """TinyHumans memory client for Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Optional
7
+
8
+ import httpx
9
+
10
+ from .types import (
11
+ TinyHumanConfig,
12
+ TinyHumanError,
13
+ BASE_URL_ENV,
14
+ DEFAULT_BASE_URL,
15
+ DeleteMemoryRequest,
16
+ DeleteMemoryResponse,
17
+ IngestMemoryRequest,
18
+ IngestMemoryResponse,
19
+ ReadMemoryItem,
20
+ ReadMemoryRequest,
21
+ ReadMemoryResponse,
22
+ )
23
+
24
+
25
+ class TinyHumanMemoryClient:
26
+ """Synchronous client for the TinyHumans memory API.
27
+
28
+ Args:
29
+ config: Connection configuration (token required, base_url optional).
30
+ """
31
+
32
+ def __init__(self, config: TinyHumanConfig) -> None:
33
+ if not config.token or not config.token.strip():
34
+ raise ValueError("token is required")
35
+ base_url = config.base_url or os.environ.get(BASE_URL_ENV) or DEFAULT_BASE_URL
36
+ self._base_url = base_url.rstrip("/")
37
+ self._token = config.token
38
+ self._http = httpx.Client(
39
+ base_url=self._base_url,
40
+ headers={"Authorization": f"Bearer {self._token}"},
41
+ timeout=30,
42
+ )
43
+
44
+ def close(self) -> None:
45
+ """Close the underlying HTTP client and release connections."""
46
+ self._http.close()
47
+
48
+ def __enter__(self) -> "AlphahumanMemoryClient":
49
+ return self
50
+
51
+ def __exit__(self, *_: object) -> None:
52
+ self.close()
53
+
54
+ def ingest_memory(self, request: IngestMemoryRequest) -> IngestMemoryResponse:
55
+ """Ingest (upsert) one or more memory items.
56
+
57
+ Items are deduped by (namespace, key). If a matching item already
58
+ exists its content and metadata are updated; otherwise a new item
59
+ is created.
60
+
61
+ Args:
62
+ request: The ingestion request containing items to upsert.
63
+
64
+ Returns:
65
+ Counts of ingested, updated, and errored items.
66
+
67
+ Raises:
68
+ ValueError: If items list is empty.
69
+ TinyHumanError: On API errors.
70
+ """
71
+ if not request.items:
72
+ raise ValueError("items must be a non-empty list")
73
+
74
+ body = {
75
+ "items": [
76
+ {
77
+ "key": item.key,
78
+ "content": item.content,
79
+ "namespace": item.namespace,
80
+ "metadata": item.metadata,
81
+ }
82
+ for item in request.items
83
+ ]
84
+ }
85
+ data = self._send("POST", "/v1/memory", body)
86
+ return IngestMemoryResponse(
87
+ ingested=data["ingested"],
88
+ updated=data["updated"],
89
+ errors=data["errors"],
90
+ )
91
+
92
+ def read_memory(
93
+ self, request: Optional[ReadMemoryRequest] = None
94
+ ) -> ReadMemoryResponse:
95
+ """Read memory items by key, keys, or namespace.
96
+
97
+ Returns all user memory if no filters are provided.
98
+
99
+ Args:
100
+ request: Optional filters for the read.
101
+
102
+ Returns:
103
+ List of matching memory items and count.
104
+
105
+ Raises:
106
+ TinyHumanError: On API errors.
107
+ """
108
+ params: list[tuple[str, str]] = []
109
+ if request:
110
+ if request.key:
111
+ params.append(("key", request.key))
112
+ if request.keys:
113
+ for k in request.keys:
114
+ params.append(("keys[]", k))
115
+ if request.namespace:
116
+ params.append(("namespace", request.namespace))
117
+
118
+ data = self._get("/v1/memory", params)
119
+ items = [
120
+ ReadMemoryItem(
121
+ key=item["key"],
122
+ content=item["content"],
123
+ namespace=item["namespace"],
124
+ metadata=item.get("metadata", {}),
125
+ created_at=item.get("createdAt", ""),
126
+ updated_at=item.get("updatedAt", ""),
127
+ )
128
+ for item in data["items"]
129
+ ]
130
+ return ReadMemoryResponse(items=items, count=data["count"])
131
+
132
+ def delete_memory(self, request: DeleteMemoryRequest) -> DeleteMemoryResponse:
133
+ """Delete memory items by key, keys, or delete all.
134
+
135
+ Args:
136
+ request: The deletion request specifying what to delete.
137
+
138
+ Returns:
139
+ Count of deleted items.
140
+
141
+ Raises:
142
+ ValueError: If no deletion target is specified.
143
+ TinyHumanError: On API errors.
144
+ """
145
+ has_key = isinstance(request.key, str) and len(request.key) > 0
146
+ has_keys = isinstance(request.keys, list) and len(request.keys) > 0
147
+ if not has_key and not has_keys and not request.delete_all:
148
+ raise ValueError('Provide "key", "keys", or set delete_all=True')
149
+
150
+ body: dict[str, Any] = {}
151
+ if request.key is not None:
152
+ body["key"] = request.key
153
+ if request.keys is not None:
154
+ body["keys"] = request.keys
155
+ if request.namespace is not None:
156
+ body["namespace"] = request.namespace
157
+ if request.delete_all:
158
+ body["deleteAll"] = True
159
+
160
+ data = self._send("DELETE", "/v1/memory", body)
161
+ return DeleteMemoryResponse(deleted=data["deleted"])
162
+
163
+ # ------------------------------------------------------------------
164
+ # Internal helpers
165
+ # ------------------------------------------------------------------
166
+
167
+ def _get(self, path: str, params: list[tuple[str, str]]) -> dict[str, Any]:
168
+ response = self._http.get(path, params=params)
169
+ return self._parse_response(response)
170
+
171
+ def _send(self, method: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
172
+ response = self._http.request(method, path, json=body)
173
+ return self._parse_response(response)
174
+
175
+ def _parse_response(self, response: httpx.Response) -> dict[str, Any]:
176
+ try:
177
+ payload = response.json()
178
+ except Exception:
179
+ raise TinyHumanError(
180
+ f"HTTP {response.status_code}: non-JSON response",
181
+ response.status_code,
182
+ response.text,
183
+ )
184
+ if not response.is_success:
185
+ message = payload.get("error", f"HTTP {response.status_code}")
186
+ raise TinyHumanError(message, response.status_code, payload)
187
+ return payload["data"]
188
+
189
+
190
+ # Backwards-compatible alias
191
+ AlphahumanMemoryClient = TinyHumanMemoryClient
@@ -0,0 +1,123 @@
1
+ """Type definitions for the TinyHumans Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Optional
7
+
8
+
9
+ DEFAULT_BASE_URL = "https://staging-api.alphahuman.xyz"
10
+
11
+ # Environment variable for base URL override (e.g. from .env)
12
+ BASE_URL_ENV = "ALPHAHUMAN_BASE_URL"
13
+
14
+
15
+ @dataclass
16
+ class TinyHumanConfig:
17
+ """Configuration for the TinyHumans client."""
18
+
19
+ token: str
20
+ """Bearer token (JWT or API key) for authentication."""
21
+
22
+ base_url: Optional[str] = None
23
+ """Base URL of the backend. If None, uses ALPHAHUMAN_BASE_URL env var or default staging URL."""
24
+
25
+
26
+ @dataclass
27
+ class MemoryItem:
28
+ """A single memory item to ingest."""
29
+
30
+ key: str
31
+ """Unique key within the namespace (used for upsert / dedup)."""
32
+
33
+ content: str
34
+ """Memory content text."""
35
+
36
+ namespace: str = "default"
37
+ """Namespace to scope this item."""
38
+
39
+ metadata: dict[str, Any] = field(default_factory=dict)
40
+ """Arbitrary metadata."""
41
+
42
+
43
+ @dataclass
44
+ class IngestMemoryRequest:
45
+ """Request payload for memory ingestion."""
46
+
47
+ items: list[MemoryItem]
48
+
49
+
50
+ @dataclass
51
+ class IngestMemoryResponse:
52
+ """Response from memory ingestion."""
53
+
54
+ ingested: int
55
+ updated: int
56
+ errors: int
57
+
58
+
59
+ @dataclass
60
+ class ReadMemoryRequest:
61
+ """Request payload for memory read."""
62
+
63
+ key: Optional[str] = None
64
+ """Single key to read."""
65
+
66
+ keys: Optional[list[str]] = None
67
+ """Array of keys to read."""
68
+
69
+ namespace: Optional[str] = None
70
+ """Namespace scope."""
71
+
72
+
73
+ @dataclass
74
+ class ReadMemoryItem:
75
+ """A single memory item returned from a read."""
76
+
77
+ key: str
78
+ content: str
79
+ namespace: str
80
+ metadata: dict[str, Any]
81
+ created_at: str
82
+ updated_at: str
83
+
84
+
85
+ @dataclass
86
+ class ReadMemoryResponse:
87
+ """Response from memory read."""
88
+
89
+ items: list[ReadMemoryItem]
90
+ count: int
91
+
92
+
93
+ @dataclass
94
+ class DeleteMemoryRequest:
95
+ """Request payload for memory deletion."""
96
+
97
+ key: Optional[str] = None
98
+ """Single key to delete."""
99
+
100
+ keys: Optional[list[str]] = None
101
+ """Array of keys to delete."""
102
+
103
+ namespace: Optional[str] = None
104
+ """Namespace scope (default: 'default')."""
105
+
106
+ delete_all: bool = False
107
+ """Delete all memory for the user (optionally scoped by namespace)."""
108
+
109
+
110
+ @dataclass
111
+ class DeleteMemoryResponse:
112
+ """Response from memory deletion."""
113
+
114
+ deleted: int
115
+
116
+
117
+ class TinyHumanError(Exception):
118
+ """Error raised by the TinyHumans API."""
119
+
120
+ def __init__(self, message: str, status: int, body: Any = None) -> None:
121
+ super().__init__(message)
122
+ self.status = status
123
+ self.body = body
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinyhumansai
3
+ Version: 0.1.0
4
+ Summary: Python SDK for TinyHumans API - ingest and delete memory
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx>=0.25
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest; extra == "dev"
11
+ Requires-Dist: pytest-asyncio; extra == "dev"
12
+ Requires-Dist: mypy; extra == "dev"
13
+ Requires-Dist: ruff; extra == "dev"
14
+
15
+ # tinyhuman
16
+
17
+ Python SDK for the [TinyHumans API](https://tinyhumans.ai).
18
+
19
+ ## Requirements
20
+
21
+ - Python ≥ 3.9
22
+ - `httpx >= 0.25`
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install tinyhuman
28
+ ```
29
+
30
+ ## Quick start (synchronous)
31
+
32
+ ```python
33
+ from tinyhumansai import (
34
+ TinyHumanMemoryClient,
35
+ TinyHumanConfig,
36
+ IngestMemoryRequest,
37
+ ReadMemoryRequest,
38
+ DeleteMemoryRequest,
39
+ MemoryItem,
40
+ )
41
+
42
+ client = TinyHumanMemoryClient(TinyHumanConfig(token="your-api-key"))
43
+
44
+ # Ingest (upsert) memory
45
+ result = client.ingest_memory(
46
+ IngestMemoryRequest(
47
+ items=[
48
+ MemoryItem(
49
+ key="user-preference-theme",
50
+ content="User prefers dark mode",
51
+ namespace="preferences",
52
+ metadata={"source": "onboarding"},
53
+ )
54
+ ]
55
+ )
56
+ )
57
+ print(result) # IngestMemoryResponse(ingested=1, updated=0, errors=0)
58
+
59
+ # Read memory
60
+ items = client.read_memory(ReadMemoryRequest(namespace="preferences"))
61
+ print(items.count, items.items)
62
+
63
+ # Delete by key
64
+ client.delete_memory(DeleteMemoryRequest(key="user-preference-theme", namespace="preferences"))
65
+
66
+ # Delete all user memory
67
+ client.delete_memory(DeleteMemoryRequest(delete_all=True))
68
+ ```
69
+
70
+ The client implements the context-manager protocol for deterministic cleanup:
71
+
72
+ ```python
73
+ with TinyHumanMemoryClient(TinyHumanConfig(token="your-api-key")) as client:
74
+ result = client.read_memory()
75
+ ```
76
+
77
+ ## Async usage
78
+
79
+ Use `AsyncTinyHumanMemoryClient` inside `async` code to avoid blocking the
80
+ event loop (e.g. FastAPI, LangGraph async pipelines):
81
+
82
+ ```python
83
+ import asyncio
84
+ from tinyhumansai import AsyncTinyHumanMemoryClient, TinyHumanConfig, ReadMemoryRequest
85
+
86
+ async def main():
87
+ async with AsyncTinyHumanMemoryClient(TinyHumanConfig(token="your-api-key")) as client:
88
+ result = await client.read_memory()
89
+ print(result.items)
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ## API reference
95
+
96
+ ### `TinyHumanMemoryClient(config)` / `AsyncTinyHumanMemoryClient(config)`
97
+
98
+ | Param | Type | Required | Description |
99
+ |-------|------|----------|-------------|
100
+ | `config.token` | `str` | ✓ | JWT or API key |
101
+ | `config.base_url` | `str` | | Override API URL. If not set, uses `ALPHAHUMAN_BASE_URL` from env (e.g. `.env`) or default `https://staging-api.alphahuman.xyz` |
102
+
103
+ ### `client.ingest_memory(request)`
104
+
105
+ Upserts memory items. Items are deduplicated by `(namespace, key)`.
106
+
107
+ Returns `IngestMemoryResponse(ingested, updated, errors)`.
108
+
109
+ ### `client.read_memory(request?)`
110
+
111
+ Read memory items, optionally filtered by `key`, `keys`, or `namespace`.
112
+
113
+ Returns `ReadMemoryResponse(items, count)`.
114
+
115
+ ### `client.delete_memory(request)`
116
+
117
+ Delete memory by `key`, `keys`, or `delete_all=True`, optionally scoped by `namespace`.
118
+
119
+ Returns `DeleteMemoryResponse(deleted)`.
120
+
121
+ ## Error handling
122
+
123
+ ```python
124
+ from tinyhumansai import TinyHumanError
125
+
126
+ try:
127
+ client.read_memory()
128
+ except TinyHumanError as e:
129
+ print(e.status, str(e))
130
+ ```
131
+
132
+ `TinyHumanError` carries `.status` (HTTP status code) and `.body` (parsed
133
+ response or raw text for non-JSON responses such as gateway errors).
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ scripts/__init__.py
4
+ scripts/run_act_publish.py
5
+ tinyhumansai/__init__.py
6
+ tinyhumansai/async_client.py
7
+ tinyhumansai/client.py
8
+ tinyhumansai/types.py
9
+ tinyhumansai.egg-info/PKG-INFO
10
+ tinyhumansai.egg-info/SOURCES.txt
11
+ tinyhumansai.egg-info/dependency_links.txt
12
+ tinyhumansai.egg-info/entry_points.txt
13
+ tinyhumansai.egg-info/requires.txt
14
+ tinyhumansai.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ run-act-publish = scripts.run_act_publish:main
@@ -0,0 +1,7 @@
1
+ httpx>=0.25
2
+
3
+ [dev]
4
+ pytest
5
+ pytest-asyncio
6
+ mypy
7
+ ruff
@@ -0,0 +1,2 @@
1
+ scripts
2
+ tinyhumansai