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, "")}
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user