wavemind 2.1.1__tar.gz → 2.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.
- {wavemind-2.1.1 → wavemind-2.2.1}/PKG-INFO +36 -1
- {wavemind-2.1.1 → wavemind-2.2.1}/README.md +35 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docker-compose.yml +1 -1
- {wavemind-2.1.1 → wavemind-2.2.1}/pyproject.toml +1 -1
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_api.py +63 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_cli_smoke.py +2 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/__init__.py +1 -1
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/api.py +34 -1
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/cli.py +22 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/core.py +39 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/storage.py +51 -0
- wavemind-2.2.1/wavemind/studio.py +707 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/PKG-INFO +36 -1
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/SOURCES.txt +1 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/CONTRIBUTING.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/Dockerfile +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/LICENSE +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/MANIFEST.in +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/SECURITY.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/SUPPORT.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/BENCHMARK_REPORT.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/agent_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/agent_memory_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/ann_index_curve_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/ann_index_curve_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/benchmark_matrix_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/benchmark_registry.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/dynamic_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/dynamic_memory_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/field_memory_dynamics_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/field_memory_dynamics_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/locomo_evidence_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/locomo_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/locomo_sentence_evidence_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/long_memory_evidence_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/long_memory_evidence_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_answer_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_answer_extractive_20_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_evidence_50_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_evidence_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/open_retrieval_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/open_retrieval_scifact_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/render_benchmark_charts.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/render_benchmark_report.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/ru_sentences_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/wavemind_capacity_results.json +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/CHROMA_MIGRATION.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/DEMO_SCRIPT.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/LAUNCH_KIT.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/PROJECT_BOARD.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/RELEASE.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/ROADMAP.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/RU_LAUNCH_POSTS.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/USE_CASES.md +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/assets/benchmark-summary.svg +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/docs/assets/wavemind-social-card.svg +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/examples/agent_with_memory.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/examples/demo.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/examples/dynamic_memory_demo.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/examples/framework_integrations.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/examples/langchain_memory.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/examples/sharded_memory.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/install.bat +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/install.sh +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/requirements-optional.txt +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/requirements.txt +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/setup.cfg +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_agent_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_ann_index_curve_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_api_process_persistence.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_benchmark_charts.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_benchmark_registry.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_benchmark_report.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_core_persistence.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_dynamic_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_examples.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_field_graph.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_field_graph_integration.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_field_memory_dynamics_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_framework_adapters.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_import_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_indexes_encoders.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_langchain_integration.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_locomo_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_long_memory_evidence_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_longmemeval_answer_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_longmemeval_memory_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_observability.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_open_retrieval_benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_packaging_files.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_postgres_storage.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_semantic_and_latency.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_sharding.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/__main__.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/benchmark.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/encoders.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/field_graph.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/importers.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/indexes.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/__init__.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/autogen.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/crewai.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/langchain.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/langgraph.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/llamaindex.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/observability.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/sharding.py +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/dependency_links.txt +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/entry_points.txt +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/requires.txt +0 -0
- {wavemind-2.1.1 → wavemind-2.2.1}/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.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Local-first dynamic memory field with vector search and wave-field re-ranking
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/CaspianG/wavemind
|
|
@@ -68,6 +68,7 @@ users or projects isolated.
|
|
|
68
68
|
|
|
69
69
|
[Quick Start](#quick-start) |
|
|
70
70
|
[CLI](#cli-cheat-sheet) |
|
|
71
|
+
[Studio](#wavemind-studio) |
|
|
71
72
|
[Python Example](#python-example) |
|
|
72
73
|
[HTTP Example](#http-example) |
|
|
73
74
|
[Where Data Lives](#where-data-lives) |
|
|
@@ -139,6 +140,12 @@ Need a reminder after install?
|
|
|
139
140
|
wavemind quickstart
|
|
140
141
|
```
|
|
141
142
|
|
|
143
|
+
Want to see and manage memory in a browser?
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
wavemind studio
|
|
147
|
+
```
|
|
148
|
+
|
|
142
149
|
By default, WaveMind creates `wavemind.sqlite3` in the current working
|
|
143
150
|
directory. That file is the local source of truth. Keep it out of git and back
|
|
144
151
|
it up like application state.
|
|
@@ -152,6 +159,7 @@ Start here if you only want to use WaveMind from the terminal:
|
|
|
152
159
|
| Show first-run help | `wavemind quickstart` |
|
|
153
160
|
| Store a memory | `wavemind remember "Andrey prefers short answers" --namespace user:42` |
|
|
154
161
|
| Search memory | `wavemind query "answer style" --namespace user:42` |
|
|
162
|
+
| Open local dashboard | `wavemind studio` |
|
|
155
163
|
| See stored state | `wavemind stats --namespace user:42` |
|
|
156
164
|
| Delete a namespace | `wavemind forget --namespace user:42` |
|
|
157
165
|
| Import notes | `wavemind import ./notes.txt --namespace project:alpha` |
|
|
@@ -161,6 +169,33 @@ Start here if you only want to use WaveMind from the terminal:
|
|
|
161
169
|
After this point, choose the integration path you need: Python, HTTP, LangChain,
|
|
162
170
|
framework adapters, benchmarks, or production deployment.
|
|
163
171
|
|
|
172
|
+
## WaveMind Studio
|
|
173
|
+
|
|
174
|
+
WaveMind Studio is the built-in local dashboard. It runs on top of the same
|
|
175
|
+
FastAPI app and SQLite database as the CLI:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
wavemind studio
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
It opens `http://127.0.0.1:8000/studio` and gives you:
|
|
182
|
+
|
|
183
|
+
| View | What it is for |
|
|
184
|
+
|---|---|
|
|
185
|
+
| Memory map | See field energy as a heatmap. |
|
|
186
|
+
| Namespace explorer | Inspect memories per user, project, agent, or tenant. |
|
|
187
|
+
| Live query tester | Test recall before wiring it into an app. |
|
|
188
|
+
| Feedback buttons | Mark recalled memories as useful or not useful. |
|
|
189
|
+
| Import/export | Import local files and export a namespace snapshot. |
|
|
190
|
+
| Backup | Create SQLite backups from the browser. |
|
|
191
|
+
| Conflict visualizer | Inspect correction groups when memories disagree. |
|
|
192
|
+
|
|
193
|
+
For a server-safe local bind:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
wavemind --db ./state/wavemind.sqlite3 studio --host 127.0.0.1 --port 8000
|
|
197
|
+
```
|
|
198
|
+
|
|
164
199
|
## Python Example
|
|
165
200
|
|
|
166
201
|
```python
|
|
@@ -18,6 +18,7 @@ users or projects isolated.
|
|
|
18
18
|
|
|
19
19
|
[Quick Start](#quick-start) |
|
|
20
20
|
[CLI](#cli-cheat-sheet) |
|
|
21
|
+
[Studio](#wavemind-studio) |
|
|
21
22
|
[Python Example](#python-example) |
|
|
22
23
|
[HTTP Example](#http-example) |
|
|
23
24
|
[Where Data Lives](#where-data-lives) |
|
|
@@ -89,6 +90,12 @@ Need a reminder after install?
|
|
|
89
90
|
wavemind quickstart
|
|
90
91
|
```
|
|
91
92
|
|
|
93
|
+
Want to see and manage memory in a browser?
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
wavemind studio
|
|
97
|
+
```
|
|
98
|
+
|
|
92
99
|
By default, WaveMind creates `wavemind.sqlite3` in the current working
|
|
93
100
|
directory. That file is the local source of truth. Keep it out of git and back
|
|
94
101
|
it up like application state.
|
|
@@ -102,6 +109,7 @@ Start here if you only want to use WaveMind from the terminal:
|
|
|
102
109
|
| Show first-run help | `wavemind quickstart` |
|
|
103
110
|
| Store a memory | `wavemind remember "Andrey prefers short answers" --namespace user:42` |
|
|
104
111
|
| Search memory | `wavemind query "answer style" --namespace user:42` |
|
|
112
|
+
| Open local dashboard | `wavemind studio` |
|
|
105
113
|
| See stored state | `wavemind stats --namespace user:42` |
|
|
106
114
|
| Delete a namespace | `wavemind forget --namespace user:42` |
|
|
107
115
|
| Import notes | `wavemind import ./notes.txt --namespace project:alpha` |
|
|
@@ -111,6 +119,33 @@ Start here if you only want to use WaveMind from the terminal:
|
|
|
111
119
|
After this point, choose the integration path you need: Python, HTTP, LangChain,
|
|
112
120
|
framework adapters, benchmarks, or production deployment.
|
|
113
121
|
|
|
122
|
+
## WaveMind Studio
|
|
123
|
+
|
|
124
|
+
WaveMind Studio is the built-in local dashboard. It runs on top of the same
|
|
125
|
+
FastAPI app and SQLite database as the CLI:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
wavemind studio
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
It opens `http://127.0.0.1:8000/studio` and gives you:
|
|
132
|
+
|
|
133
|
+
| View | What it is for |
|
|
134
|
+
|---|---|
|
|
135
|
+
| Memory map | See field energy as a heatmap. |
|
|
136
|
+
| Namespace explorer | Inspect memories per user, project, agent, or tenant. |
|
|
137
|
+
| Live query tester | Test recall before wiring it into an app. |
|
|
138
|
+
| Feedback buttons | Mark recalled memories as useful or not useful. |
|
|
139
|
+
| Import/export | Import local files and export a namespace snapshot. |
|
|
140
|
+
| Backup | Create SQLite backups from the browser. |
|
|
141
|
+
| Conflict visualizer | Inspect correction groups when memories disagree. |
|
|
142
|
+
|
|
143
|
+
For a server-safe local bind:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
wavemind --db ./state/wavemind.sqlite3 studio --host 127.0.0.1 --port 8000
|
|
147
|
+
```
|
|
148
|
+
|
|
114
149
|
## Python Example
|
|
115
150
|
|
|
116
151
|
```python
|
|
@@ -119,6 +119,69 @@ def test_fastapi_query_accepts_query_alias(tmp_path):
|
|
|
119
119
|
mind.close()
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def test_fastapi_studio_dashboard_state_heatmap_and_feedback(tmp_path):
|
|
123
|
+
mind = WaveMind(
|
|
124
|
+
db_path=tmp_path / "studio.sqlite3",
|
|
125
|
+
width=32,
|
|
126
|
+
height=32,
|
|
127
|
+
layers=2,
|
|
128
|
+
encoder=HashingTextEncoder(vector_dim=64),
|
|
129
|
+
)
|
|
130
|
+
try:
|
|
131
|
+
memory_id = mind.remember(
|
|
132
|
+
"Andrey prefers WaveMind Studio for visual memory inspection",
|
|
133
|
+
namespace="studio",
|
|
134
|
+
tags=["ui"],
|
|
135
|
+
metadata={"conflict_group": "preference.ui"},
|
|
136
|
+
)
|
|
137
|
+
mind.remember(
|
|
138
|
+
"Andrey prefers terminal-only memory inspection",
|
|
139
|
+
namespace="studio",
|
|
140
|
+
tags=["ui"],
|
|
141
|
+
metadata={"conflict_group": "preference.ui"},
|
|
142
|
+
)
|
|
143
|
+
before = mind.store.get(memory_id)
|
|
144
|
+
assert before is not None
|
|
145
|
+
|
|
146
|
+
with TestClient(create_app(mind=mind)) as client:
|
|
147
|
+
page = client.get("/studio")
|
|
148
|
+
assert page.status_code == 200
|
|
149
|
+
assert "WaveMind Studio" in page.text
|
|
150
|
+
|
|
151
|
+
state = client.get("/studio/state", params={"namespace": "studio"})
|
|
152
|
+
assert state.status_code == 200
|
|
153
|
+
payload = state.json()
|
|
154
|
+
assert "studio" in payload["namespaces"]
|
|
155
|
+
assert payload["stats"]["active_memories"] == 2
|
|
156
|
+
assert payload["memories"][0]["namespace"] == "studio"
|
|
157
|
+
assert payload["conflict_groups"][0]["group"] == "preference.ui"
|
|
158
|
+
|
|
159
|
+
fallback_state = client.get("/studio/state", params={"namespace": "default"})
|
|
160
|
+
assert fallback_state.status_code == 200
|
|
161
|
+
assert fallback_state.json()["selected_namespace"] == "studio"
|
|
162
|
+
assert fallback_state.json()["memories"][0]["namespace"] == "studio"
|
|
163
|
+
|
|
164
|
+
heatmap = client.get("/studio/heatmap", params={"bins": 8})
|
|
165
|
+
assert heatmap.status_code == 200
|
|
166
|
+
heatmap_payload = heatmap.json()
|
|
167
|
+
assert heatmap_payload["bins"] == 8
|
|
168
|
+
assert len(heatmap_payload["values"]) == 64
|
|
169
|
+
assert max(heatmap_payload["values"]) <= 1.0
|
|
170
|
+
|
|
171
|
+
feedback = client.post(
|
|
172
|
+
"/studio/feedback",
|
|
173
|
+
json={"id": memory_id, "useful": True, "strength": 0.5},
|
|
174
|
+
)
|
|
175
|
+
assert feedback.status_code == 200
|
|
176
|
+
after = mind.store.get(memory_id)
|
|
177
|
+
assert after is not None
|
|
178
|
+
assert after.priority > before.priority
|
|
179
|
+
assert after.access_count == before.access_count + 1
|
|
180
|
+
assert mind.audit_events(namespace="studio", action="feedback", limit=1)
|
|
181
|
+
finally:
|
|
182
|
+
mind.close()
|
|
183
|
+
|
|
184
|
+
|
|
122
185
|
def test_fastapi_version_matches_package_version():
|
|
123
186
|
mind = WaveMind(
|
|
124
187
|
db_path=None,
|
|
@@ -118,6 +118,7 @@ def test_legacy_script_delegates_to_new_cli(tmp_path):
|
|
|
118
118
|
check=True,
|
|
119
119
|
)
|
|
120
120
|
assert "WaveMind" in result.stdout
|
|
121
|
+
assert "studio" in result.stdout
|
|
121
122
|
|
|
122
123
|
|
|
123
124
|
def test_cli_benchmark_seeds_all_synthetic_cases(tmp_path):
|
|
@@ -145,6 +146,7 @@ def test_cli_quickstart_prints_first_run_commands():
|
|
|
145
146
|
assert "python -m pip install wavemind" in result.stdout
|
|
146
147
|
assert 'wavemind remember "Andrey is a trader" --namespace demo' in result.stdout
|
|
147
148
|
assert 'wavemind query "What does Andrey do?" --namespace demo' in result.stdout
|
|
149
|
+
assert "wavemind studio" in result.stdout
|
|
148
150
|
|
|
149
151
|
|
|
150
152
|
def test_cli_version_prints_package_version():
|
|
@@ -9,7 +9,7 @@ from threading import Lock
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from fastapi import Body, Depends, FastAPI, HTTPException, Query, Request
|
|
12
|
-
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
12
|
+
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
|
13
13
|
from pydantic import AliasChoices, BaseModel, Field
|
|
14
14
|
|
|
15
15
|
from . import __version__
|
|
@@ -17,6 +17,7 @@ from .core import WaveMind
|
|
|
17
17
|
from .encoders import create_text_encoder
|
|
18
18
|
from .importers import import_path
|
|
19
19
|
from .observability import configure_observability, instrument_fastapi_app
|
|
20
|
+
from .studio import STUDIO_HTML, field_heatmap, studio_snapshot
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger("wavemind.api")
|
|
@@ -205,6 +206,12 @@ class ObservabilityResponse(BaseModel):
|
|
|
205
206
|
reason: str | None = None
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
class FeedbackRequest(BaseModel):
|
|
210
|
+
id: int
|
|
211
|
+
useful: bool = True
|
|
212
|
+
strength: float = Field(default=0.25, ge=0.0, le=10.0)
|
|
213
|
+
|
|
214
|
+
|
|
208
215
|
def _metrics_text(stats: dict[str, Any]) -> str:
|
|
209
216
|
metric_names = {
|
|
210
217
|
"active_memories": "wavemind_active_memories",
|
|
@@ -292,6 +299,32 @@ def create_app(mind: WaveMind | None = None) -> FastAPI:
|
|
|
292
299
|
)
|
|
293
300
|
return await call_next(request)
|
|
294
301
|
|
|
302
|
+
@app.get("/studio", response_class=HTMLResponse, include_in_schema=False)
|
|
303
|
+
def studio() -> HTMLResponse:
|
|
304
|
+
return HTMLResponse(STUDIO_HTML)
|
|
305
|
+
|
|
306
|
+
@app.get("/studio/state", dependencies=[Depends(require_role("read"))])
|
|
307
|
+
def studio_state(
|
|
308
|
+
namespace: str | None = None,
|
|
309
|
+
limit: int = Query(default=200, ge=0, le=1000),
|
|
310
|
+
):
|
|
311
|
+
return studio_snapshot(app.state.mind, namespace=namespace, limit=limit)
|
|
312
|
+
|
|
313
|
+
@app.get("/studio/heatmap", dependencies=[Depends(require_role("read"))])
|
|
314
|
+
def studio_heatmap(bins: int = Query(default=18, ge=4, le=48)):
|
|
315
|
+
return field_heatmap(app.state.mind, bins=bins)
|
|
316
|
+
|
|
317
|
+
@app.post("/studio/feedback", dependencies=[Depends(require_role("write"))])
|
|
318
|
+
def studio_feedback(request: FeedbackRequest):
|
|
319
|
+
accepted = app.state.mind.feedback(
|
|
320
|
+
request.id,
|
|
321
|
+
useful=request.useful,
|
|
322
|
+
strength=request.strength,
|
|
323
|
+
)
|
|
324
|
+
if not accepted:
|
|
325
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
326
|
+
return {"ok": True}
|
|
327
|
+
|
|
295
328
|
@app.post("/remember", response_model=RememberResponse, dependencies=[Depends(require_role("write"))])
|
|
296
329
|
def remember(request: RememberRequest) -> RememberResponse:
|
|
297
330
|
id = app.state.mind.remember(
|
|
@@ -128,6 +128,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
128
128
|
serve.add_argument("--host", default="0.0.0.0")
|
|
129
129
|
serve.add_argument("--port", type=int, default=8000)
|
|
130
130
|
|
|
131
|
+
studio = sub.add_parser("studio", help="Run local WaveMind Studio dashboard")
|
|
132
|
+
studio.add_argument("--host", default="127.0.0.1")
|
|
133
|
+
studio.add_argument("--port", type=int, default=8000)
|
|
134
|
+
studio.add_argument("--no-open", action="store_true")
|
|
135
|
+
|
|
131
136
|
sub.add_parser("test", help="Run pytest suite")
|
|
132
137
|
return parser
|
|
133
138
|
|
|
@@ -177,6 +182,7 @@ Where data goes:
|
|
|
177
182
|
|
|
178
183
|
Useful next commands:
|
|
179
184
|
wavemind --help
|
|
185
|
+
wavemind studio
|
|
180
186
|
wavemind import ./notes.txt --namespace demo
|
|
181
187
|
wavemind serve --host 127.0.0.1 --port 8000
|
|
182
188
|
wavemind forget --namespace demo
|
|
@@ -247,6 +253,22 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
247
253
|
uvicorn.run(create_app(mind=make_mind(args)), host=args.host, port=args.port)
|
|
248
254
|
return 0
|
|
249
255
|
|
|
256
|
+
if args.command == "studio":
|
|
257
|
+
import webbrowser
|
|
258
|
+
from threading import Timer
|
|
259
|
+
|
|
260
|
+
import uvicorn
|
|
261
|
+
|
|
262
|
+
from .api import create_app
|
|
263
|
+
|
|
264
|
+
open_host = "127.0.0.1" if args.host in {"0.0.0.0", "::"} else args.host
|
|
265
|
+
url = f"http://{open_host}:{args.port}/studio"
|
|
266
|
+
print(f"WaveMind Studio: {url}")
|
|
267
|
+
if not args.no_open:
|
|
268
|
+
Timer(1.0, lambda: webbrowser.open(url)).start()
|
|
269
|
+
uvicorn.run(create_app(mind=make_mind(args)), host=args.host, port=args.port)
|
|
270
|
+
return 0
|
|
271
|
+
|
|
250
272
|
if args.command == "restore":
|
|
251
273
|
destination = Path(args.destination) if args.destination else (
|
|
252
274
|
Path(args.db) if args.db else Path.cwd() / "wavemind.sqlite3"
|
|
@@ -439,6 +439,45 @@ class WaveMind:
|
|
|
439
439
|
)
|
|
440
440
|
return len(records)
|
|
441
441
|
|
|
442
|
+
def feedback(
|
|
443
|
+
self,
|
|
444
|
+
id: int,
|
|
445
|
+
useful: bool = True,
|
|
446
|
+
strength: float = 0.25,
|
|
447
|
+
) -> bool:
|
|
448
|
+
record = self._records_by_id.get(int(id))
|
|
449
|
+
if record is None or record.is_expired:
|
|
450
|
+
return False
|
|
451
|
+
delta = abs(float(strength))
|
|
452
|
+
if useful:
|
|
453
|
+
record.priority += delta
|
|
454
|
+
record.access_count += 1
|
|
455
|
+
self.field.feed(record.pattern, strength=delta)
|
|
456
|
+
else:
|
|
457
|
+
record.priority = max(0.0, record.priority - delta)
|
|
458
|
+
self.field.forget(record.pattern, strength=delta)
|
|
459
|
+
update_memory_state = getattr(self.store, "update_memory_state", None)
|
|
460
|
+
if callable(update_memory_state):
|
|
461
|
+
update_memory_state(
|
|
462
|
+
record.id,
|
|
463
|
+
priority=record.priority,
|
|
464
|
+
access_count=record.access_count,
|
|
465
|
+
)
|
|
466
|
+
self.field.evolve(1)
|
|
467
|
+
self._refresh_field_magnitude()
|
|
468
|
+
self.store.log_audit_event(
|
|
469
|
+
"feedback",
|
|
470
|
+
namespace=record.namespace,
|
|
471
|
+
memory_id=record.id,
|
|
472
|
+
metadata={
|
|
473
|
+
"useful": bool(useful),
|
|
474
|
+
"strength": delta,
|
|
475
|
+
"priority": float(record.priority),
|
|
476
|
+
"access_count": int(record.access_count),
|
|
477
|
+
},
|
|
478
|
+
)
|
|
479
|
+
return True
|
|
480
|
+
|
|
442
481
|
def save(
|
|
443
482
|
self,
|
|
444
483
|
backup_path: str | Path | None = None,
|
|
@@ -244,6 +244,32 @@ class SQLiteMemoryStore:
|
|
|
244
244
|
)
|
|
245
245
|
self.conn.commit()
|
|
246
246
|
|
|
247
|
+
def update_memory_state(
|
|
248
|
+
self,
|
|
249
|
+
id: int,
|
|
250
|
+
*,
|
|
251
|
+
priority: float | None = None,
|
|
252
|
+
access_count: int | None = None,
|
|
253
|
+
) -> None:
|
|
254
|
+
fields = []
|
|
255
|
+
params: list[Any] = []
|
|
256
|
+
if priority is not None:
|
|
257
|
+
fields.append("priority = ?")
|
|
258
|
+
params.append(float(priority))
|
|
259
|
+
if access_count is not None:
|
|
260
|
+
fields.append("access_count = ?")
|
|
261
|
+
params.append(int(access_count))
|
|
262
|
+
if not fields:
|
|
263
|
+
return
|
|
264
|
+
fields.append("updated_at = ?")
|
|
265
|
+
params.append(time.time())
|
|
266
|
+
params.append(int(id))
|
|
267
|
+
self.conn.execute(
|
|
268
|
+
f"UPDATE memories SET {', '.join(fields)} WHERE id = ?",
|
|
269
|
+
params,
|
|
270
|
+
)
|
|
271
|
+
self.conn.commit()
|
|
272
|
+
|
|
247
273
|
def log_audit_event(
|
|
248
274
|
self,
|
|
249
275
|
action: str,
|
|
@@ -643,6 +669,31 @@ class PostgresMemoryStore:
|
|
|
643
669
|
(float(priority_delta), time.time(), int(id)),
|
|
644
670
|
)
|
|
645
671
|
|
|
672
|
+
def update_memory_state(
|
|
673
|
+
self,
|
|
674
|
+
id: int,
|
|
675
|
+
*,
|
|
676
|
+
priority: float | None = None,
|
|
677
|
+
access_count: int | None = None,
|
|
678
|
+
) -> None:
|
|
679
|
+
fields = []
|
|
680
|
+
params: list[Any] = []
|
|
681
|
+
if priority is not None:
|
|
682
|
+
fields.append("priority = %s")
|
|
683
|
+
params.append(float(priority))
|
|
684
|
+
if access_count is not None:
|
|
685
|
+
fields.append("access_count = %s")
|
|
686
|
+
params.append(int(access_count))
|
|
687
|
+
if not fields:
|
|
688
|
+
return
|
|
689
|
+
fields.append("updated_at = %s")
|
|
690
|
+
params.append(time.time())
|
|
691
|
+
params.append(int(id))
|
|
692
|
+
self.conn.execute(
|
|
693
|
+
f"UPDATE {self.memories_table} SET {', '.join(fields)} WHERE id = %s",
|
|
694
|
+
params,
|
|
695
|
+
)
|
|
696
|
+
|
|
646
697
|
def log_audit_event(
|
|
647
698
|
self,
|
|
648
699
|
action: str,
|