Skip to content
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

Add visualization tool #41

Merged
merged 8 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ thirdPartyAudit.enabled = false

test {
useJUnitPlatform()
include '**/*Tests.class'
systemProperty 'tests.security.manager', 'false'
testLogging {
exceptionFormat "full"
events "skipped", "passed", "failed" // "started"
Expand Down Expand Up @@ -168,7 +170,7 @@ compileJava {
compileTestJava {
options.compilerArgs.addAll(["-processor", 'lombok.launch.AnnotationProcessorHider$AnnotationProcessor'])
}

forbiddenApisTest.ignoreFailures = true

opensearchplugin {
name 'skills'
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/opensearch/agent/ToolPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.function.Supplier;

import org.opensearch.agent.tools.PPLTool;
import org.opensearch.agent.tools.VisualizationsTool;
import org.opensearch.client.Client;
import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
import org.opensearch.cluster.service.ClusterService;
Expand Down Expand Up @@ -54,11 +55,12 @@
this.xContentRegistry = xContentRegistry;

PPLTool.Factory.getInstance().init(client);
VisualizationsTool.Factory.getInstance().init(client);

Check warning on line 58 in src/main/java/org/opensearch/agent/ToolPlugin.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/ToolPlugin.java#L58

Added line #L58 was not covered by tests
return Collections.emptyList();
}

@Override
public List<Tool.Factory<? extends Tool>> getToolFactories() {
return List.of(PPLTool.Factory.getInstance());
return List.of(PPLTool.Factory.getInstance(), VisualizationsTool.Factory.getInstance());

Check warning on line 64 in src/main/java/org/opensearch/agent/ToolPlugin.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/ToolPlugin.java#L64

Added line #L64 was not covered by tests
}
}
176 changes: 176 additions & 0 deletions src/main/java/org/opensearch/agent/tools/VisualizationsTool.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.agent.tools;

import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

import org.opensearch.ExceptionsHelper;
import org.opensearch.action.search.SearchRequest;
import org.opensearch.action.search.SearchResponse;
import org.opensearch.client.Client;
import org.opensearch.client.Requests;
import org.opensearch.core.action.ActionListener;
import org.opensearch.index.IndexNotFoundException;
import org.opensearch.index.query.BoolQueryBuilder;
import org.opensearch.index.query.QueryBuilders;
import org.opensearch.ml.common.spi.tools.Tool;
import org.opensearch.ml.common.spi.tools.ToolAnnotation;
import org.opensearch.search.SearchHits;
import org.opensearch.search.builder.SearchSourceBuilder;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;

import lombok.Builder;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;

@Log4j2

Check warning on line 34 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L34

Added line #L34 was not covered by tests
@ToolAnnotation(VisualizationsTool.TYPE)
public class VisualizationsTool implements Tool {
public static final String NAME = "Find Visualizations";
Hailong-am marked this conversation as resolved.
Show resolved Hide resolved
public static final String TYPE = "VisualizationTool";
public static final String VERSION = "v1.0";

public static final String SAVED_OBJECT_TYPE = "visualization";
private static final String DEFAULT_DESCRIPTION =
"Use this tool to find user created visualizations. This tool takes the visualization name as input and returns the first 3 matching visualizations";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does 3 needs to be static? Can't we do like 3 by default but the input is dynamic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good suggestion, update the number to dynamic setting.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be update the description as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the specific number in description has been removed. now it's "Use this tool to find user created visualizations. This tool takes the visualization name as input and returns the matching visualizations"

private String description = DEFAULT_DESCRIPTION;

Check warning on line 44 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L44

Added line #L44 was not covered by tests

private String name = NAME;

Check warning on line 46 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L46

Added line #L46 was not covered by tests
private final Client client;
@Getter
private final String index;

@Builder
public VisualizationsTool(Client client, String index) {
this.client = client;
this.index = index;
}

Check warning on line 55 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L52-L55

Added lines #L52 - L55 were not covered by tests

@Override
public <T> void run(Map<String, String> parameters, ActionListener<T> listener) {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must().add(QueryBuilders.termQuery("type", SAVED_OBJECT_TYPE));
boolQueryBuilder.must().add(QueryBuilders.matchQuery(SAVED_OBJECT_TYPE + ".title", parameters.get("input")));

Check warning on line 61 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L59-L61

Added lines #L59 - L61 were not covered by tests

SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource().query(boolQueryBuilder);
searchSourceBuilder.from(0).size(3);
SearchRequest searchRequest = Requests.searchRequest(index).source(searchSourceBuilder);

Check warning on line 65 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L63-L65

Added lines #L63 - L65 were not covered by tests

client.search(searchRequest, new ActionListener<>() {

Check warning on line 67 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L67

Added line #L67 was not covered by tests
@Override
public void onResponse(SearchResponse searchResponse) {
SearchHits hits = searchResponse.getHits();
StringBuilder visBuilder = new StringBuilder();
visBuilder.append("Title,Id\n");

Check warning on line 72 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L70-L72

Added lines #L70 - L72 were not covered by tests
if (hits.getTotalHits().value > 0) {
Arrays.stream(hits.getHits()).forEach(h -> {
String id = trimIdPrefix(h.getId());
Map<String, String> visMap = (Map<String, String>) h.getSourceAsMap().get(SAVED_OBJECT_TYPE);
String title = visMap.get("title");
visBuilder.append(String.format(Locale.ROOT, "%s,%s\n", title, id));
});

Check warning on line 79 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L74-L79

Added lines #L74 - L79 were not covered by tests

listener.onResponse((T) visBuilder.toString());

Check warning on line 81 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L81

Added line #L81 was not covered by tests
} else {
listener.onResponse((T) "No Visualization found");

Check warning on line 83 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L83

Added line #L83 was not covered by tests
}
}

Check warning on line 85 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L85

Added line #L85 was not covered by tests

@Override
public void onFailure(Exception e) {
if (ExceptionsHelper.unwrapCause(e) instanceof IndexNotFoundException) {
listener.onResponse((T) "No Visualization found");

Check warning on line 90 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L90

Added line #L90 was not covered by tests
} else {
listener.onFailure(e);

Check warning on line 92 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L92

Added line #L92 was not covered by tests
}
}

Check warning on line 94 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L94

Added line #L94 was not covered by tests
});
}

Check warning on line 96 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L96

Added line #L96 was not covered by tests

@VisibleForTesting
String trimIdPrefix(String id) {
id = Optional.ofNullable(id).orElse("");

Check warning on line 100 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L100

Added line #L100 was not covered by tests
if (id.startsWith(SAVED_OBJECT_TYPE)) {
String prefix = String.format(Locale.ROOT, "%s:", SAVED_OBJECT_TYPE);
return id.substring(prefix.length());

Check warning on line 103 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L102-L103

Added lines #L102 - L103 were not covered by tests
}
return id;

Check warning on line 105 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L105

Added line #L105 was not covered by tests
}

@Override
public String getType() {
return TYPE;

Check warning on line 110 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L110

Added line #L110 was not covered by tests
}

@Override
public String getVersion() {
return VERSION;

Check warning on line 115 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L115

Added line #L115 was not covered by tests
}

@Override
public String getName() {
return name;

Check warning on line 120 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L120

Added line #L120 was not covered by tests
}

@Override
public void setName(String name) {
this.name = name;
}

Check warning on line 126 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L125-L126

Added lines #L125 - L126 were not covered by tests

@Override
public String getDescription() {
return description;

Check warning on line 130 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L130

Added line #L130 was not covered by tests
}

@Override
public void setDescription(String description) {
this.description = description;
}

Check warning on line 136 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L135-L136

Added lines #L135 - L136 were not covered by tests

Hailong-am marked this conversation as resolved.
Show resolved Hide resolved
@Override
public boolean validate(Map<String, String> parameters) {
return parameters.containsKey("input") && !Strings.isNullOrEmpty(parameters.get("input"));
}

public static class Factory implements Tool.Factory<VisualizationsTool> {

Check warning on line 143 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L143

Added line #L143 was not covered by tests
private Client client;

private static VisualizationsTool.Factory INSTANCE;

public static VisualizationsTool.Factory getInstance() {
if (INSTANCE != null) {
return INSTANCE;

Check warning on line 150 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L150

Added line #L150 was not covered by tests
}
synchronized (VisualizationsTool.class) {

Check warning on line 152 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L152

Added line #L152 was not covered by tests
if (INSTANCE != null) {
return INSTANCE;

Check warning on line 154 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L154

Added line #L154 was not covered by tests
}
INSTANCE = new VisualizationsTool.Factory();
return INSTANCE;

Check warning on line 157 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L156-L157

Added lines #L156 - L157 were not covered by tests
}
}

public void init(Client client) {
this.client = client;
}

Check warning on line 163 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L162-L163

Added lines #L162 - L163 were not covered by tests

@Override
public VisualizationsTool create(Map<String, Object> params) {
String index = params.get("index") == null ? ".kibana" : (String) params.get("index");
return VisualizationsTool.builder().client(client).index(index).build();

Check warning on line 168 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L168

Added line #L168 was not covered by tests
}

@Override
public String getDefaultDescription() {
return DEFAULT_DESCRIPTION;

Check warning on line 173 in src/main/java/org/opensearch/agent/tools/VisualizationsTool.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/agent/tools/VisualizationsTool.java#L173

Added line #L173 was not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.agent.tools;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.opensearch.action.search.SearchRequest;
import org.opensearch.action.search.SearchResponse;
import org.opensearch.client.Client;
import org.opensearch.common.xcontent.json.JsonXContent;
import org.opensearch.core.action.ActionListener;
import org.opensearch.core.xcontent.DeprecationHandler;
import org.opensearch.core.xcontent.NamedXContentRegistry;
import org.opensearch.index.IndexNotFoundException;
import org.opensearch.ml.common.spi.tools.Tool;

public class VisualizationsToolTests {
@Mock
private Client client;

private String searchResponse = "{}";
private String searchResponseNotFound = "{}";

@Before
public void setup() throws IOException {
MockitoAnnotations.openMocks(this);
VisualizationsTool.Factory.getInstance().init(client);
try (InputStream searchResponseIns = VisualizationsToolTests.class.getResourceAsStream("visualization.json")) {
if (searchResponseIns != null) {
searchResponse = new String(searchResponseIns.readAllBytes());
}
}
try (InputStream searchResponseIns = VisualizationsToolTests.class.getResourceAsStream("visualization_not_found.json")) {
if (searchResponseIns != null) {
searchResponseNotFound = new String(searchResponseIns.readAllBytes());
}
}
}

@Test
public void testToolIndexName() {
VisualizationsTool tool1 = VisualizationsTool.Factory.getInstance().create(Collections.emptyMap());
assertEquals(tool1.getIndex(), ".kibana");

VisualizationsTool tool2 = VisualizationsTool.Factory.getInstance().create(Map.of("index", "test-index"));
assertEquals(tool2.getIndex(), "test-index");
}

@Test
public void testTrimPrefix() {
VisualizationsTool tool = VisualizationsTool.Factory.getInstance().create(Collections.emptyMap());
assertEquals(tool.trimIdPrefix(null), "");
assertEquals(tool.trimIdPrefix("abc"), "abc");
assertEquals(tool.trimIdPrefix("visualization:abc"), "abc");
}

@Test
public void testParameterValidation() {
VisualizationsTool tool = VisualizationsTool.Factory.getInstance().create(Collections.emptyMap());
Assert.assertFalse(tool.validate(Collections.emptyMap()));
Assert.assertFalse(tool.validate(Map.of("input", "")));
Assert.assertTrue(tool.validate(Map.of("input", "question")));
}

@Test
public void testRunToolWithVisualizationFound() throws Exception {
Tool tool = VisualizationsTool.Factory.getInstance().create(Collections.emptyMap());
final CompletableFuture<String> future = new CompletableFuture<>();
ActionListener<String> listener = ActionListener.wrap(future::complete, future::completeExceptionally);

ArgumentCaptor<ActionListener<SearchResponse>> searchResponseListener = ArgumentCaptor.forClass(ActionListener.class);
Mockito.doNothing().when(client).search(ArgumentMatchers.any(SearchRequest.class), searchResponseListener.capture());

Map<String, String> params = Map.of("input", "Sales by gender");

tool.run(params, listener);

SearchResponse response = SearchResponse
.fromXContent(
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.IGNORE_DEPRECATIONS, searchResponse)
);
searchResponseListener.getValue().onResponse(response);

future.join();
assertEquals("Title,Id\n[Ecommerce]Sales by gender,aeb212e0-4c84-11e8-b3d7-01146121b73d\n", future.get());
}

@Test
public void testRunToolWithNoVisualizationFound() throws Exception {
Tool tool = VisualizationsTool.Factory.getInstance().create(Collections.emptyMap());
final CompletableFuture<String> future = new CompletableFuture<>();
ActionListener<String> listener = ActionListener.wrap(future::complete, future::completeExceptionally);

ArgumentCaptor<ActionListener<SearchResponse>> searchResponseListener = ArgumentCaptor.forClass(ActionListener.class);
Mockito.doNothing().when(client).search(ArgumentMatchers.any(SearchRequest.class), searchResponseListener.capture());

Map<String, String> params = Map.of("input", "Sales by gender");

tool.run(params, listener);

SearchResponse response = SearchResponse
.fromXContent(
JsonXContent.jsonXContent
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.IGNORE_DEPRECATIONS, searchResponseNotFound)
);
searchResponseListener.getValue().onResponse(response);

future.join();
assertEquals("No Visualization found", future.get());
}

@Test
public void testRunToolWithIndexNotExists() throws Exception {
Tool tool = VisualizationsTool.Factory.getInstance().create(Collections.emptyMap());
final CompletableFuture<String> future = new CompletableFuture<>();
ActionListener<String> listener = ActionListener.wrap(future::complete, future::completeExceptionally);

ArgumentCaptor<ActionListener<SearchResponse>> searchResponseListener = ArgumentCaptor.forClass(ActionListener.class);
Mockito.doNothing().when(client).search(ArgumentMatchers.any(SearchRequest.class), searchResponseListener.capture());

Map<String, String> params = Map.of("input", "Sales by gender");

tool.run(params, listener);

IndexNotFoundException notFoundException = new IndexNotFoundException("test-index");
searchResponseListener.getValue().onFailure(notFoundException);

future.join();
assertEquals("No Visualization found", future.get());
}
}
Loading
Loading