feat(mcp): report search projection coverage
This commit is contained in:
@@ -5929,7 +5929,21 @@ def node_route_reference(node):
|
||||
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]
|
||||
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)
|
||||
@@ -5945,6 +5959,12 @@ def discovery_envelope(project_id, items, args, *, view=None, next_cursor=None,
|
||||
"omitted_fields": [] if view == "full" else ["content_text"],
|
||||
"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 query_analysis is not None:
|
||||
response["query_analysis"] = query_analysis
|
||||
@@ -9661,6 +9681,9 @@ def list_project_nodes(args):
|
||||
diagnostics=diagnostics,
|
||||
query_analysis=query_analysis,
|
||||
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)
|
||||
collected = []
|
||||
sources = []
|
||||
server_search = {}
|
||||
diagnostics = {
|
||||
"query": query_analysis,
|
||||
"local_index": local_index_diagnostics(project_id),
|
||||
@@ -11692,6 +11716,11 @@ def search_memory(args):
|
||||
# calls, especially on a new/sparse local index.
|
||||
remaining = max(1, limit - len(local_filtered))
|
||||
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", [])
|
||||
diagnostics["server_queries"].append({"q": q, "matched": len(server)})
|
||||
for node in server:
|
||||
@@ -11719,6 +11748,19 @@ def search_memory(args):
|
||||
response["mode"] = mode
|
||||
response["encryption_state"] = encryption_state
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,14 @@ def test_natural_language_discovery_uses_one_backend_search_request(tmp_path):
|
||||
|
||||
def request(method, path, payload=None, **_kwargs):
|
||||
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
|
||||
|
||||
@@ -46,6 +53,9 @@ def test_natural_language_discovery_uses_one_backend_search_request(tmp_path):
|
||||
assert len(calls) == 1
|
||||
assert calls[0][0] == "GET"
|
||||
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):
|
||||
@@ -102,7 +112,7 @@ def test_complete_local_search_skips_remote_encryption_preflight_and_search(tmp_
|
||||
"items": items,
|
||||
"query": {"terms": ["result"]},
|
||||
"search_strategies": [],
|
||||
"index": {"exists": True, "graph_complete": True},
|
||||
"index": {"exists": True, "graph_complete": True, "used": True},
|
||||
}
|
||||
|
||||
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"})
|
||||
|
||||
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 all(item["node_id"].startswith("node-") for item in result["items"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user