Skip to content

Commit 0279469

Browse files
committed
Qute Debugging support.
Signed-off-by: azerr <[email protected]>
1 parent 21223a8 commit 0279469

File tree

4 files changed

+327
-5
lines changed

4 files changed

+327
-5
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package io.quarkiverse.langchain4j;
2+
3+
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
4+
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
5+
import static dev.langchain4j.spi.ServiceHelper.loadFactories;
6+
import static java.util.Collections.singletonMap;
7+
8+
import java.time.Clock;
9+
import java.time.LocalDate;
10+
import java.time.LocalDateTime;
11+
import java.time.LocalTime;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
15+
import dev.langchain4j.model.input.Prompt;
16+
import dev.langchain4j.spi.prompt.PromptTemplateFactory;
17+
import dev.langchain4j.spi.prompt.PromptTemplateFactory.Input;
18+
19+
/**
20+
* Represents a template of a prompt that can be reused multiple times.
21+
* A template typically contains one or more variables (placeholders) defined as {{variable_name}} that are
22+
* replaced with actual values to produce a Prompt.
23+
* Special variables {{current_date}}, {{current_time}}, and {{current_date_time}} are automatically
24+
* filled with LocalDate.now(), LocalTime.now(), and LocalDateTime.now() respectively.
25+
*/
26+
public class PromptTemplate {
27+
28+
private static final PromptTemplateFactory FACTORY = factory();
29+
30+
private static PromptTemplateFactory factory() {
31+
for (PromptTemplateFactory factory : loadFactories(PromptTemplateFactory.class)) {
32+
return factory;
33+
}
34+
return null; // new DefaultPromptTemplateFactory();
35+
}
36+
37+
static final String CURRENT_DATE = "current_date";
38+
static final String CURRENT_TIME = "current_time";
39+
static final String CURRENT_DATE_TIME = "current_date_time";
40+
41+
private final String templateString;
42+
private final PromptTemplateFactory.Template template;
43+
private final Clock clock;
44+
45+
/**
46+
* Create a new PromptTemplate.
47+
*
48+
* <p>
49+
* The {@code Clock} will be the system clock.
50+
* </p>
51+
*
52+
* @param template the template string of the prompt.
53+
*/
54+
public PromptTemplate(String template) {
55+
this(template, null);
56+
}
57+
58+
/**
59+
* Create a new PromptTemplate.
60+
*
61+
* <p>
62+
* The {@code Clock} will be the system clock.
63+
* </p>
64+
*
65+
* @param template the template string of the prompt.
66+
*/
67+
public PromptTemplate(String template, String templateName) {
68+
this(template, templateName, Clock.systemDefaultZone());
69+
}
70+
71+
/**
72+
* Create a new PromptTemplate.
73+
*
74+
* @param template the template string of the prompt.
75+
* @param clock the clock to use for the special variables.
76+
*/
77+
PromptTemplate(String template, String templateName, Clock clock) {
78+
this.templateString = ensureNotBlank(template, "template");
79+
if (templateName != null) {
80+
this.template = FACTORY.create(new PromptTemplateFactory.Input() {
81+
82+
@Override
83+
public String getTemplate() {
84+
return template;
85+
}
86+
87+
@Override
88+
public String getName() {
89+
return templateName;
90+
}
91+
});
92+
} else {
93+
this.template = FACTORY.create(() -> template);
94+
}
95+
this.clock = ensureNotNull(clock, "clock");
96+
}
97+
98+
/**
99+
* @return A prompt template string.
100+
*/
101+
public String template() {
102+
return templateString;
103+
}
104+
105+
/**
106+
* Applies a value to a template containing a single variable. The single variable should have the name {{it}}.
107+
*
108+
* @param value The value that will be injected in place of the {{it}} placeholder in the template.
109+
* @return A Prompt object where the {{it}} placeholder in the template has been replaced by the provided value.
110+
*/
111+
public Prompt apply(Object value) {
112+
return apply(singletonMap("it", value));
113+
}
114+
115+
/**
116+
* Applies multiple values to a template containing multiple variables.
117+
*
118+
* @param variables A map of variable names to values that will be injected in place of the corresponding placeholders in
119+
* the template.
120+
* @return A Prompt object where the placeholders in the template have been replaced by the provided values.
121+
*/
122+
public Prompt apply(Map<String, Object> variables) {
123+
ensureNotNull(variables, "variables");
124+
return Prompt.from(template.render(injectDateTimeVariables(variables)));
125+
}
126+
127+
/**
128+
* Injects the special variables {{current_date}}, {{current_time}}, and {{current_date_time}} into the given map.
129+
*
130+
* @param variables the map to inject the variables into.
131+
* @return a copy of the map with the variables injected.
132+
*/
133+
private Map<String, Object> injectDateTimeVariables(Map<String, Object> variables) {
134+
Map<String, Object> variablesCopy = new HashMap<>(variables);
135+
variablesCopy.put(CURRENT_DATE, LocalDate.now(clock));
136+
variablesCopy.put(CURRENT_TIME, LocalTime.now(clock));
137+
variablesCopy.put(CURRENT_DATE_TIME, LocalDateTime.now(clock));
138+
return variablesCopy;
139+
}
140+
141+
/**
142+
* Create a new PromptTemplate.
143+
*
144+
* @param template the template string of the prompt.
145+
* @return the PromptTemplate.
146+
*/
147+
public static PromptTemplate from(String template) {
148+
return from(template, null);
149+
}
150+
151+
/**
152+
* Create a new PromptTemplate.
153+
*
154+
* @param template the template string of the prompt.
155+
* @return the PromptTemplate.
156+
*/
157+
public static PromptTemplate from(String template, String templateName) {
158+
return new PromptTemplate(template, templateName);
159+
}
160+
}

