All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.microcks.util.ai.AICopilotHelper Maven / Gradle / Ivy

/*
 * Copyright The Microcks Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.github.microcks.util.ai;

import io.github.microcks.domain.EventMessage;
import io.github.microcks.domain.Header;
import io.github.microcks.domain.Operation;
import io.github.microcks.domain.Parameter;
import io.github.microcks.domain.Request;
import io.github.microcks.domain.RequestResponsePair;
import io.github.microcks.domain.Response;
import io.github.microcks.domain.Service;
import io.github.microcks.domain.ServiceType;
import io.github.microcks.domain.UnidirectionalEvent;
import io.github.microcks.util.DispatchCriteriaHelper;
import io.github.microcks.util.DispatchStyles;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This helper class holds general utility constants and methods for: 
    *
  • interacting with LLM (prompt template, formatting specifications
  • *
  • parsing the output of LLM interaction (converting prompt templates to Microcks domain model)
  • *
* It is intended to be use by {@code AICopilot} implementations so that they can focus on configuration, connection * and prompt refinement concerns. * @author laurent */ public class AICopilotHelper { /** A simple logger for diagnostic messages. */ private static Logger log = LoggerFactory.getLogger(AICopilotHelper.class); protected static final ObjectMapper JSON_MAPPER = new ObjectMapper(); protected static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); protected static final String OPENAPI_OPERATION_PROMPT_TEMPLATE = """ Given the OpenAPI specification below, generate %2$d full examples (request and response) for operation '%1$s' strictly (no sub-path). """; protected static final String GRAPHQL_OPERATION_PROMPT_TEMPLATE = """ Given the GraphQL schema below, generate %2$d full examples (request and response) for operation '%1$s' only. """; protected static final String ASYNCAPI_OPERATION_PROMPT_TEMPLATE = """ Given the AsyncAPI specification below, generate %2$d full examples for operation '%1$s' only. """; protected static final String YAML_FORMATTING_PROMPT = """ Use only this YAML format for output (no other text or markdown): """; protected static final String REQUEST_RESPONSE_EXAMPLE_YAML_FORMATTING_TEMPLATE = """ - example: %1$d request: url: headers: accept: application/json body: response: code: 200 headers: content-type: application/json body: """; protected static final String UNIDIRECTIONAL_EVENT_EXAMPLE_YAML_FORMATTING_TEMPLATE= """ - example: %1$d message: headers: header_1: payload: """; private static final String HEADERS_NODE = "headers"; private static final String VARIABLES_NODE = "variables"; private AICopilotHelper() { // Hides the default implicit one as it's a utility class. } /** Generate an OpenAPI prompt introduction, asking for generation of {@code numberOfSamples} samples for operation. */ protected static String getOpenAPIOperationPromptIntro(String operationName, int numberOfSamples) { return String.format(OPENAPI_OPERATION_PROMPT_TEMPLATE, operationName, numberOfSamples); } /** Generate a GraphQL prompt introduction, asking for generation of {@code numberOfSamples} samples for operation. */ protected static String getGraphQLOperationPromptIntro(String operationName, int numberOfSamples) { return String.format(GRAPHQL_OPERATION_PROMPT_TEMPLATE, operationName, numberOfSamples); } /** Generate an AsyncAPI prompt introduction, asking for generation of {@code numberOfSamples} samples for operation. */ protected static String getAsyncAPIOperationPromptIntro(String operationName, int numerOfSamples) { return String.format(ASYNCAPI_OPERATION_PROMPT_TEMPLATE, operationName, numerOfSamples); } protected static String getRequestResponseExampleYamlFormattingDirective(int numberOfSamples) { StringBuilder builder = new StringBuilder(); for (int i=0; i> fields = target.fields(); while (fields.hasNext()) { removeExamplesInNode(specNode, fields.next().getValue()); } } if (target.getNodeType() == JsonNodeType.ARRAY) { Iterator elements = target.elements(); while (elements.hasNext()){ removeExamplesInNode(specNode, elements.next()); } } } /** Transform the output respecting the {@code REQUEST_RESPONSE_EXAMPLE_YAML_FORMATTING_TEMPLATE} into Microcks domain exchanges. */ protected static List parseRequestResponseTemplateOutput(Service service, Operation operation, String content) throws Exception { List results = new ArrayList<>(); JsonNode root = YAML_MAPPER.readTree(sanitizeYamlContent(content)); if (root.getNodeType() == JsonNodeType.ARRAY) { Iterator examples = root.elements(); while (examples.hasNext()) { JsonNode example = examples.next(); // Deal with parsing request. JsonNode requestNode = example.path("request"); Request request = new Request(); JsonNode requestHeadersNode = requestNode.path(HEADERS_NODE); request.setHeaders(buildHeaders(requestHeadersNode)); request.setContent(getRequestContent(requestHeadersNode, requestNode.path("body"))); String url = requestNode.path("url").asText(); if (url.contains("?")) { String[] kvPairs = url.substring(url.indexOf("?") + 1).split("&"); for (String kvPair : kvPairs) { String[] kv = kvPair.split("="); Parameter param = new Parameter(); param.setName(kv[0]); param.setValue(kv[1]); request.addQueryParameter(param); } } // Deal with parsing response. JsonNode responseNode = example.path("response"); Response response = new Response(); JsonNode responseHeadersNode = responseNode.path(HEADERS_NODE); response.setHeaders(buildHeaders(responseHeadersNode)); response.setContent(getResponseContent(responseHeadersNode, responseNode.path("body"))); response.setMediaType(responseHeadersNode.path("content-type").asText(null)); response.setStatus(responseNode.path("code").asText("200")); response.setFault(response.getStatus().startsWith("4") || response.getStatus().startsWith("5")); // String dispatchCriteria = null; if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher())) { String resourcePathPattern = operation.getName().split(" ")[1]; dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(operation.getDispatcherRules(), resourcePathPattern, url); } else if (DispatchStyles.URI_PARAMS.equals(operation.getDispatcher())) { dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams(operation.getDispatcherRules(), url); } else if (DispatchStyles.URI_ELEMENTS.equals(operation.getDispatcher())) { String resourcePathPattern = operation.getName().split(" ")[1]; dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(operation.getDispatcherRules(), resourcePathPattern, url); dispatchCriteria += DispatchCriteriaHelper.extractFromURIParams(operation.getDispatcherRules(), url); } else if (DispatchStyles.QUERY_ARGS.equals(operation.getDispatcher())) { // This dispatcher is used for GraphQL Map variables = getGraphQLVariables(request.getContent()); dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(operation.getDispatcherRules(), variables); } response.setDispatchCriteria(dispatchCriteria); if (service.getType() == ServiceType.GRAPHQL) { adaptGraphQLRequestContent(request); } results.add(new RequestResponsePair(request, response)); } } return results; } /** Transform the output respecting the {@code UNIDIRECTIONAL_EVENT_EXAMPLE_YAML_FORMATTING_TEMPLATE} into Microcks domain exchanges. */ protected static List parseUnidirectionalEventTemplateOutput(String content) throws Exception { List results = new ArrayList<>(); JsonNode root = YAML_MAPPER.readTree(sanitizeYamlContent(content)); if (root.getNodeType() == JsonNodeType.ARRAY) { Iterator examples = root.elements(); while (examples.hasNext()) { JsonNode example = examples.next(); // Deal with parsing message. JsonNode message = example.path("message"); EventMessage event = new EventMessage(); JsonNode headersNode = message.path(HEADERS_NODE); event.setHeaders(buildHeaders(headersNode)); event.setMediaType("application/json"); event.setContent(getMessageContent("application/json", message.path("payload"))); results.add(new UnidirectionalEvent(event)); } } return results; } /** Sanitize the pseudo Yaml sometimes returned into plain valid Yaml. */ private static String sanitizeYamlContent(String pseudoYaml) { pseudoYaml = pseudoYaml.trim(); if (!pseudoYaml.startsWith("-")) { boolean inYaml = false; // Are we currently in Yaml content? boolean nextIsYaml = false; // May the next line be Yaml content? boolean addPadding = false; // Do we have to add padding? StringBuilder yaml = new StringBuilder(); String[] lines = pseudoYaml.split("\\r?\\n|\\r"); for (String line: lines) { if (line.startsWith("-")) { inYaml = true; } if (line.trim().length() == 0) { inYaml = false; } if (nextIsYaml && !line.startsWith("-")) { inYaml = true; nextIsYaml = false; addPadding = true; yaml.append("- ").append(line).append("\n"); continue; } if (line.startsWith("```")) { // Starting or ending markdown block. if (inYaml) { inYaml = false; nextIsYaml = false; addPadding = false; } else { nextIsYaml = true; } } if (inYaml) { yaml.append(addPadding ? " ":"").append(line).append("\n"); // We don't know what next is going to be... nextIsYaml = false; } } return yaml.toString(); } return pseudoYaml; } private static Set
buildHeaders(JsonNode headersNode) { Set
headers = new HashSet<>(); Iterator> headerNodes = headersNode.fields(); while (headerNodes.hasNext()) { Map.Entry headerNodeEntry = headerNodes.next(); Header header = new Header(); header.setName(headerNodeEntry.getKey()); header.setValues(Set.of(headerNodeEntry.getValue().asText())); headers.add(header); } return headers; } private static String getRequestContent(JsonNode headersNode, JsonNode bodyNode) throws Exception { String contentType = headersNode.path("accept").asText(null); return getMessageContent(contentType, bodyNode); } private static String getResponseContent(JsonNode headersNode, JsonNode bodyNode) throws Exception { String contentType = headersNode.path("content-type").asText(null); return getMessageContent(contentType, bodyNode); } private static String getMessageContent(String contentType, JsonNode bodyNode) throws Exception { if (!bodyNode.isMissingNode()) { if (!bodyNode.isTextual() && contentType != null && contentType.contains("application/json") && !bodyNode.isEmpty()) { return JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(bodyNode); } else if (bodyNode.isTextual()) { return bodyNode.asText(); } } return null; } private static Map getGraphQLVariables(String requestContent) throws Exception { JsonNode graphQL = JSON_MAPPER.readTree(requestContent); if (graphQL.has(VARIABLES_NODE)) { JsonNode variablesNode = graphQL.path(VARIABLES_NODE); Map results = new HashMap<>(); Set> elements = variablesNode.properties(); for (Map.Entry element : elements) { results.put(element.getKey(), element.getValue().asText()); } return results; } else { log.warn("GraphQL request do not contain variables..."); } return new HashMap<>(); } private static void adaptGraphQLRequestContent(Request request) throws Exception { JsonNode graphQL = JSON_MAPPER.readTree(request.getContent()); if (graphQL.has("query")) { // GraphQL query may have \n we'd like to escape for better display. String query = graphQL.path("query").asText(); if (query.contains("\n")) { query = query.replace("\n", "\\n"); ((ObjectNode) graphQL).put("query", query); request.setContent(JSON_MAPPER.writeValueAsString(graphQL)); } } } /** Follow the $ref if we have one. Otherwise return given node. */ private static JsonNode followRefIfAny(JsonNode spec, JsonNode referencableNode) { if (referencableNode.has("$ref")) { String ref = referencableNode.path("$ref").asText(); return getNodeForRef(spec, ref); } return referencableNode; } private static JsonNode getNodeForRef(JsonNode spec, String reference) { return spec.at(reference.substring(1)); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy