-
Notifications
You must be signed in to change notification settings - Fork 165
[Bug fixes] Fix ListIndexTool and SearchIndexTool #3720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
611ada6
5f6390f
ccf06a1
6c50986
a6f028d
deaa744
af458a5
cd5f189
085b8f6
2eae522
1bbb717
149880a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -693,6 +693,7 @@ private static void runTool( | |
tools.get(action).run(parameters, toolListener); // run tool | ||
} | ||
} catch (Exception e) { | ||
log.error("Failed to run tool {}", action, e); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the next line, the error message will be return to the listener, and it's going to be the same e.getMessage(). is the log needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. listener behavior might change over time and may not expose these details, so i think its better to explicitly log tool failure here irrespective of listener There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log is for debugging if later we need to deep dive to identify the root cause. And listener is for customer to be notified at that very moment. |
||
nextStepListener | ||
.onResponse(String.format(Locale.ROOT, "Failed to run the tool %s with the error message %s.", action, e.getMessage())); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,12 +8,13 @@ | |
import static org.opensearch.action.support.clustermanager.ClusterManagerNodeRequest.DEFAULT_CLUSTER_MANAGER_NODE_TIMEOUT; | ||
import static org.opensearch.ml.common.utils.StringUtils.gson; | ||
|
||
import java.util.Collections; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Map.Entry; | ||
|
||
import org.apache.commons.lang3.StringUtils; | ||
import org.apache.logging.log4j.util.Strings; | ||
import org.opensearch.action.admin.indices.get.GetIndexRequest; | ||
import org.opensearch.action.admin.indices.get.GetIndexResponse; | ||
|
@@ -34,6 +35,8 @@ | |
@ToolAnnotation(IndexMappingTool.TYPE) | ||
public class IndexMappingTool implements Tool { | ||
public static final String TYPE = "IndexMappingTool"; | ||
public static final String INPUT_SCHEMA_FIELD = "input_schema"; | ||
public static final String STRICT_FIELD = "strict"; | ||
private static final String DEFAULT_DESCRIPTION = String | ||
.join( | ||
" ", | ||
|
@@ -44,6 +47,12 @@ public class IndexMappingTool implements Tool { | |
"The mappings are in JSON format under the key 'properties' which includes the field name as a key and a JSON object with field type under the key 'type'.", | ||
"The settings are in flattened map with 'index' as the top element and key-value pairs for each setting." | ||
); | ||
public static final String DEFAULT_INPUT_SCHEMA = "{\"type\":\"object\",\"" | ||
+ "properties\":{\"index\":{\"type\":\"array\",\"description\":\"OpenSearch index name list, separated by comma. " | ||
+ "for example: [\\\"index1\\\", \\\"index2\\\"]\"," | ||
+ "\"items\":{\"type\":\"string\"}}}," | ||
+ "\"required\":[\"index\"]," | ||
+ "\"additionalProperties\":false}"; | ||
|
||
@Setter | ||
@Getter | ||
|
@@ -67,12 +76,8 @@ public IndexMappingTool(Client client) { | |
this.client = client; | ||
|
||
this.attributes = new HashMap<>(); | ||
attributes | ||
.put( | ||
"input_schema", | ||
"{\"type\":\"object\",\"properties\":{\"index\":{\"type\":\"string\",\"description\":\"OpenSearch index name\"}},\"required\":[\"index\"],\"additionalProperties\":false}" | ||
); | ||
attributes.put("strict", true); | ||
attributes.put(INPUT_SCHEMA_FIELD, DEFAULT_INPUT_SCHEMA); | ||
attributes.put(STRICT_FIELD, true); | ||
|
||
outputParser = new Parser<>() { | ||
@Override | ||
|
@@ -86,75 +91,80 @@ public Object parse(Object o) { | |
|
||
@Override | ||
public <T> void run(Map<String, String> parameters, ActionListener<T> listener) { | ||
@SuppressWarnings("unchecked") | ||
List<String> indexList = parameters.containsKey("index") | ||
? gson.fromJson(parameters.get("index"), List.class) | ||
: Collections.emptyList(); | ||
if (indexList.isEmpty()) { | ||
@SuppressWarnings("unchecked") | ||
T empty = (T) ("There were no results searching the index parameter [" + parameters.get("index") + "]."); | ||
listener.onResponse(empty); | ||
return; | ||
} | ||
try { | ||
List<String> indexList = new ArrayList<>(); | ||
if (StringUtils.isNotBlank(parameters.get("index"))) { | ||
pyek-bot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
indexList = gson.fromJson(parameters.get("index"), List.class); | ||
} | ||
|
||
final String[] indices = indexList.toArray(Strings.EMPTY_ARRAY); | ||
if (indexList.isEmpty()) { | ||
@SuppressWarnings("unchecked") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are just trying to returnning a string on listener response? it's not recommended to supresswarning, try compose the string in a better way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but I don't have the context on this listener, when it's empty index list, should it return empty list or this string? try this?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. most of it is existing code that has been moved (see above), i lack the complete context on why it was built this way tbh so the tools are executed using input provided by a model - these are always going to be strings and the response is also passed as a string to the model, therefore it is safe to work with the assumption its a string however, there is concern if at any point this object changes, i will try to see if it can be handled in a better way There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mingshl , This is to return the correct type The actionListener expects type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pyek-bot , let's create issue tracking the issue and fix in new PR ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, created issue: #3722 |
||
T empty = (T) ("There were no results searching the index parameter [" + parameters.get("index") + "]."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how do you know this guarantee that T is actually String. Otherwise this will cause a ClassCastException at runtime. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is existing code. But I think we can enhance the logic as we already know the |
||
listener.onResponse(empty); | ||
return; | ||
} | ||
|
||
final IndicesOptions indicesOptions = IndicesOptions.strictExpand(); | ||
final boolean local = Boolean.parseBoolean(parameters.get("local")); | ||
final TimeValue clusterManagerNodeTimeout = DEFAULT_CLUSTER_MANAGER_NODE_TIMEOUT; | ||
final String[] indices = indexList.toArray(Strings.EMPTY_ARRAY); | ||
|
||
ActionListener<GetIndexResponse> internalListener = new ActionListener<GetIndexResponse>() { | ||
final IndicesOptions indicesOptions = IndicesOptions.strictExpand(); | ||
final boolean local = Boolean.parseBoolean(parameters.getOrDefault("local", "false")); | ||
final TimeValue clusterManagerNodeTimeout = DEFAULT_CLUSTER_MANAGER_NODE_TIMEOUT; | ||
|
||
@Override | ||
public void onResponse(GetIndexResponse getIndexResponse) { | ||
try { | ||
// Handle empty response | ||
if (getIndexResponse.indices().length == 0) { | ||
@SuppressWarnings("unchecked") | ||
T empty = (T) ("There were no results searching the index parameter [" + parameters.get("index") + "]."); | ||
listener.onResponse(empty); | ||
return; | ||
} | ||
StringBuilder sb = new StringBuilder(); | ||
for (String index : getIndexResponse.indices()) { | ||
sb.append("index: ").append(index).append("\n\n"); | ||
|
||
MappingMetadata mapping = getIndexResponse.mappings().get(index); | ||
if (mapping != null) { | ||
sb.append("mappings:\n"); | ||
for (Entry<String, Object> entry : mapping.sourceAsMap().entrySet()) { | ||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append('\n'); | ||
} | ||
sb.append("\n\n"); | ||
ActionListener<GetIndexResponse> internalListener = new ActionListener<GetIndexResponse>() { | ||
|
||
@Override | ||
public void onResponse(GetIndexResponse getIndexResponse) { | ||
try { | ||
// Handle empty response | ||
if (getIndexResponse.indices().length == 0) { | ||
@SuppressWarnings("unchecked") | ||
T empty = (T) ("There were no results searching the index parameter [" + parameters.get("index") + "]."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, can you avoid this manual suppresswarning |
||
listener.onResponse(empty); | ||
return; | ||
} | ||
StringBuilder sb = new StringBuilder(); | ||
for (String index : getIndexResponse.indices()) { | ||
sb.append("index: ").append(index).append("\n\n"); | ||
|
||
MappingMetadata mapping = getIndexResponse.mappings().get(index); | ||
if (mapping != null) { | ||
sb.append("mappings:\n"); | ||
for (Entry<String, Object> entry : mapping.sourceAsMap().entrySet()) { | ||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append('\n'); | ||
} | ||
sb.append("\n\n"); | ||
} | ||
|
||
Settings settings = getIndexResponse.settings().get(index); | ||
if (settings != null) { | ||
sb.append("settings:\n").append(settings.toDelimitedString('\n')).append("\n\n"); | ||
Settings settings = getIndexResponse.settings().get(index); | ||
if (settings != null) { | ||
sb.append("settings:\n").append(settings.toDelimitedString('\n')).append("\n\n"); | ||
} | ||
} | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
T response = (T) sb.toString(); | ||
listener.onResponse(response); | ||
} catch (Exception e) { | ||
onFailure(e); | ||
@SuppressWarnings("unchecked") | ||
T response = (T) sb.toString(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe using the StringUtils.toString(), which takes in an object will help in this case? |
||
listener.onResponse(response); | ||
} catch (Exception e) { | ||
onFailure(e); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public void onFailure(final Exception e) { | ||
listener.onFailure(e); | ||
} | ||
@Override | ||
public void onFailure(final Exception e) { | ||
listener.onFailure(e); | ||
} | ||
|
||
}; | ||
final GetIndexRequest getIndexRequest = new GetIndexRequest() | ||
.indices(indices) | ||
.indicesOptions(indicesOptions) | ||
.local(local) | ||
.clusterManagerNodeTimeout(clusterManagerNodeTimeout); | ||
}; | ||
final GetIndexRequest getIndexRequest = new GetIndexRequest() | ||
.indices(indices) | ||
.indicesOptions(indicesOptions) | ||
.local(local) | ||
.clusterManagerNodeTimeout(clusterManagerNodeTimeout); | ||
|
||
client.admin().indices().getIndex(getIndexRequest, internalListener); | ||
client.admin().indices().getIndex(getIndexRequest, internalListener); | ||
} catch (Exception e) { | ||
listener.onFailure(e); | ||
} | ||
} | ||
|
||
@Override | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it the attributes and parameters are reading the same map? what is the intention?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
earlier, parameters was being overwritten by attributes map as we were reassigning parameters (bug)
now, we are assigning it correctly to attributes
attributes map is the last item in the bytestream, readMap() is a method that allows populating a map from the bytestream
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am noticing some weird behavior in a multi-node cluster, will validate and get back
***resolved, the issue was with reading attributes twice, this code is still valid