feat: add semantic search modes across cli api and tuis

This commit is contained in:
thePR0M3TH3AN committed 2026-03-02 12:28:24 -05:00
1 parent ec407c611a
commit f0c120f84d
16 files changed
+460 -49

No files matched your search

+29 -9
View File
@@ -1725,11 +1725,18 @@ async def test_semantic_endpoints(client, monkeypatch):
class DummySemanticService:
def __init__(self, _pm):
self.enabled = True
self.last_enabled = None
self.last_search = None
self.mode = "keyword"
def status(self):
return {"enabled": True, "built": True, "records": 4}
return {
"enabled": bool(self.enabled),
"built": True,
"records": 4,
"mode": self.mode,
}
def build(self):
return {"enabled": True, "built": True, "records": 4}
@@ -1739,10 +1746,22 @@ async def test_semantic_endpoints(client, monkeypatch):
def set_enabled(self, enabled: bool):
self.last_enabled = bool(enabled)
self.enabled = bool(enabled)
return {"enabled": bool(enabled)}
def search(self, query: str, *, k: int = 10, kind: str | None = None):
self.last_search = (query, k, kind)
def set_mode(self, mode: str):
self.mode = str(mode)
return self.status()
def search(
self,
query: str,
*,
k: int = 10,
kind: str | None = None,
mode: str | None = None,
):
self.last_search = (query, k, kind, mode)
return [{"entry_id": 10, "kind": kind or "document", "score": 0.5}]
service = DummySemanticService(None)
@@ -1763,21 +1782,22 @@ async def test_semantic_endpoints(client, monkeypatch):
configured = await cl.post(
"/api/v1/semantic/config",
json={"enabled": False},
json={"enabled": False, "mode": "hybrid"},
headers=headers,
)
assert configured.status_code == 200
assert configured.json() == {"enabled": False}
assert configured.json() == {"enabled": False, "mode": "hybrid"}
assert service.last_enabled is False
assert service.mode == "hybrid"
searched = await cl.post(
"/api/v1/semantic/search",
json={"query": "relay notes", "k": 6, "kind": "document"},
json={"query": "relay notes", "k": 6, "kind": "document", "mode": "semantic"},
headers=headers,
)
assert searched.status_code == 200
assert searched.json()["results"][0]["entry_id"] == 10
assert service.last_search == ("relay notes", 6, "document")
assert service.last_search == ("relay notes", 6, "document", "semantic")
@pytest.mark.anyio
@@ -1790,9 +1810,9 @@ async def test_semantic_search_requires_query(client):
@pytest.mark.anyio
async def test_semantic_config_requires_enabled_flag(client):
async def test_semantic_config_requires_enabled_or_mode(client):
cl, token = client
headers = {"Authorization": f"Bearer {token}"}
res = await cl.post("/api/v1/semantic/config", json={}, headers=headers)
assert res.status_code == 400
assert res.json() == {"detail": "Missing enabled"}
assert res.json() == {"detail": "Missing enabled or mode"}
+35 -3
View File
@@ -9,19 +9,33 @@ runner = CliRunner()
class _DummySemanticService:
def __init__(self) -> None:
self.mode = "keyword"
def status(self):
return {"enabled": True, "built": True, "records": 2}
return {"enabled": True, "built": True, "records": 2, "mode": self.mode}
def set_enabled(self, enabled: bool):
return {"enabled": bool(enabled), "built": True, "records": 2}
def set_mode(self, mode: str):
self.mode = str(mode)
return self.status()
def build(self):
return {"enabled": True, "built": True, "records": 3}
def rebuild(self):
return {"enabled": True, "built": True, "records": 3}
def search(self, query: str, *, k: int = 10, kind: str | None = None):
def search(
self,
query: str,
*,
k: int = 10,
kind: str | None = None,
mode: str | None = None,
):
if not query:
return []
return [
@@ -70,7 +84,25 @@ def test_semantic_cli_commands(monkeypatch):
result = runner.invoke(
app,
["semantic", "search", "relay", "--k", "3", "--kind", "document", "--json"],
[
"semantic",
"search",
"relay",
"--k",
"3",
"--kind",
"document",
"--mode",
"hybrid",
"--json",
],
)
assert result.exit_code == 0
assert '"entry_id": 7' in result.stdout
result = runner.invoke(
app,
["semantic", "config", "--enabled", "true", "--mode", "semantic"],
)
assert result.exit_code == 0
assert '"mode": "semantic"' in result.stdout
+18
View File
@@ -49,6 +49,7 @@ def test_semantic_index_build_and_search(tmp_path: Path) -> None:
class _DummyConfig:
def __init__(self) -> None:
self.enabled = False
self.mode = "keyword"
def set_semantic_index_enabled(self, enabled: bool) -> None:
self.enabled = bool(enabled)
@@ -56,6 +57,12 @@ class _DummyConfig:
def get_semantic_index_enabled(self) -> bool:
return bool(self.enabled)
def set_semantic_search_mode(self, mode: str) -> None:
self.mode = str(mode)
def get_semantic_search_mode(self) -> str:
return str(self.mode)
class _DummyEntryManager:
def __init__(self) -> None:
@@ -106,3 +113,14 @@ def test_semantic_index_service_build_and_status(tmp_path: Path) -> None:
results = service.search("agent", k=3)
assert len(results) >= 1
assert results[0]["entry_id"] == 1
service.set_mode("semantic")
semantic_only = service.search("agent", k=3, mode="semantic")
assert len(semantic_only) >= 1
service.set_mode("keyword")
keyword_only = service.search("agent", k=3, mode="keyword")
assert len(keyword_only) >= 1
hybrid = service.search("agent", k=3, mode="hybrid")
assert len(hybrid) >= 1
+9 -3
View File
@@ -375,9 +375,10 @@ def test_semantic_submenu_search_flow(monkeypatch, capsys):
class DummySemanticService:
def __init__(self, _pm):
self.last_query = None
self.mode = "keyword"
def status(self):
return {"enabled": False, "built": False, "records": 0}
return {"enabled": False, "built": False, "records": 0, "mode": self.mode}
def set_enabled(self, enabled: bool):
return {"enabled": bool(enabled), "records": 0}
@@ -388,6 +389,10 @@ def test_semantic_submenu_search_flow(monkeypatch, capsys):
def rebuild(self):
return {"enabled": True, "built": True, "records": 2}
def set_mode(self, mode: str):
self.mode = str(mode)
return {"mode": self.mode}
def search(self, query: str, *, k: int = 10, kind: str | None = None):
self.last_query = query
return [{"entry_id": 1, "kind": "document", "label": "Doc", "score": 0.5}]
@@ -397,11 +402,12 @@ def test_semantic_submenu_search_flow(monkeypatch, capsys):
pm, _, _ = setup_pm(tmp_path, monkeypatch)
monkeypatch.setattr(main, "SemanticIndexService", DummySemanticService)
monkeypatch.setattr(main, "pause", lambda: None)
inputs = iter(["1", "2", "4", "6", "relay docs", ""])
inputs = iter(["1", "7", "hybrid", "2", "4", "6", "relay docs", ""])
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
main.handle_semantic_index_menu(pm)
out = capsys.readouterr().out
assert "Status: enabled=False, built=False, records=0" in out
assert "Status: enabled=False, built=False, records=0, mode=keyword" in out
assert "Semantic search mode set to hybrid." in out
assert "Semantic index enabled" in out
assert "Semantic index built with 2 records" in out
assert "Semantic Matches:" in out
+5
View File
@@ -561,6 +561,8 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
app._run_palette_command("setting-kdf-iterations 100000")
app._run_palette_command("setting-kdf-mode pbkdf2")
app._run_palette_command("semantic-status")
app._run_palette_command("search-mode")
app._run_palette_command("search-mode nope")
app._run_palette_command("semantic-enable now")
app._run_palette_command("semantic-disable now")
app._run_palette_command("semantic-build now")
@@ -607,6 +609,9 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
app._run_palette_command("lock")
app._run_palette_command("unlock hunter2")
app._run_palette_command("search Doc")
app._run_palette_command("search-mode hybrid")
app._run_palette_command("search-mode semantic")
app._run_palette_command("search-mode keyword")
app._run_palette_command("semantic-status")
app._run_palette_command("semantic-search notes")
app.action_focus_left()
+35 -1
View File
@@ -567,6 +567,7 @@ class FakeSemanticService:
self.enabled = False
self.built = False
self.records = 0
self.mode = "keyword"
self.last_query: str | None = None
def status(self):
@@ -574,6 +575,7 @@ class FakeSemanticService:
"enabled": bool(self.enabled),
"built": bool(self.built),
"records": int(self.records),
"mode": str(self.mode),
}
def set_enabled(self, enabled: bool):
@@ -590,8 +592,21 @@ class FakeSemanticService:
self.records = 2
return self.status()
def search(self, query: str, *, k: int = 10, kind: str | None = None):
def set_mode(self, mode: str):
self.mode = str(mode)
return self.status()
def search(
self,
query: str,
*,
k: int = 10,
kind: str | None = None,
mode: str | None = None,
):
self.last_query = str(query)
if mode:
self.mode = str(mode)
if not query.strip():
return []
return [
@@ -1669,6 +1684,11 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None:
assert semantic.enabled is True
assert "Semantic index enabled" in _status_text(app)
app._run_palette_command("search-mode hybrid")
await pilot.pause()
assert semantic.mode == "hybrid"
assert "Search mode set to hybrid" in _status_text(app)
app._run_palette_command("semantic-build")
await pilot.pause()
assert semantic.built is True
@@ -1916,6 +1936,10 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
await pilot.pause()
assert "Semantic service unavailable" in _status_text(app)
app._run_palette_command("search-mode semantic")
await pilot.pause()
assert "Semantic service unavailable" in _status_text(app)
app._run_palette_command("semantic-search")
await pilot.pause()
assert "Semantic service unavailable" in _status_text(app)
@@ -1974,6 +1998,16 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
await pilot.pause()
assert "Usage: semantic-enable" in _status_text(app)
app._run_palette_command("search-mode")
await pilot.pause()
assert "Usage: search-mode <keyword|hybrid|semantic>" in _status_text(app)
app._run_palette_command("search-mode nope")
await pilot.pause()
assert "search-mode must be one of: keyword, hybrid, semantic" in _status_text(
app
)
app._run_palette_command("semantic-search")
await pilot.pause()
assert "Usage: semantic-search <query>" in _status_text(app)