lmem 0.2.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.
- lmem-0.2.1/.gitignore +15 -0
- lmem-0.2.1/LICENSE +21 -0
- lmem-0.2.1/PKG-INFO +104 -0
- lmem-0.2.1/README.md +77 -0
- lmem-0.2.1/pyproject.toml +62 -0
- lmem-0.2.1/src/lightmem/__init__.py +29 -0
- lmem-0.2.1/src/lightmem/py.typed +1 -0
- lmem-0.2.1/src/lmem/__init__.py +29 -0
- lmem-0.2.1/src/lmem/binary.py +116 -0
- lmem-0.2.1/src/lmem/cli.py +20 -0
- lmem-0.2.1/src/lmem/client.py +269 -0
- lmem-0.2.1/src/lmem/models.py +160 -0
- lmem-0.2.1/src/lmem/py.typed +1 -0
lmem-0.2.1/.gitignore
ADDED
lmem-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pathi Krishna Kanth
|
|
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.
|
lmem-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lmem
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Ultra-Fast Local AI Agent Persistent Memory Engine with Neural Extractive QA & Knowledge Graph
|
|
5
|
+
Project-URL: Homepage, https://github.com/krishnakanthpathi/lightmem
|
|
6
|
+
Project-URL: Repository, https://github.com/krishnakanthpathi/lightmem
|
|
7
|
+
Project-URL: Issues, https://github.com/krishnakanthpathi/lightmem/issues
|
|
8
|
+
Author-email: Pathi Krishna Kanth <krishnakanthpathi@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,extractive-qa,fastembed,knowledge-graph,llm,memory,onnx,rag,sqlite
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# LightMem (`lmem`) Python SDK
|
|
29
|
+
|
|
30
|
+
Ultra-Fast Local AI Agent Persistent Memory Engine with Neural Extractive QA & Knowledge Graph.
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/lmem/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ⚡ Quickstart
|
|
38
|
+
|
|
39
|
+
### Installation
|
|
40
|
+
```bash
|
|
41
|
+
pip install lmem
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Python SDK Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from lmem import LightMem
|
|
48
|
+
|
|
49
|
+
# Initialize vault (defaults to ~/.lightmem/memories.db or custom path)
|
|
50
|
+
lm = LightMem()
|
|
51
|
+
|
|
52
|
+
# 1. Store a memory (auto-infers 14 categories)
|
|
53
|
+
memory = lm.remember(
|
|
54
|
+
"PostgreSQL primary runs on port 5432 with WAL archiving enabled",
|
|
55
|
+
category="fact",
|
|
56
|
+
title="PostgreSQL Primary Config",
|
|
57
|
+
tags=["postgres", "database", "production"],
|
|
58
|
+
confidence=0.95,
|
|
59
|
+
)
|
|
60
|
+
print("Stored memory:", memory.id)
|
|
61
|
+
|
|
62
|
+
# 2. Hybrid Search (FTS5 BM25 + Vector Cosine RRF)
|
|
63
|
+
results = lm.recall("postgres port", limit=5)
|
|
64
|
+
for r in results:
|
|
65
|
+
print(f"[{r.score:.2f}] {r.memory.title}: {r.memory.content}")
|
|
66
|
+
|
|
67
|
+
# 3. Neural Extractive QA (exact fact extraction in ~6ms via ONNX SQuAD2)
|
|
68
|
+
answer = lm.answer("what port does postgresql use?")
|
|
69
|
+
print("Extracted answer:", answer.answer) # "5432"
|
|
70
|
+
print("Confidence:", answer.confidence)
|
|
71
|
+
print("Reranker:", answer.reranker_used)
|
|
72
|
+
|
|
73
|
+
# 4. Knowledge Graph Autolink (title mentions, shared tags, vector similarity)
|
|
74
|
+
link_stats = lm.autolink(min_similarity=0.75)
|
|
75
|
+
print("Connections created:", link_stats)
|
|
76
|
+
|
|
77
|
+
# 5. Multi-Hop Graph Traversal
|
|
78
|
+
related = lm.related(memory.id, hops=2)
|
|
79
|
+
for rel in related:
|
|
80
|
+
print(f"Hop {rel.distance}: {rel.memory.title} via {' -> '.join(rel.relation_path)}")
|
|
81
|
+
|
|
82
|
+
# 6. Deploy Skill to AI Agents (Antigravity, Codex, Hermes, Cursor, Claude)
|
|
83
|
+
lm.connect("antigravity")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 💻 CLI Commands (Included)
|
|
89
|
+
|
|
90
|
+
The `pip install lmem` package also installs the standalone CLI:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
lmem remember "Redis cache runs on port 6379" -t fact
|
|
94
|
+
lmem recall "redis port" -l 5
|
|
95
|
+
lmem answer "what is my pan card no"
|
|
96
|
+
lmem autolink
|
|
97
|
+
lmem graph
|
|
98
|
+
lmem connect all
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 📄 License
|
|
104
|
+
MIT License. Created by Pathi Krishna Kanth.
|
lmem-0.2.1/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# LightMem (`lmem`) Python SDK
|
|
2
|
+
|
|
3
|
+
Ultra-Fast Local AI Agent Persistent Memory Engine with Neural Extractive QA & Knowledge Graph.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/lmem/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⚡ Quickstart
|
|
11
|
+
|
|
12
|
+
### Installation
|
|
13
|
+
```bash
|
|
14
|
+
pip install lmem
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Python SDK Usage
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from lmem import LightMem
|
|
21
|
+
|
|
22
|
+
# Initialize vault (defaults to ~/.lightmem/memories.db or custom path)
|
|
23
|
+
lm = LightMem()
|
|
24
|
+
|
|
25
|
+
# 1. Store a memory (auto-infers 14 categories)
|
|
26
|
+
memory = lm.remember(
|
|
27
|
+
"PostgreSQL primary runs on port 5432 with WAL archiving enabled",
|
|
28
|
+
category="fact",
|
|
29
|
+
title="PostgreSQL Primary Config",
|
|
30
|
+
tags=["postgres", "database", "production"],
|
|
31
|
+
confidence=0.95,
|
|
32
|
+
)
|
|
33
|
+
print("Stored memory:", memory.id)
|
|
34
|
+
|
|
35
|
+
# 2. Hybrid Search (FTS5 BM25 + Vector Cosine RRF)
|
|
36
|
+
results = lm.recall("postgres port", limit=5)
|
|
37
|
+
for r in results:
|
|
38
|
+
print(f"[{r.score:.2f}] {r.memory.title}: {r.memory.content}")
|
|
39
|
+
|
|
40
|
+
# 3. Neural Extractive QA (exact fact extraction in ~6ms via ONNX SQuAD2)
|
|
41
|
+
answer = lm.answer("what port does postgresql use?")
|
|
42
|
+
print("Extracted answer:", answer.answer) # "5432"
|
|
43
|
+
print("Confidence:", answer.confidence)
|
|
44
|
+
print("Reranker:", answer.reranker_used)
|
|
45
|
+
|
|
46
|
+
# 4. Knowledge Graph Autolink (title mentions, shared tags, vector similarity)
|
|
47
|
+
link_stats = lm.autolink(min_similarity=0.75)
|
|
48
|
+
print("Connections created:", link_stats)
|
|
49
|
+
|
|
50
|
+
# 5. Multi-Hop Graph Traversal
|
|
51
|
+
related = lm.related(memory.id, hops=2)
|
|
52
|
+
for rel in related:
|
|
53
|
+
print(f"Hop {rel.distance}: {rel.memory.title} via {' -> '.join(rel.relation_path)}")
|
|
54
|
+
|
|
55
|
+
# 6. Deploy Skill to AI Agents (Antigravity, Codex, Hermes, Cursor, Claude)
|
|
56
|
+
lm.connect("antigravity")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 💻 CLI Commands (Included)
|
|
62
|
+
|
|
63
|
+
The `pip install lmem` package also installs the standalone CLI:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
lmem remember "Redis cache runs on port 6379" -t fact
|
|
67
|
+
lmem recall "redis port" -l 5
|
|
68
|
+
lmem answer "what is my pan card no"
|
|
69
|
+
lmem autolink
|
|
70
|
+
lmem graph
|
|
71
|
+
lmem connect all
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 📄 License
|
|
77
|
+
MIT License. Created by Pathi Krishna Kanth.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lmem"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Ultra-Fast Local AI Agent Persistent Memory Engine with Neural Extractive QA & Knowledge Graph"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Pathi Krishna Kanth", email = "krishnakanthpathi@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
keywords = [
|
|
16
|
+
"agent",
|
|
17
|
+
"memory",
|
|
18
|
+
"ai",
|
|
19
|
+
"llm",
|
|
20
|
+
"rag",
|
|
21
|
+
"knowledge-graph",
|
|
22
|
+
"extractive-qa",
|
|
23
|
+
"sqlite",
|
|
24
|
+
"onnx",
|
|
25
|
+
"fastembed",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"License :: OSI Approved :: MIT License",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.8",
|
|
33
|
+
"Programming Language :: Python :: 3.9",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Programming Language :: Python :: 3.14",
|
|
39
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
40
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
41
|
+
]
|
|
42
|
+
dependencies = []
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
lmem = "lmem.cli:main"
|
|
46
|
+
lightmem = "lmem.cli:main"
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/krishnakanthpathi/lightmem"
|
|
50
|
+
Repository = "https://github.com/krishnakanthpathi/lightmem"
|
|
51
|
+
Issues = "https://github.com/krishnakanthpathi/lightmem/issues"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/lmem", "src/lightmem"]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.sdist]
|
|
57
|
+
include = [
|
|
58
|
+
"/src",
|
|
59
|
+
"/README.md",
|
|
60
|
+
"/LICENSE",
|
|
61
|
+
"/pyproject.toml",
|
|
62
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LightMem alias package re-exporting lmem.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from lmem import (
|
|
6
|
+
LightMem,
|
|
7
|
+
Memory,
|
|
8
|
+
ScoredMemory,
|
|
9
|
+
AnswerResult,
|
|
10
|
+
RelatedMemory,
|
|
11
|
+
StorageStats,
|
|
12
|
+
PaginatedList,
|
|
13
|
+
MemoryInspection,
|
|
14
|
+
get_binary_path,
|
|
15
|
+
__version__,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"LightMem",
|
|
20
|
+
"Memory",
|
|
21
|
+
"ScoredMemory",
|
|
22
|
+
"AnswerResult",
|
|
23
|
+
"RelatedMemory",
|
|
24
|
+
"StorageStats",
|
|
25
|
+
"PaginatedList",
|
|
26
|
+
"MemoryInspection",
|
|
27
|
+
"get_binary_path",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LightMem (lmem): Ultra-Fast Local AI Agent Persistent Memory Engine
|
|
3
|
+
with Neural Extractive QA & Knowledge Graph.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import LightMem
|
|
7
|
+
from .models import (
|
|
8
|
+
Memory,
|
|
9
|
+
ScoredMemory,
|
|
10
|
+
AnswerResult,
|
|
11
|
+
RelatedMemory,
|
|
12
|
+
StorageStats,
|
|
13
|
+
PaginatedList,
|
|
14
|
+
MemoryInspection,
|
|
15
|
+
)
|
|
16
|
+
from .binary import get_binary_path
|
|
17
|
+
|
|
18
|
+
__version__ = "0.2.1"
|
|
19
|
+
__all__ = [
|
|
20
|
+
"LightMem",
|
|
21
|
+
"Memory",
|
|
22
|
+
"ScoredMemory",
|
|
23
|
+
"AnswerResult",
|
|
24
|
+
"RelatedMemory",
|
|
25
|
+
"StorageStats",
|
|
26
|
+
"PaginatedList",
|
|
27
|
+
"MemoryInspection",
|
|
28
|
+
"get_binary_path",
|
|
29
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
import platform
|
|
6
|
+
import subprocess
|
|
7
|
+
import urllib.request
|
|
8
|
+
import tarfile
|
|
9
|
+
import zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_binary_path() -> str:
|
|
15
|
+
"""
|
|
16
|
+
Locates the `lmem` executable.
|
|
17
|
+
Checks:
|
|
18
|
+
1. Environment variable `LMEM_BINARY_PATH` or `LIGHTMEM_BINARY_PATH`
|
|
19
|
+
2. PATH (`which lmem`)
|
|
20
|
+
3. User local directory `~/.local/bin/lmem` or `~/.cargo/bin/lmem`
|
|
21
|
+
4. Auto-downloads prebuilt binary to `~/.lightmem/bin/lmem` if missing.
|
|
22
|
+
"""
|
|
23
|
+
env_path = os.environ.get("LMEM_BINARY_PATH") or os.environ.get("LIGHTMEM_BINARY_PATH")
|
|
24
|
+
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
|
|
25
|
+
return env_path
|
|
26
|
+
|
|
27
|
+
which_path = shutil.which("lmem")
|
|
28
|
+
if which_path:
|
|
29
|
+
return which_path
|
|
30
|
+
|
|
31
|
+
home = Path.home()
|
|
32
|
+
candidate_paths = [
|
|
33
|
+
home / ".local" / "bin" / "lmem",
|
|
34
|
+
home / ".cargo" / "bin" / "lmem",
|
|
35
|
+
home / ".lightmem" / "bin" / "lmem",
|
|
36
|
+
home / ".lightmem" / "bin" / "lmem.exe",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
for p in candidate_paths:
|
|
40
|
+
if p.is_file() and os.access(str(p), os.X_OK):
|
|
41
|
+
return str(p)
|
|
42
|
+
|
|
43
|
+
# If not found, attempt to auto-download to ~/.lightmem/bin/lmem
|
|
44
|
+
try:
|
|
45
|
+
downloaded = ensure_binary_downloaded()
|
|
46
|
+
if downloaded and os.path.isfile(downloaded) and os.access(downloaded, os.X_OK):
|
|
47
|
+
return downloaded
|
|
48
|
+
except Exception as e:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
raise FileNotFoundError(
|
|
52
|
+
"Could not find the 'lmem' native binary on this system. "
|
|
53
|
+
"Install it via:\n"
|
|
54
|
+
" curl -fsSL https://raw.githubusercontent.com/krishnakanthpathi/lightmem/neural-reranker/install.sh | sh\n"
|
|
55
|
+
"Or set the LMEM_BINARY_PATH environment variable."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ensure_binary_downloaded(version: str = "v0.2.1") -> str:
|
|
60
|
+
"""Downloads pre-built release binary for current system into ~/.lightmem/bin/lmem."""
|
|
61
|
+
system = platform.system().lower()
|
|
62
|
+
machine = platform.machine().lower()
|
|
63
|
+
|
|
64
|
+
if system == "darwin":
|
|
65
|
+
target = "aarch64-apple-darwin" if machine in ["arm64", "aarch64"] else "x86_64-apple-darwin"
|
|
66
|
+
archive_name = f"lmem-{target}.tar.gz"
|
|
67
|
+
exe_name = "lmem"
|
|
68
|
+
elif system == "linux":
|
|
69
|
+
target = "aarch64-unknown-linux-gnu" if machine in ["arm64", "aarch64"] else "x86_64-unknown-linux-gnu"
|
|
70
|
+
archive_name = f"lmem-{target}.tar.gz"
|
|
71
|
+
exe_name = "lmem"
|
|
72
|
+
elif system == "windows":
|
|
73
|
+
target = "x86_64-pc-windows-msvc"
|
|
74
|
+
archive_name = f"lmem-{target}.zip"
|
|
75
|
+
exe_name = "lmem.exe"
|
|
76
|
+
else:
|
|
77
|
+
raise RuntimeError(f"Unsupported OS: {system}")
|
|
78
|
+
|
|
79
|
+
dest_dir = Path.home() / ".lightmem" / "bin"
|
|
80
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
dest_exe = dest_dir / exe_name
|
|
82
|
+
|
|
83
|
+
if dest_exe.is_file() and os.access(str(dest_exe), os.X_OK):
|
|
84
|
+
return str(dest_exe)
|
|
85
|
+
|
|
86
|
+
url = f"https://github.com/krishnakanthpathi/lightmem/releases/download/{version}/{archive_name}"
|
|
87
|
+
tmp_archive = dest_dir / archive_name
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
urllib.request.urlretrieve(url, tmp_archive)
|
|
91
|
+
if archive_name.endswith(".zip"):
|
|
92
|
+
with zipfile.ZipFile(tmp_archive, "r") as z:
|
|
93
|
+
z.extractall(dest_dir)
|
|
94
|
+
else:
|
|
95
|
+
with tarfile.open(tmp_archive, "r:gz") as t:
|
|
96
|
+
t.extractall(dest_dir)
|
|
97
|
+
|
|
98
|
+
tmp_archive.unlink(missing_ok=True)
|
|
99
|
+
dest_exe.chmod(0o755)
|
|
100
|
+
return str(dest_exe)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
tmp_archive.unlink(missing_ok=True)
|
|
103
|
+
raise RuntimeError(f"Failed to auto-download lmem binary from {url}: {e}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def execute_lmem(args: List[str], input_text: Optional[str] = None) -> Tuple[int, str, str]:
|
|
107
|
+
binary = get_binary_path()
|
|
108
|
+
proc = subprocess.Popen(
|
|
109
|
+
[binary] + args,
|
|
110
|
+
stdin=subprocess.PIPE if input_text is not None else None,
|
|
111
|
+
stdout=subprocess.PIPE,
|
|
112
|
+
stderr=subprocess.PIPE,
|
|
113
|
+
text=True,
|
|
114
|
+
)
|
|
115
|
+
stdout, stderr = proc.communicate(input=input_text)
|
|
116
|
+
return proc.returncode, stdout, stderr
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
from .binary import get_binary_path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
"""CLI entrypoint for lmem Python package."""
|
|
9
|
+
try:
|
|
10
|
+
binary = get_binary_path()
|
|
11
|
+
except FileNotFoundError as e:
|
|
12
|
+
sys.stderr.write(f"Error: {e}\n")
|
|
13
|
+
sys.exit(1)
|
|
14
|
+
|
|
15
|
+
result = subprocess.call([binary] + sys.argv[1:])
|
|
16
|
+
sys.exit(result)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
from typing import List, Optional, Dict, Any, Tuple
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .binary import execute_lmem, get_binary_path
|
|
7
|
+
from .models import (
|
|
8
|
+
Memory,
|
|
9
|
+
ScoredMemory,
|
|
10
|
+
AnswerResult,
|
|
11
|
+
RelatedMemory,
|
|
12
|
+
StorageStats,
|
|
13
|
+
PaginatedList,
|
|
14
|
+
MemoryInspection,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LightMem:
|
|
19
|
+
"""
|
|
20
|
+
LightMem: Ultra-Fast Local AI Agent Persistent Memory Engine.
|
|
21
|
+
Provides Pythonic methods for remember, recall, answer, graph, and maintenance.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
db_path: Optional[str | Path] = None,
|
|
27
|
+
global_db: bool = False,
|
|
28
|
+
):
|
|
29
|
+
self.db_path = str(db_path) if db_path else None
|
|
30
|
+
self.global_db = global_db
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def binary_path(self) -> str:
|
|
34
|
+
return get_binary_path()
|
|
35
|
+
|
|
36
|
+
def _build_base_args(self) -> List[str]:
|
|
37
|
+
args: List[str] = []
|
|
38
|
+
if self.global_db:
|
|
39
|
+
args.append("--global")
|
|
40
|
+
elif self.db_path:
|
|
41
|
+
args.extend(["--db", self.db_path])
|
|
42
|
+
return args
|
|
43
|
+
|
|
44
|
+
def _call(self, subcommand: str, extra_args: List[str], input_text: Optional[str] = None) -> Any:
|
|
45
|
+
full_args = self._build_base_args() + [subcommand] + extra_args + ["--json"]
|
|
46
|
+
code, stdout, stderr = execute_lmem(full_args, input_text=input_text)
|
|
47
|
+
if code != 0:
|
|
48
|
+
err = stderr.strip() or stdout.strip()
|
|
49
|
+
raise RuntimeError(f"lmem {subcommand} failed (exit code {code}): {err}")
|
|
50
|
+
try:
|
|
51
|
+
return json.loads(stdout)
|
|
52
|
+
except json.JSONDecodeError as e:
|
|
53
|
+
raise RuntimeError(f"Failed to parse JSON output from lmem {subcommand}: {stdout}") from e
|
|
54
|
+
|
|
55
|
+
# 1. Store
|
|
56
|
+
def remember(
|
|
57
|
+
self,
|
|
58
|
+
content: str,
|
|
59
|
+
category: Optional[str] = None,
|
|
60
|
+
title: Optional[str] = None,
|
|
61
|
+
tags: Optional[List[str]] = None,
|
|
62
|
+
confidence: float = 0.9,
|
|
63
|
+
ttl: Optional[str] = None,
|
|
64
|
+
supersede: bool = False,
|
|
65
|
+
) -> Memory:
|
|
66
|
+
args = [content]
|
|
67
|
+
if category:
|
|
68
|
+
args.extend(["-t", category])
|
|
69
|
+
if title:
|
|
70
|
+
args.extend(["--title", title])
|
|
71
|
+
if tags:
|
|
72
|
+
args.extend(["--tags", ",".join(tags)])
|
|
73
|
+
if confidence is not None:
|
|
74
|
+
args.extend(["--confidence", str(confidence)])
|
|
75
|
+
if ttl:
|
|
76
|
+
args.extend(["--ttl", ttl])
|
|
77
|
+
if supersede:
|
|
78
|
+
args.append("--supersede")
|
|
79
|
+
|
|
80
|
+
data = self._call("remember", args)
|
|
81
|
+
return Memory.from_dict(data)
|
|
82
|
+
|
|
83
|
+
# 2. Hybrid Recall
|
|
84
|
+
def recall(
|
|
85
|
+
self,
|
|
86
|
+
query: str,
|
|
87
|
+
limit: int = 5,
|
|
88
|
+
category: Optional[str] = None,
|
|
89
|
+
min_similarity: Optional[float] = None,
|
|
90
|
+
multi_hop: bool = False,
|
|
91
|
+
as_of: Optional[str] = None,
|
|
92
|
+
date: Optional[str] = None,
|
|
93
|
+
) -> List[ScoredMemory]:
|
|
94
|
+
args = [query, "-l", str(limit)]
|
|
95
|
+
if category:
|
|
96
|
+
args.extend(["-t", category])
|
|
97
|
+
if min_similarity is not None:
|
|
98
|
+
args.extend(["--min-similarity", str(min_similarity)])
|
|
99
|
+
if multi_hop:
|
|
100
|
+
args.append("--multi-hop")
|
|
101
|
+
if as_of:
|
|
102
|
+
args.extend(["--as-of", as_of])
|
|
103
|
+
if date:
|
|
104
|
+
args.extend(["--date", date])
|
|
105
|
+
|
|
106
|
+
data = self._call("recall", args)
|
|
107
|
+
if isinstance(data, list):
|
|
108
|
+
return [ScoredMemory.from_dict(item) for item in data]
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
# 3. Neural Extractive QA Answer
|
|
112
|
+
def answer(
|
|
113
|
+
self,
|
|
114
|
+
question: str,
|
|
115
|
+
category: Optional[str] = None,
|
|
116
|
+
reranker: Optional[str] = None,
|
|
117
|
+
limit: int = 10,
|
|
118
|
+
as_of: Optional[str] = None,
|
|
119
|
+
date: Optional[str] = None,
|
|
120
|
+
) -> AnswerResult:
|
|
121
|
+
args = [question, "-l", str(limit)]
|
|
122
|
+
if category:
|
|
123
|
+
args.extend(["-t", category])
|
|
124
|
+
if reranker:
|
|
125
|
+
args.extend(["-r", reranker])
|
|
126
|
+
if as_of:
|
|
127
|
+
args.extend(["--as-of", as_of])
|
|
128
|
+
if date:
|
|
129
|
+
args.extend(["--date", date])
|
|
130
|
+
|
|
131
|
+
data = self._call("answer", args)
|
|
132
|
+
return AnswerResult.from_dict(data)
|
|
133
|
+
|
|
134
|
+
# 4. List
|
|
135
|
+
def list(
|
|
136
|
+
self,
|
|
137
|
+
page: Optional[int] = None,
|
|
138
|
+
limit: int = 20,
|
|
139
|
+
offset: int = 0,
|
|
140
|
+
category: Optional[str] = None,
|
|
141
|
+
status: str = "active",
|
|
142
|
+
as_of: Optional[str] = None,
|
|
143
|
+
date: Optional[str] = None,
|
|
144
|
+
) -> PaginatedList:
|
|
145
|
+
args = ["-l", str(limit), "--status", status]
|
|
146
|
+
if page is not None:
|
|
147
|
+
args.extend(["--page", str(page)])
|
|
148
|
+
elif offset > 0:
|
|
149
|
+
args.extend(["--offset", str(offset)])
|
|
150
|
+
if category:
|
|
151
|
+
args.extend(["-t", category])
|
|
152
|
+
if as_of:
|
|
153
|
+
args.extend(["--as-of", as_of])
|
|
154
|
+
if date:
|
|
155
|
+
args.extend(["--date", date])
|
|
156
|
+
|
|
157
|
+
data = self._call("list", args)
|
|
158
|
+
items = [Memory.from_dict(item) for item in data.get("items", [])]
|
|
159
|
+
return PaginatedList(
|
|
160
|
+
items=items,
|
|
161
|
+
total=int(data.get("total", len(items))),
|
|
162
|
+
limit=int(data.get("limit", limit)),
|
|
163
|
+
offset=int(data.get("offset", offset)),
|
|
164
|
+
page=int(data.get("page", page or 1)),
|
|
165
|
+
total_pages=int(data.get("total_pages", 1)),
|
|
166
|
+
has_more=bool(data.get("has_more", False)),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# 5. Get Single Memory
|
|
170
|
+
def get(self, memory_id: str) -> Optional[Memory]:
|
|
171
|
+
try:
|
|
172
|
+
data = self._call("get", [memory_id])
|
|
173
|
+
return Memory.from_dict(data) if data else None
|
|
174
|
+
except RuntimeError:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
# 5b. Inspect Memory & Graph Links
|
|
178
|
+
def inspect(self, memory_id: str) -> Optional[MemoryInspection]:
|
|
179
|
+
try:
|
|
180
|
+
data = self._call("inspect", [memory_id])
|
|
181
|
+
return MemoryInspection.from_dict(data) if data else None
|
|
182
|
+
except RuntimeError:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
# 6. Forget (Soft or Hard)
|
|
186
|
+
def forget(self, memory_id: str, hard: bool = False) -> bool:
|
|
187
|
+
args = [memory_id]
|
|
188
|
+
if hard:
|
|
189
|
+
args.append("--hard")
|
|
190
|
+
data = self._call("forget", args)
|
|
191
|
+
return bool(data.get("success", False))
|
|
192
|
+
|
|
193
|
+
# 7. Deduplicate & Merge
|
|
194
|
+
def deduplicate(self) -> int:
|
|
195
|
+
data = self._call("dedup", [])
|
|
196
|
+
return int(data.get("merged", 0))
|
|
197
|
+
|
|
198
|
+
# 8. Nearest-Neighbor Conflicts
|
|
199
|
+
def conflicts(
|
|
200
|
+
self,
|
|
201
|
+
yes: bool = False,
|
|
202
|
+
min_similarity: float = 0.78,
|
|
203
|
+
reranker: Optional[str] = None,
|
|
204
|
+
) -> List[Dict[str, Any]]:
|
|
205
|
+
args = ["--min-similarity", str(min_similarity)]
|
|
206
|
+
if yes:
|
|
207
|
+
args.append("--yes")
|
|
208
|
+
if reranker:
|
|
209
|
+
args.extend(["-r", reranker])
|
|
210
|
+
data = self._call("conflicts", args)
|
|
211
|
+
return data if isinstance(data, list) else []
|
|
212
|
+
|
|
213
|
+
# 9. Knowledge Graph Links
|
|
214
|
+
def link(
|
|
215
|
+
self,
|
|
216
|
+
source: str,
|
|
217
|
+
target: str,
|
|
218
|
+
relation: str = "relates_to",
|
|
219
|
+
weight: float = 1.0,
|
|
220
|
+
) -> Tuple[str, str]:
|
|
221
|
+
args = [source, target, "-r", relation, "-w", str(weight)]
|
|
222
|
+
data = self._call("link", args)
|
|
223
|
+
return (data.get("source", source), data.get("target", target))
|
|
224
|
+
|
|
225
|
+
def unlink(self, source: str, target: str, relation: Optional[str] = None) -> bool:
|
|
226
|
+
args = [source, target]
|
|
227
|
+
if relation:
|
|
228
|
+
args.extend(["-r", relation])
|
|
229
|
+
data = self._call("unlink", args)
|
|
230
|
+
return bool(data.get("removed", False))
|
|
231
|
+
|
|
232
|
+
# 10. Multi-Hop Graph Traversal
|
|
233
|
+
def related(self, memory_id: str, hops: int = 2) -> List[RelatedMemory]:
|
|
234
|
+
data = self._call("related", [memory_id, "-n", str(hops)])
|
|
235
|
+
if isinstance(data, list):
|
|
236
|
+
return [RelatedMemory.from_dict(item) for item in data]
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
# 11. Graph Snapshot
|
|
240
|
+
def graph(self, focus: Optional[str] = None, hops: int = 2) -> Dict[str, Any]:
|
|
241
|
+
args = ["-n", str(hops)]
|
|
242
|
+
if focus:
|
|
243
|
+
args.extend(["-f", focus])
|
|
244
|
+
return self._call("graph", args)
|
|
245
|
+
|
|
246
|
+
# 12. Autolink Vault
|
|
247
|
+
def autolink(self, min_similarity: float = 0.75) -> Dict[str, Any]:
|
|
248
|
+
return self._call("autolink", ["--min-similarity", str(min_similarity)])
|
|
249
|
+
|
|
250
|
+
# 13. Connect AI Agent Skills
|
|
251
|
+
def connect(
|
|
252
|
+
self,
|
|
253
|
+
platform: Optional[str] = None,
|
|
254
|
+
workspace: bool = False,
|
|
255
|
+
path: Optional[str] = None,
|
|
256
|
+
) -> List[Dict[str, Any]]:
|
|
257
|
+
args = []
|
|
258
|
+
if platform:
|
|
259
|
+
args.append(platform)
|
|
260
|
+
if workspace:
|
|
261
|
+
args.append("--workspace")
|
|
262
|
+
if path:
|
|
263
|
+
args.extend(["--path", path])
|
|
264
|
+
return self._call("connect", args)
|
|
265
|
+
|
|
266
|
+
# 14. Vault Telemetry & Stats
|
|
267
|
+
def stats(self) -> StorageStats:
|
|
268
|
+
data = self._call("stats", [])
|
|
269
|
+
return StorageStats.from_dict(data)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List, Optional, Any, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Memory:
|
|
8
|
+
id: str
|
|
9
|
+
category: str
|
|
10
|
+
title: str
|
|
11
|
+
content: str
|
|
12
|
+
tags: List[str] = field(default_factory=list)
|
|
13
|
+
confidence: float = 0.9
|
|
14
|
+
status: str = "active"
|
|
15
|
+
provenance: str = "explicit_statement"
|
|
16
|
+
created_at: str = ""
|
|
17
|
+
updated_at: str = ""
|
|
18
|
+
expired_at: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_dict(cls, data: Dict[str, Any]) -> Memory:
|
|
22
|
+
mem_data = data.get("memory", data)
|
|
23
|
+
return cls(
|
|
24
|
+
id=mem_data.get("id", ""),
|
|
25
|
+
category=mem_data.get("category", "fact"),
|
|
26
|
+
title=mem_data.get("title", ""),
|
|
27
|
+
content=mem_data.get("content", ""),
|
|
28
|
+
tags=mem_data.get("tags", []),
|
|
29
|
+
confidence=float(mem_data.get("confidence", 0.9)),
|
|
30
|
+
status=mem_data.get("status", "active"),
|
|
31
|
+
provenance=mem_data.get("provenance", "explicit_statement"),
|
|
32
|
+
created_at=mem_data.get("created_at", ""),
|
|
33
|
+
updated_at=mem_data.get("updated_at", ""),
|
|
34
|
+
expired_at=mem_data.get("expired_at"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class MemoryInspection:
|
|
40
|
+
memory: Memory
|
|
41
|
+
links: List[Dict[str, Any]]
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data: Dict[str, Any]) -> MemoryInspection:
|
|
45
|
+
return cls(
|
|
46
|
+
memory=Memory.from_dict(data.get("memory", data)),
|
|
47
|
+
links=data.get("links", []),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ScoredMemory:
|
|
53
|
+
memory: Memory
|
|
54
|
+
score: float
|
|
55
|
+
vector_rank: Optional[int] = None
|
|
56
|
+
bm25_rank: Optional[int] = None
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: Dict[str, Any]) -> ScoredMemory:
|
|
60
|
+
mem_data = data.get("memory", data)
|
|
61
|
+
return cls(
|
|
62
|
+
memory=Memory.from_dict(mem_data),
|
|
63
|
+
score=float(data.get("score", 0.0)),
|
|
64
|
+
vector_rank=data.get("vector_rank"),
|
|
65
|
+
bm25_rank=data.get("bm25_rank"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class AnswerResult:
|
|
71
|
+
answer: str
|
|
72
|
+
confidence: float
|
|
73
|
+
reranker_used: str
|
|
74
|
+
selected_memory: Optional[Memory] = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def evidence(self) -> Optional[str]:
|
|
78
|
+
return self.selected_memory.content if self.selected_memory else None
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: Dict[str, Any]) -> AnswerResult:
|
|
82
|
+
selected = data.get("selected_memory")
|
|
83
|
+
return cls(
|
|
84
|
+
answer=data.get("answer", ""),
|
|
85
|
+
confidence=float(data.get("confidence", 0.0)),
|
|
86
|
+
reranker_used=data.get("reranker_used", ""),
|
|
87
|
+
selected_memory=Memory.from_dict(selected) if selected else None,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class RelatedMemory:
|
|
93
|
+
memory: Memory
|
|
94
|
+
distance: int
|
|
95
|
+
relation_path: List[str]
|
|
96
|
+
score: float
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, data: Dict[str, Any]) -> RelatedMemory:
|
|
100
|
+
mem_data = data.get("memory", {})
|
|
101
|
+
return cls(
|
|
102
|
+
memory=Memory.from_dict(mem_data),
|
|
103
|
+
distance=int(data.get("distance", 1)),
|
|
104
|
+
relation_path=data.get("relation_path", []),
|
|
105
|
+
score=float(data.get("score", 1.0)),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class StorageStats:
|
|
111
|
+
total_memories: int
|
|
112
|
+
active_memories: int
|
|
113
|
+
expired_memories: int
|
|
114
|
+
vector_count: int
|
|
115
|
+
categories: Dict[str, int]
|
|
116
|
+
storage_size_bytes: int
|
|
117
|
+
db_path: str
|
|
118
|
+
backend: Optional[str] = None
|
|
119
|
+
embedding_model: Optional[str] = None
|
|
120
|
+
reranker: Optional[str] = None
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, data: Dict[str, Any]) -> StorageStats:
|
|
124
|
+
return cls(
|
|
125
|
+
total_memories=int(data.get("total_memories", 0)),
|
|
126
|
+
active_memories=int(data.get("active_memories", 0)),
|
|
127
|
+
expired_memories=int(data.get("expired_memories", 0)),
|
|
128
|
+
vector_count=int(data.get("vector_count", 0)),
|
|
129
|
+
categories=data.get("categories", {}),
|
|
130
|
+
storage_size_bytes=int(data.get("storage_size_bytes", 0)),
|
|
131
|
+
db_path=data.get("db_path", ""),
|
|
132
|
+
backend=data.get("backend"),
|
|
133
|
+
embedding_model=data.get("embedding_model"),
|
|
134
|
+
reranker=data.get("reranker"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class PaginatedList(list):
|
|
139
|
+
"""A list of items that also carries pagination metadata (total, page, etc.)."""
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
items: List[Memory],
|
|
143
|
+
total: int = 0,
|
|
144
|
+
limit: int = 20,
|
|
145
|
+
offset: int = 0,
|
|
146
|
+
page: int = 1,
|
|
147
|
+
total_pages: int = 1,
|
|
148
|
+
has_more: bool = False,
|
|
149
|
+
):
|
|
150
|
+
super().__init__(items)
|
|
151
|
+
self.total = total
|
|
152
|
+
self.limit = limit
|
|
153
|
+
self.offset = offset
|
|
154
|
+
self.page = page
|
|
155
|
+
self.total_pages = total_pages
|
|
156
|
+
self.has_more = has_more
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def items(self) -> List[Memory]:
|
|
160
|
+
return list(self)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561
|