core/runtime/src/main/java/io/quarkiverse/langchain4j/QuarkusPromptTemplateFactory.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import dev.langchain4j.spi.prompt.PromptTemplateFactory;
1414
import io.quarkiverse.langchain4j.spi.PromptTemplateFactoryContentFilterProvider;
1515
import io.quarkus.arc.Arc;
16+
import io.quarkus.arc.ArcContainer;
1617
import io.quarkus.arc.impl.LazyValue;
1718
import io.quarkus.qute.Engine;
19+
import io.quarkus.qute.EngineBuilder;
1820
import io.quarkus.qute.ParserHelper;
1921
import io.quarkus.qute.ParserHook;
2022
import io.quarkus.qute.TemplateInstance;
@@ -27,8 +29,14 @@ public QuarkusPromptTemplateFactory() {
2729
engineLazyValue.set(new LazyValue<>(new Supplier<Engine>() {
2830
@Override
2931
public Engine get() {
30-
return Arc.container().instance(Engine.class).get().newBuilder()
31-
.addParserHook(new MustacheTemplateVariableStyleParserHook()).build();
32+
ArcContainer container = Arc.container();
33+
EngineBuilder builder = container.instance(Engine.class).get().newBuilder()
34+
.addParserHook(new MustacheTemplateVariableStyleParserHook());
35+
// fire event to call DebugQuteEngineObserver#configureEngine(@Observes EngineBuilder builder, QuteConfig config)
36+
// to track the langchain4j engine builder with Qute debugger
37+
// see https://github.com/quarkusio/quarkus/blob/84414f0fd571881f5601c1dc73a0f43c07080a87/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/debug/DebugQuteEngineObserver.java#L41
38+
container.beanManager().getEvent().fire(builder);
39+
return builder.build();
3240
}
3341
}));
3442
}
@@ -42,7 +50,8 @@ public static void clear() {
4250

4351
@Override
4452
public Template create(Input input) {
45-
return new QuteTemplate(engineLazyValue.get().get().parse(input.getTemplate()));
53+
String javaElementUri = input.getName();
54+
return new QuteTemplate(engineLazyValue.get().get().parse(input.getTemplate(), null, javaElementUri));
4655
}
4756

4857
public static class MustacheTemplateVariableStyleParserHook implements ParserHook {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package io.quarkiverse.langchain4j.runtime;
2+
3+
import java.net.URI;
4+
5+
/**
6+
* Builder for Qute-specific URIs that reference a Java element
7+
* (class, method, or annotation) from a template.
8+
* <p>
9+
* These URIs have the format:
10+
*
11+
* <pre>
12+
* qute-java://&lt;fully-qualified-class-name&gt;[#method][@annotation]
13+
* </pre>
14+
*
15+
* Examples:
16+
* <ul>
17+
* <li>Class-level annotation: <code>qute-java://[email protected]</code></li>
18+
* <li>Method-level annotation: <code>qute-java://com.acme.Bean#[email protected]</code></li>
19+
* </ul>
20+
* </p>
21+
*
22+
* <p>
23+
* This builder is used to construct such URIs in a type-safe way and to provide
24+
* utility methods to identify and parse them. It is aligned with
25+
* {@link io.quarkus.qute.debug.client.JavaSourceLocationArguments#javaElementUri}.
26+
* </p>
27+
*/
28+
public class JavaElementUriBuilder {
29+
30+
/** Scheme used for Qute Java URIs. */
31+
public static final String QUTE_JAVA_SCHEME = "qute-java";
32+
33+
/** Prefix for Qute Java URIs. */
34+
public static final String QUTE_JAVA_URI_PREFIX = QUTE_JAVA_SCHEME + "://";
35+
36+
private final String typeName;
37+
private String method;
38+
private String annotation;
39+
40+
private JavaElementUriBuilder(String typeName) {
41+
this.typeName = typeName;
42+
}
43+
44+
/**
45+
* Returns the fully qualified Java class name for this URI.
46+
*
47+
* @return the class name
48+
*/
49+
public String getTypeName() {
50+
return typeName;
51+
}
52+
53+
/**
54+
* Returns the Java method name (nullable if not specified).
55+
*
56+
* @return the method name or {@code null}
57+
*/
58+
public String getMethod() {
59+
return method;
60+
}
61+
62+
/**
63+
* Sets the Java method name.
64+
*
65+
* @param method the method name to set
66+
* @return this builder
67+
*/
68+
public JavaElementUriBuilder setMethod(String method) {
69+
this.method = method;
70+
return this;
71+
}
72+
73+
/**
74+
* Returns the fully qualified Java annotation name (nullable if not specified).
75+
*
76+
* @return the annotation name or {@code null}
77+
*/
78+
public String getAnnotation() {
79+
return annotation;
80+
}
81+
82+
/**
83+
* Sets the fully qualified Java annotation name.
84+
*
85+
* @param annotation the annotation name to set
86+
* @return this builder
87+
*/
88+
public JavaElementUriBuilder setAnnotation(String annotation) {
89+
this.annotation = annotation;
90+
return this;
91+
}
92+
93+
/**
94+
* Creates a new builder for the given Java class name.
95+
*
96+
* @param typeName fully qualified Java class name
97+
* @return a new {@link JavaElementUriBuilder}
98+
*/
99+
public static JavaElementUriBuilder builder(String typeName) {
100+
return new JavaElementUriBuilder(typeName);
101+
}
102+
103+
/**
104+
* Builds the Qute Java URI representing the element.
105+
*
106+
* @return a {@link URI} for the Java element
107+
*/
108+
public URI build() {
109+
StringBuilder uri = new StringBuilder(QUTE_JAVA_URI_PREFIX);
110+
uri.append(typeName);
111+
if (method != null) {
112+
uri.append("#").append(method);
113+
}
114+
if (annotation != null) {
115+
uri.append("@").append(annotation);
116+
}
117+
return URI.create(uri.toString());
118+
}
119+
120+
/**
121+
* Returns true if the given URI uses the qute-java scheme.
122+
*
123+
* @param uri the URI to check
124+
* @return {@code true} if this URI is a Qute Java element URI
125+
*/
126+
public static boolean isJavaUri(URI uri) {
127+
return uri != null && QUTE_JAVA_SCHEME.equals(uri.getScheme());
128+
}
129+
130+
/**
131+
* Returns true if the given string starts with the qute-java URI prefix.
132+
*
133+
* @param uri the URI string to check
134+
* @return {@code true} if this string is a Qute Java element URI
135+
*/
136+
public static boolean isJavaUri(String uri) {
137+
return uri != null && uri.startsWith(QUTE_JAVA_URI_PREFIX);
138+
}
139+
}

core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import dev.langchain4j.model.chat.response.ChatResponse;
6969
import dev.langchain4j.model.input.Prompt;
7070
import dev.langchain4j.model.input.PromptTemplate;
71+
//import dev.langchain4j.model.input.PromptTemplate;
7172
import dev.langchain4j.model.input.structured.StructuredPrompt;
7273
import dev.langchain4j.model.input.structured.StructuredPromptProcessor;
7374
import dev.langchain4j.model.moderation.Moderation;
@@ -100,6 +101,7 @@
100101
import io.quarkiverse.langchain4j.VideoUrl;
101102
import io.quarkiverse.langchain4j.response.ResponseAugmenterParams;
102103
import io.quarkiverse.langchain4j.runtime.ContextLocals;
104+
import io.quarkiverse.langchain4j.runtime.JavaElementUriBuilder;
103105
import io.quarkiverse.langchain4j.runtime.QuarkusServiceOutputParser;
104106
import io.quarkiverse.langchain4j.runtime.ResponseSchemaUtil;
105107
import io.quarkiverse.langchain4j.runtime.aiservice.GuardrailsSupport.OutputGuardrailStreamingMapper;
@@ -887,12 +889,23 @@ private static Optional<SystemMessage> prepareSystemMessage(AiServiceMethodCreat
887889
templateParams.put("chat_memory", previousChatMessages);
888890
Optional<String> maybeText = systemMessageInfo.text();
889891
if (maybeText.isPresent()) {
890-
return Optional.of(PromptTemplate.from(maybeText.get()).apply(templateParams).toSystemMessage());
892+
String templateName = getTemplateName(createInfo.getInterfaceName(), createInfo.getMethodName(), true);
893+
return Optional.of(PromptTemplate.from(maybeText.get(), templateName).apply(templateParams).toSystemMessage());
891894
} else {
892895
return Optional.empty();
893896
}
894897
}
895898

899+
private static String getTemplateName(String interfaceName, String methodName, boolean userMessage) {
900+
return JavaElementUriBuilder
901+
.builder(interfaceName)
902+
.setMethod(methodName)
903+
.setAnnotation(userMessage ? dev.langchain4j.service.UserMessage.class.getName()
904+
: dev.langchain4j.service.SystemMessage.class.getName())
905+
.build()
906+
.toString();
907+
}
908+
896909
private static UserMessage prepareUserMessage(AiServiceContext context, AiServiceMethodCreateInfo createInfo,
897910
Object[] methodArgs, boolean supportsJsonSchema) {
898911
AiServiceMethodCreateInfo.UserMessageInfo userMessageInfo = createInfo.getUserMessageInfo();
@@ -935,7 +948,8 @@ private static UserMessage prepareUserMessage(AiServiceContext context, AiServic
935948
createInfo.getResponseSchemaInfo().outputFormatInstructions());
936949
}
937950

938-
Prompt prompt = PromptTemplate.from(templateText).apply(templateVariables);
951+
String templateName = getTemplateName(createInfo.getInterfaceName(), createInfo.getMethodName(), false);
952+
Prompt prompt = PromptTemplate.from(templateText, templateName).apply(templateVariables);
939953
List<Content> finalContents = new ArrayList<>();
940954
finalContents.add(TextContent.from(prompt.text()));
941955
handleSpecialContentTypes(createInfo, methodArgs, finalContents);

0 commit comments

Comments
 (0)