Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* The {@code Clock} will be the system clock.
* </p>
*
* @param template the template string of the prompt.
*/
public PromptTemplate(String template) {
this(template, null);
}

/**
* Create a new PromptTemplate.
*
* <p>
* The {@code Clock} will be the system clock.
* </p>
*
* @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<String, Object> 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<String, Object> injectDateTimeVariables(Map<String, Object> variables) {
Map<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,8 +29,14 @@ public QuarkusPromptTemplateFactory() {
engineLazyValue.set(new LazyValue<>(new Supplier<Engine>() {
@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();
}
}));
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* These URIs have the format:
*
* <pre>
* qute-java://&lt;fully-qualified-class-name&gt;[#method][@annotation]
* </pre>
*
* Examples:
* <ul>
* <li>Class-level annotation: <code>qute-java://[email protected]</code></li>
* <li>Method-level annotation: <code>qute-java://com.acme.Bean#[email protected]</code></li>
* </ul>
* </p>
*
* <p>
* 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}.
* </p>
*/
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -887,12 +889,23 @@ private static Optional<SystemMessage> prepareSystemMessage(AiServiceMethodCreat
templateParams.put("chat_memory", previousChatMessages);
Optional<String> 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();
Expand Down Expand Up @@ -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<Content> finalContents = new ArrayList<>();
finalContents.add(TextContent.from(prompt.text()));
handleSpecialContentTypes(createInfo, methodArgs, finalContents);
Expand Down