hotmem 0.1.3__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.
- hotmem-0.1.3/LICENSE +21 -0
- hotmem-0.1.3/PKG-INFO +169 -0
- hotmem-0.1.3/README.md +144 -0
- hotmem-0.1.3/pyproject.toml +57 -0
- hotmem-0.1.3/setup.cfg +4 -0
- hotmem-0.1.3/src/hotmem/__init__.py +3 -0
- hotmem-0.1.3/src/hotmem/cli.py +118 -0
- hotmem-0.1.3/src/hotmem/client.py +100 -0
- hotmem-0.1.3/src/hotmem/db.py +152 -0
- hotmem-0.1.3/src/hotmem/embed.py +69 -0
- hotmem-0.1.3/src/hotmem/mount.py +69 -0
- hotmem-0.1.3/src/hotmem/py.typed +0 -0
- hotmem-0.1.3/src/hotmem/search.py +100 -0
- hotmem-0.1.3/src/hotmem/server.py +206 -0
- hotmem-0.1.3/src/hotmem/swap.py +120 -0
- hotmem-0.1.3/src/hotmem/trace.py +129 -0
- hotmem-0.1.3/src/hotmem.egg-info/PKG-INFO +169 -0
- hotmem-0.1.3/src/hotmem.egg-info/SOURCES.txt +27 -0
- hotmem-0.1.3/src/hotmem.egg-info/dependency_links.txt +1 -0
- hotmem-0.1.3/src/hotmem.egg-info/entry_points.txt +2 -0
- hotmem-0.1.3/src/hotmem.egg-info/requires.txt +8 -0
- hotmem-0.1.3/src/hotmem.egg-info/top_level.txt +1 -0
- hotmem-0.1.3/tests/test_api.py +98 -0
- hotmem-0.1.3/tests/test_client.py +52 -0
- hotmem-0.1.3/tests/test_db.py +55 -0
- hotmem-0.1.3/tests/test_embed.py +52 -0
- hotmem-0.1.3/tests/test_mount.py +33 -0
- hotmem-0.1.3/tests/test_search.py +52 -0
- hotmem-0.1.3/tests/test_swap.py +66 -0
hotmem-0.1.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 valiantone
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hotmem-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hotmem
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A local-first memory sidecar for agent applications
|
|
5
|
+
Author-email: valiantone <zrjohn@yahoo.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/valiantone/hotmem
|
|
7
|
+
Project-URL: Repository, https://github.com/valiantone/hotmem
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: click<9,>=8.1
|
|
18
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
19
|
+
Requires-Dist: httpx<1,>=0.28
|
|
20
|
+
Requires-Dist: uvicorn<1,>=0.30
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest<10,>=9; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff<1,>=0.15; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# HotMem
|
|
27
|
+
|
|
28
|
+
A local-first memory sidecar for agent applications. One SQLite DB. One port: 8711.
|
|
29
|
+
|
|
30
|
+
HotMem provides fast, queryable working memory with hybrid vector + keyword search. Store facts, retrieve them ranked, and get back LLM-ready message objects you can stitch directly into prompts.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install hotmem@git+https://github.com/KnowGuard-AI/HotMem.git
|
|
36
|
+
# or
|
|
37
|
+
uv add hotmem@git+https://github.com/KnowGuard-AI/HotMem.git
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or add to `requirements.txt`:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
hotmem @ git+https://github.com/KnowGuard-AI/HotMem.git
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Start with a mount directory (portable memory)
|
|
50
|
+
hotmem serve --mount ./hotmem
|
|
51
|
+
|
|
52
|
+
# Or just start (uses temp DB)
|
|
53
|
+
hotmem serve
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
hotmem serve --port 8711 --mount ./data/hotmem
|
|
60
|
+
hotmem serve --db ./my.sqlite
|
|
61
|
+
hotmem hydrate --file swap.jsonl --db ./my.sqlite
|
|
62
|
+
hotmem snapshot --file swap.jsonl --db ./my.sqlite
|
|
63
|
+
hotmem status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
All endpoints under `/v1`. Default: `http://127.0.0.1:8711`
|
|
69
|
+
|
|
70
|
+
### `GET /v1/health`
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{"status": "ok", "memory_count": 42, "db_path": "...", "uptime_s": 120.5}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `POST /v1/add`
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{"identifier": "vendor_x", "fact": "Invoice total was $5000", "importance": 0.8}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `POST /v1/search`
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{"query": "duplicate invoice risk", "top_k": 5, "max_chars": 1500}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Returns ranked message objects ready for LLM stitching:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"memories": [
|
|
93
|
+
{"role": "system", "content": "...", "memory_id": "...", "identifier": "...", "score": 0.87}
|
|
94
|
+
],
|
|
95
|
+
"count": 5,
|
|
96
|
+
"trace_ms": 2.1
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `POST /v1/hydrate`
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{"file": "swap.jsonl"}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `POST /v1/snapshot`
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{"file": "swap.jsonl"}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Python Client
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from hotmem.client import HotMemClient
|
|
116
|
+
|
|
117
|
+
with HotMemClient("http://127.0.0.1:8711") as client:
|
|
118
|
+
client.add("vendor_x", "Invoice total $5000", importance=0.8)
|
|
119
|
+
|
|
120
|
+
memories = client.search("duplicate invoice risk", top_k=5, max_chars=1500)
|
|
121
|
+
|
|
122
|
+
# memories are LLM-ready message objects
|
|
123
|
+
messages = memories + [{"role": "user", "content": "Analyze this vendor."}]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Mounting
|
|
127
|
+
|
|
128
|
+
Any directory can be a HotMem mount. The mount contains:
|
|
129
|
+
|
|
130
|
+
- `hotmem.sqlite` — the database
|
|
131
|
+
- `swap.jsonl` — portable JSONL backup
|
|
132
|
+
- `manifest.json` — mount metadata
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
hotmem serve --mount /mnt/usb/hotmem # portable memory on USB
|
|
136
|
+
hotmem serve --mount ./data/hotmem # local project memory
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv sync # install deps
|
|
143
|
+
uv run pytest # run tests
|
|
144
|
+
uv run ruff check src/ tests/ # lint
|
|
145
|
+
uv run ruff format src/ tests/ # format
|
|
146
|
+
uv build # build wheel
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Architecture (for agents)
|
|
150
|
+
|
|
151
|
+
Each source module is independently extensible with self-documenting headers:
|
|
152
|
+
|
|
153
|
+
| Module | Purpose | Extension Point |
|
|
154
|
+
|--------|---------|------------------|
|
|
155
|
+
| `trace.py` | Structured JSON logging | Add sinks, formatters |
|
|
156
|
+
| `embed.py` | Hash-based embedder (dim=64) | Swap for real model |
|
|
157
|
+
| `db.py` | SQLite + cosine UDF | Add FTS5, indexes |
|
|
158
|
+
| `search.py` | Hybrid ranking | Add reranking, MMR |
|
|
159
|
+
| `swap.py` | JSONL hydrate/snapshot | Add compression |
|
|
160
|
+
| `mount.py` | Directory bootstrap | Add remote sync |
|
|
161
|
+
| `server.py` | FastAPI endpoints | Add CORS, new routes |
|
|
162
|
+
| `cli.py` | Click CLI | Add subcommands |
|
|
163
|
+
| `client.py` | httpx SDK | Add async client |
|
|
164
|
+
|
|
165
|
+
Every operation emits structured JSON traces to stderr with component tags:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
hotmem serve --mount ./data 2>&1 | grep '"component": "search"'
|
|
169
|
+
```
|
hotmem-0.1.3/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# HotMem
|
|
2
|
+
|
|
3
|
+
A local-first memory sidecar for agent applications. One SQLite DB. One port: 8711.
|
|
4
|
+
|
|
5
|
+
HotMem provides fast, queryable working memory with hybrid vector + keyword search. Store facts, retrieve them ranked, and get back LLM-ready message objects you can stitch directly into prompts.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install hotmem@git+https://github.com/KnowGuard-AI/HotMem.git
|
|
11
|
+
# or
|
|
12
|
+
uv add hotmem@git+https://github.com/KnowGuard-AI/HotMem.git
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add to `requirements.txt`:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
hotmem @ git+https://github.com/KnowGuard-AI/HotMem.git
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Start with a mount directory (portable memory)
|
|
25
|
+
hotmem serve --mount ./hotmem
|
|
26
|
+
|
|
27
|
+
# Or just start (uses temp DB)
|
|
28
|
+
hotmem serve
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
hotmem serve --port 8711 --mount ./data/hotmem
|
|
35
|
+
hotmem serve --db ./my.sqlite
|
|
36
|
+
hotmem hydrate --file swap.jsonl --db ./my.sqlite
|
|
37
|
+
hotmem snapshot --file swap.jsonl --db ./my.sqlite
|
|
38
|
+
hotmem status
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
All endpoints under `/v1`. Default: `http://127.0.0.1:8711`
|
|
44
|
+
|
|
45
|
+
### `GET /v1/health`
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"status": "ok", "memory_count": 42, "db_path": "...", "uptime_s": 120.5}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `POST /v1/add`
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{"identifier": "vendor_x", "fact": "Invoice total was $5000", "importance": 0.8}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `POST /v1/search`
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{"query": "duplicate invoice risk", "top_k": 5, "max_chars": 1500}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Returns ranked message objects ready for LLM stitching:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"memories": [
|
|
68
|
+
{"role": "system", "content": "...", "memory_id": "...", "identifier": "...", "score": 0.87}
|
|
69
|
+
],
|
|
70
|
+
"count": 5,
|
|
71
|
+
"trace_ms": 2.1
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `POST /v1/hydrate`
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{"file": "swap.jsonl"}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `POST /v1/snapshot`
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{"file": "swap.jsonl"}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Python Client
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from hotmem.client import HotMemClient
|
|
91
|
+
|
|
92
|
+
with HotMemClient("http://127.0.0.1:8711") as client:
|
|
93
|
+
client.add("vendor_x", "Invoice total $5000", importance=0.8)
|
|
94
|
+
|
|
95
|
+
memories = client.search("duplicate invoice risk", top_k=5, max_chars=1500)
|
|
96
|
+
|
|
97
|
+
# memories are LLM-ready message objects
|
|
98
|
+
messages = memories + [{"role": "user", "content": "Analyze this vendor."}]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Mounting
|
|
102
|
+
|
|
103
|
+
Any directory can be a HotMem mount. The mount contains:
|
|
104
|
+
|
|
105
|
+
- `hotmem.sqlite` — the database
|
|
106
|
+
- `swap.jsonl` — portable JSONL backup
|
|
107
|
+
- `manifest.json` — mount metadata
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
hotmem serve --mount /mnt/usb/hotmem # portable memory on USB
|
|
111
|
+
hotmem serve --mount ./data/hotmem # local project memory
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
uv sync # install deps
|
|
118
|
+
uv run pytest # run tests
|
|
119
|
+
uv run ruff check src/ tests/ # lint
|
|
120
|
+
uv run ruff format src/ tests/ # format
|
|
121
|
+
uv build # build wheel
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Architecture (for agents)
|
|
125
|
+
|
|
126
|
+
Each source module is independently extensible with self-documenting headers:
|
|
127
|
+
|
|
128
|
+
| Module | Purpose | Extension Point |
|
|
129
|
+
|--------|---------|------------------|
|
|
130
|
+
| `trace.py` | Structured JSON logging | Add sinks, formatters |
|
|
131
|
+
| `embed.py` | Hash-based embedder (dim=64) | Swap for real model |
|
|
132
|
+
| `db.py` | SQLite + cosine UDF | Add FTS5, indexes |
|
|
133
|
+
| `search.py` | Hybrid ranking | Add reranking, MMR |
|
|
134
|
+
| `swap.py` | JSONL hydrate/snapshot | Add compression |
|
|
135
|
+
| `mount.py` | Directory bootstrap | Add remote sync |
|
|
136
|
+
| `server.py` | FastAPI endpoints | Add CORS, new routes |
|
|
137
|
+
| `cli.py` | Click CLI | Add subcommands |
|
|
138
|
+
| `client.py` | httpx SDK | Add async client |
|
|
139
|
+
|
|
140
|
+
Every operation emits structured JSON traces to stderr with component tags:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
hotmem serve --mount ./data 2>&1 | grep '"component": "search"'
|
|
144
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hotmem"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
description = "A local-first memory sidecar for agent applications"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "valiantone", email = "zrjohn@yahoo.com" }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"click>=8.1,<9",
|
|
23
|
+
"fastapi>=0.115,<1",
|
|
24
|
+
"httpx>=0.28,<1",
|
|
25
|
+
"uvicorn>=0.30,<1",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/valiantone/hotmem"
|
|
30
|
+
Repository = "https://github.com/valiantone/hotmem"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=9,<10",
|
|
35
|
+
"ruff>=0.15,<1",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
hotmem = "hotmem.cli:main"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = [
|
|
43
|
+
"setuptools>=75",
|
|
44
|
+
"wheel"
|
|
45
|
+
]
|
|
46
|
+
build-backend = "setuptools.build_meta"
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
target-version = "py311"
|
|
50
|
+
line-length = 100
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
|
hotmem-0.1.3/setup.cfg
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""HotMem CLI — command-line interface for the memory sidecar.
|
|
2
|
+
|
|
3
|
+
Purpose:
|
|
4
|
+
Provide serve, hydrate, snapshot, and status commands.
|
|
5
|
+
Entry point: `hotmem` (registered in pyproject.toml).
|
|
6
|
+
|
|
7
|
+
Interface:
|
|
8
|
+
main() — Click group with subcommands
|
|
9
|
+
|
|
10
|
+
Deps: click, uvicorn, hotmem.server, hotmem.mount, hotmem.db, hotmem.swap, hotmem.trace
|
|
11
|
+
Extension: add new subcommands (e.g. `hotmem inspect`, `hotmem gc`) here.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from hotmem.trace import get_tracer
|
|
21
|
+
|
|
22
|
+
_trace = get_tracer("cli")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
@click.version_option(package_name="hotmem")
|
|
27
|
+
def main():
|
|
28
|
+
"""HotMem — local-first memory sidecar for agent applications."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@main.command()
|
|
32
|
+
@click.option("--port", default=8711, type=int, help="Port to listen on.")
|
|
33
|
+
@click.option("--mount", default=None, type=click.Path(), help="Mount directory path.")
|
|
34
|
+
@click.option("--db", "db_path", default=None, type=click.Path(), help="Explicit database path.")
|
|
35
|
+
@click.option("--host", default="127.0.0.1", help="Host to bind to.")
|
|
36
|
+
def serve(port: int, mount: str | None, db_path: str | None, host: str):
|
|
37
|
+
"""Start the HotMem sidecar server."""
|
|
38
|
+
import uvicorn
|
|
39
|
+
|
|
40
|
+
from hotmem.mount import bootstrap_mount
|
|
41
|
+
from hotmem.server import create_app
|
|
42
|
+
|
|
43
|
+
swap_path = None
|
|
44
|
+
|
|
45
|
+
if mount:
|
|
46
|
+
config = bootstrap_mount(mount)
|
|
47
|
+
db_path = str(config.db_path)
|
|
48
|
+
swap_path = str(config.swap_path)
|
|
49
|
+
elif not db_path:
|
|
50
|
+
db_path = tempfile.mktemp(suffix=".sqlite", prefix="hotmem_")
|
|
51
|
+
_trace.warn(
|
|
52
|
+
"serve", "no mount or db path specified, using temp db",
|
|
53
|
+
detail={"path": db_path},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
app = create_app(db_path=db_path, swap_path=swap_path, port=port)
|
|
57
|
+
|
|
58
|
+
_trace.info(
|
|
59
|
+
"serve", f"starting server on {host}:{port}",
|
|
60
|
+
detail={"db": db_path, "mount": mount},
|
|
61
|
+
)
|
|
62
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@main.command()
|
|
66
|
+
@click.option(
|
|
67
|
+
"--file", "swap_file", default="swap.jsonl",
|
|
68
|
+
type=click.Path(), help="Swap file path.",
|
|
69
|
+
)
|
|
70
|
+
@click.option("--db", "db_path", required=True, type=click.Path(), help="Database path.")
|
|
71
|
+
def hydrate(swap_file: str, db_path: str):
|
|
72
|
+
"""Load a swap file into the database."""
|
|
73
|
+
from hotmem.db import MemoryDB
|
|
74
|
+
from hotmem.swap import hydrate as do_hydrate
|
|
75
|
+
|
|
76
|
+
db = MemoryDB(db_path)
|
|
77
|
+
result = do_hydrate(db, swap_file)
|
|
78
|
+
db.close()
|
|
79
|
+
|
|
80
|
+
click.echo(f"Loaded: {result.loaded}, Skipped dupes: {result.skipped_dupes}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@main.command()
|
|
84
|
+
@click.option(
|
|
85
|
+
"--file", "swap_file", default="swap.jsonl",
|
|
86
|
+
type=click.Path(), help="Output swap file path.",
|
|
87
|
+
)
|
|
88
|
+
@click.option("--db", "db_path", required=True, type=click.Path(), help="Database path.")
|
|
89
|
+
def snapshot(swap_file: str, db_path: str):
|
|
90
|
+
"""Export database memories to a swap file."""
|
|
91
|
+
from hotmem.db import MemoryDB
|
|
92
|
+
from hotmem.swap import snapshot as do_snapshot
|
|
93
|
+
|
|
94
|
+
db = MemoryDB(db_path)
|
|
95
|
+
result = do_snapshot(db, swap_file)
|
|
96
|
+
db.close()
|
|
97
|
+
|
|
98
|
+
click.echo(f"Exported: {result.exported} → {result.path}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@main.command()
|
|
102
|
+
@click.option("--port", default=8711, type=int, help="Port to check.")
|
|
103
|
+
@click.option("--host", default="127.0.0.1", help="Host to check.")
|
|
104
|
+
def status(port: int, host: str):
|
|
105
|
+
"""Check if a HotMem server is running."""
|
|
106
|
+
import httpx
|
|
107
|
+
|
|
108
|
+
url = f"http://{host}:{port}/v1/health"
|
|
109
|
+
try:
|
|
110
|
+
resp = httpx.get(url, timeout=3.0)
|
|
111
|
+
data = resp.json()
|
|
112
|
+
click.echo(f"Status: {data['status']}")
|
|
113
|
+
click.echo(f"Memories: {data['memory_count']}")
|
|
114
|
+
click.echo(f"DB: {data['db_path']}")
|
|
115
|
+
click.echo(f"Uptime: {data['uptime_s']}s")
|
|
116
|
+
except httpx.ConnectError as err:
|
|
117
|
+
click.echo(f"No HotMem server found at {url}", err=True)
|
|
118
|
+
raise SystemExit(1) from err
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""HotMem client — Python SDK for the memory sidecar.
|
|
2
|
+
|
|
3
|
+
Purpose:
|
|
4
|
+
Provide a simple, typed client for the HotMem HTTP API.
|
|
5
|
+
Designed for direct use in agent applications and SPA backends.
|
|
6
|
+
|
|
7
|
+
Interface:
|
|
8
|
+
HotMemClient(base_url)
|
|
9
|
+
.add(identifier, fact, ...) -> dict
|
|
10
|
+
.search(query, top_k, max_chars?) -> list[MessageObject]
|
|
11
|
+
.health() -> dict
|
|
12
|
+
.hydrate(file?) -> dict
|
|
13
|
+
.snapshot(file?) -> dict
|
|
14
|
+
|
|
15
|
+
Deps: httpx
|
|
16
|
+
Extension: add async client, retry logic, or connection pooling here.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HotMemClient:
|
|
27
|
+
"""Synchronous client for the HotMem sidecar API."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, base_url: str = "http://127.0.0.1:8711") -> None:
|
|
30
|
+
self.base_url = base_url.rstrip("/")
|
|
31
|
+
self._client = httpx.Client(base_url=self.base_url, timeout=30.0)
|
|
32
|
+
|
|
33
|
+
def health(self) -> dict[str, Any]:
|
|
34
|
+
"""Check server health."""
|
|
35
|
+
resp = self._client.get("/v1/health")
|
|
36
|
+
resp.raise_for_status()
|
|
37
|
+
return resp.json()
|
|
38
|
+
|
|
39
|
+
def add(
|
|
40
|
+
self,
|
|
41
|
+
identifier: str,
|
|
42
|
+
fact: str,
|
|
43
|
+
*,
|
|
44
|
+
source: str = "",
|
|
45
|
+
importance: float = 0.5,
|
|
46
|
+
metadata: dict[str, Any] | None = None,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""Add a fact to memory."""
|
|
49
|
+
payload = {
|
|
50
|
+
"identifier": identifier,
|
|
51
|
+
"fact": fact,
|
|
52
|
+
"source": source,
|
|
53
|
+
"importance": importance,
|
|
54
|
+
"metadata": metadata or {},
|
|
55
|
+
}
|
|
56
|
+
resp = self._client.post("/v1/add", json=payload)
|
|
57
|
+
resp.raise_for_status()
|
|
58
|
+
return resp.json()
|
|
59
|
+
|
|
60
|
+
def search(
|
|
61
|
+
self,
|
|
62
|
+
query: str,
|
|
63
|
+
top_k: int = 5,
|
|
64
|
+
max_chars: int | None = None,
|
|
65
|
+
) -> list[dict[str, Any]]:
|
|
66
|
+
"""Search memories and return LLM-ready message objects."""
|
|
67
|
+
payload: dict[str, Any] = {"query": query, "top_k": top_k}
|
|
68
|
+
if max_chars is not None:
|
|
69
|
+
payload["max_chars"] = max_chars
|
|
70
|
+
resp = self._client.post("/v1/search", json=payload)
|
|
71
|
+
resp.raise_for_status()
|
|
72
|
+
return resp.json()["memories"]
|
|
73
|
+
|
|
74
|
+
def hydrate(self, file: str | None = None) -> dict[str, Any]:
|
|
75
|
+
"""Trigger swap file hydration."""
|
|
76
|
+
payload: dict[str, Any] = {}
|
|
77
|
+
if file:
|
|
78
|
+
payload["file"] = file
|
|
79
|
+
resp = self._client.post("/v1/hydrate", json=payload)
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
return resp.json()
|
|
82
|
+
|
|
83
|
+
def snapshot(self, file: str | None = None) -> dict[str, Any]:
|
|
84
|
+
"""Trigger database snapshot to swap file."""
|
|
85
|
+
payload: dict[str, Any] = {}
|
|
86
|
+
if file:
|
|
87
|
+
payload["file"] = file
|
|
88
|
+
resp = self._client.post("/v1/snapshot", json=payload)
|
|
89
|
+
resp.raise_for_status()
|
|
90
|
+
return resp.json()
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
"""Close the underlying HTTP client."""
|
|
94
|
+
self._client.close()
|
|
95
|
+
|
|
96
|
+
def __enter__(self) -> HotMemClient:
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def __exit__(self, *args: object) -> None:
|
|
100
|
+
self.close()
|