forked from enderofwings/NexusOS
feat: workspaces, agentic action tools, local Whisper STT, vec recall
- Projects/workspaces: documents grouped into projects; chat RAG scopes to the active project. Switcher in the Documents page. - Agentic action tools: web_search, fetch_url, and remember (first write tool), allowlist-gated per playbook. - Local Whisper STT (faster-whisper, no torch): on-device dictation replacing the browser Web Speech API. POST /stt + GET /stt/status; browser fallback. - Vector index extended to conversation recall (message_vectors), with the brute-force cosine scan kept as the fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,62 @@ def test_search_empty_query_returns_nothing():
|
||||
assert asyncio.run(s.search_documents("", _fake_embed)) == []
|
||||
|
||||
|
||||
def test_conversation_recall_uses_vec_and_matches_brute_force():
|
||||
s = _store()
|
||||
if not s.vec_enabled:
|
||||
import pytest
|
||||
pytest.skip("sqlite-vec not loadable on this host")
|
||||
|
||||
async def run():
|
||||
s.create_conversation("c1")
|
||||
s.add_message("c1", "user", "tell me about lego star wars")
|
||||
s.add_message("c1", "assistant", "lego star wars is a fun game")
|
||||
s.create_conversation("c2")
|
||||
s.add_message("c2", "user", "gpu vega vram notes")
|
||||
s.add_message("c2", "assistant", "vega has 4gb")
|
||||
|
||||
hits = await s.semantic_search_conversations("lego star wars", _fake_embed, limit=2, min_score=0.1)
|
||||
assert hits and hits[0]["id"] == "c1"
|
||||
|
||||
conn = s._connect()
|
||||
n = conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0]
|
||||
conn.close()
|
||||
assert n >= 2 # dual-write populated the message vec index
|
||||
|
||||
s.vec_enabled = False
|
||||
bf = await s.semantic_search_conversations("lego star wars", _fake_embed, limit=2, min_score=0.1)
|
||||
assert bf[0]["id"] == hits[0]["id"]
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_projects_scope_documents_and_survive_delete():
|
||||
s = _store()
|
||||
|
||||
async def run():
|
||||
p = s.create_project("Star Wars")
|
||||
assert [x["name"] for x in s.list_projects()] == ["Star Wars"]
|
||||
|
||||
await s.add_document("Lego", "lego star wars boss tips", _fake_embed, project_id=p["id"])
|
||||
await s.add_document("GPU", "gpu vega notes", _fake_embed) # unscoped
|
||||
|
||||
# project sees only its own; unscoped/all sees both
|
||||
assert [d["title"] for d in s.list_documents(p["id"])] == ["Lego"]
|
||||
assert len(s.list_documents(None)) == 2
|
||||
|
||||
# scoped search only returns the project's docs
|
||||
scoped = await s.search_documents("star wars", _fake_embed, min_score=0.1, project_id=p["id"])
|
||||
assert scoped and all(h["title"] == "Lego" for h in scoped)
|
||||
|
||||
# deleting the project keeps the docs but unscopes them
|
||||
assert s.delete_project(p["id"]) is True
|
||||
assert s.list_projects() == []
|
||||
assert len(s.list_documents(None)) == 2
|
||||
assert len(s.list_documents(p["id"])) == 0 # nothing left in that project
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_chunker_overlap_and_hard_split():
|
||||
s = _store()
|
||||
# a single oversized paragraph (no blank lines, as in PDF text) is split
|
||||
|
||||
@@ -43,6 +43,13 @@ def test_keep_alive_pins_the_model():
|
||||
assert "keep_alive" not in mgr._apply_keep_alive({"model": "x"})
|
||||
|
||||
|
||||
def test_stt_status_endpoint():
|
||||
# Reports whether local Whisper is installed; wiring must respond either way.
|
||||
resp = TestClient(app).get("/stt/status")
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["available"], bool)
|
||||
|
||||
|
||||
def test_num_ctx_option_only_when_positive():
|
||||
# 0 / None -> omit num_ctx so Ollama uses the model default; positive -> set it.
|
||||
from synapse.ollama_manager import _chat_options
|
||||
|
||||
@@ -23,6 +23,24 @@ async def _drain(gen):
|
||||
return [s async for s in gen]
|
||||
|
||||
|
||||
def test_remember_writes_a_memory_fact(tmp_path, monkeypatch):
|
||||
# The `remember` action tool persists a fact through the store.
|
||||
import synapse.memory.store as store_mod
|
||||
from synapse.memory.store import PersistentMemoryStore
|
||||
fresh = PersistentMemoryStore(tmp_path / "m.db")
|
||||
monkeypatch.setattr(tools, "store", fresh)
|
||||
out = asyncio.run(tools.dispatch("remember", {"text": "user likes tea", "section": "Prefs"}))
|
||||
assert "user likes tea" in out
|
||||
assert any(it.text == "user likes tea" for it in fresh.all())
|
||||
|
||||
|
||||
def test_action_tools_registered():
|
||||
for name in ("web_search", "fetch_url", "remember"):
|
||||
assert name in tools.REGISTRY
|
||||
names = [s["function"]["name"] for s in tools.schemas_for(["web_search", "remember", "nope"])]
|
||||
assert names == ["web_search", "remember"]
|
||||
|
||||
|
||||
class _FakeManager:
|
||||
"""Returns a tool_call on the first chat() call, plain content after."""
|
||||
def __init__(self):
|
||||
|
||||
Reference in New Issue
Block a user