memorybrain 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- memorybrain-1.0.0/Changelog.txt +14 -0
- memorybrain-1.0.0/LICENSE.txt +21 -0
- memorybrain-1.0.0/MANIFEST.in +5 -0
- memorybrain-1.0.0/PKG-INFO +163 -0
- memorybrain-1.0.0/README.txt +101 -0
- memorybrain-1.0.0/memorybrain/__init__.py +20 -0
- memorybrain-1.0.0/memorybrain/brain.py +166 -0
- memorybrain-1.0.0/memorybrain/ranking.py +88 -0
- memorybrain-1.0.0/memorybrain/search.py +103 -0
- memorybrain-1.0.0/memorybrain/storage.py +255 -0
- memorybrain-1.0.0/memorybrain.egg-info/PKG-INFO +163 -0
- memorybrain-1.0.0/memorybrain.egg-info/SOURCES.txt +16 -0
- memorybrain-1.0.0/memorybrain.egg-info/dependency_links.txt +1 -0
- memorybrain-1.0.0/memorybrain.egg-info/top_level.txt +1 -0
- memorybrain-1.0.0/requirements.txt +8 -0
- memorybrain-1.0.0/setup.cfg +4 -0
- memorybrain-1.0.0/setup.py +44 -0
- memorybrain-1.0.0/tests/test_memorybrain.py +57 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Changelog
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
All notable changes to memorybrain are documented in this file.
|
|
5
|
+
|
|
6
|
+
[1.0.0] - 2026-06-03
|
|
7
|
+
--------------------
|
|
8
|
+
- Initial PyPI release
|
|
9
|
+
- MemoryBrain class with remember, recall, search, forget
|
|
10
|
+
- SQLite storage backend (automatic local database)
|
|
11
|
+
- Memory ranking by relevance, importance, and recency
|
|
12
|
+
- Export and import memories as JSON
|
|
13
|
+
- Statistics: total memories, most common tags, database size
|
|
14
|
+
- Python 3.10+ support, zero runtime dependencies
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MemoryBrain Contributors
|
|
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,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memorybrain
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Long-term memory library for AI assistants and agents
|
|
5
|
+
Home-page: https://github.com/memorybrain/memorybrain
|
|
6
|
+
Author: MemoryBrain Contributors
|
|
7
|
+
Author-email: memorybrain@example.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Source, https://github.com/memorybrain/memorybrain
|
|
10
|
+
Project-URL: Tracker, https://github.com/memorybrain/memorybrain/issues
|
|
11
|
+
Keywords: ai memory agents llm chatbot sqlite
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/plain
|
|
26
|
+
License-File: LICENSE.txt
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: keywords
|
|
34
|
+
Dynamic: license
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: project-url
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+
MemoryBrain
|
|
41
|
+
===========
|
|
42
|
+
|
|
43
|
+
Long-term memory library for AI assistants, chatbots, and autonomous agents.
|
|
44
|
+
Stores memories in a local SQLite database with intelligent ranked retrieval.
|
|
45
|
+
|
|
46
|
+
Requirements
|
|
47
|
+
------------
|
|
48
|
+
- Python 3.10 or newer
|
|
49
|
+
- No third-party runtime dependencies
|
|
50
|
+
|
|
51
|
+
Installation
|
|
52
|
+
------------
|
|
53
|
+
pip install memorybrain
|
|
54
|
+
|
|
55
|
+
Or install from source:
|
|
56
|
+
|
|
57
|
+
pip install .
|
|
58
|
+
|
|
59
|
+
Quick Start
|
|
60
|
+
-----------
|
|
61
|
+
from memorybrain import MemoryBrain
|
|
62
|
+
|
|
63
|
+
brain = MemoryBrain()
|
|
64
|
+
|
|
65
|
+
brain.remember(
|
|
66
|
+
"User likes Flutter development",
|
|
67
|
+
tags=["flutter", "programming"],
|
|
68
|
+
importance=10,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
results = brain.recall("What framework does the user like?")
|
|
72
|
+
for memory in results:
|
|
73
|
+
print(memory.text)
|
|
74
|
+
|
|
75
|
+
matches = brain.search("Flutter")
|
|
76
|
+
print(matches)
|
|
77
|
+
|
|
78
|
+
print(brain.stats())
|
|
79
|
+
|
|
80
|
+
API Overview
|
|
81
|
+
------------
|
|
82
|
+
remember(text, tags=None, importance=5)
|
|
83
|
+
Store a new memory. Returns a Memory object.
|
|
84
|
+
|
|
85
|
+
recall(query, limit=10)
|
|
86
|
+
Retrieve memories ranked for a natural-language query.
|
|
87
|
+
|
|
88
|
+
search(query, limit=10)
|
|
89
|
+
Search memories by keyword relevance.
|
|
90
|
+
|
|
91
|
+
forget(memory_id)
|
|
92
|
+
Delete a memory by ID. Returns True if deleted.
|
|
93
|
+
|
|
94
|
+
export("memories.json")
|
|
95
|
+
Export all memories to a JSON file.
|
|
96
|
+
|
|
97
|
+
import_file("memories.json")
|
|
98
|
+
Import memories from JSON (upsert by ID).
|
|
99
|
+
|
|
100
|
+
stats()
|
|
101
|
+
Return statistics:
|
|
102
|
+
- total_memories
|
|
103
|
+
- most_common_tags (list of {tag, count})
|
|
104
|
+
- database_size_bytes
|
|
105
|
+
|
|
106
|
+
Custom Database Path
|
|
107
|
+
--------------------
|
|
108
|
+
brain = MemoryBrain(db_path="./data/my_memories.db")
|
|
109
|
+
|
|
110
|
+
Default location: ~/.memorybrain/memories.db
|
|
111
|
+
|
|
112
|
+
Build and Publish to PyPI
|
|
113
|
+
-------------------------
|
|
114
|
+
Install build tools:
|
|
115
|
+
|
|
116
|
+
pip install wheel twine build
|
|
117
|
+
|
|
118
|
+
Build source distribution and wheel:
|
|
119
|
+
|
|
120
|
+
python setup.py sdist bdist_wheel
|
|
121
|
+
|
|
122
|
+
Or using the modern build frontend:
|
|
123
|
+
|
|
124
|
+
python -m build
|
|
125
|
+
|
|
126
|
+
Validate packages:
|
|
127
|
+
|
|
128
|
+
twine check dist/*
|
|
129
|
+
|
|
130
|
+
Upload to PyPI (requires PyPI account and API token):
|
|
131
|
+
|
|
132
|
+
twine upload dist/*
|
|
133
|
+
|
|
134
|
+
Test upload to TestPyPI first:
|
|
135
|
+
|
|
136
|
+
twine upload --repository testpypi dist/*
|
|
137
|
+
|
|
138
|
+
License
|
|
139
|
+
-------
|
|
140
|
+
MIT License — see LICENSE.txt
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
MIT License
|
|
144
|
+
|
|
145
|
+
Copyright (c) 2026 MemoryBrain Contributors
|
|
146
|
+
|
|
147
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
148
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
149
|
+
in the Software without restriction, including without limitation the rights
|
|
150
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
151
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
152
|
+
furnished to do so, subject to the following conditions:
|
|
153
|
+
|
|
154
|
+
The above copyright notice and this permission notice shall be included in all
|
|
155
|
+
copies or substantial portions of the Software.
|
|
156
|
+
|
|
157
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
158
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
159
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
160
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
161
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
162
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
163
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
MemoryBrain
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
Long-term memory library for AI assistants, chatbots, and autonomous agents.
|
|
5
|
+
Stores memories in a local SQLite database with intelligent ranked retrieval.
|
|
6
|
+
|
|
7
|
+
Requirements
|
|
8
|
+
------------
|
|
9
|
+
- Python 3.10 or newer
|
|
10
|
+
- No third-party runtime dependencies
|
|
11
|
+
|
|
12
|
+
Installation
|
|
13
|
+
------------
|
|
14
|
+
pip install memorybrain
|
|
15
|
+
|
|
16
|
+
Or install from source:
|
|
17
|
+
|
|
18
|
+
pip install .
|
|
19
|
+
|
|
20
|
+
Quick Start
|
|
21
|
+
-----------
|
|
22
|
+
from memorybrain import MemoryBrain
|
|
23
|
+
|
|
24
|
+
brain = MemoryBrain()
|
|
25
|
+
|
|
26
|
+
brain.remember(
|
|
27
|
+
"User likes Flutter development",
|
|
28
|
+
tags=["flutter", "programming"],
|
|
29
|
+
importance=10,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
results = brain.recall("What framework does the user like?")
|
|
33
|
+
for memory in results:
|
|
34
|
+
print(memory.text)
|
|
35
|
+
|
|
36
|
+
matches = brain.search("Flutter")
|
|
37
|
+
print(matches)
|
|
38
|
+
|
|
39
|
+
print(brain.stats())
|
|
40
|
+
|
|
41
|
+
API Overview
|
|
42
|
+
------------
|
|
43
|
+
remember(text, tags=None, importance=5)
|
|
44
|
+
Store a new memory. Returns a Memory object.
|
|
45
|
+
|
|
46
|
+
recall(query, limit=10)
|
|
47
|
+
Retrieve memories ranked for a natural-language query.
|
|
48
|
+
|
|
49
|
+
search(query, limit=10)
|
|
50
|
+
Search memories by keyword relevance.
|
|
51
|
+
|
|
52
|
+
forget(memory_id)
|
|
53
|
+
Delete a memory by ID. Returns True if deleted.
|
|
54
|
+
|
|
55
|
+
export("memories.json")
|
|
56
|
+
Export all memories to a JSON file.
|
|
57
|
+
|
|
58
|
+
import_file("memories.json")
|
|
59
|
+
Import memories from JSON (upsert by ID).
|
|
60
|
+
|
|
61
|
+
stats()
|
|
62
|
+
Return statistics:
|
|
63
|
+
- total_memories
|
|
64
|
+
- most_common_tags (list of {tag, count})
|
|
65
|
+
- database_size_bytes
|
|
66
|
+
|
|
67
|
+
Custom Database Path
|
|
68
|
+
--------------------
|
|
69
|
+
brain = MemoryBrain(db_path="./data/my_memories.db")
|
|
70
|
+
|
|
71
|
+
Default location: ~/.memorybrain/memories.db
|
|
72
|
+
|
|
73
|
+
Build and Publish to PyPI
|
|
74
|
+
-------------------------
|
|
75
|
+
Install build tools:
|
|
76
|
+
|
|
77
|
+
pip install wheel twine build
|
|
78
|
+
|
|
79
|
+
Build source distribution and wheel:
|
|
80
|
+
|
|
81
|
+
python setup.py sdist bdist_wheel
|
|
82
|
+
|
|
83
|
+
Or using the modern build frontend:
|
|
84
|
+
|
|
85
|
+
python -m build
|
|
86
|
+
|
|
87
|
+
Validate packages:
|
|
88
|
+
|
|
89
|
+
twine check dist/*
|
|
90
|
+
|
|
91
|
+
Upload to PyPI (requires PyPI account and API token):
|
|
92
|
+
|
|
93
|
+
twine upload dist/*
|
|
94
|
+
|
|
95
|
+
Test upload to TestPyPI first:
|
|
96
|
+
|
|
97
|
+
twine upload --repository testpypi dist/*
|
|
98
|
+
|
|
99
|
+
License
|
|
100
|
+
-------
|
|
101
|
+
MIT License — see LICENSE.txt
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memorybrain — long-term memory for AI assistants and agents.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
from memorybrain import MemoryBrain
|
|
7
|
+
|
|
8
|
+
brain = MemoryBrain()
|
|
9
|
+
brain.remember(
|
|
10
|
+
"User likes Flutter development",
|
|
11
|
+
tags=["flutter", "programming"],
|
|
12
|
+
importance=10,
|
|
13
|
+
)
|
|
14
|
+
results = brain.recall("What framework does the user like?")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from memorybrain.brain import MemoryBrain
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
|
20
|
+
__all__ = ["MemoryBrain", "__version__"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""MemoryBrain — main public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from memorybrain.ranking import RankWeights, rank_memories
|
|
11
|
+
from memorybrain.search import compute_relevance
|
|
12
|
+
from memorybrain.storage import Memory, SQLiteStorage, utc_now
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MemoryBrain:
|
|
16
|
+
"""
|
|
17
|
+
Long-term memory for AI assistants and agents.
|
|
18
|
+
|
|
19
|
+
Stores memories in a local SQLite database with ranked recall and search.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
db_path: str | Path | None = None,
|
|
25
|
+
rank_weights: RankWeights | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._storage = SQLiteStorage(db_path)
|
|
28
|
+
self._rank_weights = rank_weights
|
|
29
|
+
self._lock = threading.RLock()
|
|
30
|
+
|
|
31
|
+
def remember(
|
|
32
|
+
self,
|
|
33
|
+
text: str,
|
|
34
|
+
*,
|
|
35
|
+
tags: list[str] | None = None,
|
|
36
|
+
importance: float = 5.0,
|
|
37
|
+
metadata: dict[str, Any] | None = None,
|
|
38
|
+
) -> Memory:
|
|
39
|
+
"""
|
|
40
|
+
Store a new memory.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
text: Memory content.
|
|
44
|
+
tags: Optional list of tags.
|
|
45
|
+
importance: Priority from 0 to 10 (default 5).
|
|
46
|
+
metadata: Optional extra key-value data.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The stored Memory instance.
|
|
50
|
+
"""
|
|
51
|
+
with self._lock:
|
|
52
|
+
memory = Memory(
|
|
53
|
+
text=text,
|
|
54
|
+
tags=tags or [],
|
|
55
|
+
importance=importance,
|
|
56
|
+
metadata=metadata or {},
|
|
57
|
+
)
|
|
58
|
+
return self._storage.add(memory)
|
|
59
|
+
|
|
60
|
+
def recall(self, query: str, limit: int = 10) -> list[Memory]:
|
|
61
|
+
"""
|
|
62
|
+
Retrieve memories ranked for a natural-language query.
|
|
63
|
+
|
|
64
|
+
Uses relevance, importance, and recency scoring.
|
|
65
|
+
"""
|
|
66
|
+
return self._retrieve(query, limit=limit, min_relevance=0.0)
|
|
67
|
+
|
|
68
|
+
def search(self, query: str, limit: int = 10) -> list[Memory]:
|
|
69
|
+
"""
|
|
70
|
+
Search memories by keyword relevance.
|
|
71
|
+
|
|
72
|
+
Slightly stricter pre-filter than recall (any token match required when
|
|
73
|
+
query has tokens).
|
|
74
|
+
"""
|
|
75
|
+
tokens = query.strip()
|
|
76
|
+
min_rel = 0.01 if tokens else 0.0
|
|
77
|
+
return self._retrieve(query, limit=limit, min_relevance=min_rel)
|
|
78
|
+
|
|
79
|
+
def _retrieve(self, query: str, limit: int, min_relevance: float) -> list[Memory]:
|
|
80
|
+
with self._lock:
|
|
81
|
+
candidates = self._storage.list_all()
|
|
82
|
+
if not candidates:
|
|
83
|
+
return []
|
|
84
|
+
ranked = rank_memories(
|
|
85
|
+
query,
|
|
86
|
+
candidates,
|
|
87
|
+
weights=self._rank_weights,
|
|
88
|
+
min_relevance=min_relevance,
|
|
89
|
+
)
|
|
90
|
+
if not ranked and query.strip():
|
|
91
|
+
ranked = rank_memories(
|
|
92
|
+
query,
|
|
93
|
+
candidates,
|
|
94
|
+
weights=self._rank_weights,
|
|
95
|
+
min_relevance=0.0,
|
|
96
|
+
)
|
|
97
|
+
top = ranked[:limit]
|
|
98
|
+
now = utc_now()
|
|
99
|
+
for memory, _ in top:
|
|
100
|
+
memory.access_count += 1
|
|
101
|
+
memory.metadata["last_accessed"] = now.isoformat()
|
|
102
|
+
self._storage.update(memory)
|
|
103
|
+
return [m for m, _ in top]
|
|
104
|
+
|
|
105
|
+
def forget(self, memory_id: str) -> bool:
|
|
106
|
+
"""Delete a memory by ID."""
|
|
107
|
+
with self._lock:
|
|
108
|
+
return self._storage.delete(memory_id)
|
|
109
|
+
|
|
110
|
+
def get(self, memory_id: str) -> Memory | None:
|
|
111
|
+
"""Fetch a single memory by ID."""
|
|
112
|
+
with self._lock:
|
|
113
|
+
return self._storage.get(memory_id)
|
|
114
|
+
|
|
115
|
+
def export(self, path: str | Path) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Export all memories to a JSON file.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Number of memories exported.
|
|
121
|
+
"""
|
|
122
|
+
with self._lock:
|
|
123
|
+
records = self._storage.export_records()
|
|
124
|
+
path = Path(path)
|
|
125
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
path.write_text(json.dumps(records, indent=2), encoding="utf-8")
|
|
127
|
+
return len(records)
|
|
128
|
+
|
|
129
|
+
def import_file(self, path: str | Path) -> int:
|
|
130
|
+
"""
|
|
131
|
+
Import memories from a JSON file (upsert by ID).
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Number of memories imported.
|
|
135
|
+
"""
|
|
136
|
+
with self._lock:
|
|
137
|
+
path = Path(path)
|
|
138
|
+
records = json.loads(path.read_text(encoding="utf-8"))
|
|
139
|
+
if not isinstance(records, list):
|
|
140
|
+
raise ValueError("Import file must contain a JSON array of memories")
|
|
141
|
+
return self._storage.import_records(records)
|
|
142
|
+
|
|
143
|
+
def stats(self) -> dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
Return memory statistics.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict with total_memories, most_common_tags, database_size_bytes
|
|
149
|
+
"""
|
|
150
|
+
with self._lock:
|
|
151
|
+
total = self._storage.count()
|
|
152
|
+
tag_counts = self._storage.tag_counts()
|
|
153
|
+
sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0]))
|
|
154
|
+
most_common = [
|
|
155
|
+
{"tag": tag, "count": count} for tag, count in sorted_tags[:10]
|
|
156
|
+
]
|
|
157
|
+
return {
|
|
158
|
+
"total_memories": total,
|
|
159
|
+
"most_common_tags": most_common,
|
|
160
|
+
"database_size_bytes": self._storage.db_file_size_bytes(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def db_path(self) -> Path:
|
|
165
|
+
"""Path to the SQLite database file."""
|
|
166
|
+
return self._storage.db_path
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Memory ranking by relevance, importance, and recency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
from memorybrain.search import compute_relevance
|
|
10
|
+
from memorybrain.storage import Memory, utc_now
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class RankWeights:
|
|
15
|
+
"""Weights for composite memory ranking."""
|
|
16
|
+
|
|
17
|
+
relevance: float = 0.5
|
|
18
|
+
importance: float = 0.3
|
|
19
|
+
recency: float = 0.2
|
|
20
|
+
|
|
21
|
+
def normalized(self) -> RankWeights:
|
|
22
|
+
total = self.relevance + self.importance + self.recency
|
|
23
|
+
if total <= 0:
|
|
24
|
+
return RankWeights(1 / 3, 1 / 3, 1 / 3)
|
|
25
|
+
return RankWeights(
|
|
26
|
+
relevance=self.relevance / total,
|
|
27
|
+
importance=self.importance / total,
|
|
28
|
+
recency=self.recency / total,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def recency_score(memory: Memory, now: datetime, half_life_days: float = 30.0) -> float:
|
|
33
|
+
"""Exponential decay score based on memory age."""
|
|
34
|
+
ts = memory.timestamp
|
|
35
|
+
if ts.tzinfo is None:
|
|
36
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
37
|
+
if now.tzinfo is None:
|
|
38
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
39
|
+
age_days = max(0.0, (now - ts).total_seconds() / 86400.0)
|
|
40
|
+
return math.exp(-age_days / half_life_days)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def importance_score(memory: Memory) -> float:
|
|
44
|
+
"""Normalize importance to [0, 1]."""
|
|
45
|
+
return memory.importance / 10.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def score_memory(
|
|
49
|
+
memory: Memory,
|
|
50
|
+
relevance: float,
|
|
51
|
+
now: datetime | None = None,
|
|
52
|
+
weights: RankWeights | None = None,
|
|
53
|
+
) -> float:
|
|
54
|
+
"""Compute weighted composite score for a memory."""
|
|
55
|
+
w = (weights or RankWeights()).normalized()
|
|
56
|
+
now = now or utc_now()
|
|
57
|
+
rec = recency_score(memory, now)
|
|
58
|
+
imp = importance_score(memory)
|
|
59
|
+
return w.relevance * relevance + w.importance * imp + w.recency * rec
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def rank_memories(
|
|
63
|
+
query: str,
|
|
64
|
+
memories: list[Memory],
|
|
65
|
+
weights: RankWeights | None = None,
|
|
66
|
+
min_relevance: float = 0.0,
|
|
67
|
+
) -> list[tuple[Memory, float]]:
|
|
68
|
+
"""
|
|
69
|
+
Rank memories by composite score.
|
|
70
|
+
|
|
71
|
+
Returns list of (memory, score) tuples sorted descending by score.
|
|
72
|
+
"""
|
|
73
|
+
now = utc_now()
|
|
74
|
+
scored: list[tuple[Memory, float]] = []
|
|
75
|
+
for memory in memories:
|
|
76
|
+
rel = compute_relevance(query, memory)
|
|
77
|
+
if rel < min_relevance:
|
|
78
|
+
continue
|
|
79
|
+
total = score_memory(memory, rel, now=now, weights=weights)
|
|
80
|
+
scored.append((memory, total))
|
|
81
|
+
scored.sort(
|
|
82
|
+
key=lambda item: (
|
|
83
|
+
-item[1],
|
|
84
|
+
-item[0].importance,
|
|
85
|
+
-item[0].timestamp.timestamp(),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return scored
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Keyword search and relevance scoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from memorybrain.storage import Memory
|
|
9
|
+
|
|
10
|
+
_TOKEN_RE = re.compile(r"[a-z0-9]+", re.IGNORECASE)
|
|
11
|
+
_STOPWORDS = frozenset(
|
|
12
|
+
{
|
|
13
|
+
"a",
|
|
14
|
+
"an",
|
|
15
|
+
"the",
|
|
16
|
+
"is",
|
|
17
|
+
"are",
|
|
18
|
+
"was",
|
|
19
|
+
"were",
|
|
20
|
+
"be",
|
|
21
|
+
"been",
|
|
22
|
+
"being",
|
|
23
|
+
"have",
|
|
24
|
+
"has",
|
|
25
|
+
"had",
|
|
26
|
+
"do",
|
|
27
|
+
"does",
|
|
28
|
+
"did",
|
|
29
|
+
"will",
|
|
30
|
+
"would",
|
|
31
|
+
"could",
|
|
32
|
+
"should",
|
|
33
|
+
"may",
|
|
34
|
+
"might",
|
|
35
|
+
"must",
|
|
36
|
+
"can",
|
|
37
|
+
"to",
|
|
38
|
+
"of",
|
|
39
|
+
"in",
|
|
40
|
+
"for",
|
|
41
|
+
"on",
|
|
42
|
+
"with",
|
|
43
|
+
"at",
|
|
44
|
+
"by",
|
|
45
|
+
"from",
|
|
46
|
+
"as",
|
|
47
|
+
"what",
|
|
48
|
+
"which",
|
|
49
|
+
"who",
|
|
50
|
+
"whom",
|
|
51
|
+
"this",
|
|
52
|
+
"that",
|
|
53
|
+
"these",
|
|
54
|
+
"those",
|
|
55
|
+
"and",
|
|
56
|
+
"but",
|
|
57
|
+
"or",
|
|
58
|
+
"if",
|
|
59
|
+
"user",
|
|
60
|
+
"like",
|
|
61
|
+
"likes",
|
|
62
|
+
"does",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def tokenize(text: str) -> list[str]:
|
|
68
|
+
"""Extract lowercase alphanumeric tokens, skipping stopwords."""
|
|
69
|
+
tokens = [m.group(0).lower() for m in _TOKEN_RE.finditer(text)]
|
|
70
|
+
return [t for t in tokens if t not in _STOPWORDS and len(t) > 1]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def relevance_score(query: str, text: str, tags: Iterable[str] | None = None) -> float:
|
|
74
|
+
"""
|
|
75
|
+
Score relevance in [0, 1] based on query token overlap with text and tags.
|
|
76
|
+
"""
|
|
77
|
+
query_tokens = set(tokenize(query))
|
|
78
|
+
if not query_tokens:
|
|
79
|
+
return 0.0
|
|
80
|
+
text_tokens = set(tokenize(text))
|
|
81
|
+
tag_tokens: set[str] = set()
|
|
82
|
+
if tags:
|
|
83
|
+
for tag in tags:
|
|
84
|
+
tag_tokens.add(tag.lower().strip())
|
|
85
|
+
tag_tokens.update(tokenize(tag))
|
|
86
|
+
matched = len(query_tokens & (text_tokens | tag_tokens))
|
|
87
|
+
return min(1.0, matched / len(query_tokens))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def compute_relevance(query: str, memory: Memory) -> float:
|
|
91
|
+
"""Compute relevance score for a memory."""
|
|
92
|
+
return relevance_score(query, memory.text, memory.tags)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def filter_candidates(
|
|
96
|
+
memories: list[Memory],
|
|
97
|
+
query: str,
|
|
98
|
+
min_relevance: float = 0.0,
|
|
99
|
+
) -> list[Memory]:
|
|
100
|
+
"""Return memories meeting minimum relevance threshold."""
|
|
101
|
+
if min_relevance <= 0:
|
|
102
|
+
return list(memories)
|
|
103
|
+
return [m for m in memories if compute_relevance(query, m) >= min_relevance]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""SQLite storage backend for MemoryBrain."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
import threading
|
|
8
|
+
import uuid
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def utc_now() -> datetime:
|
|
16
|
+
"""Return current UTC datetime."""
|
|
17
|
+
return datetime.now(timezone.utc)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize_tags(tags: list[str] | None) -> list[str]:
|
|
21
|
+
"""Normalize tags to lowercase stripped unique sorted list."""
|
|
22
|
+
if not tags:
|
|
23
|
+
return []
|
|
24
|
+
return sorted({t.strip().lower() for t in tags if t.strip()})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Memory:
|
|
29
|
+
"""A single stored memory entry."""
|
|
30
|
+
|
|
31
|
+
text: str
|
|
32
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
33
|
+
timestamp: datetime = field(default_factory=utc_now)
|
|
34
|
+
tags: list[str] = field(default_factory=list)
|
|
35
|
+
importance: float = 5.0
|
|
36
|
+
access_count: int = 0
|
|
37
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
self.tags = normalize_tags(self.tags)
|
|
41
|
+
self.importance = max(0.0, min(10.0, float(self.importance)))
|
|
42
|
+
if self.timestamp.tzinfo is None:
|
|
43
|
+
self.timestamp = self.timestamp.replace(tzinfo=timezone.utc)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, Any]:
|
|
46
|
+
"""Serialize memory to a JSON-compatible dict."""
|
|
47
|
+
return {
|
|
48
|
+
"id": self.id,
|
|
49
|
+
"text": self.text,
|
|
50
|
+
"timestamp": self.timestamp.isoformat(),
|
|
51
|
+
"tags": self.tags,
|
|
52
|
+
"importance": self.importance,
|
|
53
|
+
"access_count": self.access_count,
|
|
54
|
+
"metadata": self.metadata,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, data: dict[str, Any]) -> Memory:
|
|
59
|
+
"""Deserialize memory from a dict."""
|
|
60
|
+
ts = data.get("timestamp")
|
|
61
|
+
if isinstance(ts, str):
|
|
62
|
+
timestamp = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
63
|
+
elif isinstance(ts, datetime):
|
|
64
|
+
timestamp = ts
|
|
65
|
+
else:
|
|
66
|
+
timestamp = utc_now()
|
|
67
|
+
return cls(
|
|
68
|
+
id=str(data.get("id", str(uuid.uuid4()))),
|
|
69
|
+
text=str(data["text"]),
|
|
70
|
+
timestamp=timestamp,
|
|
71
|
+
tags=data.get("tags", []),
|
|
72
|
+
importance=float(data.get("importance", 5.0)),
|
|
73
|
+
access_count=int(data.get("access_count", 0)),
|
|
74
|
+
metadata=dict(data.get("metadata", {})),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_SCHEMA = """
|
|
79
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
text TEXT NOT NULL,
|
|
82
|
+
timestamp TEXT NOT NULL,
|
|
83
|
+
tags TEXT NOT NULL,
|
|
84
|
+
importance REAL NOT NULL,
|
|
85
|
+
access_count INTEGER DEFAULT 0,
|
|
86
|
+
metadata TEXT
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories(timestamp);
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def default_db_path() -> Path:
|
|
93
|
+
"""Default SQLite database path."""
|
|
94
|
+
return Path.home() / ".memorybrain" / "memories.db"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SQLiteStorage:
|
|
98
|
+
"""Thread-safe SQLite storage for memories."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, db_path: str | Path | None = None) -> None:
|
|
101
|
+
path = Path(db_path).expanduser() if db_path else default_db_path()
|
|
102
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
self._db_path = path
|
|
104
|
+
self._lock = threading.RLock()
|
|
105
|
+
self._init_db()
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def db_path(self) -> Path:
|
|
109
|
+
return self._db_path
|
|
110
|
+
|
|
111
|
+
def _connect(self) -> sqlite3.Connection:
|
|
112
|
+
conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
|
|
113
|
+
conn.row_factory = sqlite3.Row
|
|
114
|
+
return conn
|
|
115
|
+
|
|
116
|
+
def _init_db(self) -> None:
|
|
117
|
+
with self._lock:
|
|
118
|
+
conn = self._connect()
|
|
119
|
+
try:
|
|
120
|
+
conn.executescript(_SCHEMA)
|
|
121
|
+
conn.commit()
|
|
122
|
+
finally:
|
|
123
|
+
conn.close()
|
|
124
|
+
|
|
125
|
+
def _row_to_memory(self, row: sqlite3.Row) -> Memory:
|
|
126
|
+
return Memory(
|
|
127
|
+
id=row["id"],
|
|
128
|
+
text=row["text"],
|
|
129
|
+
timestamp=datetime.fromisoformat(row["timestamp"].replace("Z", "+00:00")),
|
|
130
|
+
tags=json.loads(row["tags"]),
|
|
131
|
+
importance=float(row["importance"]),
|
|
132
|
+
access_count=int(row["access_count"] or 0),
|
|
133
|
+
metadata=json.loads(row["metadata"] or "{}"),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _memory_to_row(self, memory: Memory) -> tuple[Any, ...]:
|
|
137
|
+
return (
|
|
138
|
+
memory.id,
|
|
139
|
+
memory.text,
|
|
140
|
+
memory.timestamp.isoformat(),
|
|
141
|
+
json.dumps(memory.tags),
|
|
142
|
+
memory.importance,
|
|
143
|
+
memory.access_count,
|
|
144
|
+
json.dumps(memory.metadata),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def add(self, memory: Memory) -> Memory:
|
|
148
|
+
with self._lock:
|
|
149
|
+
conn = self._connect()
|
|
150
|
+
try:
|
|
151
|
+
conn.execute(
|
|
152
|
+
"""
|
|
153
|
+
INSERT INTO memories (
|
|
154
|
+
id, text, timestamp, tags, importance, access_count, metadata
|
|
155
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
156
|
+
""",
|
|
157
|
+
self._memory_to_row(memory),
|
|
158
|
+
)
|
|
159
|
+
conn.commit()
|
|
160
|
+
finally:
|
|
161
|
+
conn.close()
|
|
162
|
+
return memory
|
|
163
|
+
|
|
164
|
+
def get(self, memory_id: str) -> Memory | None:
|
|
165
|
+
with self._lock:
|
|
166
|
+
conn = self._connect()
|
|
167
|
+
try:
|
|
168
|
+
row = conn.execute(
|
|
169
|
+
"SELECT * FROM memories WHERE id = ?", (memory_id,)
|
|
170
|
+
).fetchone()
|
|
171
|
+
return self._row_to_memory(row) if row else None
|
|
172
|
+
finally:
|
|
173
|
+
conn.close()
|
|
174
|
+
|
|
175
|
+
def update(self, memory: Memory) -> Memory:
|
|
176
|
+
with self._lock:
|
|
177
|
+
conn = self._connect()
|
|
178
|
+
try:
|
|
179
|
+
conn.execute(
|
|
180
|
+
"""
|
|
181
|
+
UPDATE memories SET
|
|
182
|
+
text=?, timestamp=?, tags=?, importance=?,
|
|
183
|
+
access_count=?, metadata=?
|
|
184
|
+
WHERE id=?
|
|
185
|
+
""",
|
|
186
|
+
(
|
|
187
|
+
memory.text,
|
|
188
|
+
memory.timestamp.isoformat(),
|
|
189
|
+
json.dumps(memory.tags),
|
|
190
|
+
memory.importance,
|
|
191
|
+
memory.access_count,
|
|
192
|
+
json.dumps(memory.metadata),
|
|
193
|
+
memory.id,
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
conn.commit()
|
|
197
|
+
finally:
|
|
198
|
+
conn.close()
|
|
199
|
+
return memory
|
|
200
|
+
|
|
201
|
+
def delete(self, memory_id: str) -> bool:
|
|
202
|
+
with self._lock:
|
|
203
|
+
conn = self._connect()
|
|
204
|
+
try:
|
|
205
|
+
cur = conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
|
|
206
|
+
conn.commit()
|
|
207
|
+
return cur.rowcount > 0
|
|
208
|
+
finally:
|
|
209
|
+
conn.close()
|
|
210
|
+
|
|
211
|
+
def list_all(self) -> list[Memory]:
|
|
212
|
+
with self._lock:
|
|
213
|
+
conn = self._connect()
|
|
214
|
+
try:
|
|
215
|
+
rows = conn.execute(
|
|
216
|
+
"SELECT * FROM memories ORDER BY timestamp DESC"
|
|
217
|
+
).fetchall()
|
|
218
|
+
return [self._row_to_memory(r) for r in rows]
|
|
219
|
+
finally:
|
|
220
|
+
conn.close()
|
|
221
|
+
|
|
222
|
+
def count(self) -> int:
|
|
223
|
+
with self._lock:
|
|
224
|
+
conn = self._connect()
|
|
225
|
+
try:
|
|
226
|
+
row = conn.execute("SELECT COUNT(*) AS c FROM memories").fetchone()
|
|
227
|
+
return int(row["c"]) if row else 0
|
|
228
|
+
finally:
|
|
229
|
+
conn.close()
|
|
230
|
+
|
|
231
|
+
def export_records(self) -> list[dict[str, Any]]:
|
|
232
|
+
return [m.to_dict() for m in self.list_all()]
|
|
233
|
+
|
|
234
|
+
def import_records(self, records: list[dict[str, Any]]) -> int:
|
|
235
|
+
count = 0
|
|
236
|
+
for record in records:
|
|
237
|
+
memory = Memory.from_dict(record)
|
|
238
|
+
if self.get(memory.id):
|
|
239
|
+
self.update(memory)
|
|
240
|
+
else:
|
|
241
|
+
self.add(memory)
|
|
242
|
+
count += 1
|
|
243
|
+
return count
|
|
244
|
+
|
|
245
|
+
def db_file_size_bytes(self) -> int:
|
|
246
|
+
if self._db_path.exists():
|
|
247
|
+
return self._db_path.stat().st_size
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
def tag_counts(self) -> dict[str, int]:
|
|
251
|
+
counts: dict[str, int] = {}
|
|
252
|
+
for memory in self.list_all():
|
|
253
|
+
for tag in memory.tags:
|
|
254
|
+
counts[tag] = counts.get(tag, 0) + 1
|
|
255
|
+
return counts
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memorybrain
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Long-term memory library for AI assistants and agents
|
|
5
|
+
Home-page: https://github.com/memorybrain/memorybrain
|
|
6
|
+
Author: MemoryBrain Contributors
|
|
7
|
+
Author-email: memorybrain@example.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Source, https://github.com/memorybrain/memorybrain
|
|
10
|
+
Project-URL: Tracker, https://github.com/memorybrain/memorybrain/issues
|
|
11
|
+
Keywords: ai memory agents llm chatbot sqlite
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/plain
|
|
26
|
+
License-File: LICENSE.txt
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: keywords
|
|
34
|
+
Dynamic: license
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: project-url
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+
MemoryBrain
|
|
41
|
+
===========
|
|
42
|
+
|
|
43
|
+
Long-term memory library for AI assistants, chatbots, and autonomous agents.
|
|
44
|
+
Stores memories in a local SQLite database with intelligent ranked retrieval.
|
|
45
|
+
|
|
46
|
+
Requirements
|
|
47
|
+
------------
|
|
48
|
+
- Python 3.10 or newer
|
|
49
|
+
- No third-party runtime dependencies
|
|
50
|
+
|
|
51
|
+
Installation
|
|
52
|
+
------------
|
|
53
|
+
pip install memorybrain
|
|
54
|
+
|
|
55
|
+
Or install from source:
|
|
56
|
+
|
|
57
|
+
pip install .
|
|
58
|
+
|
|
59
|
+
Quick Start
|
|
60
|
+
-----------
|
|
61
|
+
from memorybrain import MemoryBrain
|
|
62
|
+
|
|
63
|
+
brain = MemoryBrain()
|
|
64
|
+
|
|
65
|
+
brain.remember(
|
|
66
|
+
"User likes Flutter development",
|
|
67
|
+
tags=["flutter", "programming"],
|
|
68
|
+
importance=10,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
results = brain.recall("What framework does the user like?")
|
|
72
|
+
for memory in results:
|
|
73
|
+
print(memory.text)
|
|
74
|
+
|
|
75
|
+
matches = brain.search("Flutter")
|
|
76
|
+
print(matches)
|
|
77
|
+
|
|
78
|
+
print(brain.stats())
|
|
79
|
+
|
|
80
|
+
API Overview
|
|
81
|
+
------------
|
|
82
|
+
remember(text, tags=None, importance=5)
|
|
83
|
+
Store a new memory. Returns a Memory object.
|
|
84
|
+
|
|
85
|
+
recall(query, limit=10)
|
|
86
|
+
Retrieve memories ranked for a natural-language query.
|
|
87
|
+
|
|
88
|
+
search(query, limit=10)
|
|
89
|
+
Search memories by keyword relevance.
|
|
90
|
+
|
|
91
|
+
forget(memory_id)
|
|
92
|
+
Delete a memory by ID. Returns True if deleted.
|
|
93
|
+
|
|
94
|
+
export("memories.json")
|
|
95
|
+
Export all memories to a JSON file.
|
|
96
|
+
|
|
97
|
+
import_file("memories.json")
|
|
98
|
+
Import memories from JSON (upsert by ID).
|
|
99
|
+
|
|
100
|
+
stats()
|
|
101
|
+
Return statistics:
|
|
102
|
+
- total_memories
|
|
103
|
+
- most_common_tags (list of {tag, count})
|
|
104
|
+
- database_size_bytes
|
|
105
|
+
|
|
106
|
+
Custom Database Path
|
|
107
|
+
--------------------
|
|
108
|
+
brain = MemoryBrain(db_path="./data/my_memories.db")
|
|
109
|
+
|
|
110
|
+
Default location: ~/.memorybrain/memories.db
|
|
111
|
+
|
|
112
|
+
Build and Publish to PyPI
|
|
113
|
+
-------------------------
|
|
114
|
+
Install build tools:
|
|
115
|
+
|
|
116
|
+
pip install wheel twine build
|
|
117
|
+
|
|
118
|
+
Build source distribution and wheel:
|
|
119
|
+
|
|
120
|
+
python setup.py sdist bdist_wheel
|
|
121
|
+
|
|
122
|
+
Or using the modern build frontend:
|
|
123
|
+
|
|
124
|
+
python -m build
|
|
125
|
+
|
|
126
|
+
Validate packages:
|
|
127
|
+
|
|
128
|
+
twine check dist/*
|
|
129
|
+
|
|
130
|
+
Upload to PyPI (requires PyPI account and API token):
|
|
131
|
+
|
|
132
|
+
twine upload dist/*
|
|
133
|
+
|
|
134
|
+
Test upload to TestPyPI first:
|
|
135
|
+
|
|
136
|
+
twine upload --repository testpypi dist/*
|
|
137
|
+
|
|
138
|
+
License
|
|
139
|
+
-------
|
|
140
|
+
MIT License — see LICENSE.txt
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
MIT License
|
|
144
|
+
|
|
145
|
+
Copyright (c) 2026 MemoryBrain Contributors
|
|
146
|
+
|
|
147
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
148
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
149
|
+
in the Software without restriction, including without limitation the rights
|
|
150
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
151
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
152
|
+
furnished to do so, subject to the following conditions:
|
|
153
|
+
|
|
154
|
+
The above copyright notice and this permission notice shall be included in all
|
|
155
|
+
copies or substantial portions of the Software.
|
|
156
|
+
|
|
157
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
158
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
159
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
160
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
161
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
162
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
163
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Changelog.txt
|
|
2
|
+
LICENSE.txt
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.txt
|
|
5
|
+
requirements.txt
|
|
6
|
+
setup.py
|
|
7
|
+
memorybrain/__init__.py
|
|
8
|
+
memorybrain/brain.py
|
|
9
|
+
memorybrain/ranking.py
|
|
10
|
+
memorybrain/search.py
|
|
11
|
+
memorybrain/storage.py
|
|
12
|
+
memorybrain.egg-info/PKG-INFO
|
|
13
|
+
memorybrain.egg-info/SOURCES.txt
|
|
14
|
+
memorybrain.egg-info/dependency_links.txt
|
|
15
|
+
memorybrain.egg-info/top_level.txt
|
|
16
|
+
tests/test_memorybrain.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
memorybrain
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Setup script for memorybrain package."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from setuptools import find_packages, setup
|
|
6
|
+
|
|
7
|
+
HERE = Path(__file__).parent.resolve()
|
|
8
|
+
README = (HERE / "README.txt").read_text(encoding="utf-8")
|
|
9
|
+
LICENSE = (HERE / "LICENSE.txt").read_text(encoding="utf-8")
|
|
10
|
+
|
|
11
|
+
setup(
|
|
12
|
+
name="memorybrain",
|
|
13
|
+
version="1.0.0",
|
|
14
|
+
description="Long-term memory library for AI assistants and agents",
|
|
15
|
+
long_description=README + "\n\n" + LICENSE,
|
|
16
|
+
long_description_content_type="text/plain",
|
|
17
|
+
author="MemoryBrain Contributors",
|
|
18
|
+
author_email="memorybrain@example.com",
|
|
19
|
+
url="https://github.com/memorybrain/memorybrain",
|
|
20
|
+
license="MIT",
|
|
21
|
+
packages=find_packages(exclude=["tests", "tests.*"]),
|
|
22
|
+
python_requires=">=3.10",
|
|
23
|
+
install_requires=[],
|
|
24
|
+
include_package_data=True,
|
|
25
|
+
classifiers=[
|
|
26
|
+
"Development Status :: 5 - Production/Stable",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
],
|
|
39
|
+
keywords="ai memory agents llm chatbot sqlite",
|
|
40
|
+
project_urls={
|
|
41
|
+
"Source": "https://github.com/memorybrain/memorybrain",
|
|
42
|
+
"Tracker": "https://github.com/memorybrain/memorybrain/issues",
|
|
43
|
+
},
|
|
44
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Tests for memorybrain package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from memorybrain import MemoryBrain
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_remember_recall_search() -> None:
|
|
13
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
14
|
+
db = Path(tmp) / "test.db"
|
|
15
|
+
brain = MemoryBrain(db_path=db)
|
|
16
|
+
m = brain.remember(
|
|
17
|
+
"User likes Flutter development",
|
|
18
|
+
tags=["flutter", "programming"],
|
|
19
|
+
importance=10,
|
|
20
|
+
)
|
|
21
|
+
assert m.id
|
|
22
|
+
recall = brain.recall("What framework does the user like?")
|
|
23
|
+
assert len(recall) >= 1
|
|
24
|
+
assert "Flutter" in recall[0].text
|
|
25
|
+
search = brain.search("Flutter")
|
|
26
|
+
assert len(search) >= 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_forget_export_import_stats() -> None:
|
|
30
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
31
|
+
db = Path(tmp) / "test.db"
|
|
32
|
+
brain = MemoryBrain(db_path=db)
|
|
33
|
+
m = brain.remember("Temporary memory", tags=["temp"], importance=3)
|
|
34
|
+
export_path = Path(tmp) / "memories.json"
|
|
35
|
+
assert brain.export(export_path) == 1
|
|
36
|
+
assert brain.forget(m.id) is True
|
|
37
|
+
assert brain.get(m.id) is None
|
|
38
|
+
count = brain.import_file(export_path)
|
|
39
|
+
assert count == 1
|
|
40
|
+
assert brain.get(m.id) is not None
|
|
41
|
+
stats = brain.stats()
|
|
42
|
+
assert stats["total_memories"] == 1
|
|
43
|
+
assert "most_common_tags" in stats
|
|
44
|
+
assert "database_size_bytes" in stats
|
|
45
|
+
assert stats["database_size_bytes"] > 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_import_export_roundtrip() -> None:
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
50
|
+
db = Path(tmp) / "test.db"
|
|
51
|
+
brain = MemoryBrain(db_path=db)
|
|
52
|
+
brain.remember("Note one", tags=["a"])
|
|
53
|
+
brain.remember("Note two", tags=["b"])
|
|
54
|
+
path = Path(tmp) / "out.json"
|
|
55
|
+
brain.export(path)
|
|
56
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
57
|
+
assert len(data) == 2
|