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