Skip to content

Commit 172f37b

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

File tree

3 files changed

+180
-6
lines changed

3 files changed

+180
-6
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: 11 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,7 @@ public static void clear() {
4250

4351
@Override
4452
public Template create(Input input) {
45-
return new QuteTemplate(engineLazyValue.get().get().parse(input.getTemplate()));
53+
return new QuteTemplate(engineLazyValue.get().get().parse(input.getTemplate(), null, input.getName()));
4654
}
4755

4856
public static class MustacheTemplateVariableStyleParserHook implements ParserHook {

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
import dev.langchain4j.model.chat.request.json.JsonSchema;
6868
import dev.langchain4j.model.chat.response.ChatResponse;
6969
import dev.langchain4j.model.input.Prompt;
70-
import dev.langchain4j.model.input.PromptTemplate;
70+
//import dev.langchain4j.model.input.PromptTemplate;
7171
import dev.langchain4j.model.input.structured.StructuredPrompt;
7272
import dev.langchain4j.model.input.structured.StructuredPromptProcessor;
7373
import dev.langchain4j.model.moderation.Moderation;
@@ -97,6 +97,7 @@
9797
import io.quarkiverse.langchain4j.AudioUrl;
9898
import io.quarkiverse.langchain4j.ImageUrl;
9999
import io.quarkiverse.langchain4j.PdfUrl;
100+
import io.quarkiverse.langchain4j.PromptTemplate;
100101
import io.quarkiverse.langchain4j.VideoUrl;
101102
import io.quarkiverse.langchain4j.response.ResponseAugmenterParams;
102103
import io.quarkiverse.langchain4j.runtime.ContextLocals;
@@ -117,7 +118,7 @@
117118
*/
118119
public class AiServiceMethodImplementationSupport {
119120

120-
private static final Logger log = Logger.getLogger(AiServiceMethodImplementationSupport.class);
121+
private static final Logger log = Logger.getLogger(AiServiceMethodImplementationSupport.class);
121122
private static final int DEFAULT_MAX_SEQUENTIAL_TOOL_EXECUTIONS = 10;
122123
private static final List<DefaultMemoryIdProvider> DEFAULT_MEMORY_ID_PROVIDERS;
123124

@@ -887,12 +888,17 @@ private static Optional<SystemMessage> prepareSystemMessage(AiServiceMethodCreat
887888
templateParams.put("chat_memory", previousChatMessages);
888889
Optional<String> maybeText = systemMessageInfo.text();
889890
if (maybeText.isPresent()) {
890-
return Optional.of(PromptTemplate.from(maybeText.get()).apply(templateParams).toSystemMessage());
891+
String templateName = getTemplateName(createInfo.getInterfaceName(), createInfo.getMethodName(), false);
892+
return Optional.of(PromptTemplate.from(maybeText.get(), templateName).apply(templateParams).toSystemMessage());
891893
} else {
892894
return Optional.empty();
893895
}
894896
}
895897

898+
private static String getTemplateName(String interfaceName, String methodName, boolean userMessage) {
899+
return "java://annotation:" + interfaceName + "#" + methodName + (userMessage ? "@UserMessage" : "@SystemMessage");
900+
}
901+
896902
private static UserMessage prepareUserMessage(AiServiceContext context, AiServiceMethodCreateInfo createInfo,
897903
Object[] methodArgs, boolean supportsJsonSchema) {
898904
AiServiceMethodCreateInfo.UserMessageInfo userMessageInfo = createInfo.getUserMessageInfo();

0 commit comments

Comments
 (0)