diff --git a/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java b/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java index 2af1511..91c3578 100644 --- a/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java +++ b/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java @@ -72,6 +72,7 @@ import com.azure.core.credential.AzureKeyCredential; import com.olympus.hermione.stepSolvers.ExternalCodeGenieSolver; import com.olympus.hermione.stepSolvers.OlympusAgentSolver; import com.olympus.hermione.stepSolvers.OlynmpusChatClientSolver; +import com.olympus.hermione.stepSolvers.OpenSearchQuerySolver; @Service public class ScenarioExecutionService { @@ -282,6 +283,9 @@ public class ScenarioExecutionService { case "DELETE_TEMPORARY_DOC": solver = new DeleteDocTempSolver(); break; + case "OPENSEARCH_RAG_QUERY": + solver = new OpenSearchQuerySolver(); + break; default: break; } diff --git a/src/main/java/com/olympus/hermione/stepSolvers/OpenSearchQuerySolver.java b/src/main/java/com/olympus/hermione/stepSolvers/OpenSearchQuerySolver.java new file mode 100644 index 0000000..d0a8bba --- /dev/null +++ b/src/main/java/com/olympus/hermione/stepSolvers/OpenSearchQuerySolver.java @@ -0,0 +1,317 @@ +package com.olympus.hermione.stepSolvers; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import com.olympus.hermione.models.ScenarioExecution; +import com.olympus.hermione.utility.AttributeParser; + +import ch.qos.logback.classic.Logger; + +/** + * OpenSearchQuerySolver integrates the OpenSearch document search service + * into Hermione workflows. Replaces the legacy vector search with the new + * unified search API that supports multiple search types (semantic, keyword, + * fulltext, hybrid) and advanced filtering. + */ +public class OpenSearchQuerySolver extends StepSolver { + + private String opensearch_base_url; + // private String opensearch_project; + private String opensearch_query; + private String opensearch_search_type; + private Integer opensearch_k; + private Integer opensearch_max_context_chars; + private Double opensearch_vector_weight; + private Double opensearch_entity_weight; + private String opensearch_output_variable; + + // Optional Ks* filter fields + // private String opensearch_ks_application_name; + // private String opensearch_ks_doctype; + // private String opensearch_ks_doc_source; + // private String opensearch_ks_file_source; + // private String opensearch_ks_document_id; + // private String opensearch_ks_project_name; + // private String opensearch_ks_knowledge_path; + + // Optional tags filter (as JSON string) + // private String opensearch_tags; + + Logger logger = (Logger) LoggerFactory.getLogger(OpenSearchQuerySolver.class); + + private void loadParameters(){ + logger.info("Loading parameters for OpenSearch Query"); + + // Base URL (required) + if(this.step.getAttributes().get("opensearch_base_url") != null){ + this.opensearch_base_url = (String) this.step.getAttributes().get("opensearch_base_url"); + logger.info("opensearch_base_url: " + this.opensearch_base_url); + } else { + throw new IllegalArgumentException("opensearch_base_url is required"); + } + + // Project (required) + // if(this.step.getAttributes().get("opensearch_project") != null){ + // this.opensearch_project = (String) this.step.getAttributes().get("opensearch_project"); + // logger.info("opensearch_project: " + this.opensearch_project); + // } else { + // throw new IllegalArgumentException("opensearch_project is required"); + // } + + // Query (required) + if(this.step.getAttributes().get("opensearch_query") != null){ + this.opensearch_query = (String) this.step.getAttributes().get("opensearch_query"); + logger.info("opensearch_query: " + this.opensearch_query); + } else { + throw new IllegalArgumentException("opensearch_query is required"); + } + + // Search type (default: hybrid) + if(this.step.getAttributes().get("opensearch_search_type") != null){ + this.opensearch_search_type = (String) this.step.getAttributes().get("opensearch_search_type"); + } else { + this.opensearch_search_type = "hybrid"; + } + logger.info("opensearch_search_type: " + this.opensearch_search_type); + + // K (number of results, default: 5) + if(this.step.getAttributes().get("opensearch_k") != null){ + Object kValue = this.step.getAttributes().get("opensearch_k"); + if(kValue instanceof Integer){ + this.opensearch_k = (Integer) kValue; + } else { + this.opensearch_k = Integer.parseInt(kValue.toString()); + } + } else { + this.opensearch_k = 5; + } + logger.info("opensearch_k: " + this.opensearch_k); + + // Max context chars (default: 4000) + if(this.step.getAttributes().get("opensearch_max_context_chars") != null){ + Object maxCharsValue = this.step.getAttributes().get("opensearch_max_context_chars"); + if(maxCharsValue instanceof Integer){ + this.opensearch_max_context_chars = (Integer) maxCharsValue; + } else { + this.opensearch_max_context_chars = Integer.parseInt(maxCharsValue.toString()); + } + } else { + this.opensearch_max_context_chars = 4000; + } + logger.info("opensearch_max_context_chars: " + this.opensearch_max_context_chars); + + // Vector weight (default: 0.7) + if(this.step.getAttributes().get("opensearch_vector_weight") != null){ + Object weightValue = this.step.getAttributes().get("opensearch_vector_weight"); + if(weightValue instanceof Double){ + this.opensearch_vector_weight = (Double) weightValue; + } else { + this.opensearch_vector_weight = Double.parseDouble(weightValue.toString()); + } + } else { + this.opensearch_vector_weight = 0.7; + } + logger.info("opensearch_vector_weight: " + this.opensearch_vector_weight); + + // Entity weight (default: 0.3) + if(this.step.getAttributes().get("opensearch_entity_weight") != null){ + Object weightValue = this.step.getAttributes().get("opensearch_entity_weight"); + if(weightValue instanceof Double){ + this.opensearch_entity_weight = (Double) weightValue; + } else { + this.opensearch_entity_weight = Double.parseDouble(weightValue.toString()); + } + } else { + this.opensearch_entity_weight = 0.3; + } + logger.info("opensearch_entity_weight: " + this.opensearch_entity_weight); + + // Output variable (default: opensearch_output) + if(this.step.getAttributes().get("opensearch_output_variable") != null){ + this.opensearch_output_variable = (String) this.step.getAttributes().get("opensearch_output_variable"); + } else { + this.opensearch_output_variable = "opensearch_output"; + } + logger.info("opensearch_output_variable: " + this.opensearch_output_variable); + + // Load optional Ks* filter fields + // if(this.step.getAttributes().get("opensearch_ks_application_name") != null){ + // this.opensearch_ks_application_name = (String) this.step.getAttributes().get("opensearch_ks_application_name"); + // logger.info("opensearch_ks_application_name: " + this.opensearch_ks_application_name); + // } + // if(this.step.getAttributes().get("opensearch_ks_doctype") != null){ + // this.opensearch_ks_doctype = (String) this.step.getAttributes().get("opensearch_ks_doctype"); + // logger.info("opensearch_ks_doctype: " + this.opensearch_ks_doctype); + // } + // if(this.step.getAttributes().get("opensearch_ks_doc_source") != null){ + // this.opensearch_ks_doc_source = (String) this.step.getAttributes().get("opensearch_ks_doc_source"); + // logger.info("opensearch_ks_doc_source: " + this.opensearch_ks_doc_source); + // } + // if(this.step.getAttributes().get("opensearch_ks_file_source") != null){ + // this.opensearch_ks_file_source = (String) this.step.getAttributes().get("opensearch_ks_file_source"); + // logger.info("opensearch_ks_file_source: " + this.opensearch_ks_file_source); + // } + // if(this.step.getAttributes().get("opensearch_ks_document_id") != null){ + // this.opensearch_ks_document_id = (String) this.step.getAttributes().get("opensearch_ks_document_id"); + // logger.info("opensearch_ks_document_id: " + this.opensearch_ks_document_id); + // } + // if(this.step.getAttributes().get("opensearch_ks_project_name") != null){ + // this.opensearch_ks_project_name = (String) this.step.getAttributes().get("opensearch_ks_project_name"); + // logger.info("opensearch_ks_project_name: " + this.opensearch_ks_project_name); + // } + // if(this.step.getAttributes().get("opensearch_ks_knowledge_path") != null){ + // this.opensearch_ks_knowledge_path = (String) this.step.getAttributes().get("opensearch_ks_knowledge_path"); + // logger.info("opensearch_ks_knowledge_path: " + this.opensearch_ks_knowledge_path); + // } + // if(this.step.getAttributes().get("opensearch_tags") != null){ + // this.opensearch_tags = (String) this.step.getAttributes().get("opensearch_tags"); + // logger.info("opensearch_tags: " + this.opensearch_tags); + // } + + // Parse variables from execution context + AttributeParser attributeParser = new AttributeParser(this.scenarioExecution); + + this.opensearch_query = attributeParser.parse(this.opensearch_query); + // this.opensearch_project = attributeParser.parse(this.opensearch_project); + + // if(this.opensearch_ks_application_name != null){ + // this.opensearch_ks_application_name = attributeParser.parse(this.opensearch_ks_application_name); + // } + // if(this.opensearch_ks_doctype != null){ + // this.opensearch_ks_doctype = attributeParser.parse(this.opensearch_ks_doctype); + // } + // if(this.opensearch_ks_doc_source != null){ + // this.opensearch_ks_doc_source = attributeParser.parse(this.opensearch_ks_doc_source); + // } + // if(this.opensearch_ks_file_source != null){ + // this.opensearch_ks_file_source = attributeParser.parse(this.opensearch_ks_file_source); + // } + // if(this.opensearch_ks_document_id != null){ + // this.opensearch_ks_document_id = attributeParser.parse(this.opensearch_ks_document_id); + // } + // if(this.opensearch_ks_project_name != null){ + // this.opensearch_ks_project_name = attributeParser.parse(this.opensearch_ks_project_name); + // } + // if(this.opensearch_ks_knowledge_path != null){ + // this.opensearch_ks_knowledge_path = attributeParser.parse(this.opensearch_ks_knowledge_path); + // } + // if(this.opensearch_tags != null){ + // this.opensearch_tags = attributeParser.parse(this.opensearch_tags); + // } + } + + @Override + public ScenarioExecution solveStep() throws Exception { + + logger.info("Solving OpenSearch Query step: " + this.step.getName()); + + this.scenarioExecution.setCurrentStepId(this.step.getStepId()); + + loadParameters(); + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + String endpoint = this.opensearch_base_url + "/api/agent/search"; + JSONObject requestBody = new JSONObject(); + + // Required fields + // requestBody.put("project", this.opensearch_project); + requestBody.put("query", this.opensearch_query); + requestBody.put("search_type", this.opensearch_search_type); + requestBody.put("k", this.opensearch_k); + requestBody.put("max_context_chars", this.opensearch_max_context_chars); + requestBody.put("vector_weight", this.opensearch_vector_weight); + requestBody.put("entity_weight", this.opensearch_entity_weight); + + // Add optional Ks* filters + // if(this.opensearch_ks_application_name != null){ + // requestBody.put("KsApplicationName", this.opensearch_ks_application_name); + // } + // if(this.opensearch_ks_doctype != null){ + // requestBody.put("KsDoctype", this.opensearch_ks_doctype); + // } + // if(this.opensearch_ks_doc_source != null){ + // requestBody.put("KsDocSource", this.opensearch_ks_doc_source); + // } + // if(this.opensearch_ks_file_source != null){ + // requestBody.put("KsFileSource", this.opensearch_ks_file_source); + // } + // if(this.opensearch_ks_document_id != null){ + // requestBody.put("KsDocumentId", this.opensearch_ks_document_id); + // } + // if(this.opensearch_ks_project_name != null){ + // requestBody.put("KsProjectName", this.opensearch_ks_project_name); + // } + // if(this.opensearch_ks_knowledge_path != null){ + // requestBody.put("KsKnowledgePath", this.opensearch_ks_knowledge_path); + // } + + // // Add tags filter if provided + // if(this.opensearch_tags != null && !this.opensearch_tags.isEmpty()){ + // try { + // JSONObject tagsObj = new JSONObject(this.opensearch_tags); + // requestBody.put("tags", tagsObj); + // } catch (Exception e) { + // logger.warn("Failed to parse opensearch_tags as JSON, skipping", e); + // } + // } + + logger.info("Calling OpenSearch endpoint: " + endpoint); + logger.info("Request body: " + requestBody.toString(2)); + + HttpEntity request = new HttpEntity<>(requestBody.toString(), headers); + + try { + ResponseEntity response = restTemplate.exchange( + endpoint, + HttpMethod.POST, + request, + String.class + ); + + JSONObject jsonResponse = new JSONObject(response.getBody()); + + logger.info("OpenSearch query completed successfully"); + logger.info("Total results: " + jsonResponse.optInt("total")); + logger.info("Search type: " + jsonResponse.optString("search_type")); + + // Store the complete response + this.scenarioExecution.getExecSharedMap().put(this.opensearch_output_variable, jsonResponse.toString()); + + // Store formatted context separately for easy access + String formattedContext = jsonResponse.optString("formatted_context", ""); + this.scenarioExecution.getExecSharedMap().put( + this.opensearch_output_variable + "_context", + formattedContext + ); + + // Store just the results array for processing + JSONArray results = jsonResponse.optJSONArray("results"); + if(results != null){ + this.scenarioExecution.getExecSharedMap().put( + this.opensearch_output_variable + "_results", + results.toString() + ); + } + + // Move to next step + this.scenarioExecution.setNextStepId(this.step.getNextStepId()); + + } catch (Exception e) { + logger.error("Error calling OpenSearch service", e); + throw new Exception("OpenSearch query failed: " + e.getMessage()); + } + + return this.scenarioExecution; + } +}