diff --git a/core/runtime/src/main/java/dev/langchain4j/model/input/PromptTemplate.java b/core/runtime/src/main/java/dev/langchain4j/model/input/PromptTemplate.java new file mode 100644 index 000000000..2c5b6719f --- /dev/null +++ b/core/runtime/src/main/java/dev/langchain4j/model/input/PromptTemplate.java @@ -0,0 +1,160 @@ +package io.quarkiverse.langchain4j; + +import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.internal.ValidationUtils.ensureNotNull; +import static dev.langchain4j.spi.ServiceHelper.loadFactories; +import static java.util.Collections.singletonMap; + +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; + +import dev.langchain4j.model.input.Prompt; +import dev.langchain4j.spi.prompt.PromptTemplateFactory; +import dev.langchain4j.spi.prompt.PromptTemplateFactory.Input; + +/** + * Represents a template of a prompt that can be reused multiple times. + * A template typically contains one or more variables (placeholders) defined as {{variable_name}} that are + * replaced with actual values to produce a Prompt. + * Special variables {{current_date}}, {{current_time}}, and {{current_date_time}} are automatically + * filled with LocalDate.now(), LocalTime.now(), and LocalDateTime.now() respectively. + */ +public class PromptTemplate { + + private static final PromptTemplateFactory FACTORY = factory(); + + private static PromptTemplateFactory factory() { + for (PromptTemplateFactory factory : loadFactories(PromptTemplateFactory.class)) { + return factory; + } + return null; // new DefaultPromptTemplateFactory(); + } + + static final String CURRENT_DATE = "current_date"; + static final String CURRENT_TIME = "current_time"; + static final String CURRENT_DATE_TIME = "current_date_time"; + + private final String templateString; + private final PromptTemplateFactory.Template template; + private final Clock clock; + + /** + * Create a new PromptTemplate. + * + *

+ * The {@code Clock} will be the system clock. + *

+ * + * @param template the template string of the prompt. + */ + public PromptTemplate(String template) { + this(template, null); + } + + /** + * Create a new PromptTemplate. + * + *

+ * The {@code Clock} will be the system clock. + *

+ * + * @param template the template string of the prompt. + */ + public PromptTemplate(String template, String templateName) { + this(template, templateName, Clock.systemDefaultZone()); + } + + /** + * Create a new PromptTemplate. + * + * @param template the template string of the prompt. + * @param clock the clock to use for the special variables. + */ + PromptTemplate(String template, String templateName, Clock clock) { + this.templateString = ensureNotBlank(template, "template"); + if (templateName != null) { + this.template = FACTORY.create(new PromptTemplateFactory.Input() { + + @Override + public String getTemplate() { + return template; + } + + @Override + public String getName() { + return templateName; + } + }); + } else { + this.template = FACTORY.create(() -> template); + } + this.clock = ensureNotNull(clock, "clock"); + } + + /** + * @return A prompt template string. + */ + public String template() { + return templateString; + } + + /** + * Applies a value to a template containing a single variable. The single variable should have the name {{it}}. + * + * @param value The value that will be injected in place of the {{it}} placeholder in the template. + * @return A Prompt object where the {{it}} placeholder in the template has been replaced by the provided value. + */ + public Prompt apply(Object value) { + return apply(singletonMap("it", value)); + } + + /** + * Applies multiple values to a template containing multiple variables. + * + * @param variables A map of variable names to values that will be injected in place of the corresponding placeholders in + * the template. + * @return A Prompt object where the placeholders in the template have been replaced by the provided values. + */ + public Prompt apply(Map variables) { + ensureNotNull(variables, "variables"); + return Prompt.from(template.render(injectDateTimeVariables(variables))); + } + + /** + * Injects the special variables {{current_date}}, {{current_time}}, and {{current_date_time}} into the given map. + * + * @param variables the map to inject the variables into. + * @return a copy of the map with the variables injected. + */ + private Map injectDateTimeVariables(Map variables) { + Map variablesCopy = new HashMap<>(variables); + variablesCopy.put(CURRENT_DATE, LocalDate.now(clock)); + variablesCopy.put(CURRENT_TIME, LocalTime.now(clock)); + variablesCopy.put(CURRENT_DATE_TIME, LocalDateTime.now(clock)); + return variablesCopy; + } + + /** + * Create a new PromptTemplate. + * + * @param template the template string of the prompt. + * @return the PromptTemplate. + */ + public static PromptTemplate from(String template) { + return from(template, null); + } + + /** + * Create a new PromptTemplate. + * + * @param template the template string of the prompt. + * @return the PromptTemplate. + */ + public static PromptTemplate from(String template, String templateName) { + return new PromptTemplate(template, templateName); + } +} diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/QuarkusPromptTemplateFactory.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/QuarkusPromptTemplateFactory.java index 5c4fd3faf..543a6ddca 100644 --- a/core/runtime/src/main/java/io/quarkiverse/langchain4j/QuarkusPromptTemplateFactory.java +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/QuarkusPromptTemplateFactory.java @@ -13,8 +13,10 @@ import dev.langchain4j.spi.prompt.PromptTemplateFactory; import io.quarkiverse.langchain4j.spi.PromptTemplateFactoryContentFilterProvider; import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; import io.quarkus.arc.impl.LazyValue; import io.quarkus.qute.Engine; +import io.quarkus.qute.EngineBuilder; import io.quarkus.qute.ParserHelper; import io.quarkus.qute.ParserHook; import io.quarkus.qute.TemplateInstance; @@ -27,8 +29,14 @@ public QuarkusPromptTemplateFactory() { engineLazyValue.set(new LazyValue<>(new Supplier() { @Override public Engine get() { - return Arc.container().instance(Engine.class).get().newBuilder() - .addParserHook(new MustacheTemplateVariableStyleParserHook()).build(); + ArcContainer container = Arc.container(); + EngineBuilder builder = container.instance(Engine.class).get().newBuilder() + .addParserHook(new MustacheTemplateVariableStyleParserHook()); + // fire event to call DebugQuteEngineObserver#configureEngine(@Observes EngineBuilder builder, QuteConfig config) + // to track the langchain4j engine builder with Qute debugger + // see https://github.com/quarkusio/quarkus/blob/84414f0fd571881f5601c1dc73a0f43c07080a87/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/debug/DebugQuteEngineObserver.java#L41 + container.beanManager().getEvent().fire(builder); + return builder.build(); } })); } @@ -42,7 +50,8 @@ public static void clear() { @Override public Template create(Input input) { - return new QuteTemplate(engineLazyValue.get().get().parse(input.getTemplate())); + String javaElementUri = input.getName(); + return new QuteTemplate(engineLazyValue.get().get().parse(input.getTemplate(), null, javaElementUri)); } public static class MustacheTemplateVariableStyleParserHook implements ParserHook { diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/JavaElementUriBuilder.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/JavaElementUriBuilder.java new file mode 100644 index 000000000..f3992cb40 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/JavaElementUriBuilder.java @@ -0,0 +1,139 @@ +package io.quarkiverse.langchain4j.runtime; + +import java.net.URI; + +/** + * Builder for Qute-specific URIs that reference a Java element + * (class, method, or annotation) from a template. + *

+ * These URIs have the format: + * + *

+ * qute-java://<fully-qualified-class-name>[#method][@annotation]
+ * 
+ * + * Examples: + *
    + *
  • Class-level annotation: qute-java://com.acme.Bean@io.quarkus.qute.TemplateContents
  • + *
  • Method-level annotation: qute-java://com.acme.Bean#process@io.quarkus.qute.TemplateContents
  • + *
+ *

+ * + *

+ * This builder is used to construct such URIs in a type-safe way and to provide + * utility methods to identify and parse them. It is aligned with + * {@link io.quarkus.qute.debug.client.JavaSourceLocationArguments#javaElementUri}. + *

+ */ +public class JavaElementUriBuilder { + + /** Scheme used for Qute Java URIs. */ + public static final String QUTE_JAVA_SCHEME = "qute-java"; + + /** Prefix for Qute Java URIs. */ + public static final String QUTE_JAVA_URI_PREFIX = QUTE_JAVA_SCHEME + "://"; + + private final String typeName; + private String method; + private String annotation; + + private JavaElementUriBuilder(String typeName) { + this.typeName = typeName; + } + + /** + * Returns the fully qualified Java class name for this URI. + * + * @return the class name + */ + public String getTypeName() { + return typeName; + } + + /** + * Returns the Java method name (nullable if not specified). + * + * @return the method name or {@code null} + */ + public String getMethod() { + return method; + } + + /** + * Sets the Java method name. + * + * @param method the method name to set + * @return this builder + */ + public JavaElementUriBuilder setMethod(String method) { + this.method = method; + return this; + } + + /** + * Returns the fully qualified Java annotation name (nullable if not specified). + * + * @return the annotation name or {@code null} + */ + public String getAnnotation() { + return annotation; + } + + /** + * Sets the fully qualified Java annotation name. + * + * @param annotation the annotation name to set + * @return this builder + */ + public JavaElementUriBuilder setAnnotation(String annotation) { + this.annotation = annotation; + return this; + } + + /** + * Creates a new builder for the given Java class name. + * + * @param typeName fully qualified Java class name + * @return a new {@link JavaElementUriBuilder} + */ + public static JavaElementUriBuilder builder(String typeName) { + return new JavaElementUriBuilder(typeName); + } + + /** + * Builds the Qute Java URI representing the element. + * + * @return a {@link URI} for the Java element + */ + public URI build() { + StringBuilder uri = new StringBuilder(QUTE_JAVA_URI_PREFIX); + uri.append(typeName); + if (method != null) { + uri.append("#").append(method); + } + if (annotation != null) { + uri.append("@").append(annotation); + } + return URI.create(uri.toString()); + } + + /** + * Returns true if the given URI uses the qute-java scheme. + * + * @param uri the URI to check + * @return {@code true} if this URI is a Qute Java element URI + */ + public static boolean isJavaUri(URI uri) { + return uri != null && QUTE_JAVA_SCHEME.equals(uri.getScheme()); + } + + /** + * Returns true if the given string starts with the qute-java URI prefix. + * + * @param uri the URI string to check + * @return {@code true} if this string is a Qute Java element URI + */ + public static boolean isJavaUri(String uri) { + return uri != null && uri.startsWith(QUTE_JAVA_URI_PREFIX); + } +} diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java index d751628c3..db4adbb99 100644 --- a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java @@ -68,6 +68,7 @@ import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.input.Prompt; import dev.langchain4j.model.input.PromptTemplate; +//import dev.langchain4j.model.input.PromptTemplate; import dev.langchain4j.model.input.structured.StructuredPrompt; import dev.langchain4j.model.input.structured.StructuredPromptProcessor; import dev.langchain4j.model.moderation.Moderation; @@ -100,6 +101,7 @@ import io.quarkiverse.langchain4j.VideoUrl; import io.quarkiverse.langchain4j.response.ResponseAugmenterParams; import io.quarkiverse.langchain4j.runtime.ContextLocals; +import io.quarkiverse.langchain4j.runtime.JavaElementUriBuilder; import io.quarkiverse.langchain4j.runtime.QuarkusServiceOutputParser; import io.quarkiverse.langchain4j.runtime.ResponseSchemaUtil; import io.quarkiverse.langchain4j.runtime.aiservice.GuardrailsSupport.OutputGuardrailStreamingMapper; @@ -887,12 +889,23 @@ private static Optional prepareSystemMessage(AiServiceMethodCreat templateParams.put("chat_memory", previousChatMessages); Optional maybeText = systemMessageInfo.text(); if (maybeText.isPresent()) { - return Optional.of(PromptTemplate.from(maybeText.get()).apply(templateParams).toSystemMessage()); + String templateName = getTemplateName(createInfo.getInterfaceName(), createInfo.getMethodName(), true); + return Optional.of(PromptTemplate.from(maybeText.get(), templateName).apply(templateParams).toSystemMessage()); } else { return Optional.empty(); } } + private static String getTemplateName(String interfaceName, String methodName, boolean userMessage) { + return JavaElementUriBuilder + .builder(interfaceName) + .setMethod(methodName) + .setAnnotation(userMessage ? dev.langchain4j.service.UserMessage.class.getName() + : dev.langchain4j.service.SystemMessage.class.getName()) + .build() + .toString(); + } + private static UserMessage prepareUserMessage(AiServiceContext context, AiServiceMethodCreateInfo createInfo, Object[] methodArgs, boolean supportsJsonSchema) { AiServiceMethodCreateInfo.UserMessageInfo userMessageInfo = createInfo.getUserMessageInfo(); @@ -935,7 +948,8 @@ private static UserMessage prepareUserMessage(AiServiceContext context, AiServic createInfo.getResponseSchemaInfo().outputFormatInstructions()); } - Prompt prompt = PromptTemplate.from(templateText).apply(templateVariables); + String templateName = getTemplateName(createInfo.getInterfaceName(), createInfo.getMethodName(), false); + Prompt prompt = PromptTemplate.from(templateText, templateName).apply(templateVariables); List finalContents = new ArrayList<>(); finalContents.add(TextContent.from(prompt.text())); handleSpecialContentTypes(createInfo, methodArgs, finalContents);