Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/tool-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,13 @@ datasets.
returns `not_found` if its telemetry type does not match.
- Otherwise it scores the visible datasets that match `telemetryType` by name
against `query`, ignoring case: 1000 if the name equals the query, 750 if the
query contains the name, or 500 if the name contains the query, plus 25 for
each query term longer than two characters that the name contains, plus 15
when `telemetryType` is given. It resolves only a unique top score of at least
500. In every other case it returns `ambiguous` with up to `limit` scored
`candidates`, or `not_found` if no visible dataset matches `telemetryType`.
query contains the name as a whole word (`-` and `_` count as part of a name,
so `api` is not in `rapid` or `api-gateway`), or 500 if the name contains the
query, plus 25 for each query term longer than two characters that the name
contains, plus 15 when `telemetryType` is given. It resolves only a unique top
score of at least 500. In every other case it returns `ambiguous` with up to
`limit` scored `candidates`, or `not_found` if no visible dataset matches
`telemetryType`.

It never falls back to an arbitrary dataset.

Expand Down
49 changes: 48 additions & 1 deletion src/tool_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,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")
Expand Down Expand Up @@ -470,7 +488,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) {
Expand Down Expand Up @@ -2203,6 +2221,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_dataset_resolution_prefers_exact_case_selection() {
let datasets = json!([{ "name": "Logs" }, { "name": "logs" }, { "name": "Traces" }]);
Expand Down
Loading