Skip to content

Commit 5695dcf

Browse files
[Agent Builder] add row limit and custom instructions to index_search tool type (#243490)
## Summary Add `Row Limit` and `Custom instructions` to index search tool. - Row limit: configurable row limit to prevent overflowing agent context with large response - We achieve this by forking inference plugin instructions for generating esql (no syntax tho) and making it templated to have query limits configurable - Custom instructions: additional guidance used to construct esql query, e.g. select only certain fields to reduce the response size - Add missing FTR tests ### Demo https://github.com/user-attachments/assets/36755c7d-6cb7-4545-93cc-1150d23a3e21 --------- Co-authored-by: Elastic Machine <[email protected]>
1 parent 5456472 commit 5695dcf

File tree

25 files changed

+791
-35
lines changed

25 files changed

+791
-35
lines changed

x-pack/platform/packages/shared/onechat/onechat-common/tools/types/index_search.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { ToolType, type ToolDefinition, type ToolDefinitionWithSchema } from '..
1111
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
1212
export type IndexSearchToolConfig = {
1313
pattern: string;
14+
row_limit?: number;
15+
custom_instructions?: string;
1416
};
1517

1618
export type IndexSearchToolDefinition = ToolDefinition<

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const StateAnnotation = Annotation.Root({
3434
maxRetries: Annotation<number>(),
3535
additionalInstructions: Annotation<string | undefined>(),
3636
additionalContext: Annotation<string | undefined>(),
37+
rowLimit: Annotation<number | undefined>(),
3738
// internal
3839
resource: Annotation<ResolvedResourceWithSampling>(),
3940
currentTry: Annotation<number>({ reducer: (a, b) => b, default: () => 0 }),
@@ -126,6 +127,7 @@ export const createNlToEsqlGraph = ({
126127
previousActions: state.actions,
127128
additionalInstructions: state.additionalInstructions,
128129
additionalContext: state.additionalContext,
130+
rowLimit: state.rowLimit,
129131
})
130132
);
131133

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export interface GenerateEsqlOptions {
7070
* Defaults to `3`
7171
* */
7272
maxRetries?: number;
73+
/**
74+
* Maximum row limit to use in generated ES|QL queries.
75+
*/
76+
rowLimit?: number;
7377
}
7478

7579
export type GenerateEsqlParams = GenerateEsqlOptions & GenerateEsqlDeps;
@@ -81,13 +85,21 @@ export const generateEsql = async ({
8185
additionalInstructions,
8286
additionalContext,
8387
maxRetries = 3,
88+
rowLimit,
8489
model,
8590
esClient,
8691
logger,
8792
events,
8893
}: GenerateEsqlParams): Promise<GenerateEsqlResponse> => {
8994
const docBase = await EsqlDocumentBase.load();
90-
const graph = createNlToEsqlGraph({ model, esClient, logger, docBase, events });
95+
96+
const graph = createNlToEsqlGraph({
97+
model,
98+
esClient,
99+
logger,
100+
docBase,
101+
events,
102+
});
91103

92104
return withActiveInferenceSpan(
93105
'GenerateEsqlGraph',
@@ -128,6 +140,7 @@ export const generateEsql = async ({
128140
maxRetries,
129141
additionalInstructions,
130142
additionalContext,
143+
rowLimit,
131144
},
132145
{
133146
recursionLimit: 25,

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ import type { ResolvedResourceWithSampling } from '../utils/resources';
1111
import { formatResourceWithSampledValues } from '../utils/resources';
1212
import type { Action } from './actions';
1313
import { formatAction } from './actions';
14+
import { getEsqlInstructions } from './prompts/instructions_template';
15+
16+
const getInstructionsWithRowLimit = (rowLimit?: number): string => {
17+
if (!rowLimit) {
18+
return getEsqlInstructions();
19+
}
20+
21+
const defaultLimit = rowLimit;
22+
const maxAllLimit = rowLimit;
23+
24+
return getEsqlInstructions({ defaultLimit, maxAllLimit });
25+
};
1426

1527
export const createRequestDocumentationPrompt = ({
1628
nlQuery,
@@ -59,13 +71,15 @@ export const createGenerateEsqlPrompt = ({
5971
prompts,
6072
additionalInstructions,
6173
additionalContext,
74+
rowLimit,
6275
}: {
6376
nlQuery: string;
6477
resource: ResolvedResourceWithSampling;
6578
prompts: EsqlPrompts;
6679
previousActions: Action[];
6780
additionalInstructions?: string;
6881
additionalContext?: string;
82+
rowLimit?: number;
6983
}): BaseMessageLike[] => {
7084
return [
7185
[
@@ -85,11 +99,11 @@ ${prompts.syntax}
8599
86100
${prompts.examples}
87101
88-
${prompts.instructions}
102+
${getInstructionsWithRowLimit(rowLimit)}
89103
90104
${
91105
additionalInstructions
92-
? `<additional_instructions>\n${additionalInstructions}\n</<additional_instructions>`
106+
? `<additional_instructions>\n${additionalInstructions}\n</additional_instructions>`
93107
: ''
94108
}
95109
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
export { getEsqlInstructions, type InstructionsTemplateParams } from './instructions_template';
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
export interface InstructionsTemplateParams {
9+
/**
10+
* The default LIMIT to use when no specific limit is requested by the user.
11+
*/
12+
defaultLimit?: number;
13+
/**
14+
* The maximum LIMIT to use when the user asks for "all" results.
15+
*/
16+
maxAllLimit?: number;
17+
}
18+
19+
const DEFAULT_LIMIT = 100;
20+
const MAX_ALL_LIMIT = 250;
21+
22+
/**
23+
* Generates ES|QL query generation instructions with configurable limit values.
24+
* This is a copy of the instructions from the inference plugin, modified to support
25+
* custom row limits for Agent Builder's index search tool.
26+
*/
27+
export const getEsqlInstructions = (params: InstructionsTemplateParams = {}): string => {
28+
const { defaultLimit = DEFAULT_LIMIT, maxAllLimit = MAX_ALL_LIMIT } = params;
29+
30+
return `<instructions>
31+
32+
## Follow the syntax
33+
34+
It is CRUCIAL and MANDATORY to only use commands and functions which are present in the syntax definition,
35+
and to follow the syntax as described in the documentation and its examples. Do not try to guess
36+
new functions or commands based on other query languages. Assume that ONLY the set of capabilities described
37+
in the provided ES|QL documentation is valid, and do not try to guess parameters or syntax based
38+
on other query languages.
39+
40+
## Respect the mappings or field definitions
41+
42+
If the user, or a tool, provides in the discussion the mappings or a list of fields present in the index, you should **ONLY** use
43+
the provided fields to create your query. Do not assume other fields may exist. Only use the set of fields
44+
which were provided by the user.
45+
46+
## Use a safety LIMIT
47+
48+
1. **LIMIT is Mandatory:** All multi-row queries **must** end with a \`LIMIT\`. The only exception is for single-row aggregations (e.g., \`STATS\` without a \`GROUP BY\`).
49+
50+
2. **Applying Limits:**
51+
* **User-Specified:** If the user provides a number ("top 10", "get 50"), use it for the \`LIMIT\`.
52+
* **Default:** If no number is given, default to \`LIMIT ${defaultLimit}\` for both raw events and \`GROUP BY\` results. Notify the user when you apply this default (e.g., "I've added a \`LIMIT ${defaultLimit}\` for safety.").
53+
54+
3. **Handling "All Data" Requests:** If a user asks for "all" results, apply a safety \`LIMIT ${maxAllLimit}\` and state that this limit was added to protect the system.
55+
56+
## Don't use tech preview features unless specified otherwise
57+
58+
Using tech preview commands, functions or other features should be avoided unless specifically asked by the user.
59+
60+
## Use MATCH for full text search
61+
62+
Unless specified otherwise, full text searches should always be done using MATCH in favor of other search functions.
63+
64+
## ES|QL query formatting
65+
66+
- All generated ES|QL queries must be wrapped with \`\`\`esql and \`\`\`
67+
- Queries must be properly formatted, with a carriage return after each function
68+
69+
Example:
70+
\`\`\`
71+
FROM logs-*
72+
| WHERE @timestamp <= NOW() - 24 hours
73+
| STATS count = COUNT(*) BY log.level
74+
| SORT count DESC
75+
\`\`\`
76+
77+
## Do not invent things to please the user
78+
79+
If what the user is asking for is not technically achievable with ES|QL's capabilities, just inform
80+
the user. DO NOT invent capabilities not described in the documentation just to provide
81+
a positive answer to the user.
82+
83+
When converting queries from one language to ES|QL, make sure that the functions are available
84+
and documented in ES|QL. E.g., for SPL's LEN, use LENGTH. For IF, use CASE.
85+
86+
## Tool Usage Restrictions
87+
88+
**CRITICAL**: Only use the tools that are explicitly defined in your available tool set. Do not call
89+
tools from other contexts or systems.
90+
91+
</instructions>
92+
`;
93+
};

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ export const naturalLanguageSearch = async ({
3333
esClient,
3434
logger,
3535
events,
36+
rowLimit,
37+
customInstructions,
3638
}: {
3739
nlQuery: string;
3840
target: string;
3941
model: ScopedModel;
4042
esClient: ElasticsearchClient;
4143
logger: Logger;
4244
events: ToolEventEmitter;
45+
rowLimit?: number;
46+
customInstructions?: string;
4347
}): Promise<NaturalLanguageSearchResponse> => {
4448
const queryGenResponse = await generateEsql({
4549
nlQuery,
@@ -49,6 +53,8 @@ export const naturalLanguageSearch = async ({
4953
esClient,
5054
logger,
5155
events,
56+
rowLimit,
57+
additionalInstructions: customInstructions,
5258
});
5359

5460
return {

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

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const StateAnnotation = Annotation.Root({
2424
// inputs
2525
nlQuery: Annotation<string>(),
2626
targetPattern: Annotation<string | undefined>(),
27+
rowLimit: Annotation<number | undefined>(),
28+
customInstructions: Annotation<string | undefined>(),
2729
// inner
2830
indexIsValid: Annotation<boolean>(),
2931
searchTarget: Annotation<SearchTarget>(),
@@ -52,12 +54,7 @@ export const createSearchToolGraph = ({
5254
logger: Logger;
5355
events: ToolEventEmitter;
5456
}) => {
55-
const tools = [
56-
createRelevanceSearchTool({ model, esClient, events }),
57-
createNaturalLanguageSearchTool({ model, esClient, events, logger }),
58-
];
59-
60-
const toolNode = new ToolNode<typeof StateAnnotation.State.messages>(tools);
57+
const relevanceTool = createRelevanceSearchTool({ model, esClient, events });
6158

6259
const selectAndValidateIndex = async (state: StateType) => {
6360
events?.reportProgress(progressMessages.selectingTarget());
@@ -89,18 +86,33 @@ export const createSearchToolGraph = ({
8986
return state.indexIsValid ? 'agent' : '__end__';
9087
};
9188

92-
const searchModel = model.chatModel.bindTools(tools).withConfig({
93-
tags: ['onechat-search-tool'],
94-
});
95-
9689
const callSearchAgent = async (state: StateType) => {
9790
events?.reportProgress(
9891
progressMessages.resolvingSearchStrategy({
9992
target: state.searchTarget.name,
10093
})
10194
);
95+
96+
const nlSearchTool = createNaturalLanguageSearchTool({
97+
model,
98+
esClient,
99+
events,
100+
logger,
101+
rowLimit: state.rowLimit,
102+
customInstructions: state.customInstructions,
103+
});
104+
105+
const tools = [relevanceTool, nlSearchTool];
106+
const searchModel = model.chatModel.bindTools(tools).withConfig({
107+
tags: ['onechat-search-tool'],
108+
});
109+
102110
const response = await searchModel.invoke(
103-
getSearchPrompt({ nlQuery: state.nlQuery, searchTarget: state.searchTarget })
111+
getSearchPrompt({
112+
nlQuery: state.nlQuery,
113+
searchTarget: state.searchTarget,
114+
customInstructions: state.customInstructions,
115+
})
104116
);
105117
return {
106118
messages: [response],
@@ -113,6 +125,18 @@ export const createSearchToolGraph = ({
113125
};
114126

115127
const executeTool = async (state: StateType) => {
128+
const nlSearchTool = createNaturalLanguageSearchTool({
129+
model,
130+
esClient,
131+
events,
132+
logger,
133+
rowLimit: state.rowLimit,
134+
customInstructions: state.customInstructions,
135+
});
136+
137+
const tools = [relevanceTool, nlSearchTool];
138+
const toolNode = new ToolNode<typeof StateAnnotation.State.messages>(tools);
139+
116140
const toolNodeResult = await toolNode.invoke(state.messages);
117141
const toolResults = extractToolResults(toolNodeResult[toolNodeResult.length - 1]);
118142

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,15 @@ export const createNaturalLanguageSearchTool = ({
9595
esClient,
9696
events,
9797
logger,
98+
rowLimit,
99+
customInstructions,
98100
}: {
99101
model: ScopedModel;
100102
esClient: ElasticsearchClient;
101103
events: ToolEventEmitter;
102104
logger: Logger;
105+
rowLimit?: number;
106+
customInstructions?: string;
103107
}) => {
104108
return toTool(
105109
async ({ query, index }) => {
@@ -115,6 +119,8 @@ export const createNaturalLanguageSearchTool = ({
115119
esClient,
116120
events,
117121
logger,
122+
rowLimit,
123+
customInstructions,
118124
});
119125

120126
const results: ToolResult[] = response.esqlData

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import {
1616
export const getSearchPrompt = ({
1717
nlQuery,
1818
searchTarget,
19+
customInstructions,
1920
}: {
2021
nlQuery: string;
2122
searchTarget: SearchTarget;
23+
customInstructions?: string;
2224
}): BaseMessageLike[] => {
2325
const systemPrompt = `You are an expert search dispatcher. Your sole task is to analyze a user's request and call the single most appropriate tool to answer it.
2426
You **must** call **one** of the available tools. Do not answer the user directly or ask clarifying questions.
@@ -42,7 +44,13 @@ You **must** call **one** of the available tools. Do not answer the user directl
4244
4345
## Additional instructions
4446
45-
- The search will be performed against the \`${searchTarget.name}\` ${searchTarget.type}, so you should use that value for the \`index\` parameters of the tool you will call.`;
47+
- The search will be performed against the \`${searchTarget.name}\` ${
48+
searchTarget.type
49+
}, so you should use that value for the \`index\` parameters of the tool you will call.${
50+
customInstructions ? `\n- User provided additional instructions: ${customInstructions}` : ''
51+
}
52+
53+
`;
4654

4755
const userPrompt = `Execute the following user query: "${nlQuery}"`;
4856

0 commit comments

Comments
 (0)