Skip to content
Open
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
53 changes: 53 additions & 0 deletions src/main/java/com/langfuse/client/prompt/PromptCompiler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.langfuse.client.prompt;

import com.langfuse.client.resources.prompts.types.ChatMessage;
import com.langfuse.client.resources.prompts.types.ChatMessageWithPlaceholders;
import com.langfuse.client.resources.prompts.types.ChatPrompt;
import com.langfuse.client.resources.prompts.types.PlaceholderMessage;
import com.langfuse.client.resources.prompts.types.TextPrompt;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public final class PromptCompiler {

private PromptCompiler() {
}

public static String compile(TextPrompt prompt, Map<String, ?> variables) {
return PromptTemplate.compile(prompt.getPrompt(), variables);
}

public static List<ChatMessage> compile(ChatPrompt prompt, Map<String, ?> variables) {
if (prompt.getPrompt() == null || prompt.getPrompt().isEmpty()) {
return Collections.emptyList();
}

List<ChatMessage> compiled = new ArrayList<>();

for (ChatMessageWithPlaceholders messageOrPlaceholder : prompt.getPrompt()) {
messageOrPlaceholder.visit(new ChatMessageWithPlaceholders.Visitor<Void>() {
@Override
public Void visit(ChatMessage chatMessage) {
String compiledContent = PromptTemplate.compile(chatMessage.getContent(), variables);
compiled.add(ChatMessage.builder()
.role(chatMessage.getRole())
.content(compiledContent)
.build());
return null;
}

@Override
public Void visit(PlaceholderMessage placeholderMessage) {
// Placeholder messages are skipped in this version.
// A future version may support placeholder expansion.
return null;
}
});
}

return Collections.unmodifiableList(compiled);
}
}
86 changes: 86 additions & 0 deletions src/main/java/com/langfuse/client/prompt/PromptTemplate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.langfuse.client.prompt;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public final class PromptTemplate {

private static final String OPENING = "{{";
private static final String CLOSING = "}}";

private PromptTemplate() {
}

public static String compile(String template, Map<String, ?> variables) {
if (template == null || template.isEmpty()) {
return template;
}
if (variables == null || variables.isEmpty()) {
return template;
}

StringBuilder result = new StringBuilder();
int currentIndex = 0;

while (currentIndex < template.length()) {
int openIndex = template.indexOf(OPENING, currentIndex);
if (openIndex == -1) {
result.append(template, currentIndex, template.length());
break;
}

int closeIndex = template.indexOf(CLOSING, openIndex + OPENING.length());
if (closeIndex == -1) {
result.append(template, currentIndex, template.length());
break;
}

result.append(template, currentIndex, openIndex);

String variableName = template.substring(openIndex + OPENING.length(), closeIndex).trim();

if (variables.containsKey(variableName)) {
Object value = variables.get(variableName);
result.append(value != null ? String.valueOf(value) : "");
} else {
result.append(template, openIndex, closeIndex + CLOSING.length());
}

currentIndex = closeIndex + CLOSING.length();
}

return result.toString();
}

public static List<String> extractVariables(String template) {
if (template == null || template.isEmpty()) {
return Collections.emptyList();
}

List<String> variables = new ArrayList<>();
int currentIndex = 0;

while (currentIndex < template.length()) {
int openIndex = template.indexOf(OPENING, currentIndex);
if (openIndex == -1) {
break;
}

int closeIndex = template.indexOf(CLOSING, openIndex + OPENING.length());
if (closeIndex == -1) {
break;
}

String variableName = template.substring(openIndex + OPENING.length(), closeIndex).trim();
if (!variableName.isEmpty() && !variables.contains(variableName)) {
variables.add(variableName);
}

currentIndex = closeIndex + CLOSING.length();
}

return Collections.unmodifiableList(variables);
}
}
101 changes: 101 additions & 0 deletions src/test/java/com/langfuse/client/prompt/PromptCompilerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.langfuse.client.prompt;

import com.langfuse.client.resources.prompts.types.ChatMessage;
import com.langfuse.client.resources.prompts.types.ChatMessageWithPlaceholders;
import com.langfuse.client.resources.prompts.types.ChatPrompt;
import com.langfuse.client.resources.prompts.types.PlaceholderMessage;
import com.langfuse.client.resources.prompts.types.TextPrompt;

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

