Fix memory page to use local store

This commit is contained in:
mirivlad 2026-05-20 23:41:39 +08:00
parent 061cc9225a
commit 9a8f058008
4 changed files with 96 additions and 11 deletions

View File

@ -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 app

View File

@ -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);

View File

@ -1,2 +1,33 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>DuckLM Memory</title><link rel="stylesheet" href="/static/style.css"></head><body><main class="shell"><h1>Memory</h1><input id="memory-query" placeholder="Search memory"><button id="memory-search">Search</button><pre id="memory-results"></pre><script src="/static/app.js"></script></main></body></html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DuckLM Memory</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<main class="simple-page">
<header class="simple-header">
<div>
<h1>Memory</h1>
<p>Local DuckLM memory stored in SQLite.</p>
</div>
<a class="secondary-button" href="/">Chat</a>
</header>
<section class="memory-page-panel">
<form id="memory-page-form" class="memory-form">
<input id="memory-page-text" placeholder="Add memory" autocomplete="off">
<input id="memory-page-workspace" placeholder="Workspace, optional" autocomplete="off">
<button type="submit">Add</button>
</form>
<div class="memory-search-row">
<input id="memory-query" placeholder="Search memory" autocomplete="off">
<button id="memory-search" type="button">Search</button>
<button id="memory-list-all" type="button">All</button>
</div>
<div id="memory-results" class="memory-list"></div>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>

View File

@ -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()