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.
Files changed (112) hide show
  1. {wavemind-2.1.1 → wavemind-2.2.1}/PKG-INFO +36 -1
  2. {wavemind-2.1.1 → wavemind-2.2.1}/README.md +35 -0
  3. {wavemind-2.1.1 → wavemind-2.2.1}/docker-compose.yml +1 -1
  4. {wavemind-2.1.1 → wavemind-2.2.1}/pyproject.toml +1 -1
  5. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_api.py +63 -0
  6. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_cli_smoke.py +2 -0
  7. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/__init__.py +1 -1
  8. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/api.py +34 -1
  9. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/cli.py +22 -0
  10. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/core.py +39 -0
  11. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/storage.py +51 -0
  12. wavemind-2.2.1/wavemind/studio.py +707 -0
  13. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/PKG-INFO +36 -1
  14. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/SOURCES.txt +1 -0
  15. {wavemind-2.1.1 → wavemind-2.2.1}/CONTRIBUTING.md +0 -0
  16. {wavemind-2.1.1 → wavemind-2.2.1}/Dockerfile +0 -0
  17. {wavemind-2.1.1 → wavemind-2.2.1}/LICENSE +0 -0
  18. {wavemind-2.1.1 → wavemind-2.2.1}/MANIFEST.in +0 -0
  19. {wavemind-2.1.1 → wavemind-2.2.1}/SECURITY.md +0 -0
  20. {wavemind-2.1.1 → wavemind-2.2.1}/SUPPORT.md +0 -0
  21. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/BENCHMARK_REPORT.md +0 -0
  22. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/agent_memory_benchmark.py +0 -0
  23. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/agent_memory_results.json +0 -0
  24. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/ann_index_curve_benchmark.py +0 -0
  25. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/ann_index_curve_results.json +0 -0
  26. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/benchmark_matrix_results.json +0 -0
  27. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/benchmark_registry.py +0 -0
  28. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/dynamic_memory_benchmark.py +0 -0
  29. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/dynamic_memory_results.json +0 -0
  30. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/field_memory_dynamics_benchmark.py +0 -0
  31. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/field_memory_dynamics_results.json +0 -0
  32. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/locomo_evidence_results.json +0 -0
  33. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/locomo_memory_benchmark.py +0 -0
  34. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/locomo_sentence_evidence_results.json +0 -0
  35. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/long_memory_evidence_benchmark.py +0 -0
  36. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/long_memory_evidence_results.json +0 -0
  37. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_answer_benchmark.py +0 -0
  38. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_answer_extractive_20_results.json +0 -0
  39. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_evidence_50_results.json +0 -0
  40. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_evidence_results.json +0 -0
  41. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/longmemeval_memory_benchmark.py +0 -0
  42. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/open_retrieval_benchmark.py +0 -0
  43. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/open_retrieval_scifact_results.json +0 -0
  44. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/render_benchmark_charts.py +0 -0
  45. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/render_benchmark_report.py +0 -0
  46. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/ru_sentences_benchmark.py +0 -0
  47. {wavemind-2.1.1 → wavemind-2.2.1}/benchmarks/wavemind_capacity_results.json +0 -0
  48. {wavemind-2.1.1 → wavemind-2.2.1}/docs/CHROMA_MIGRATION.md +0 -0
  49. {wavemind-2.1.1 → wavemind-2.2.1}/docs/DEMO_SCRIPT.md +0 -0
  50. {wavemind-2.1.1 → wavemind-2.2.1}/docs/LAUNCH_KIT.md +0 -0
  51. {wavemind-2.1.1 → wavemind-2.2.1}/docs/PROJECT_BOARD.md +0 -0
  52. {wavemind-2.1.1 → wavemind-2.2.1}/docs/RELEASE.md +0 -0
  53. {wavemind-2.1.1 → wavemind-2.2.1}/docs/ROADMAP.md +0 -0
  54. {wavemind-2.1.1 → wavemind-2.2.1}/docs/RU_LAUNCH_POSTS.md +0 -0
  55. {wavemind-2.1.1 → wavemind-2.2.1}/docs/USE_CASES.md +0 -0
  56. {wavemind-2.1.1 → wavemind-2.2.1}/docs/assets/benchmark-summary.svg +0 -0
  57. {wavemind-2.1.1 → wavemind-2.2.1}/docs/assets/wavemind-social-card.svg +0 -0
  58. {wavemind-2.1.1 → wavemind-2.2.1}/examples/agent_with_memory.py +0 -0
  59. {wavemind-2.1.1 → wavemind-2.2.1}/examples/demo.py +0 -0
  60. {wavemind-2.1.1 → wavemind-2.2.1}/examples/dynamic_memory_demo.py +0 -0
  61. {wavemind-2.1.1 → wavemind-2.2.1}/examples/framework_integrations.py +0 -0
  62. {wavemind-2.1.1 → wavemind-2.2.1}/examples/langchain_memory.py +0 -0
  63. {wavemind-2.1.1 → wavemind-2.2.1}/examples/sharded_memory.py +0 -0
  64. {wavemind-2.1.1 → wavemind-2.2.1}/install.bat +0 -0
  65. {wavemind-2.1.1 → wavemind-2.2.1}/install.sh +0 -0
  66. {wavemind-2.1.1 → wavemind-2.2.1}/requirements-optional.txt +0 -0
  67. {wavemind-2.1.1 → wavemind-2.2.1}/requirements.txt +0 -0
  68. {wavemind-2.1.1 → wavemind-2.2.1}/setup.cfg +0 -0
  69. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_agent_memory_benchmark.py +0 -0
  70. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_ann_index_curve_benchmark.py +0 -0
  71. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_api_process_persistence.py +0 -0
  72. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_benchmark_charts.py +0 -0
  73. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_benchmark_registry.py +0 -0
  74. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_benchmark_report.py +0 -0
  75. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_core_persistence.py +0 -0
  76. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_dynamic_memory_benchmark.py +0 -0
  77. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_examples.py +0 -0
  78. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_field_graph.py +0 -0
  79. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_field_graph_integration.py +0 -0
  80. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_field_memory_dynamics_benchmark.py +0 -0
  81. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_framework_adapters.py +0 -0
  82. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_import_benchmark.py +0 -0
  83. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_indexes_encoders.py +0 -0
  84. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_langchain_integration.py +0 -0
  85. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_locomo_memory_benchmark.py +0 -0
  86. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_long_memory_evidence_benchmark.py +0 -0
  87. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_longmemeval_answer_benchmark.py +0 -0
  88. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_longmemeval_memory_benchmark.py +0 -0
  89. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_observability.py +0 -0
  90. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_open_retrieval_benchmark.py +0 -0
  91. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_packaging_files.py +0 -0
  92. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_postgres_storage.py +0 -0
  93. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_semantic_and_latency.py +0 -0
  94. {wavemind-2.1.1 → wavemind-2.2.1}/tests/test_sharding.py +0 -0
  95. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/__main__.py +0 -0
  96. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/benchmark.py +0 -0
  97. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/encoders.py +0 -0
  98. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/field_graph.py +0 -0
  99. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/importers.py +0 -0
  100. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/indexes.py +0 -0
  101. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/__init__.py +0 -0
  102. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/autogen.py +0 -0
  103. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/crewai.py +0 -0
  104. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/langchain.py +0 -0
  105. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/langgraph.py +0 -0
  106. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/integrations/llamaindex.py +0 -0
  107. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/observability.py +0 -0
  108. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind/sharding.py +0 -0
  109. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/dependency_links.txt +0 -0
  110. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/entry_points.txt +0 -0
  111. {wavemind-2.1.1 → wavemind-2.2.1}/wavemind.egg-info/requires.txt +0 -0
  112. {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.1.1
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
@@ -4,7 +4,7 @@ services:
4
4
  context: .
5
5
  args:
6
6
  INSTALL_OPTIONAL: "false"
7
- image: wavemind:2.1.1
7
+ image: wavemind:2.2.1
8
8
  restart: unless-stopped
9
9
  environment:
10
10
  WAVEMIND_DB: /data/wavemind.sqlite3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wavemind"
7
- version = "2.1.1"
7
+ version = "2.2.1"
8
8
  description = "Local-first dynamic memory field with vector search and wave-field re-ranking"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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():
@@ -17,7 +17,7 @@ from .storage import (
17
17
  create_memory_store,
18
18
  )
19
19
 
20
- __version__ = "2.1.1"
20
+ __version__ = "2.2.1"
21
21
 
22
22
  __all__ = [
23
23
  "FieldProjector",
@@ -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,