Skip to content

Commit 5535b2d

Browse files
committed
[agent builder] ES|QL tools: interpolate query
1 parent 945d21d commit 5535b2d

5 files changed

Lines changed: 123 additions & 2 deletions

File tree

x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/utils/esql/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77

88
export { executeEsql, type EsqlResponse } from './execute_esql';
99
export { extractEsqlQueries, esqlResponseToJson } from './misc';
10+
export { interpolateEsqlQuery } from './interpolate_query';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { interpolateEsqlQuery } from './interpolate_query';
9+
10+
describe('interpolateEsqlQuery', () => {
11+
it('should correctly interpolate string and number parameters', () => {
12+
const template = 'FROM support_ticket | WHERE priority == ?priority AND assigne_id == ?user_id';
13+
const params = { priority: 'high', user_id: 45 };
14+
const expected = 'FROM support_ticket | WHERE priority == "high" AND assigne_id == 45';
15+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
16+
});
17+
18+
it('should correctly interpolate boolean parameters', () => {
19+
const template = 'FROM nodes | WHERE is_active == ?active AND is_primary == ?primary';
20+
const params = { active: true, primary: false };
21+
const expected = 'FROM nodes | WHERE is_active == true AND is_primary == false';
22+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
23+
});
24+
25+
it('should replace all occurrences of a placeholder', () => {
26+
const template = 'FROM logs | WHERE user.id == ?user_id OR target.id == ?user_id';
27+
const params = { user_id: 101 };
28+
const expected = 'FROM logs | WHERE user.id == 101 OR target.id == 101';
29+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
30+
});
31+
32+
it('should handle an empty parameters object without changing the template', () => {
33+
const template = 'FROM events | WHERE event.code == ?code';
34+
const params = {};
35+
expect(interpolateEsqlQuery(template, params)).toBe(template);
36+
});
37+
38+
it('should ignore extra parameters that are not in the template', () => {
39+
const template = 'FROM metrics | WHERE host == ?host';
40+
const params = { host: 'server-alpha', unused_param: 'ignore' };
41+
const expected = 'FROM metrics | WHERE host == "server-alpha"';
42+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
43+
});
44+
45+
it('should not partially replace longer placeholders (word boundary test)', () => {
46+
const template = 'FROM users | WHERE user_name == ?user_name AND user_id == ?user_id';
47+
const params = { user: 'test', user_id: 99 };
48+
const expected = 'FROM users | WHERE user_name == ?user_name AND user_id == 99';
49+
// Only ?user_id should be replaced, ?user should not affect ?user_name
50+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
51+
});
52+
53+
it('should correctly handle an empty string parameter', () => {
54+
const template = 'FROM products | WHERE category == ?category';
55+
const params = { category: '' };
56+
const expected = 'FROM products | WHERE category == ""';
57+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
58+
});
59+
60+
it('should correctly handle the number 0 as a parameter', () => {
61+
const template = 'FROM tasks | WHERE status_code == ?status';
62+
const params = { status: 0 };
63+
const expected = 'FROM tasks | WHERE status_code == 0';
64+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
65+
});
66+
67+
it('should handle a mix of all data types in one query', () => {
68+
const template =
69+
'FROM telemetry | WHERE device_id == ?device AND temp > ?temp AND enabled == ?enabled';
70+
const params = { device: 'sensor-3-14', temp: 25.5, enabled: true };
71+
const expected =
72+
'FROM telemetry | WHERE device_id == "sensor-3-14" AND temp > 25.5 AND enabled == true';
73+
expect(interpolateEsqlQuery(template, params)).toBe(expected);
74+
});
75+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
/**
9+
* Interpolates parameters into a templated ESQL query string.
10+
*
11+
* @param template The ESQL query template with '?' placeholders (e.g., "?user_id").
12+
* @param params An object where keys match the placeholder names and values are the data to insert.
13+
* @returns The interpolated ESQL query string.
14+
*
15+
* **Important** This is meant as a workaround until a proper util gets exposed from `@kbn/esql-ast`,
16+
* and likely doesn't cover all edge cases.
17+
*/
18+
export const interpolateEsqlQuery = (template: string, params: Record<string, unknown>): string => {
19+
let interpolatedQuery = template;
20+
21+
for (const key in params) {
22+
if (Object.prototype.hasOwnProperty.call(params, key)) {
23+
const value = params[key];
24+
const placeholder = new RegExp(`\\?${key}\\b`, 'g');
25+
26+
// Format the value based on its type
27+
const formattedValue = typeof value === 'string' ? `"${value}"` : String(value);
28+
29+
// Replace all occurrences of the placeholder
30+
interpolatedQuery = interpolatedQuery.replace(placeholder, formattedValue);
31+
}
32+
}
33+
34+
return interpolatedQuery;
35+
};

x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
export { esqlResponseToJson, extractEsqlQueries, executeEsql } from './esql';
8+
export { esqlResponseToJson, extractEsqlQueries, executeEsql, interpolateEsqlQuery } from './esql';
99
export {
1010
flattenMapping,
1111
cleanupMapping,

x-pack/platform/plugins/shared/onechat/server/services/tools/persisted/tool_types/esql/to_tool_definition.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { z } from '@kbn/zod';
1111
import { ToolResultType } from '@kbn/onechat-common/tools/tool_result';
1212
import type { FieldValue } from '@elastic/elasticsearch/lib/api/types';
1313
import { getToolResultId } from '@kbn/onechat-server/src/tools';
14+
import { interpolateEsqlQuery } from '@kbn/onechat-genai-utils/tools/utils';
1415
import type { ToolPersistedDefinition } from '../../client';
1516
import type { InternalToolDefinition } from '../../../tool_provider';
1617

@@ -36,14 +37,23 @@ export function toToolDefinition<TSchema extends z.ZodObject<any> = z.ZodObject<
3637
params: paramArray as unknown as FieldValue[],
3738
});
3839

40+
// need the interpolated query to return in the results / to display in the UI
41+
const interpolatedQuery = interpolateEsqlQuery(configuration.query, params);
42+
3943
return {
4044
results: [
45+
{
46+
type: ToolResultType.query,
47+
data: {
48+
esql: interpolatedQuery,
49+
},
50+
},
4151
{
4252
tool_result_id: getToolResultId(),
4353
type: ToolResultType.tabularData,
4454
data: {
4555
source: 'esql',
46-
query: configuration.query,
56+
query: interpolatedQuery,
4757
columns: result.columns,
4858
values: result.values,
4959
},

0 commit comments

Comments
 (0)