fluctlightdb 0.4.1__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.
- fluctlightdb-0.4.1/LICENSE +21 -0
- fluctlightdb-0.4.1/PKG-INFO +96 -0
- fluctlightdb-0.4.1/README.md +68 -0
- fluctlightdb-0.4.1/fluctlightdb/__init__.py +289 -0
- fluctlightdb-0.4.1/fluctlightdb/worker.py +233 -0
- fluctlightdb-0.4.1/fluctlightdb.egg-info/PKG-INFO +96 -0
- fluctlightdb-0.4.1/fluctlightdb.egg-info/SOURCES.txt +10 -0
- fluctlightdb-0.4.1/fluctlightdb.egg-info/dependency_links.txt +1 -0
- fluctlightdb-0.4.1/fluctlightdb.egg-info/requires.txt +3 -0
- fluctlightdb-0.4.1/fluctlightdb.egg-info/top_level.txt +1 -0
- fluctlightdb-0.4.1/pyproject.toml +39 -0
- fluctlightdb-0.4.1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Voxmastery
|
|
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.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluctlightdb
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Python client for FluctlightDB — brain-native memory for AI agents
|
|
5
|
+
Author-email: Voxmastery <voxmastery@roppashreeganesh.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/voxmastery/FluctlightDB
|
|
8
|
+
Project-URL: Documentation, https://github.com/voxmastery/FluctlightDB/blob/main/docs/GETTING_STARTED.md
|
|
9
|
+
Project-URL: Repository, https://github.com/voxmastery/FluctlightDB
|
|
10
|
+
Project-URL: Issues, https://github.com/voxmastery/FluctlightDB/issues
|
|
11
|
+
Keywords: ai,agents,memory,database,llm,recall
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: native
|
|
26
|
+
Requires-Dist: fluctlightdb-native>=0.4.0; extra == "native"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# fluctlightdb
|
|
30
|
+
|
|
31
|
+
Python client for [FluctlightDB](https://github.com/voxmastery/FluctlightDB) — a brain-native memory store for AI agents.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install fluctlightdb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Optional in-process recall (Rust extension, when wheels are available for your platform):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install "fluctlightdb[native]"
|
|
43
|
+
# or: pip install fluctlightdb-native
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
No `cargo` or Rust toolchain required for the HTTP client.
|
|
47
|
+
|
|
48
|
+
## Quick start (HTTP — like `qdrant-client`)
|
|
49
|
+
|
|
50
|
+
Run a FluctlightDB server (download a [release binary](https://github.com/voxmastery/FluctlightDB/releases) or use your own deployment), then:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fluctlightdb import FluctlightClient
|
|
54
|
+
|
|
55
|
+
client = FluctlightClient(
|
|
56
|
+
base_url="http://127.0.0.1:8792",
|
|
57
|
+
api_key="your-key",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
client.experience("user prefers dark mode", context="settings")
|
|
61
|
+
print(client.activate_lite("theme preference"))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or use environment variables:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export FLUCTLIGHT_SERVE_URL=http://127.0.0.1:8792
|
|
68
|
+
export FLUCTLIGHT_API_KEY=your-key
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from fluctlightdb import FluctlightClient
|
|
73
|
+
|
|
74
|
+
client = FluctlightClient.from_env()
|
|
75
|
+
print(client.activate("dark mode"))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## In-process recall (optional)
|
|
79
|
+
|
|
80
|
+
When `fluctlightdb-native` is installed:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from fluctlightdb import get_recall_client
|
|
84
|
+
|
|
85
|
+
brain = get_recall_client("~/.fluctlight/tenants/default/brain")
|
|
86
|
+
print(brain.activate("dark mode"))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Docs
|
|
90
|
+
|
|
91
|
+
- [Getting started](https://github.com/voxmastery/FluctlightDB/blob/main/docs/GETTING_STARTED.md)
|
|
92
|
+
- [HTTP API (OpenAPI)](https://github.com/voxmastery/FluctlightDB/blob/main/docs/openapi.yaml)
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# fluctlightdb
|
|
2
|
+
|
|
3
|
+
Python client for [FluctlightDB](https://github.com/voxmastery/FluctlightDB) — a brain-native memory store for AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install fluctlightdb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional in-process recall (Rust extension, when wheels are available for your platform):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install "fluctlightdb[native]"
|
|
15
|
+
# or: pip install fluctlightdb-native
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
No `cargo` or Rust toolchain required for the HTTP client.
|
|
19
|
+
|
|
20
|
+
## Quick start (HTTP — like `qdrant-client`)
|
|
21
|
+
|
|
22
|
+
Run a FluctlightDB server (download a [release binary](https://github.com/voxmastery/FluctlightDB/releases) or use your own deployment), then:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from fluctlightdb import FluctlightClient
|
|
26
|
+
|
|
27
|
+
client = FluctlightClient(
|
|
28
|
+
base_url="http://127.0.0.1:8792",
|
|
29
|
+
api_key="your-key",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
client.experience("user prefers dark mode", context="settings")
|
|
33
|
+
print(client.activate_lite("theme preference"))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or use environment variables:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export FLUCTLIGHT_SERVE_URL=http://127.0.0.1:8792
|
|
40
|
+
export FLUCTLIGHT_API_KEY=your-key
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from fluctlightdb import FluctlightClient
|
|
45
|
+
|
|
46
|
+
client = FluctlightClient.from_env()
|
|
47
|
+
print(client.activate("dark mode"))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## In-process recall (optional)
|
|
51
|
+
|
|
52
|
+
When `fluctlightdb-native` is installed:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from fluctlightdb import get_recall_client
|
|
56
|
+
|
|
57
|
+
brain = get_recall_client("~/.fluctlight/tenants/default/brain")
|
|
58
|
+
print(brain.activate("dark mode"))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Docs
|
|
62
|
+
|
|
63
|
+
- [Getting started](https://github.com/voxmastery/FluctlightDB/blob/main/docs/GETTING_STARTED.md)
|
|
64
|
+
- [HTTP API (OpenAPI)](https://github.com/voxmastery/FluctlightDB/blob/main/docs/openapi.yaml)
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""FluctlightDB Python client — agent memory HTTP API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import http.client
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from .worker import FluctlightNative, FluctlightWorker, get_recall_client, get_worker
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"FluctlightClient",
|
|
18
|
+
"FluctlightNative",
|
|
19
|
+
"FluctlightWorker",
|
|
20
|
+
"get_recall_client",
|
|
21
|
+
"get_worker",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class FluctlightClient:
|
|
27
|
+
base_url: str = "http://127.0.0.1:8792"
|
|
28
|
+
api_key: str = ""
|
|
29
|
+
timeout: float = 60.0
|
|
30
|
+
_http: Optional[http.client.HTTPConnection] = field(default=None, repr=False, compare=False)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_env(cls) -> "FluctlightClient":
|
|
34
|
+
return cls(
|
|
35
|
+
base_url=os.environ.get("FLUCTLIGHT_SERVE_URL", "http://127.0.0.1:8792").rstrip("/"),
|
|
36
|
+
api_key=os.environ.get("FLUCTLIGHT_API_KEY", ""),
|
|
37
|
+
timeout=float(os.environ.get("FLUCTLIGHT_HTTP_TIMEOUT", "60")),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _headers(self) -> dict[str, str]:
|
|
41
|
+
headers = {"Content-Type": "application/json", "Connection": "keep-alive"}
|
|
42
|
+
if self.api_key:
|
|
43
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
44
|
+
return headers
|
|
45
|
+
|
|
46
|
+
def _conn(self) -> http.client.HTTPConnection:
|
|
47
|
+
if self._http is not None:
|
|
48
|
+
return self._http
|
|
49
|
+
parsed = urllib.parse.urlparse(self.base_url)
|
|
50
|
+
host = parsed.hostname or "127.0.0.1"
|
|
51
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
52
|
+
if parsed.scheme == "https":
|
|
53
|
+
self._http = http.client.HTTPSConnection(host, port, timeout=self.timeout)
|
|
54
|
+
else:
|
|
55
|
+
self._http = http.client.HTTPConnection(host, port, timeout=self.timeout)
|
|
56
|
+
return self._http
|
|
57
|
+
|
|
58
|
+
def _post(self, path: str, payload: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
|
59
|
+
body = json.dumps(payload or {})
|
|
60
|
+
headers = self._headers()
|
|
61
|
+
try:
|
|
62
|
+
conn = self._conn()
|
|
63
|
+
conn.request("POST", path, body=body, headers=headers)
|
|
64
|
+
resp = conn.getresponse()
|
|
65
|
+
raw = resp.read().decode("utf-8")
|
|
66
|
+
if resp.status >= 400:
|
|
67
|
+
raise RuntimeError(f"Fluctlight HTTP {resp.status}: {raw}")
|
|
68
|
+
return json.loads(raw) if raw else {}
|
|
69
|
+
except (http.client.HTTPException, OSError, RuntimeError):
|
|
70
|
+
self._http = None
|
|
71
|
+
url = f"{self.base_url}{path}"
|
|
72
|
+
req = urllib.request.Request(
|
|
73
|
+
url, data=body.encode("utf-8"), headers=self._headers(), method="POST"
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
77
|
+
text = resp.read().decode("utf-8")
|
|
78
|
+
return json.loads(text) if text else {}
|
|
79
|
+
except urllib.error.HTTPError as e:
|
|
80
|
+
err_body = e.read().decode("utf-8", errors="replace")
|
|
81
|
+
raise RuntimeError(f"Fluctlight HTTP {e.code}: {err_body}") from e
|
|
82
|
+
|
|
83
|
+
def health(self) -> bool:
|
|
84
|
+
req = urllib.request.Request(f"{self.base_url}/health", method="GET")
|
|
85
|
+
try:
|
|
86
|
+
with urllib.request.urlopen(req, timeout=min(self.timeout, 5.0)) as resp:
|
|
87
|
+
return resp.status == 200
|
|
88
|
+
except Exception:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def status(self) -> dict[str, Any]:
|
|
92
|
+
return self._post("/api/v1/status")
|
|
93
|
+
|
|
94
|
+
def replica_status(self) -> dict[str, Any]:
|
|
95
|
+
return self._post("/api/v1/replica-status")
|
|
96
|
+
|
|
97
|
+
def experience(
|
|
98
|
+
self,
|
|
99
|
+
content: str,
|
|
100
|
+
context: str = "api",
|
|
101
|
+
salience: float = 0.5,
|
|
102
|
+
outcome: Optional[str] = None,
|
|
103
|
+
semantic_vector: Optional[list[float]] = None,
|
|
104
|
+
agent_id: Optional[str] = None,
|
|
105
|
+
doc_id: Optional[str] = None,
|
|
106
|
+
chunk_id: Optional[str] = None,
|
|
107
|
+
source_uri: Optional[str] = None,
|
|
108
|
+
**extra: Any,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
payload: dict[str, Any] = {
|
|
111
|
+
"content": content,
|
|
112
|
+
"context": context,
|
|
113
|
+
"salience": salience,
|
|
114
|
+
}
|
|
115
|
+
if outcome is not None:
|
|
116
|
+
payload["outcome"] = outcome
|
|
117
|
+
if semantic_vector is not None:
|
|
118
|
+
payload["semantic_vector"] = semantic_vector
|
|
119
|
+
if agent_id is not None:
|
|
120
|
+
payload["agent_id"] = agent_id
|
|
121
|
+
if doc_id is not None:
|
|
122
|
+
payload["doc_id"] = doc_id
|
|
123
|
+
if chunk_id is not None:
|
|
124
|
+
payload["chunk_id"] = chunk_id
|
|
125
|
+
if source_uri is not None:
|
|
126
|
+
payload["source_uri"] = source_uri
|
|
127
|
+
payload.update(extra)
|
|
128
|
+
return self._post("/api/v1/experience", payload)
|
|
129
|
+
|
|
130
|
+
def ingest_chunk(
|
|
131
|
+
self,
|
|
132
|
+
content: str,
|
|
133
|
+
doc_id: str,
|
|
134
|
+
chunk_id: str,
|
|
135
|
+
source_uri: Optional[str] = None,
|
|
136
|
+
semantic_vector: Optional[list[float]] = None,
|
|
137
|
+
salience: float = 0.55,
|
|
138
|
+
agent_id: Optional[str] = None,
|
|
139
|
+
outcome: Optional[str] = None,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
payload: dict[str, Any] = {
|
|
142
|
+
"content": content,
|
|
143
|
+
"doc_id": doc_id,
|
|
144
|
+
"chunk_id": chunk_id,
|
|
145
|
+
"salience": salience,
|
|
146
|
+
}
|
|
147
|
+
if source_uri:
|
|
148
|
+
payload["source_uri"] = source_uri
|
|
149
|
+
if semantic_vector is not None:
|
|
150
|
+
payload["semantic_vector"] = semantic_vector
|
|
151
|
+
if agent_id:
|
|
152
|
+
payload["agent_id"] = agent_id
|
|
153
|
+
if outcome:
|
|
154
|
+
payload["outcome"] = outcome
|
|
155
|
+
return self._post("/api/v1/ingest-chunk", payload)
|
|
156
|
+
|
|
157
|
+
def activate(
|
|
158
|
+
self,
|
|
159
|
+
cue: str,
|
|
160
|
+
semantic_vector: Optional[list[float]] = None,
|
|
161
|
+
agent_id: Optional[str] = None,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
payload: dict[str, Any] = {"cue": cue}
|
|
164
|
+
if semantic_vector is not None:
|
|
165
|
+
payload["semantic_vector"] = semantic_vector
|
|
166
|
+
if agent_id is not None:
|
|
167
|
+
payload["agent_id"] = agent_id
|
|
168
|
+
return self._post("/api/v1/activate", payload)
|
|
169
|
+
|
|
170
|
+
def activate_lite(
|
|
171
|
+
self,
|
|
172
|
+
cue: str,
|
|
173
|
+
semantic_vector: Optional[list[float]] = None,
|
|
174
|
+
agent_id: Optional[str] = None,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
"""Top-1 recall — minimal JSON (~200 B) for HTTP agent hot paths."""
|
|
177
|
+
payload: dict[str, Any] = {"cue": cue, "limit": 1}
|
|
178
|
+
if semantic_vector is not None:
|
|
179
|
+
payload["semantic_vector"] = semantic_vector
|
|
180
|
+
if agent_id is not None:
|
|
181
|
+
payload["agent_id"] = agent_id
|
|
182
|
+
return self._post("/api/v1/activate-lite", payload)
|
|
183
|
+
|
|
184
|
+
def activate_batch(
|
|
185
|
+
self,
|
|
186
|
+
items: list[dict[str, Any]],
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
return self._post("/api/v1/activate-batch", {"batch": items})
|
|
189
|
+
|
|
190
|
+
def query(self, op: dict[str, Any]) -> dict[str, Any]:
|
|
191
|
+
return self._post("/api/v1/query", {"query": op})
|
|
192
|
+
|
|
193
|
+
def search_by_rag(
|
|
194
|
+
self,
|
|
195
|
+
doc_id: str,
|
|
196
|
+
chunk_id: Optional[str] = None,
|
|
197
|
+
page: int = 0,
|
|
198
|
+
page_size: int = 50,
|
|
199
|
+
) -> dict[str, Any]:
|
|
200
|
+
q: dict[str, Any] = {
|
|
201
|
+
"op": "search_by_rag",
|
|
202
|
+
"doc_id": doc_id,
|
|
203
|
+
"page": page,
|
|
204
|
+
"page_size": page_size,
|
|
205
|
+
}
|
|
206
|
+
if chunk_id is not None:
|
|
207
|
+
q["chunk_id"] = chunk_id
|
|
208
|
+
return self.query(q)
|
|
209
|
+
|
|
210
|
+
def tick(self, n: int = 1) -> Any:
|
|
211
|
+
return self._post("/api/v1/tick", {"n": int(n)})
|
|
212
|
+
|
|
213
|
+
def compact(self) -> dict[str, Any]:
|
|
214
|
+
return self._post("/api/v1/compact")
|
|
215
|
+
|
|
216
|
+
def export_viz(self) -> dict[str, Any]:
|
|
217
|
+
return self._post("/api/v1/export-viz")
|
|
218
|
+
|
|
219
|
+
def export_graph(self, *, lite: bool = False) -> dict[str, Any]:
|
|
220
|
+
path = "/api/v1/export-graph-lite" if lite else "/api/v1/export-graph"
|
|
221
|
+
return self._post(path)
|
|
222
|
+
|
|
223
|
+
def export_raw(self) -> dict[str, Any]:
|
|
224
|
+
return self._post("/api/v1/export-raw")
|
|
225
|
+
|
|
226
|
+
def reward(self, magnitude: float = 0.5) -> dict[str, Any]:
|
|
227
|
+
return self._post("/api/v1/reward", {"magnitude": float(magnitude)})
|
|
228
|
+
|
|
229
|
+
def death(self, cause: str = "api") -> dict[str, Any]:
|
|
230
|
+
return self._post("/api/v1/death", {"cause": cause[:200]})
|
|
231
|
+
|
|
232
|
+
def mark_core(self, engram_id: str, key: str = "core") -> dict[str, Any]:
|
|
233
|
+
return self._post("/api/v1/mark-core", {"engram_id": str(engram_id), "key": key})
|
|
234
|
+
|
|
235
|
+
def verify_fact(
|
|
236
|
+
self,
|
|
237
|
+
engram_id: str,
|
|
238
|
+
*,
|
|
239
|
+
provenance_kind: str = "ledger_verified",
|
|
240
|
+
source_uri: Optional[str] = None,
|
|
241
|
+
confidence: float = 0.95,
|
|
242
|
+
) -> dict[str, Any]:
|
|
243
|
+
payload: dict[str, Any] = {
|
|
244
|
+
"engram_id": str(engram_id),
|
|
245
|
+
"provenance_kind": provenance_kind,
|
|
246
|
+
"confidence": confidence,
|
|
247
|
+
}
|
|
248
|
+
if source_uri:
|
|
249
|
+
payload["source_uri"] = source_uri
|
|
250
|
+
return self._post("/api/v1/verify-fact", payload)
|
|
251
|
+
|
|
252
|
+
def ground_wallet(
|
|
253
|
+
self,
|
|
254
|
+
balance: float,
|
|
255
|
+
level: int = 1,
|
|
256
|
+
wallet_path: Optional[str] = None,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
uri = wallet_path or "file://wallet.json"
|
|
259
|
+
content = (
|
|
260
|
+
f"ledger verified: agent wallet balance is ${balance:.2f} at level {level} "
|
|
261
|
+
f"(source: wallet.json — ground truth, not chat memory)"
|
|
262
|
+
)
|
|
263
|
+
rep = self.experience(
|
|
264
|
+
content[:500],
|
|
265
|
+
context="ledger:wallet",
|
|
266
|
+
salience=0.98,
|
|
267
|
+
doc_id="ledger",
|
|
268
|
+
chunk_id="wallet-balance",
|
|
269
|
+
source_uri=uri,
|
|
270
|
+
verified=True,
|
|
271
|
+
provenance_kind="ledger_verified",
|
|
272
|
+
confidence=0.99,
|
|
273
|
+
)
|
|
274
|
+
eid = rep.get("engram_id")
|
|
275
|
+
if eid and not rep.get("deduplicated"):
|
|
276
|
+
self.mark_core(str(eid), "ledger-wallet-balance")
|
|
277
|
+
return rep
|
|
278
|
+
|
|
279
|
+
def preplay(self, goal: str, steps: int = 4) -> dict[str, Any]:
|
|
280
|
+
return self._post("/api/v1/preplay", {"goal": goal, "steps": int(steps)})
|
|
281
|
+
|
|
282
|
+
def neurogenesis(self) -> dict[str, Any]:
|
|
283
|
+
return self._post("/api/v1/neurogenesis", {})
|
|
284
|
+
|
|
285
|
+
def verified_context(self, limit: int = 12) -> dict[str, Any]:
|
|
286
|
+
return self._post("/api/v1/verified-context", {"limit": int(limit)})
|
|
287
|
+
|
|
288
|
+
def stage_report(self) -> dict[str, Any]:
|
|
289
|
+
return self._post("/api/v1/stage-report", {})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Persistent in-process recall — native library (best) or worker subprocess."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional, Protocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _default_fluctlight_bin() -> str:
|
|
15
|
+
env = os.environ.get("FLUCTLIGHT_BIN")
|
|
16
|
+
if env:
|
|
17
|
+
return env
|
|
18
|
+
repo_root = Path(__file__).resolve().parents[3]
|
|
19
|
+
release = repo_root / "target" / "release" / "fluctlight"
|
|
20
|
+
if release.is_file():
|
|
21
|
+
return str(release)
|
|
22
|
+
debug = repo_root / "target" / "debug" / "fluctlight"
|
|
23
|
+
if debug.is_file():
|
|
24
|
+
return str(debug)
|
|
25
|
+
found = shutil.which("fluctlight")
|
|
26
|
+
return found if found else "fluctlight"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RecallClient(Protocol):
|
|
30
|
+
def activate(
|
|
31
|
+
self,
|
|
32
|
+
cue: str,
|
|
33
|
+
semantic_vector: Optional[list[float]] = None,
|
|
34
|
+
agent_id: Optional[str] = None,
|
|
35
|
+
limit: Optional[int] = None,
|
|
36
|
+
) -> dict[str, Any]: ...
|
|
37
|
+
|
|
38
|
+
def activate_batch(
|
|
39
|
+
self,
|
|
40
|
+
items: list[dict[str, Any]],
|
|
41
|
+
limit: Optional[int] = None,
|
|
42
|
+
) -> dict[str, Any]: ...
|
|
43
|
+
|
|
44
|
+
def status(self) -> dict[str, Any]: ...
|
|
45
|
+
|
|
46
|
+
def verified_context(self, limit: int = 12) -> dict[str, Any]: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FluctlightNative:
|
|
50
|
+
"""Direct Rust library call via PyO3 — same process as Python (like sqlite3)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, brain_path: str, readonly: bool = True) -> None:
|
|
53
|
+
import fluctlightdb_native as native # type: ignore
|
|
54
|
+
|
|
55
|
+
self._native = native
|
|
56
|
+
if readonly:
|
|
57
|
+
self._brain = native.Brain.open_readonly(brain_path)
|
|
58
|
+
else:
|
|
59
|
+
self._brain = native.Brain.open(brain_path)
|
|
60
|
+
self.brain_path = brain_path
|
|
61
|
+
|
|
62
|
+
def activate(
|
|
63
|
+
self,
|
|
64
|
+
cue: str,
|
|
65
|
+
semantic_vector: Optional[list[float]] = None,
|
|
66
|
+
agent_id: Optional[str] = None,
|
|
67
|
+
limit: Optional[int] = None,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
return self._brain.activate(cue, semantic_vector, agent_id, limit)
|
|
70
|
+
|
|
71
|
+
def activate_batch(
|
|
72
|
+
self,
|
|
73
|
+
items: list[dict[str, Any]],
|
|
74
|
+
limit: Optional[int] = None,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
return self._brain.activate_batch_json(json.dumps(items), limit)
|
|
77
|
+
|
|
78
|
+
def status(self) -> dict[str, Any]:
|
|
79
|
+
return self._brain.status()
|
|
80
|
+
|
|
81
|
+
def verified_context(self, limit: int = 12) -> dict[str, Any]:
|
|
82
|
+
return self._brain.verified_context(limit)
|
|
83
|
+
|
|
84
|
+
def has_sidecar_index(self) -> bool:
|
|
85
|
+
return bool(self._brain.has_sidecar_index())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FluctlightWorker:
|
|
89
|
+
"""Long-lived `fluctlight worker` subprocess — brain loaded once, sub-ms recall."""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
brain_path: str,
|
|
94
|
+
bin_path: Optional[str] = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
self.brain_path = brain_path
|
|
97
|
+
self.bin_path = bin_path or _default_fluctlight_bin()
|
|
98
|
+
self._lock = threading.Lock()
|
|
99
|
+
self._id = 0
|
|
100
|
+
self._proc: Optional[subprocess.Popen[str]] = None
|
|
101
|
+
self._start()
|
|
102
|
+
|
|
103
|
+
def _start(self) -> None:
|
|
104
|
+
if not os.path.isfile(self.bin_path):
|
|
105
|
+
raise FileNotFoundError(self.bin_path)
|
|
106
|
+
self._proc = subprocess.Popen(
|
|
107
|
+
[self.bin_path, "worker", "--path", self.brain_path],
|
|
108
|
+
stdin=subprocess.PIPE,
|
|
109
|
+
stdout=subprocess.PIPE,
|
|
110
|
+
stderr=subprocess.PIPE,
|
|
111
|
+
text=True,
|
|
112
|
+
bufsize=1,
|
|
113
|
+
)
|
|
114
|
+
if self._proc.stdin is None or self._proc.stdout is None:
|
|
115
|
+
raise RuntimeError("worker pipes unavailable")
|
|
116
|
+
|
|
117
|
+
def close(self) -> None:
|
|
118
|
+
with self._lock:
|
|
119
|
+
if self._proc and self._proc.poll() is None:
|
|
120
|
+
try:
|
|
121
|
+
self._call("shutdown")
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
self._proc.terminate()
|
|
125
|
+
self._proc = None
|
|
126
|
+
|
|
127
|
+
def _call(self, op: str, **kwargs: Any) -> dict[str, Any]:
|
|
128
|
+
with self._lock:
|
|
129
|
+
if self._proc is None or self._proc.poll() is not None:
|
|
130
|
+
self._start()
|
|
131
|
+
assert self._proc is not None
|
|
132
|
+
assert self._proc.stdin is not None
|
|
133
|
+
assert self._proc.stdout is not None
|
|
134
|
+
self._id += 1
|
|
135
|
+
req = {"op": op, "id": self._id, **kwargs}
|
|
136
|
+
self._proc.stdin.write(json.dumps(req) + "\n")
|
|
137
|
+
self._proc.stdin.flush()
|
|
138
|
+
line = self._proc.stdout.readline()
|
|
139
|
+
if not line:
|
|
140
|
+
err = (self._proc.stderr.read() if self._proc.stderr else "")[:300]
|
|
141
|
+
raise RuntimeError(f"worker closed: {err}")
|
|
142
|
+
resp = json.loads(line)
|
|
143
|
+
if not resp.get("ok"):
|
|
144
|
+
raise RuntimeError(resp.get("error") or "worker error")
|
|
145
|
+
return resp
|
|
146
|
+
|
|
147
|
+
def ping(self) -> bool:
|
|
148
|
+
return bool(self._call("ping").get("pong"))
|
|
149
|
+
|
|
150
|
+
def activate(
|
|
151
|
+
self,
|
|
152
|
+
cue: str,
|
|
153
|
+
semantic_vector: Optional[list[float]] = None,
|
|
154
|
+
agent_id: Optional[str] = None,
|
|
155
|
+
limit: Optional[int] = None,
|
|
156
|
+
) -> dict[str, Any]:
|
|
157
|
+
payload: dict[str, Any] = {"cue": cue}
|
|
158
|
+
if semantic_vector is not None:
|
|
159
|
+
payload["semantic_vector"] = semantic_vector
|
|
160
|
+
if agent_id is not None:
|
|
161
|
+
payload["agent_id"] = agent_id
|
|
162
|
+
if limit is not None:
|
|
163
|
+
payload["limit"] = limit
|
|
164
|
+
return self._call("activate", **payload)["result"]
|
|
165
|
+
|
|
166
|
+
def activate_batch(
|
|
167
|
+
self,
|
|
168
|
+
items: list[dict[str, Any]],
|
|
169
|
+
limit: Optional[int] = None,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
payload: dict[str, Any] = {"batch": items}
|
|
172
|
+
if limit is not None:
|
|
173
|
+
payload["limit"] = limit
|
|
174
|
+
resp = self._call("activate_batch", **payload)
|
|
175
|
+
return {"results": resp.get("results") or [], "count": resp.get("count", 0)}
|
|
176
|
+
|
|
177
|
+
def status(self) -> dict[str, Any]:
|
|
178
|
+
return self._call("status")["status"]
|
|
179
|
+
|
|
180
|
+
def verified_context(self, limit: int = 12) -> dict[str, Any]:
|
|
181
|
+
return self._call("verified_context", limit=limit)["context"]
|
|
182
|
+
|
|
183
|
+
def reload(self) -> dict[str, Any]:
|
|
184
|
+
return self._call("reload")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
_worker_singleton: Optional[FluctlightWorker] = None
|
|
188
|
+
_native_singleton: Optional[FluctlightNative] = None
|
|
189
|
+
_client_lock = threading.Lock()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_recall_client(
|
|
193
|
+
brain_path: Optional[str] = None,
|
|
194
|
+
bin_path: Optional[str] = None,
|
|
195
|
+
) -> RecallClient:
|
|
196
|
+
"""Best available in-process recall: native library > worker subprocess."""
|
|
197
|
+
global _native_singleton, _worker_singleton
|
|
198
|
+
path = brain_path or os.environ.get(
|
|
199
|
+
"FLUCTLIGHT_BRAIN",
|
|
200
|
+
os.environ.get(
|
|
201
|
+
"FLUCTLIGHT_BRAIN_PATH",
|
|
202
|
+
os.path.expanduser("~/.fluctlight/tenants/default/brain"),
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
prefer_native = os.environ.get("FLUCTLIGHT_NATIVE", "1").lower() not in (
|
|
206
|
+
"0",
|
|
207
|
+
"false",
|
|
208
|
+
"no",
|
|
209
|
+
)
|
|
210
|
+
with _client_lock:
|
|
211
|
+
if prefer_native and _native_singleton is None:
|
|
212
|
+
try:
|
|
213
|
+
_native_singleton = FluctlightNative(path, readonly=True)
|
|
214
|
+
return _native_singleton
|
|
215
|
+
except ImportError:
|
|
216
|
+
pass
|
|
217
|
+
except Exception:
|
|
218
|
+
_native_singleton = None
|
|
219
|
+
if _native_singleton is not None:
|
|
220
|
+
return _native_singleton
|
|
221
|
+
if _worker_singleton is None:
|
|
222
|
+
_worker_singleton = FluctlightWorker(path, bin_path=bin_path)
|
|
223
|
+
return _worker_singleton
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_worker(
|
|
227
|
+
brain_path: Optional[str] = None,
|
|
228
|
+
bin_path: Optional[str] = None,
|
|
229
|
+
) -> FluctlightWorker:
|
|
230
|
+
client = get_recall_client(brain_path=brain_path, bin_path=bin_path)
|
|
231
|
+
if isinstance(client, FluctlightWorker):
|
|
232
|
+
return client
|
|
233
|
+
raise TypeError("native client active — use get_recall_client() instead")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluctlightdb
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Python client for FluctlightDB — brain-native memory for AI agents
|
|
5
|
+
Author-email: Voxmastery <voxmastery@roppashreeganesh.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/voxmastery/FluctlightDB
|
|
8
|
+
Project-URL: Documentation, https://github.com/voxmastery/FluctlightDB/blob/main/docs/GETTING_STARTED.md
|
|
9
|
+
Project-URL: Repository, https://github.com/voxmastery/FluctlightDB
|
|
10
|
+
Project-URL: Issues, https://github.com/voxmastery/FluctlightDB/issues
|
|
11
|
+
Keywords: ai,agents,memory,database,llm,recall
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: native
|
|
26
|
+
Requires-Dist: fluctlightdb-native>=0.4.0; extra == "native"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# fluctlightdb
|
|
30
|
+
|
|
31
|
+
Python client for [FluctlightDB](https://github.com/voxmastery/FluctlightDB) — a brain-native memory store for AI agents.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install fluctlightdb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Optional in-process recall (Rust extension, when wheels are available for your platform):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install "fluctlightdb[native]"
|
|
43
|
+
# or: pip install fluctlightdb-native
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
No `cargo` or Rust toolchain required for the HTTP client.
|
|
47
|
+
|
|
48
|
+
## Quick start (HTTP — like `qdrant-client`)
|
|
49
|
+
|
|
50
|
+
Run a FluctlightDB server (download a [release binary](https://github.com/voxmastery/FluctlightDB/releases) or use your own deployment), then:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fluctlightdb import FluctlightClient
|
|
54
|
+
|
|
55
|
+
client = FluctlightClient(
|
|
56
|
+
base_url="http://127.0.0.1:8792",
|
|
57
|
+
api_key="your-key",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
client.experience("user prefers dark mode", context="settings")
|
|
61
|
+
print(client.activate_lite("theme preference"))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or use environment variables:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export FLUCTLIGHT_SERVE_URL=http://127.0.0.1:8792
|
|
68
|
+
export FLUCTLIGHT_API_KEY=your-key
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from fluctlightdb import FluctlightClient
|
|
73
|
+
|
|
74
|
+
client = FluctlightClient.from_env()
|
|
75
|
+
print(client.activate("dark mode"))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## In-process recall (optional)
|
|
79
|
+
|
|
80
|
+
When `fluctlightdb-native` is installed:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from fluctlightdb import get_recall_client
|
|
84
|
+
|
|
85
|
+
brain = get_recall_client("~/.fluctlight/tenants/default/brain")
|
|
86
|
+
print(brain.activate("dark mode"))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Docs
|
|
90
|
+
|
|
91
|
+
- [Getting started](https://github.com/voxmastery/FluctlightDB/blob/main/docs/GETTING_STARTED.md)
|
|
92
|
+
- [HTTP API (OpenAPI)](https://github.com/voxmastery/FluctlightDB/blob/main/docs/openapi.yaml)
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
fluctlightdb/__init__.py
|
|
5
|
+
fluctlightdb/worker.py
|
|
6
|
+
fluctlightdb.egg-info/PKG-INFO
|
|
7
|
+
fluctlightdb.egg-info/SOURCES.txt
|
|
8
|
+
fluctlightdb.egg-info/dependency_links.txt
|
|
9
|
+
fluctlightdb.egg-info/requires.txt
|
|
10
|
+
fluctlightdb.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fluctlightdb
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fluctlightdb"
|
|
7
|
+
version = "0.4.1"
|
|
8
|
+
description = "Python client for FluctlightDB — brain-native memory for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Voxmastery", email = "voxmastery@roppashreeganesh.com" }]
|
|
14
|
+
keywords = ["ai", "agents", "memory", "database", "llm", "recall"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Database",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/voxmastery/FluctlightDB"
|
|
30
|
+
Documentation = "https://github.com/voxmastery/FluctlightDB/blob/main/docs/GETTING_STARTED.md"
|
|
31
|
+
Repository = "https://github.com/voxmastery/FluctlightDB"
|
|
32
|
+
Issues = "https://github.com/voxmastery/FluctlightDB/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
native = ["fluctlightdb-native>=0.4.0"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["fluctlightdb*"]
|