From 9a8f0580081f55700ad7d6041f9a652b8abf1803 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Wed, 20 May 2026 23:41:39 +0800 Subject: [PATCH] Fix memory page to use local store --- duck_core/api.py | 9 +---- duck_core/web/static/app.js | 56 +++++++++++++++++++++++++++-- duck_core/web/templates/memory.html | 33 ++++++++++++++++- tests/smoke/test_memory_store.py | 9 +++++ 4 files changed, 96 insertions(+), 11 deletions(-) diff --git a/duck_core/api.py b/duck_core/api.py index 727dbca..3dcc1bc 100644 --- a/duck_core/api.py +++ b/duck_core/api.py @@ -16,7 +16,6 @@ from duck_core.config import get_settings from duck_core.conversations.store import ConversationStore from duck_core.events.store import EventStore from duck_core.experience.recorder import ExperienceRecorder -from duck_core.memory.vector_memory import EmbeddingsUnavailableError, VectorMemory from duck_core.memory.store import MemoryStore from duck_core.model_client import ModelClient from duck_core.runtime_loop import RuntimeLoop @@ -79,7 +78,6 @@ def create_app() -> FastAPI: runtime = RuntimeLoop(task_store, event_store, model_client, approval_service=approvals) skills = SkillRegistry("skills") experience = ExperienceRecorder(settings.db_path) - memory = VectorMemory(settings.qdrant_url, embeddings_base_url=None) memory_store = MemoryStore(settings.db_path) @app.on_event("startup") @@ -867,12 +865,7 @@ def create_app() -> FastAPI: q: str, workspace: str | None = None, limit: int = 20 ) -> dict[str, Any]: local_results = await memory_store.search(q, workspace=workspace, limit=limit) - if local_results: - return {"results": [record.model_dump() for record in local_results]} - try: - return {"results": await memory.search_memory(q)} - except EmbeddingsUnavailableError as exc: - return {"results": [], "warning": str(exc)} + return {"results": [record.model_dump() for record in local_results]} return app diff --git a/duck_core/web/static/app.js b/duck_core/web/static/app.js index c376ba7..4d1817b 100644 --- a/duck_core/web/static/app.js +++ b/duck_core/web/static/app.js @@ -929,10 +929,62 @@ document.querySelector("#approvals")?.addEventListener("click", async (event) => document.querySelector("#memory-search")?.addEventListener("click", async () => { const q = document.querySelector("#memory-query").value; - document.querySelector("#memory-results").textContent = - JSON.stringify(await jsonFetch(`/v1/memory/search?q=${encodeURIComponent(q)}`), null, 2); + await renderMemoryPageResults(q); }); +document.querySelector("#memory-list-all")?.addEventListener("click", async () => { + await renderMemoryPageResults(""); +}); + +document.querySelector("#memory-page-form")?.addEventListener("submit", async (event) => { + event.preventDefault(); + const textInput = document.querySelector("#memory-page-text"); + const workspaceInput = document.querySelector("#memory-page-workspace"); + const text = textInput?.value.trim() || ""; + if (!text) return; + await jsonFetch("/v1/memory", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + text, + workspace: workspaceInput?.value.trim() || null, + memory_type: "note", + importance: 0.6, + }), + }); + textInput.value = ""; + await renderMemoryPageResults(""); +}); + +async function renderMemoryPageResults(query) { + const container = document.querySelector("#memory-results"); + if (!container) return; + const payload = query.trim() + ? await jsonFetch(`/v1/memory/search?q=${encodeURIComponent(query.trim())}`) + : await jsonFetch("/v1/memory?limit=100"); + const results = payload.results || []; + container.innerHTML = ""; + if (!results.length) { + const empty = document.createElement("p"); + empty.className = "compact-empty"; + empty.textContent = "No memories found."; + container.append(empty); + return; + } + for (const memory of results) { + const item = document.createElement("article"); + item.className = "memory-item"; + const text = document.createElement("p"); + text.textContent = memory.text; + const meta = document.createElement("span"); + meta.textContent = `${memory.scope || "memory"} · ${memory.workspace || "global"} · ${memory.memory_type || "note"}`; + item.append(text, meta); + container.append(item); + } +} + +renderMemoryPageResults("").catch(console.error); + bindChat(); checkRuntime(); loadSimplePages().catch(console.error); diff --git a/duck_core/web/templates/memory.html b/duck_core/web/templates/memory.html index 2053b08..a40a5d1 100644 --- a/duck_core/web/templates/memory.html +++ b/duck_core/web/templates/memory.html @@ -1,2 +1,33 @@ -DuckLM Memory

Memory

+ + + + DuckLM Memory + + + +
+
+
+

Memory

+

Local DuckLM memory stored in SQLite.

+
+ Chat +
+
+
+ + + +
+
+ + + +
+
+
+
+ + + diff --git a/tests/smoke/test_memory_store.py b/tests/smoke/test_memory_store.py index 83dd7a4..6b4d590 100644 --- a/tests/smoke/test_memory_store.py +++ b/tests/smoke/test_memory_store.py @@ -44,6 +44,15 @@ def test_memory_api_stores_workspace_scoped_notes(tmp_path, monkeypatch): assert "Different workspace note." not in str(search) +def test_memory_search_returns_empty_local_result_without_vector_warning(tmp_path, monkeypatch): + monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3")) + client = TestClient(create_app()) + + response = client.get("/v1/memory/search", params={"q": "missing memory"}).json() + + assert response == {"results": []} + + async def test_memory_store_searches_text_and_metadata(tmp_path): store = MemoryStore(str(tmp_path / "duck.sqlite3")) await store.init()