wavemind 2.0.1__tar.gz → 2.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wavemind-2.0.1/wavemind.egg-info → wavemind-2.0.3}/PKG-INFO +88 -9
- {wavemind-2.0.1 → wavemind-2.0.3}/README.md +86 -8
- {wavemind-2.0.1 → wavemind-2.0.3}/pyproject.toml +2 -1
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_api.py +40 -1
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_packaging_files.py +21 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/__init__.py +3 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/api.py +4 -3
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/integrations/langchain.py +4 -11
- {wavemind-2.0.1 → wavemind-2.0.3/wavemind.egg-info}/PKG-INFO +88 -9
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind.egg-info/requires.txt +1 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/LICENSE +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/setup.cfg +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_agent_memory_benchmark.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_api_process_persistence.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_cli_smoke.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_core_persistence.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_examples.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_import_benchmark.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_indexes_encoders.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_langchain_integration.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/tests/test_semantic_and_latency.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/__main__.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/benchmark.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/cli.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/core.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/encoders.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/importers.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/indexes.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/integrations/__init__.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind/storage.py +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind.egg-info/SOURCES.txt +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind.egg-info/dependency_links.txt +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind.egg-info/entry_points.txt +0 -0
- {wavemind-2.0.1 → wavemind-2.0.3}/wavemind.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavemind
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: Persistent dynamic memory engine with vector search and wave-field re-ranking
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/CaspianG/wavemind
|
|
@@ -27,6 +27,7 @@ Requires-Dist: langchain-classic>=1.0; extra == "langchain"
|
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
29
29
|
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
30
|
+
Requires-Dist: langchain-classic>=1.0; extra == "dev"
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
32
33
|
# WaveMind is persistent dynamic memory for AI agents: vector search first, wave-field priority second, SQLite as the source of truth.
|
|
@@ -37,6 +38,8 @@ Dynamic: license-file
|
|
|
37
38
|
|
|
38
39
|
## Terminal Demo
|
|
39
40
|
|
|
41
|
+
From a cloned repository:
|
|
42
|
+
|
|
40
43
|
```text
|
|
41
44
|
$ python examples/demo.py
|
|
42
45
|
✓ Remembered: "Andrey is a trader who tracks market breakouts."
|
|
@@ -51,23 +54,66 @@ The demo is offline, keyless, and uses the built-in hash encoder.
|
|
|
51
54
|
|
|
52
55
|
## Quick Start
|
|
53
56
|
|
|
57
|
+
Install from PyPI and create your first local memory:
|
|
58
|
+
|
|
54
59
|
```sh
|
|
55
|
-
python -m pip install
|
|
60
|
+
python -m pip install wavemind
|
|
56
61
|
wavemind remember "Andrey is a trader" --namespace demo
|
|
57
62
|
wavemind query "trader" --namespace demo
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
What happens here:
|
|
66
|
+
|
|
67
|
+
- `remember` writes the text and its vector pattern into a local SQLite database.
|
|
68
|
+
- By default, the database file is `wavemind.sqlite3` in your current working directory.
|
|
69
|
+
- `--namespace demo` keeps this memory separate from other users, agents, or projects.
|
|
70
|
+
- `query` reads from the same SQLite file and returns the closest remembered texts.
|
|
71
|
+
|
|
72
|
+
## Optional Embeddings
|
|
61
73
|
|
|
62
74
|
For sentence-transformer embeddings:
|
|
63
75
|
|
|
64
76
|
```sh
|
|
65
|
-
python -m pip install
|
|
77
|
+
python -m pip install "wavemind[sentence]"
|
|
66
78
|
wavemind --encoder sentence remember "Andrey is a trader" --namespace demo
|
|
67
79
|
wavemind --encoder sentence query "What does Andrey do?" --namespace demo
|
|
68
80
|
```
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
## Data Location
|
|
83
|
+
|
|
84
|
+
For an explicit database path, put global options before the command:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
wavemind --db ./agent_memory.sqlite3 remember "Andrey is a trader" --namespace demo
|
|
88
|
+
wavemind --db ./agent_memory.sqlite3 query "trader" --namespace demo
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## HTTP API
|
|
92
|
+
|
|
93
|
+
Run the local FastAPI server:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
wavemind --db ./agent_memory.sqlite3 serve --host 127.0.0.1 --port 8000
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Store and query memory over HTTP:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
curl -X POST http://127.0.0.1:8000/remember -H "Content-Type: application/json" -d "{\"text\":\"Andrey is a trader\",\"namespace\":\"demo\"}"
|
|
103
|
+
curl -X POST http://127.0.0.1:8000/query -H "Content-Type: application/json" -d "{\"query\":\"trader\",\"namespace\":\"demo\",\"top_k\":1}"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Install From Source
|
|
107
|
+
|
|
108
|
+
For contributors installing from a local clone:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
git clone https://github.com/CaspianG/wavemind.git
|
|
112
|
+
cd wavemind
|
|
113
|
+
python -m pip install -e ".[sentence]"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
One-file setup scripts are also included in the repository:
|
|
71
117
|
|
|
72
118
|
```sh
|
|
73
119
|
sh install.sh
|
|
@@ -94,12 +140,41 @@ memory = WaveMindMemory(db_path="agent_memory.sqlite3")
|
|
|
94
140
|
# Replace: memory = ConversationBufferMemory()
|
|
95
141
|
```
|
|
96
142
|
|
|
97
|
-
Offline runnable example:
|
|
143
|
+
Offline runnable example from a cloned repository:
|
|
98
144
|
|
|
99
145
|
```sh
|
|
100
146
|
python examples/langchain_memory.py
|
|
101
147
|
```
|
|
102
148
|
|
|
149
|
+
## Why Dynamic Memory
|
|
150
|
+
|
|
151
|
+
WaveMind is not positioned as "a faster Chroma." Chroma, Qdrant, Pinecone, and Weaviate are vector databases: they store embeddings and return nearest neighbors. That is the right tool for many static RAG workloads.
|
|
152
|
+
|
|
153
|
+
WaveMind is an agent memory layer. It still uses vector search first, but then applies memory-specific signals that a plain vector store does not model by default:
|
|
154
|
+
|
|
155
|
+
| memory behavior | Why it matters for agents | WaveMind mechanism |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| Hot memories | Facts recalled repeatedly should become easier to recall again. | Wave-field hotness and priority updates. |
|
|
158
|
+
| Aging memories | Old low-value facts should fade instead of competing forever. | TTL and decay-aware scoring. |
|
|
159
|
+
| Scoped memory | One user, agent, workspace, or project should not leak into another. | Namespaces and tags. |
|
|
160
|
+
| Explicit forgetting | Agents need deletion, privacy cleanup, and correction workflows. | `forget()` plus SQLite persistence. |
|
|
161
|
+
| Stable restart behavior | A memory system must survive process restarts. | SQLite source of truth, reloadable indexes. |
|
|
162
|
+
| Vector plus memory rank | Semantic similarity is necessary but not sufficient for long-running agents. | k-NN candidates first, wave field as re-ranker. |
|
|
163
|
+
|
|
164
|
+
The current Chroma benchmark below is intentionally conservative: it compares static retrieval on the same facts and the same hash embeddings. That benchmark is useful, but it does not exercise WaveMind's main product thesis: memory that changes over time as an agent recalls, reinforces, ages, and forgets information.
|
|
165
|
+
|
|
166
|
+
The benchmark that should decide whether WaveMind is worth using is a dynamic agent-memory benchmark:
|
|
167
|
+
|
|
168
|
+
| scenario | What should happen |
|
|
169
|
+
|---|---|
|
|
170
|
+
| A user repeats a preference many times. | WaveMind should rank it higher than equally similar but unused facts. |
|
|
171
|
+
| A fact expires via TTL. | WaveMind should suppress it without requiring manual vector cleanup. |
|
|
172
|
+
| A user corrects an old fact. | WaveMind should prefer the newer or reinforced memory. |
|
|
173
|
+
| A query is ambiguous across namespaces. | WaveMind should return only the scoped user's memory. |
|
|
174
|
+
| A long conversation has many irrelevant facts. | WaveMind should preserve useful recall instead of treating all vectors equally. |
|
|
175
|
+
|
|
176
|
+
In short: static vector search answers "what is nearest?" Agent memory also asks "what is still relevant, reinforced, scoped, and allowed to be remembered?"
|
|
177
|
+
|
|
103
178
|
## Benchmark
|
|
104
179
|
|
|
105
180
|
Real Russian sentences from Tatoeba, 50 one-word queries, NumPy exact index.
|
|
@@ -118,7 +193,7 @@ Capacity check with the hash encoder:
|
|
|
118
193
|
| 1000 | 0.88 | 1.00 | 1.50 ms |
|
|
119
194
|
| 5000 | 0.72 | 0.88 | 5.68 ms |
|
|
120
195
|
|
|
121
|
-
Run locally:
|
|
196
|
+
Run locally from a cloned repository:
|
|
122
197
|
|
|
123
198
|
```sh
|
|
124
199
|
python benchmarks/ru_sentences_benchmark.py --sentences 200 --queries 50 --encoder hash --index numpy
|
|
@@ -130,12 +205,14 @@ Agent-memory benchmark against Chroma:
|
|
|
130
205
|
200 Russian user facts, 50 natural-language questions, same precomputed `HashingTextEncoder` embeddings for WaveMind and Chroma.
|
|
131
206
|
Full machine-readable result: `benchmarks/agent_memory_results.json`.
|
|
132
207
|
|
|
208
|
+
This is a static retrieval benchmark. It measures baseline ranking and latency, not hotness, TTL, repeated recall, or memory aging.
|
|
209
|
+
|
|
133
210
|
| engine | precision@1 | precision@3 | avg latency |
|
|
134
211
|
|---|---:|---:|---:|
|
|
135
212
|
| WaveMind | 0.82 | 0.90 | 2.25 ms |
|
|
136
213
|
| Chroma | 0.82 | 0.88 | 0.93 ms |
|
|
137
214
|
|
|
138
|
-
Run locally:
|
|
215
|
+
Run locally from a cloned repository:
|
|
139
216
|
|
|
140
217
|
```sh
|
|
141
218
|
pip install -e ".[bench]"
|
|
@@ -154,7 +231,7 @@ python benchmarks/agent_memory_benchmark.py --engines wavemind chroma --facts 20
|
|
|
154
231
|
| Best fit | Small to medium agent memory with dynamic recall | Local RAG apps and prototypes | Large-scale vector search |
|
|
155
232
|
| Scale target today | Up to 1000 optimal on NumPy, FAISS recommended beyond 5000 | Larger than WaveMind local mode | Production scale |
|
|
156
233
|
|
|
157
|
-
WaveMind is not trying to replace dedicated vector databases at scale.
|
|
234
|
+
WaveMind is not trying to replace dedicated vector databases at scale. The intended product gap is dynamic priority: frequently used memories can become hotter while old or low-priority memories fade. For static RAG over large document collections, use a mature vector database. For agent memory that needs persistence, scoped recall, TTL, forgetting, and reinforcement, WaveMind is designed to sit above or beside the vector index.
|
|
158
235
|
|
|
159
236
|
## Known Limitations
|
|
160
237
|
|
|
@@ -164,10 +241,12 @@ WaveMind is not trying to replace dedicated vector databases at scale. Its diffe
|
|
|
164
241
|
- `sentence-transformers/paraphrase-multilingual-mpnet-base-v2` requires about 420 MB of model files and measured about 53 ms per query on the benchmark machine.
|
|
165
242
|
- The Chroma comparison currently uses shared precomputed hash embeddings to isolate retrieval/ranking behavior; semantic model comparisons should be run separately.
|
|
166
243
|
- In the 200-fact agent benchmark, Chroma is faster on average while WaveMind is slightly higher at `precision@3`.
|
|
244
|
+
- The current public benchmark does not yet prove the dynamic-memory advantage. The next benchmark must test hotness, TTL, corrections, namespace isolation, and repeated recall.
|
|
167
245
|
|
|
168
246
|
## Roadmap
|
|
169
247
|
|
|
170
248
|
- FAISS-first production index path with persisted index rebuilds.
|
|
249
|
+
- Dynamic agent-memory benchmark against Chroma/Qdrant: hotness, TTL, stale-fact suppression, corrections, and namespace isolation.
|
|
171
250
|
- Expand the agent-memory benchmark to sentence-transformers, FAISS, Chroma default embeddings, and Qdrant.
|
|
172
251
|
- Better semantic query expansion for short and ambiguous queries.
|
|
173
252
|
- Namespace quotas, backups, and daemon hardening for SaaS use.
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
## Terminal Demo
|
|
8
8
|
|
|
9
|
+
From a cloned repository:
|
|
10
|
+
|
|
9
11
|
```text
|
|
10
12
|
$ python examples/demo.py
|
|
11
13
|
✓ Remembered: "Andrey is a trader who tracks market breakouts."
|
|
@@ -20,23 +22,66 @@ The demo is offline, keyless, and uses the built-in hash encoder.
|
|
|
20
22
|
|
|
21
23
|
## Quick Start
|
|
22
24
|
|
|
25
|
+
Install from PyPI and create your first local memory:
|
|
26
|
+
|
|
23
27
|
```sh
|
|
24
|
-
python -m pip install
|
|
28
|
+
python -m pip install wavemind
|
|
25
29
|
wavemind remember "Andrey is a trader" --namespace demo
|
|
26
30
|
wavemind query "trader" --namespace demo
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
What happens here:
|
|
34
|
+
|
|
35
|
+
- `remember` writes the text and its vector pattern into a local SQLite database.
|
|
36
|
+
- By default, the database file is `wavemind.sqlite3` in your current working directory.
|
|
37
|
+
- `--namespace demo` keeps this memory separate from other users, agents, or projects.
|
|
38
|
+
- `query` reads from the same SQLite file and returns the closest remembered texts.
|
|
39
|
+
|
|
40
|
+
## Optional Embeddings
|
|
30
41
|
|
|
31
42
|
For sentence-transformer embeddings:
|
|
32
43
|
|
|
33
44
|
```sh
|
|
34
|
-
python -m pip install
|
|
45
|
+
python -m pip install "wavemind[sentence]"
|
|
35
46
|
wavemind --encoder sentence remember "Andrey is a trader" --namespace demo
|
|
36
47
|
wavemind --encoder sentence query "What does Andrey do?" --namespace demo
|
|
37
48
|
```
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
## Data Location
|
|
51
|
+
|
|
52
|
+
For an explicit database path, put global options before the command:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
wavemind --db ./agent_memory.sqlite3 remember "Andrey is a trader" --namespace demo
|
|
56
|
+
wavemind --db ./agent_memory.sqlite3 query "trader" --namespace demo
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## HTTP API
|
|
60
|
+
|
|
61
|
+
Run the local FastAPI server:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
wavemind --db ./agent_memory.sqlite3 serve --host 127.0.0.1 --port 8000
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Store and query memory over HTTP:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
curl -X POST http://127.0.0.1:8000/remember -H "Content-Type: application/json" -d "{\"text\":\"Andrey is a trader\",\"namespace\":\"demo\"}"
|
|
71
|
+
curl -X POST http://127.0.0.1:8000/query -H "Content-Type: application/json" -d "{\"query\":\"trader\",\"namespace\":\"demo\",\"top_k\":1}"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Install From Source
|
|
75
|
+
|
|
76
|
+
For contributors installing from a local clone:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
git clone https://github.com/CaspianG/wavemind.git
|
|
80
|
+
cd wavemind
|
|
81
|
+
python -m pip install -e ".[sentence]"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
One-file setup scripts are also included in the repository:
|
|
40
85
|
|
|
41
86
|
```sh
|
|
42
87
|
sh install.sh
|
|
@@ -63,12 +108,41 @@ memory = WaveMindMemory(db_path="agent_memory.sqlite3")
|
|
|
63
108
|
# Replace: memory = ConversationBufferMemory()
|
|
64
109
|
```
|
|
65
110
|
|
|
66
|
-
Offline runnable example:
|
|
111
|
+
Offline runnable example from a cloned repository:
|
|
67
112
|
|
|
68
113
|
```sh
|
|
69
114
|
python examples/langchain_memory.py
|
|
70
115
|
```
|
|
71
116
|
|
|
117
|
+
## Why Dynamic Memory
|
|
118
|
+
|
|
119
|
+
WaveMind is not positioned as "a faster Chroma." Chroma, Qdrant, Pinecone, and Weaviate are vector databases: they store embeddings and return nearest neighbors. That is the right tool for many static RAG workloads.
|
|
120
|
+
|
|
121
|
+
WaveMind is an agent memory layer. It still uses vector search first, but then applies memory-specific signals that a plain vector store does not model by default:
|
|
122
|
+
|
|
123
|
+
| memory behavior | Why it matters for agents | WaveMind mechanism |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| Hot memories | Facts recalled repeatedly should become easier to recall again. | Wave-field hotness and priority updates. |
|
|
126
|
+
| Aging memories | Old low-value facts should fade instead of competing forever. | TTL and decay-aware scoring. |
|
|
127
|
+
| Scoped memory | One user, agent, workspace, or project should not leak into another. | Namespaces and tags. |
|
|
128
|
+
| Explicit forgetting | Agents need deletion, privacy cleanup, and correction workflows. | `forget()` plus SQLite persistence. |
|
|
129
|
+
| Stable restart behavior | A memory system must survive process restarts. | SQLite source of truth, reloadable indexes. |
|
|
130
|
+
| Vector plus memory rank | Semantic similarity is necessary but not sufficient for long-running agents. | k-NN candidates first, wave field as re-ranker. |
|
|
131
|
+
|
|
132
|
+
The current Chroma benchmark below is intentionally conservative: it compares static retrieval on the same facts and the same hash embeddings. That benchmark is useful, but it does not exercise WaveMind's main product thesis: memory that changes over time as an agent recalls, reinforces, ages, and forgets information.
|
|
133
|
+
|
|
134
|
+
The benchmark that should decide whether WaveMind is worth using is a dynamic agent-memory benchmark:
|
|
135
|
+
|
|
136
|
+
| scenario | What should happen |
|
|
137
|
+
|---|---|
|
|
138
|
+
| A user repeats a preference many times. | WaveMind should rank it higher than equally similar but unused facts. |
|
|
139
|
+
| A fact expires via TTL. | WaveMind should suppress it without requiring manual vector cleanup. |
|
|
140
|
+
| A user corrects an old fact. | WaveMind should prefer the newer or reinforced memory. |
|
|
141
|
+
| A query is ambiguous across namespaces. | WaveMind should return only the scoped user's memory. |
|
|
142
|
+
| A long conversation has many irrelevant facts. | WaveMind should preserve useful recall instead of treating all vectors equally. |
|
|
143
|
+
|
|
144
|
+
In short: static vector search answers "what is nearest?" Agent memory also asks "what is still relevant, reinforced, scoped, and allowed to be remembered?"
|
|
145
|
+
|
|
72
146
|
## Benchmark
|
|
73
147
|
|
|
74
148
|
Real Russian sentences from Tatoeba, 50 one-word queries, NumPy exact index.
|
|
@@ -87,7 +161,7 @@ Capacity check with the hash encoder:
|
|
|
87
161
|
| 1000 | 0.88 | 1.00 | 1.50 ms |
|
|
88
162
|
| 5000 | 0.72 | 0.88 | 5.68 ms |
|
|
89
163
|
|
|
90
|
-
Run locally:
|
|
164
|
+
Run locally from a cloned repository:
|
|
91
165
|
|
|
92
166
|
```sh
|
|
93
167
|
python benchmarks/ru_sentences_benchmark.py --sentences 200 --queries 50 --encoder hash --index numpy
|
|
@@ -99,12 +173,14 @@ Agent-memory benchmark against Chroma:
|
|
|
99
173
|
200 Russian user facts, 50 natural-language questions, same precomputed `HashingTextEncoder` embeddings for WaveMind and Chroma.
|
|
100
174
|
Full machine-readable result: `benchmarks/agent_memory_results.json`.
|
|
101
175
|
|
|
176
|
+
This is a static retrieval benchmark. It measures baseline ranking and latency, not hotness, TTL, repeated recall, or memory aging.
|
|
177
|
+
|
|
102
178
|
| engine | precision@1 | precision@3 | avg latency |
|
|
103
179
|
|---|---:|---:|---:|
|
|
104
180
|
| WaveMind | 0.82 | 0.90 | 2.25 ms |
|
|
105
181
|
| Chroma | 0.82 | 0.88 | 0.93 ms |
|
|
106
182
|
|
|
107
|
-
Run locally:
|
|
183
|
+
Run locally from a cloned repository:
|
|
108
184
|
|
|
109
185
|
```sh
|
|
110
186
|
pip install -e ".[bench]"
|
|
@@ -123,7 +199,7 @@ python benchmarks/agent_memory_benchmark.py --engines wavemind chroma --facts 20
|
|
|
123
199
|
| Best fit | Small to medium agent memory with dynamic recall | Local RAG apps and prototypes | Large-scale vector search |
|
|
124
200
|
| Scale target today | Up to 1000 optimal on NumPy, FAISS recommended beyond 5000 | Larger than WaveMind local mode | Production scale |
|
|
125
201
|
|
|
126
|
-
WaveMind is not trying to replace dedicated vector databases at scale.
|
|
202
|
+
WaveMind is not trying to replace dedicated vector databases at scale. The intended product gap is dynamic priority: frequently used memories can become hotter while old or low-priority memories fade. For static RAG over large document collections, use a mature vector database. For agent memory that needs persistence, scoped recall, TTL, forgetting, and reinforcement, WaveMind is designed to sit above or beside the vector index.
|
|
127
203
|
|
|
128
204
|
## Known Limitations
|
|
129
205
|
|
|
@@ -133,10 +209,12 @@ WaveMind is not trying to replace dedicated vector databases at scale. Its diffe
|
|
|
133
209
|
- `sentence-transformers/paraphrase-multilingual-mpnet-base-v2` requires about 420 MB of model files and measured about 53 ms per query on the benchmark machine.
|
|
134
210
|
- The Chroma comparison currently uses shared precomputed hash embeddings to isolate retrieval/ranking behavior; semantic model comparisons should be run separately.
|
|
135
211
|
- In the 200-fact agent benchmark, Chroma is faster on average while WaveMind is slightly higher at `precision@3`.
|
|
212
|
+
- The current public benchmark does not yet prove the dynamic-memory advantage. The next benchmark must test hotness, TTL, corrections, namespace isolation, and repeated recall.
|
|
136
213
|
|
|
137
214
|
## Roadmap
|
|
138
215
|
|
|
139
216
|
- FAISS-first production index path with persisted index rebuilds.
|
|
217
|
+
- Dynamic agent-memory benchmark against Chroma/Qdrant: hotness, TTL, stale-fact suppression, corrections, and namespace isolation.
|
|
140
218
|
- Expand the agent-memory benchmark to sentence-transformers, FAISS, Chroma default embeddings, and Qdrant.
|
|
141
219
|
- Better semantic query expansion for short and ambiguous queries.
|
|
142
220
|
- Namespace quotas, backups, and daemon hardening for SaaS use.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wavemind"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.3"
|
|
8
8
|
description = "Persistent dynamic memory engine with vector search and wave-field re-ranking"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -37,6 +37,7 @@ langchain = [
|
|
|
37
37
|
dev = [
|
|
38
38
|
"pytest>=8",
|
|
39
39
|
"httpx>=0.27",
|
|
40
|
+
"langchain-classic>=1.0",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.scripts]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from fastapi.testclient import TestClient
|
|
2
2
|
|
|
3
|
-
from wavemind import HashingTextEncoder, WaveMind
|
|
3
|
+
from wavemind import HashingTextEncoder, WaveMind, __version__
|
|
4
4
|
from wavemind.api import create_app
|
|
5
5
|
|
|
6
6
|
|
|
@@ -50,3 +50,42 @@ def test_fastapi_remember_query_forget_and_stats(tmp_path):
|
|
|
50
50
|
empty = client.post("/query", json={"text": "кошка", "namespace": "pets"})
|
|
51
51
|
assert empty.json()["results"] == []
|
|
52
52
|
|
|
53
|
+
|
|
54
|
+
def test_fastapi_query_accepts_query_alias(tmp_path):
|
|
55
|
+
mind = WaveMind(
|
|
56
|
+
db_path=tmp_path / "api.sqlite3",
|
|
57
|
+
width=32,
|
|
58
|
+
height=32,
|
|
59
|
+
layers=2,
|
|
60
|
+
encoder=HashingTextEncoder(vector_dim=64),
|
|
61
|
+
score_threshold=0.0,
|
|
62
|
+
)
|
|
63
|
+
client = TestClient(create_app(mind=mind))
|
|
64
|
+
|
|
65
|
+
remember = client.post(
|
|
66
|
+
"/remember",
|
|
67
|
+
json={"text": "Andrey is a trader", "namespace": "demo"},
|
|
68
|
+
)
|
|
69
|
+
assert remember.status_code == 200
|
|
70
|
+
|
|
71
|
+
query = client.post(
|
|
72
|
+
"/query",
|
|
73
|
+
json={"query": "trader", "namespace": "demo", "top_k": 1},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert query.status_code == 200
|
|
77
|
+
assert query.json()["results"][0]["text"] == "Andrey is a trader"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_fastapi_version_matches_package_version():
|
|
81
|
+
app = create_app(
|
|
82
|
+
mind=WaveMind(
|
|
83
|
+
db_path=None,
|
|
84
|
+
width=16,
|
|
85
|
+
height=16,
|
|
86
|
+
layers=1,
|
|
87
|
+
encoder=HashingTextEncoder(vector_dim=16),
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert app.version == __version__
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
import tomllib
|
|
3
|
+
|
|
4
|
+
import wavemind
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_package_version_matches_pyproject():
|
|
8
|
+
pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
|
|
9
|
+
|
|
10
|
+
assert wavemind.__version__ == pyproject["project"]["version"]
|
|
2
11
|
|
|
3
12
|
|
|
4
13
|
def test_sentence_extra_is_available_for_install_scripts():
|
|
@@ -22,6 +31,18 @@ def test_langchain_extra_installs_classic_memory_api():
|
|
|
22
31
|
assert '"langchain-classic>=1.0"' in pyproject
|
|
23
32
|
|
|
24
33
|
|
|
34
|
+
def test_dev_extra_runs_against_real_langchain_memory_api():
|
|
35
|
+
pyproject = Path("pyproject.toml").read_text(encoding="utf-8")
|
|
36
|
+
integration = Path("wavemind/integrations/langchain.py").read_text(
|
|
37
|
+
encoding="utf-8"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert "dev = [" in pyproject
|
|
41
|
+
assert '"langchain-classic>=1.0"' in pyproject
|
|
42
|
+
assert "class BaseMemory" not in integration
|
|
43
|
+
assert 'pip install "wavemind[langchain]"' in integration
|
|
44
|
+
|
|
45
|
+
|
|
25
46
|
def test_install_scripts_create_venv_and_install_sentence_extra():
|
|
26
47
|
install_sh = Path("install.sh").read_text(encoding="utf-8")
|
|
27
48
|
install_bat = Path("install.bat").read_text(encoding="utf-8")
|
|
@@ -8,6 +8,8 @@ from .encoders import (
|
|
|
8
8
|
)
|
|
9
9
|
from .storage import MemoryRecord, SQLiteMemoryStore
|
|
10
10
|
|
|
11
|
+
__version__ = "2.0.3"
|
|
12
|
+
|
|
11
13
|
__all__ = [
|
|
12
14
|
"FieldProjector",
|
|
13
15
|
"HashingTextEncoder",
|
|
@@ -18,5 +20,6 @@ __all__ = [
|
|
|
18
20
|
"TextEncoder",
|
|
19
21
|
"WaveField",
|
|
20
22
|
"WaveMind",
|
|
23
|
+
"__version__",
|
|
21
24
|
"create_text_encoder",
|
|
22
25
|
]
|
|
@@ -6,8 +6,9 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from fastapi import Body, FastAPI, Query
|
|
9
|
-
from pydantic import BaseModel, Field
|
|
9
|
+
from pydantic import AliasChoices, BaseModel, Field
|
|
10
10
|
|
|
11
|
+
from . import __version__
|
|
11
12
|
from .core import WaveMind
|
|
12
13
|
from .encoders import create_text_encoder
|
|
13
14
|
from .importers import import_path
|
|
@@ -30,7 +31,7 @@ class RememberResponse(BaseModel):
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class QueryRequest(BaseModel):
|
|
33
|
-
text: str
|
|
34
|
+
text: str = Field(validation_alias=AliasChoices("text", "query"))
|
|
34
35
|
namespace: str = "default"
|
|
35
36
|
top_k: int = 3
|
|
36
37
|
tags: list[str] = Field(default_factory=list)
|
|
@@ -101,7 +102,7 @@ def build_default_mind() -> WaveMind:
|
|
|
101
102
|
|
|
102
103
|
def create_app(mind: WaveMind | None = None) -> FastAPI:
|
|
103
104
|
logging.basicConfig(level=os.environ.get("WAVEMIND_LOG_LEVEL", "INFO"))
|
|
104
|
-
app = FastAPI(title="WaveMind", version=
|
|
105
|
+
app = FastAPI(title="WaveMind", version=__version__)
|
|
105
106
|
app.state.mind = mind or build_default_mind()
|
|
106
107
|
|
|
107
108
|
@app.post("/remember", response_model=RememberResponse)
|
|
@@ -11,17 +11,10 @@ from wavemind import WaveMind
|
|
|
11
11
|
|
|
12
12
|
try:
|
|
13
13
|
from langchain_classic.base_memory import BaseMemory
|
|
14
|
-
except ImportError:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class BaseMemory:
|
|
20
|
-
"""Small fallback so the integration can be imported without LangChain."""
|
|
21
|
-
|
|
22
|
-
def __init__(self, **data: Any):
|
|
23
|
-
for key, value in data.items():
|
|
24
|
-
setattr(self, key, value)
|
|
14
|
+
except ImportError as exc: # pragma: no cover - exercised in clean installs.
|
|
15
|
+
raise ImportError(
|
|
16
|
+
'WaveMindMemory requires LangChain. Install it with: pip install "wavemind[langchain]"'
|
|
17
|
+
) from exc
|
|
25
18
|
|
|
26
19
|
|
|
27
20
|
class WaveMindMemory(BaseMemory):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavemind
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: Persistent dynamic memory engine with vector search and wave-field re-ranking
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/CaspianG/wavemind
|
|
@@ -27,6 +27,7 @@ Requires-Dist: langchain-classic>=1.0; extra == "langchain"
|
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
29
29
|
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
30
|
+
Requires-Dist: langchain-classic>=1.0; extra == "dev"
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
32
33
|
# WaveMind is persistent dynamic memory for AI agents: vector search first, wave-field priority second, SQLite as the source of truth.
|
|
@@ -37,6 +38,8 @@ Dynamic: license-file
|
|
|
37
38
|
|
|
38
39
|
## Terminal Demo
|
|
39
40
|
|
|
41
|
+
From a cloned repository:
|
|
42
|
+
|
|
40
43
|
```text
|
|
41
44
|
$ python examples/demo.py
|
|
42
45
|
✓ Remembered: "Andrey is a trader who tracks market breakouts."
|
|
@@ -51,23 +54,66 @@ The demo is offline, keyless, and uses the built-in hash encoder.
|
|
|
51
54
|
|
|
52
55
|
## Quick Start
|
|
53
56
|
|
|
57
|
+
Install from PyPI and create your first local memory:
|
|
58
|
+
|
|
54
59
|
```sh
|
|
55
|
-
python -m pip install
|
|
60
|
+
python -m pip install wavemind
|
|
56
61
|
wavemind remember "Andrey is a trader" --namespace demo
|
|
57
62
|
wavemind query "trader" --namespace demo
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
What happens here:
|
|
66
|
+
|
|
67
|
+
- `remember` writes the text and its vector pattern into a local SQLite database.
|
|
68
|
+
- By default, the database file is `wavemind.sqlite3` in your current working directory.
|
|
69
|
+
- `--namespace demo` keeps this memory separate from other users, agents, or projects.
|
|
70
|
+
- `query` reads from the same SQLite file and returns the closest remembered texts.
|
|
71
|
+
|
|
72
|
+
## Optional Embeddings
|
|
61
73
|
|
|
62
74
|
For sentence-transformer embeddings:
|
|
63
75
|
|
|
64
76
|
```sh
|
|
65
|
-
python -m pip install
|
|
77
|
+
python -m pip install "wavemind[sentence]"
|
|
66
78
|
wavemind --encoder sentence remember "Andrey is a trader" --namespace demo
|
|
67
79
|
wavemind --encoder sentence query "What does Andrey do?" --namespace demo
|
|
68
80
|
```
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
## Data Location
|
|
83
|
+
|
|
84
|
+
For an explicit database path, put global options before the command:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
wavemind --db ./agent_memory.sqlite3 remember "Andrey is a trader" --namespace demo
|
|
88
|
+
wavemind --db ./agent_memory.sqlite3 query "trader" --namespace demo
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## HTTP API
|
|
92
|
+
|
|
93
|
+
Run the local FastAPI server:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
wavemind --db ./agent_memory.sqlite3 serve --host 127.0.0.1 --port 8000
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Store and query memory over HTTP:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
curl -X POST http://127.0.0.1:8000/remember -H "Content-Type: application/json" -d "{\"text\":\"Andrey is a trader\",\"namespace\":\"demo\"}"
|
|
103
|
+
curl -X POST http://127.0.0.1:8000/query -H "Content-Type: application/json" -d "{\"query\":\"trader\",\"namespace\":\"demo\",\"top_k\":1}"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Install From Source
|
|
107
|
+
|
|
108
|
+
For contributors installing from a local clone:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
git clone https://github.com/CaspianG/wavemind.git
|
|
112
|
+
cd wavemind
|
|
113
|
+
python -m pip install -e ".[sentence]"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
One-file setup scripts are also included in the repository:
|
|
71
117
|
|
|
72
118
|
```sh
|
|
73
119
|
sh install.sh
|
|
@@ -94,12 +140,41 @@ memory = WaveMindMemory(db_path="agent_memory.sqlite3")
|
|
|
94
140
|
# Replace: memory = ConversationBufferMemory()
|
|
95
141
|
```
|
|
96
142
|
|
|
97
|
-
Offline runnable example:
|
|
143
|
+
Offline runnable example from a cloned repository:
|
|
98
144
|
|
|
99
145
|
```sh
|
|
100
146
|
python examples/langchain_memory.py
|
|
101
147
|
```
|
|
102
148
|
|
|
149
|
+
## Why Dynamic Memory
|
|
150
|
+
|
|
151
|
+
WaveMind is not positioned as "a faster Chroma." Chroma, Qdrant, Pinecone, and Weaviate are vector databases: they store embeddings and return nearest neighbors. That is the right tool for many static RAG workloads.
|
|
152
|
+
|
|
153
|
+
WaveMind is an agent memory layer. It still uses vector search first, but then applies memory-specific signals that a plain vector store does not model by default:
|
|
154
|
+
|
|
155
|
+
| memory behavior | Why it matters for agents | WaveMind mechanism |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| Hot memories | Facts recalled repeatedly should become easier to recall again. | Wave-field hotness and priority updates. |
|
|
158
|
+
| Aging memories | Old low-value facts should fade instead of competing forever. | TTL and decay-aware scoring. |
|
|
159
|
+
| Scoped memory | One user, agent, workspace, or project should not leak into another. | Namespaces and tags. |
|
|
160
|
+
| Explicit forgetting | Agents need deletion, privacy cleanup, and correction workflows. | `forget()` plus SQLite persistence. |
|
|
161
|
+
| Stable restart behavior | A memory system must survive process restarts. | SQLite source of truth, reloadable indexes. |
|
|
162
|
+
| Vector plus memory rank | Semantic similarity is necessary but not sufficient for long-running agents. | k-NN candidates first, wave field as re-ranker. |
|
|
163
|
+
|
|
164
|
+
The current Chroma benchmark below is intentionally conservative: it compares static retrieval on the same facts and the same hash embeddings. That benchmark is useful, but it does not exercise WaveMind's main product thesis: memory that changes over time as an agent recalls, reinforces, ages, and forgets information.
|
|
165
|
+
|
|
166
|
+
The benchmark that should decide whether WaveMind is worth using is a dynamic agent-memory benchmark:
|
|
167
|
+
|
|
168
|
+
| scenario | What should happen |
|
|
169
|
+
|---|---|
|
|
170
|
+
| A user repeats a preference many times. | WaveMind should rank it higher than equally similar but unused facts. |
|
|
171
|
+
| A fact expires via TTL. | WaveMind should suppress it without requiring manual vector cleanup. |
|
|
172
|
+
| A user corrects an old fact. | WaveMind should prefer the newer or reinforced memory. |
|
|
173
|
+
| A query is ambiguous across namespaces. | WaveMind should return only the scoped user's memory. |
|
|
174
|
+
| A long conversation has many irrelevant facts. | WaveMind should preserve useful recall instead of treating all vectors equally. |
|
|
175
|
+
|
|
176
|
+
In short: static vector search answers "what is nearest?" Agent memory also asks "what is still relevant, reinforced, scoped, and allowed to be remembered?"
|
|
177
|
+
|
|
103
178
|
## Benchmark
|
|
104
179
|
|
|
105
180
|
Real Russian sentences from Tatoeba, 50 one-word queries, NumPy exact index.
|
|
@@ -118,7 +193,7 @@ Capacity check with the hash encoder:
|
|
|
118
193
|
| 1000 | 0.88 | 1.00 | 1.50 ms |
|
|
119
194
|
| 5000 | 0.72 | 0.88 | 5.68 ms |
|
|
120
195
|
|
|
121
|
-
Run locally:
|
|
196
|
+
Run locally from a cloned repository:
|
|
122
197
|
|
|
123
198
|
```sh
|
|
124
199
|
python benchmarks/ru_sentences_benchmark.py --sentences 200 --queries 50 --encoder hash --index numpy
|
|
@@ -130,12 +205,14 @@ Agent-memory benchmark against Chroma:
|
|
|
130
205
|
200 Russian user facts, 50 natural-language questions, same precomputed `HashingTextEncoder` embeddings for WaveMind and Chroma.
|
|
131
206
|
Full machine-readable result: `benchmarks/agent_memory_results.json`.
|
|
132
207
|
|
|
208
|
+
This is a static retrieval benchmark. It measures baseline ranking and latency, not hotness, TTL, repeated recall, or memory aging.
|
|
209
|
+
|
|
133
210
|
| engine | precision@1 | precision@3 | avg latency |
|
|
134
211
|
|---|---:|---:|---:|
|
|
135
212
|
| WaveMind | 0.82 | 0.90 | 2.25 ms |
|
|
136
213
|
| Chroma | 0.82 | 0.88 | 0.93 ms |
|
|
137
214
|
|
|
138
|
-
Run locally:
|
|
215
|
+
Run locally from a cloned repository:
|
|
139
216
|
|
|
140
217
|
```sh
|
|
141
218
|
pip install -e ".[bench]"
|
|
@@ -154,7 +231,7 @@ python benchmarks/agent_memory_benchmark.py --engines wavemind chroma --facts 20
|
|
|
154
231
|
| Best fit | Small to medium agent memory with dynamic recall | Local RAG apps and prototypes | Large-scale vector search |
|
|
155
232
|
| Scale target today | Up to 1000 optimal on NumPy, FAISS recommended beyond 5000 | Larger than WaveMind local mode | Production scale |
|
|
156
233
|
|
|
157
|
-
WaveMind is not trying to replace dedicated vector databases at scale.
|
|
234
|
+
WaveMind is not trying to replace dedicated vector databases at scale. The intended product gap is dynamic priority: frequently used memories can become hotter while old or low-priority memories fade. For static RAG over large document collections, use a mature vector database. For agent memory that needs persistence, scoped recall, TTL, forgetting, and reinforcement, WaveMind is designed to sit above or beside the vector index.
|
|
158
235
|
|
|
159
236
|
## Known Limitations
|
|
160
237
|
|
|
@@ -164,10 +241,12 @@ WaveMind is not trying to replace dedicated vector databases at scale. Its diffe
|
|
|
164
241
|
- `sentence-transformers/paraphrase-multilingual-mpnet-base-v2` requires about 420 MB of model files and measured about 53 ms per query on the benchmark machine.
|
|
165
242
|
- The Chroma comparison currently uses shared precomputed hash embeddings to isolate retrieval/ranking behavior; semantic model comparisons should be run separately.
|
|
166
243
|
- In the 200-fact agent benchmark, Chroma is faster on average while WaveMind is slightly higher at `precision@3`.
|
|
244
|
+
- The current public benchmark does not yet prove the dynamic-memory advantage. The next benchmark must test hotness, TTL, corrections, namespace isolation, and repeated recall.
|
|
167
245
|
|
|
168
246
|
## Roadmap
|
|
169
247
|
|
|
170
248
|
- FAISS-first production index path with persisted index rebuilds.
|
|
249
|
+
- Dynamic agent-memory benchmark against Chroma/Qdrant: hotness, TTL, stale-fact suppression, corrections, and namespace isolation.
|
|
171
250
|
- Expand the agent-memory benchmark to sentence-transformers, FAISS, Chroma default embeddings, and Qdrant.
|
|
172
251
|
- Better semantic query expansion for short and ambiguous queries.
|
|
173
252
|
- Namespace quotas, backups, and daemon hardening for SaaS use.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|