class PromptCompilerTest {

@Test
void compileTextPrompt() {
TextPrompt prompt = TextPrompt.builder()
.name("test")
.version(1)
.config(Map.of())
.prompt("Hello {{name}}, you are a {{role}}")
.build();

String result = PromptCompiler.compile(prompt, Map.of("name", "Alice", "role", "developer"));
assertEquals("Hello Alice, you are a developer", result);
}

@Test
void compileTextPromptMissingVariable() {
TextPrompt prompt = TextPrompt.builder()
.name("test")
.version(1)
.config(Map.of())
.prompt("Hello {{name}}, age {{age}}")
.build();

String result = PromptCompiler.compile(prompt, Map.of("name", "Alice"));
assertEquals("Hello Alice, age {{age}}", result);
}

@Test
void compileChatPrompt() {
ChatPrompt prompt = ChatPrompt.builder()
.name("test")
.version(1)
.config(Map.of())
.prompt(List.of(
ChatMessageWithPlaceholders.of(
ChatMessage.builder().role("system").content("You are a {{role}}").build()),
ChatMessageWithPlaceholders.of(
ChatMessage.builder().role("user").content("{{user_input}}").build())
))
.build();

List<ChatMessage> result = PromptCompiler.compile(prompt, Map.of("role", "helpful assistant", "user_input", "Hi"));

assertEquals(2, result.size());
assertEquals("system", result.get(0).getRole());
assertEquals("You are a helpful assistant", result.get(0).getContent());
assertEquals("user", result.get(1).getRole());
assertEquals("Hi", result.get(1).getContent());
}

@Test
void compileChatPromptSkipsPlaceholders() {
ChatPrompt prompt = ChatPrompt.builder()
.name("test")
.version(1)
.config(Map.of())
.prompt(List.of(
ChatMessageWithPlaceholders.of(
ChatMessage.builder().role("system").content("You are a {{role}}").build()),
ChatMessageWithPlaceholders.of(
PlaceholderMessage.builder().name("examples").build()),
ChatMessageWithPlaceholders.of(
ChatMessage.builder().role("user").content("{{question}}").build())
))
.build();

List<ChatMessage> result = PromptCompiler.compile(prompt, Map.of("role", "tutor", "question", "What is Java?"));

assertEquals(2, result.size());
assertEquals("You are a tutor", result.get(0).getContent());
assertEquals("What is Java?", result.get(1).getContent());
}

@Test
void compileChatPromptEmpty() {
ChatPrompt prompt = ChatPrompt.builder()
.name("test")
.version(1)
.config(Map.of())
.build();

List<ChatMessage> result = PromptCompiler.compile(prompt, Map.of("x", "y"));
assertTrue(result.isEmpty());
}
}
129 changes: 129 additions & 0 deletions src/test/java/com/langfuse/client/prompt/PromptTemplateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.langfuse.client.prompt;

import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

class PromptTemplateTest {

@Test
void basicReplacement() {
String result = PromptTemplate.compile("Hello {{name}}", Map.of("name", "World"));
assertEquals("Hello World", result);
}

@Test
void multipleVariables() {
String template = "{{greeting}}, {{name}}! Welcome to {{place}}.";
Map<String, String> vars = Map.of("greeting", "Hi", "name", "Alice", "place", "Langfuse");
assertEquals("Hi, Alice! Welcome to Langfuse.", PromptTemplate.compile(template, vars));
}

@Test
void duplicateVariables() {
String result = PromptTemplate.compile("{{x}} and {{x}}", Map.of("x", "same"));
assertEquals("same and same", result);
}

@Test
void missingVariableKeepsOriginal() {
String result = PromptTemplate.compile("Hello {{name}}, age {{age}}", Map.of("name", "Alice"));
assertEquals("Hello Alice, age {{age}}", result);
}

@Test
void extraVariablesIgnored() {
String result = PromptTemplate.compile("Hello {{name}}", Map.of("name", "Alice", "extra", "ignored"));
assertEquals("Hello Alice", result);
}

@Test
void nullValueBecomesEmpty() {
Map<String, Object> vars = new HashMap<>();
vars.put("name", null);
assertEquals("Hello ", PromptTemplate.compile("Hello {{name}}", vars));
}

@Test
void noVariablesReturnsOriginal() {
assertEquals("No variables here", PromptTemplate.compile("No variables here", Map.of("x", "y")));
}

@Test
void emptyTemplate() {
assertEquals("", PromptTemplate.compile("", Map.of("x", "y")));
}

@Test
void nullTemplate() {
assertNull(PromptTemplate.compile(null, Map.of("x", "y")));
}

@Test
void nullVariables() {
assertEquals("Hello {{name}}", PromptTemplate.compile("Hello {{name}}", null));
}

@Test
void emptyVariables() {
assertEquals("Hello {{name}}", PromptTemplate.compile("Hello {{name}}", Collections.emptyMap()));
}

@Test
void whitespaceInVariableName() {
String result = PromptTemplate.compile("Hello {{ name }}", Map.of("name", "Alice"));
assertEquals("Hello Alice", result);
}

@Test
void unclosedBracketKeepsOriginal() {
assertEquals("Hello {{name", PromptTemplate.compile("Hello {{name", Map.of("name", "Alice")));
}

@Test
void singleCurlyBracesIgnored() {
assertEquals("Hello {name}", PromptTemplate.compile("Hello {name}", Map.of("name", "Alice")));
}

@Test
void caseSensitive() {
String result = PromptTemplate.compile("{{Name}} and {{name}}", Map.of("Name", "A", "name", "b"));
assertEquals("A and b", result);
}

@Test
void objectValueUsesToString() {
String result = PromptTemplate.compile("Count: {{n}}", Map.of("n", 42));
assertEquals("Count: 42", result);
}

@Test
void jsonInTemplate() {
String template = "Config: {\"key\": \"{{value}}\"}";
assertEquals("Config: {\"key\": \"test\"}", PromptTemplate.compile(template, Map.of("value", "test")));
}

@Test
void extractVariablesBasic() {
List<String> vars = PromptTemplate.extractVariables("Hello {{name}}, welcome to {{place}}");
assertEquals(List.of("name", "place"), vars);
}

@Test
void extractVariablesDeduplicates() {
List<String> vars = PromptTemplate.extractVariables("{{x}} and {{x}} and {{y}}");
assertEquals(List.of("x", "y"), vars);
}

@Test
void extractVariablesEmpty() {
assertTrue(PromptTemplate.extractVariables("no variables").isEmpty());
assertTrue(PromptTemplate.extractVariables("").isEmpty());
assertTrue(PromptTemplate.extractVariables(null).isEmpty());
}
}