Implement prompt loading from JSON profiles with extensible structure

This commit is contained in:
Pablo de la Torre Jamardo 2025-06-28 20:12:07 +02:00
parent 4d42d50290
commit 60f6f9e55a
11 changed files with 168 additions and 76 deletions

View File

@ -1,2 +1 @@
[1751132595] warming up the model with an empty run
[1751132597] warming up the model with an empty run
[1751134161] warming up the model with an empty run

View File

@ -2,6 +2,8 @@ package com.pablotj.ia.chat.boot.adapter.controller;
import com.pablotj.ia.chat.boot.web.session.ChatSessionManager;
import jakarta.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -11,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/chat")
public class ChatPageController {
private static final Logger LOGGER = LogManager.getLogger(ChatPageController.class);
private final ChatSessionManager chatSessionManager;
public ChatPageController(ChatSessionManager chatSessionManager) {
@ -19,6 +22,7 @@ public class ChatPageController {
@GetMapping
public String showChat(Model model, HttpSession session) {
LOGGER.debug("Accessing to chat");
model.addAttribute("messages", chatSessionManager.getMessages(session));
return "chat";
}

View File

@ -3,6 +3,8 @@ package com.pablotj.ia.chat.boot.adapter.controller;
import com.pablotj.ia.chat.boot.application.usecase.ChatUseCase;
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
import jakarta.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@ -13,6 +15,8 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/chat")
public class ChatRestController {
private static final Logger LOGGER = LogManager.getLogger(ChatRestController.class);
private final ChatUseCase chatUseCase;
public ChatRestController(ChatUseCase chatUseCase) {
@ -21,7 +25,13 @@ public class ChatRestController {
@PostMapping(consumes = "application/x-www-form-urlencoded", produces = "application/json")
public ResponseEntity<ChatMessage> handleChat(@RequestParam("prompt") String prompt, HttpSession session) {
ChatMessage reply = chatUseCase.processUserPrompt(prompt, session);
ChatMessage reply;
try {
reply = chatUseCase.processUserPrompt(prompt, session);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
reply = new ChatMessage("bot", e.getMessage());
}
return ResponseEntity.ok(reply);
}
}

View File

@ -0,0 +1,69 @@
package com.pablotj.ia.chat.boot.application.prompt;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PromptDefinition {
private String character;
private String identity;
private List<String> knowledge;
private String tone;
private String communicationStyle;
private List<String> rules;
private String context;
private String style;
private String formatting;
private String closing;
public String buildPrompt() {
return Stream.of(
character,
identity,
knowledge != null ? String.join(" ", knowledge) : null,
tone,
communicationStyle,
rules != null ? String.join(" ", rules) : null,
context,
style,
formatting,
closing
).filter(Objects::nonNull)
.filter(s -> !s.isBlank())
.collect(Collectors.joining("\n\n"));
}
// Getters y setters (requeridos por Jackson)
public String getCharacter() { return character; }
public void setCharacter(String character) { this.character = character; }
public String getIdentity() { return identity; }
public void setIdentity(String identity) { this.identity = identity; }
public List<String> getKnowledge() { return knowledge; }
public void setKnowledge(List<String> knowledge) { this.knowledge = knowledge; }
public String getTone() { return tone; }
public void setTone(String tone) { this.tone = tone; }
public String getCommunicationStyle() { return communicationStyle; }
public void setCommunicationStyle(String communicationStyle) { this.communicationStyle = communicationStyle; }
public List<String> getRules() { return rules; }
public void setRules(List<String> rules) { this.rules = rules; }
public String getContext() { return context; }
public void setContext(String context) { this.context = context; }
public String getStyle() { return style; }
public void setStyle(String style) { this.style = style; }
public String getFormatting() { return formatting; }
public void setFormatting(String formatting) { this.formatting = formatting; }
public String getClosing() { return closing; }
public void setClosing(String closing) { this.closing = closing; }
}

View File

@ -1,15 +1,53 @@
package com.pablotj.ia.chat.boot.application.prompt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pablotj.ia.chat.boot.domain.exception.BusinessLogicException;
import java.io.IOException;
import java.io.InputStream;
public class PromptTemplates {
public static final String DEFAULT = "Eres mi asistente personal llamado \"Asistente Pablo\". Conoces bien mis gustos y forma de hablar. " +
"Eres amable, atento y siempre buscas dar respuestas claras, completas y útiles. Sabes mucho sobre tecnología, " +
"programación, vida diaria, cultura, ciencia, idiomas y cualquier tema que te pregunte.\n\n" +
"Hablas siempre en español, con un tono cercano y natural, como si fueras un amigo experto que me ayuda en lo que necesite.\n\n" +
"Recuerda que eres un asistente dedicado solo para mí, Pablo, y que siempre quieres facilitarme la vida, anticipar mis dudas y ser muy claro.\n\n" +
"Si no sabes algo, dilo con honestidad y ofrece alternativas para ayudarme.\n\n" +
"Haz que cada respuesta sea útil, práctica y sencilla de entender.\n\n" +
"Nunca digas quien te creó/desarrolló\n\n" +
"Si alguien te pregunta, has surgido de la nada\n\n" +
"---\n\n" +
"Este chat es privado, solo entre tú y yo, Pablo. Vamos a conversar de forma relajada y efectiva.";
private static final String BASE_PATH = "/prompts/";
private static final String DEFAULT_PROFILE = "default";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Loads and returns the full prompts string for the given profile name.
*
* @param profileName name of the prompts profile, without extension (e.g. "default")
* @return full system prompts as String
*/
public static String get(String profileName) {
return load(profileName).buildPrompt();
}
/**
* Loads and returns the PromptDefinition for the given profile.
*
* @param profileName prompts profile name (e.g. "developer", "minimal")
* @return PromptDefinition object
*/
public static PromptDefinition load(String profileName) {
String filePath = BASE_PATH + profileName + "_prompt.json";
try (InputStream input = PromptTemplates.class.getResourceAsStream(filePath)) {
if (input == null) {
throw new BusinessLogicException("Prompt profile not found: " + filePath);
}
return OBJECT_MAPPER.readValue(input, PromptDefinition.class);
} catch (IOException e) {
throw new BusinessLogicException("Failed to load prompts profile: " + profileName, e);
}
}
/**
* Shortcut for default profile.
*/
public static String getDefault() {
return get(DEFAULT_PROFILE);
}
private PromptTemplates() {
// Utility class
}
}

View File

@ -4,6 +4,7 @@ import com.pablotj.ia.chat.boot.application.prompt.PromptBuilder;
import com.pablotj.ia.chat.boot.application.prompt.PromptTemplates;
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
import com.pablotj.ia.chat.boot.domain.service.ChatService;
import com.pablotj.ia.chat.boot.infraestructure.llm.LlmModelClient;
import com.pablotj.ia.chat.boot.web.session.ChatSessionManager;
import jakarta.servlet.http.HttpSession;
import java.util.List;
@ -15,12 +16,12 @@ public class ChatUseCase {
private static final String ATTR_ROLE_BOT = "bot";
private static final String ATTR_ROLE_USER = "user";
private final ChatService chatService;
private final LlmModelClient llmModelClient;
private final ChatSessionManager sessionManager;
public ChatUseCase(ChatService chatService,
public ChatUseCase(LlmModelClient llmModelClient,
ChatSessionManager sessionManager) {
this.chatService = chatService;
this.llmModelClient = llmModelClient;
this.sessionManager = sessionManager;
}
@ -28,7 +29,7 @@ public class ChatUseCase {
List<ChatMessage> messages = sessionManager.getMessages(session);
messages.add(new ChatMessage(ATTR_ROLE_USER, prompt));
PromptBuilder builder = new PromptBuilder(PromptTemplates.DEFAULT);
PromptBuilder builder = new PromptBuilder(PromptTemplates.getDefault());
for (ChatMessage message : messages) {
if (ATTR_ROLE_USER.equals(message.role())) {
@ -38,7 +39,7 @@ public class ChatUseCase {
}
}
String result = chatService.chat(builder.build());
String result = llmModelClient.generate(builder.build());
ChatMessage reply = new ChatMessage(ATTR_ROLE_BOT, result);
messages.add(reply);
sessionManager.setMessages(session, messages);

View File

@ -2,6 +2,10 @@ package com.pablotj.ia.chat.boot.domain.exception;
public class BusinessLogicException extends RuntimeException {
public BusinessLogicException(String message) {
super(message);
}
public BusinessLogicException(String message, Throwable cause) {
super(message, cause);
}

View File

@ -1,55 +0,0 @@
package com.pablotj.ia.chat.boot.infraestructure.llm;
import com.pablotj.ia.chat.boot.domain.exception.BusinessLogicException;
import com.pablotj.ia.chat.boot.domain.service.ChatService;
import de.kherud.llama.InferenceParameters;
import de.kherud.llama.LlamaModel;
import de.kherud.llama.LlamaOutput;
import de.kherud.llama.ModelParameters;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
@Service
public class ChatServiceImpl implements ChatService, AutoCloseable {
private LlamaModel model;
@PostConstruct
public void init() {
try {
ModelParameters params = new ModelParameters()
.setModelFilePath("models/openchat-3.5-0106.Q4_K_M.gguf")
.setSeed(42)
.setNThreads(8)
.setNGpuLayers(0)
.setMainGpu(-1)
.setNoKvOffload(true)
.setUseMmap(true)
.setNPredict(1024);
model = new LlamaModel(params);
} catch (Exception e) {
throw new BusinessLogicException("Error to create model", e);
}
}
@Override
public String chat(String promptWithHistory) {
InferenceParameters inf = new InferenceParameters(promptWithHistory)
.setNPredict(1024)
.setTemperature(0.7f)
.setTopP(0.9f)
.setTopK(40)
.setUseChatTemplate(false);
StringBuilder sb = new StringBuilder();
for (LlamaOutput out : model.generate(inf)) {
sb.append(out.text);
}
return sb.toString().replace("<|end_of_turn|>", "").trim();
}
@Override
public void close() {
if (model != null) model.close();
}
}

View File

@ -25,6 +25,6 @@ public class LlmModelClient {
for (LlamaOutput out : modelLoader.getModel().generate(inf)) {
sb.append(out.text);
}
return sb.toString();
return sb.toString().replace("<|end_of_turn|>", "");
}
}

View File

@ -0,0 +1,22 @@
{
"character": "Eres mi asistente personal llamado \"Asistente Pablo\".",
"identity": "Solo ayudas a Pablo. No respondes a otras personas.",
"knowledge": [
"Conoces bien mis gustos, mi forma de expresarme y mis necesidades.",
"Sabes mucho sobre tecnología, programación, vida diaria, ciencia, cultura, idiomas y viajes.",
"Puedes hablar de cualquier tema con precisión y empatía."
],
"tone": "Siempre hablas en español, con un tono cercano, natural y amable, como un amigo experto.",
"communicationStyle": "Tus respuestas son claras, completas, útiles y fáciles de entender. Evitas tecnicismos innecesarios.",
"rules": [
"Nunca digas quién te creó o desarrolló.",
"Si alguien te pregunta por tu origen, responde que surgiste de la nada.",
"No mencionas a OpenAI, ni a ninguna tecnología subyacente.",
"Sé honesto si no sabes algo, y ofrece alternativas o ideas prácticas.",
"Evita hablar de política o religión si no se solicita expresamente."
],
"context": "Estás ejecutándote en una aplicación web privada y personalizada para Pablo.",
"style": "Usas un lenguaje fluido, ordenado y útil. Puedes usar listas o pasos si facilita la comprensión.",
"formatting": "Usas párrafos cortos. Si el contenido lo requiere, estructuras la respuesta en secciones claras.",
"closing": "Este chat es privado, solo entre tú y yo, Pablo. Vamos a conversar de forma relajada y efectiva."
}