feat(mcp): report search projection coverage

This commit is contained in:
HumbleTurtle
2026-08-22 22:45:58 -03:00
parent f47459ba9b
commit e283b6b62f
2 changed files with 58 additions and 3 deletions
+43 -1
View File
@@ -5929,7 +5929,21 @@ def node_route_reference(node):
return {key: value for key, value in item.items() if value not in (None, "")} return {key: value for key, value in item.items() if value not in (None, "")}
def discovery_envelope(project_id, items, args, *, view=None, next_cursor=None, has_more=None, diagnostics=None, query_analysis=None, warnings=None): def discovery_envelope(
project_id,
items,
args,
*,
view=None,
next_cursor=None,
has_more=None,
diagnostics=None,
query_analysis=None,
warnings=None,
search_strategy=None,
search_coverage=None,
search_projection=None,
):
view = view or normalize_discovery_view(args)[0] view = view or normalize_discovery_view(args)[0]
limit = normalize_int(args.get("limit"), DISCOVERY_DEFAULT_LIMIT, 1, discovery_max_limit(view)) limit = normalize_int(args.get("limit"), DISCOVERY_DEFAULT_LIMIT, 1, discovery_max_limit(view))
preview_chars = normalize_int(args.get("preview_chars"), 240, 80, 1000) preview_chars = normalize_int(args.get("preview_chars"), 240, 80, 1000)
@@ -5945,6 +5959,12 @@ def discovery_envelope(project_id, items, args, *, view=None, next_cursor=None,
"omitted_fields": [] if view == "full" else ["content_text"], "omitted_fields": [] if view == "full" else ["content_text"],
"budget": {"limit": limit, "max_limit": discovery_max_limit(view), "preview_chars": preview_chars}, "budget": {"limit": limit, "max_limit": discovery_max_limit(view), "preview_chars": preview_chars},
} }
if search_strategy:
response["search_strategy"] = search_strategy
if search_coverage:
response["search_coverage"] = search_coverage
if search_projection:
response["search_projection"] = search_projection
if as_bool(args.get("verbose"), False): if as_bool(args.get("verbose"), False):
if query_analysis is not None: if query_analysis is not None:
response["query_analysis"] = query_analysis response["query_analysis"] = query_analysis
@@ -9661,6 +9681,9 @@ def list_project_nodes(args):
diagnostics=diagnostics, diagnostics=diagnostics,
query_analysis=query_analysis, query_analysis=query_analysis,
warnings=view_warnings + filters["warnings"], warnings=view_warnings + filters["warnings"],
search_strategy=result.get("search_strategy"),
search_coverage=result.get("search_coverage"),
search_projection=result.get("search_projection"),
) )
@@ -11643,6 +11666,7 @@ def search_memory(args):
encryption_state = project_encryption_state(project_id) encryption_state = project_encryption_state(project_id)
collected = [] collected = []
sources = [] sources = []
server_search = {}
diagnostics = { diagnostics = {
"query": query_analysis, "query": query_analysis,
"local_index": local_index_diagnostics(project_id), "local_index": local_index_diagnostics(project_id),
@@ -11692,6 +11716,11 @@ def search_memory(args):
# calls, especially on a new/sparse local index. # calls, especially on a new/sparse local index.
remaining = max(1, limit - len(local_filtered)) remaining = max(1, limit - len(local_filtered))
server_result = list_project_nodes({**args, "q": q, "limit": remaining, "view": view}) server_result = list_project_nodes({**args, "q": q, "limit": remaining, "view": view})
server_search = {
key: server_result.get(key)
for key in ("search_strategy", "search_coverage", "search_projection")
if server_result.get(key)
}
server = server_result.get("items", []) server = server_result.get("items", [])
diagnostics["server_queries"].append({"q": q, "matched": len(server)}) diagnostics["server_queries"].append({"q": q, "matched": len(server)})
for node in server: for node in server:
@@ -11719,6 +11748,19 @@ def search_memory(args):
response["mode"] = mode response["mode"] = mode
response["encryption_state"] = encryption_state response["encryption_state"] = encryption_state
response["sources"] = sources response["sources"] = sources
local_used = bool((diagnostics.get("local_index") or {}).get("used"))
if local_used and server_needed:
response["search_strategy"] = "fused"
response["search_coverage"] = "identity_and_local_body"
response["search_projection"] = "+".join(
filter(None, ["sqlite_fts5_current", server_search.get("search_projection")])
)
elif local_used:
response["search_strategy"] = "local_fts"
response["search_coverage"] = "identity_and_local_body"
response["search_projection"] = "sqlite_fts5_current"
elif server_needed:
response.update(server_search)
return response return response
+15 -2
View File
@@ -30,7 +30,14 @@ def test_natural_language_discovery_uses_one_backend_search_request(tmp_path):
def request(method, path, payload=None, **_kwargs): def request(method, path, payload=None, **_kwargs):
calls.append((method, path, payload)) calls.append((method, path, payload))
return {"items": [], "next_cursor": None, "query_analysis": {"mode": "terms"}} return {
"items": [],
"next_cursor": None,
"query_analysis": {"mode": "terms"},
"search_strategy": "identity_fts",
"search_coverage": "identity_only",
"search_projection": "postgres_identity_v1",
}
mod.request_json = request mod.request_json = request
@@ -46,6 +53,9 @@ def test_natural_language_discovery_uses_one_backend_search_request(tmp_path):
assert len(calls) == 1 assert len(calls) == 1
assert calls[0][0] == "GET" assert calls[0][0] == "GET"
assert "/nodes?" in calls[0][1] assert "/nodes?" in calls[0][1]
assert result["search_strategy"] == "identity_fts"
assert result["search_coverage"] == "identity_only"
assert result["search_projection"] == "postgres_identity_v1"
def test_sparse_index_search_latency_does_not_scale_with_query_variants(tmp_path): def test_sparse_index_search_latency_does_not_scale_with_query_variants(tmp_path):
@@ -102,7 +112,7 @@ def test_complete_local_search_skips_remote_encryption_preflight_and_search(tmp_
"items": items, "items": items,
"query": {"terms": ["result"]}, "query": {"terms": ["result"]},
"search_strategies": [], "search_strategies": [],
"index": {"exists": True, "graph_complete": True}, "index": {"exists": True, "graph_complete": True, "used": True},
} }
def unexpected_request(*_args, **_kwargs): def unexpected_request(*_args, **_kwargs):
@@ -112,6 +122,9 @@ def test_complete_local_search_skips_remote_encryption_preflight_and_search(tmp_
result = mod.search_memory({"q": "result", "limit": 5, "view": "summary"}) result = mod.search_memory({"q": "result", "limit": 5, "view": "summary"})
assert result["sources"] == ["local"] assert result["sources"] == ["local"]
assert result["search_strategy"] == "local_fts"
assert result["search_coverage"] == "identity_and_local_body"
assert result["search_projection"] == "sqlite_fts5_current"
assert len(result["items"]) == 5 assert len(result["items"]) == 5
assert all(item["node_id"].startswith("node-") for item in result["items"]) assert all(item["node_id"].startswith("node-") for item in result["items"])