From 991558118d45f50d1a37bcb9fffe0096c76373a4 Mon Sep 17 00:00:00 2001 From: VibeSWE <338779468+VibeSWE@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:40:41 -0500 Subject: [PATCH] Match dataset names on token boundaries in resolve_dataset resolve_dataset awarded the 750-point "explicitly mentioned" score whenever a dataset name occurred anywhere inside the query, so `api` matched inside "rapid" and resolved with confidence 0.95. Require the name to be a whole token, treating `-` and `_` as part of dataset names. Co-Authored-By: Claude Opus 5.5 --- docs/tool-api.md | 4 +++- src/tool_executor.rs | 49 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/docs/tool-api.md b/docs/tool-api.md index fc9cca74f..008a295a0 100644 --- a/docs/tool-api.md +++ b/docs/tool-api.md @@ -153,7 +153,9 @@ datasets. `traces`) and `limit` (1–20, default 5). It resolves a `selectedDataset` or `contextDataset` exactly, or resolves the only visible dataset. Otherwise it ranks visible datasets by name match against `query`. It resolves only a -unique high-confidence name match. In every other case it returns +unique high-confidence name match. A dataset name counts as mentioned in +`query` only as a whole word, where `-` and `_` are part of a name, so `api` +is not mentioned in `rapid` or `api-gateway`. In every other case it returns `status: "ambiguous"` with up to `limit` scored `candidates`, or `"not_found"`. It never falls back to an arbitrary dataset. A selected dataset whose telemetry type does not match `telemetryType` returns `not_found`. diff --git a/src/tool_executor.rs b/src/tool_executor.rs index f2c477041..07f325071 100644 --- a/src/tool_executor.rs +++ b/src/tool_executor.rs @@ -341,6 +341,24 @@ fn dataset_matches_telemetry_type(dataset: &Value, expected: Option<&str>) -> bo }) } +/// Whether `name` occurs in `query` as a whole token. Dataset names may contain +/// `-` and `_`, so those count as part of a name rather than as boundaries. +fn query_mentions_dataset_name(query: &str, name: &str) -> bool { + let is_name_character = + |character: char| character.is_ascii_alphanumeric() || character == '_' || character == '-'; + !name.is_empty() + && query.match_indices(name).any(|(start, matched)| { + !query[..start] + .chars() + .next_back() + .is_some_and(is_name_character) + && !query[start + matched.len()..] + .chars() + .next() + .is_some_and(is_name_character) + }) +} + pub fn resolve_dataset_candidates(payload: Value, arguments: &Value) -> Value { let query = arguments .get("query") @@ -436,7 +454,7 @@ pub fn resolve_dataset_candidates(payload: Value, arguments: &Value) -> Value { if !query.is_empty() && name == query { score += 1_000; reasons.push("exact dataset name"); - } else if !query.is_empty() && query.contains(&name) { + } else if !query.is_empty() && query_mentions_dataset_name(&query, &name) { score += 750; reasons.push("dataset name explicitly mentioned in the question"); } else if !query.is_empty() && name.contains(&query) { @@ -2010,6 +2028,35 @@ mod tests { assert_eq!(sole_mismatch["status"], "not_found"); } + #[actix_web::test] + async fn shared_dataset_resolution_requires_whole_name_mentions() { + let datasets = json!([{ "name": "api" }, { "name": "web" }]); + let embedded = resolve_dataset_candidates( + datasets.clone(), + &json!({ "query": "show rapid failures" }), + ); + assert_ne!(embedded["status"], "resolved"); + + let mentioned = + resolve_dataset_candidates(datasets.clone(), &json!({ "query": "show api errors" })); + assert_eq!(mentioned["status"], "resolved"); + assert_eq!(mentioned["selectedDatasets"], json!(["api"])); + + let punctuated = + resolve_dataset_candidates(datasets, &json!({ "query": "errors in (api)?" })); + assert_eq!(punctuated["selectedDatasets"], json!(["api"])); + + let hyphenated = json!([{ "name": "otel-metrics" }, { "name": "otel-traces" }]); + let longer_name = resolve_dataset_candidates( + hyphenated.clone(), + &json!({ "query": "show otel-metrics-v2 errors" }), + ); + assert_ne!(longer_name["status"], "resolved"); + let underscored = + resolve_dataset_candidates(hyphenated, &json!({ "query": "show my_otel-metrics" })); + assert_ne!(underscored["status"], "resolved"); + } + #[actix_web::test] async fn shared_time_resolution_honors_timezone_and_reference() { let reference = DateTime::parse_from_rfc3339("2026-10-01T12:00:00Z")