fix(ia): make memory persistence async, wire heuristic fact extraction and /ia lembrar, and add tag and event tests
This commit is contained in:
@@ -457,7 +457,7 @@ final class Ai {
|
||||
if (asker != null) {
|
||||
boolean bedrock = Platform.isBedrock(asker);
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
asker.sendMessage(style(segments.get(i), question, persona, settings, bedrock, i == 0));
|
||||
asker.sendMessage(style(segments.get(i), question, persona, settings.aiFancy(), bedrock, i == 0));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -469,7 +469,7 @@ final class Ai {
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
String segment = segments.get(i);
|
||||
boolean first = i == 0;
|
||||
plugin.broadcastPerPlatform(bedrock -> style(segment, question, persona, settings, bedrock, first));
|
||||
plugin.broadcastPerPlatform(bedrock -> style(segment, question, persona, settings.aiFancy(), bedrock, first));
|
||||
}
|
||||
plugin.openAiReactions(askerId);
|
||||
}
|
||||
@@ -477,10 +477,10 @@ final class Ai {
|
||||
/**
|
||||
* Renders one answer for chat.
|
||||
*/
|
||||
private Component style(String answer, String question, Persona persona, Settings settings, boolean bedrock, boolean firstLine) {
|
||||
static Component style(String answer, String question, Persona persona, boolean fancy, boolean bedrock, boolean firstLine) {
|
||||
Component body = Component.text(answer, NamedTextColor.WHITE)
|
||||
.decoration(TextDecoration.BOLD, false);
|
||||
if (!bedrock && settings.aiFancy()) {
|
||||
if (!bedrock && fancy) {
|
||||
body = body
|
||||
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
|
||||
Component.text("Pergunta: ", NamedTextColor.GRAY)
|
||||
|
||||
@@ -975,6 +975,14 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
iaForget(sender);
|
||||
return true;
|
||||
}
|
||||
if (sub.equals("lembrar") && args.length >= 2) {
|
||||
String fact = String.join(" ", Arrays.copyOfRange(args, 1, args.length)).trim();
|
||||
if (!fact.isBlank() && plugin.playerMemory() != null) {
|
||||
plugin.playerMemory().addFact(player.getUniqueId(), player.getName(), fact);
|
||||
Msg.ok(sender, "Fato gravado na memória da sua IA: \"" + fact + "\"");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Personalities: player can switch for themselves (/ia persona <nome>)
|
||||
// or reset to default (/ia persona padrao).
|
||||
if ((sub.equals("personalidade") || sub.equals("persona"))
|
||||
@@ -1840,7 +1848,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
if (name.equals("ia") || name.equals("iap")) {
|
||||
if (args.length == 1) {
|
||||
List<String> subs = new ArrayList<>(List.of("persona", "personalidade", "status", "esquecer"));
|
||||
List<String> subs = new ArrayList<>(List.of("persona", "personalidade", "status", "lembrar", "esquecer"));
|
||||
if (sender.hasPermission("canalhandia.ia.perfil")) {
|
||||
subs.addAll(List.of("perfil", "eventos", "saudacao", "corrigir"));
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Persistent per-player memory and preferences for the AI companion.
|
||||
@@ -54,6 +58,11 @@ final class PlayerMemory {
|
||||
|
||||
private final File file;
|
||||
private final Map<UUID, Profile> profiles = new HashMap<>();
|
||||
private final ExecutorService io = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "canalhandia-player-memory-io");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
PlayerMemory(File file) {
|
||||
this.file = file;
|
||||
@@ -108,7 +117,25 @@ final class PlayerMemory {
|
||||
}
|
||||
}
|
||||
|
||||
String summary(UUID uuid) {
|
||||
synchronized (profiles) {
|
||||
Profile p = profiles.get(uuid);
|
||||
return p != null ? p.summary() : null;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> facts(UUID uuid) {
|
||||
synchronized (profiles) {
|
||||
Profile p = profiles.get(uuid);
|
||||
return p != null ? Collections.unmodifiableList(p.facts()) : List.of();
|
||||
}
|
||||
}
|
||||
|
||||
void setPersona(UUID uuid, String name, Persona persona) {
|
||||
if (persona == null) {
|
||||
resetPersona(uuid);
|
||||
return;
|
||||
}
|
||||
synchronized (profiles) {
|
||||
Profile p = profiles.computeIfAbsent(uuid, k -> new Profile(k, name, null, "", new ArrayList<>(), System.currentTimeMillis()));
|
||||
profiles.put(uuid, p.withPersona(persona));
|
||||
@@ -126,20 +153,6 @@ final class PlayerMemory {
|
||||
save();
|
||||
}
|
||||
|
||||
String summary(UUID uuid) {
|
||||
synchronized (profiles) {
|
||||
Profile p = profiles.get(uuid);
|
||||
return p != null ? p.summary() : null;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> facts(UUID uuid) {
|
||||
synchronized (profiles) {
|
||||
Profile p = profiles.get(uuid);
|
||||
return p != null ? Collections.unmodifiableList(new ArrayList<>(p.facts())) : List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condenses a conversation turn into the player's persistent summary.
|
||||
*/
|
||||
@@ -178,7 +191,39 @@ final class PlayerMemory {
|
||||
}
|
||||
profiles.put(uuid, p.withSummary(updated));
|
||||
}
|
||||
save();
|
||||
|
||||
// Automatic heuristic fact extraction from player statements
|
||||
String heuristicFact = extractHeuristicFact(question);
|
||||
if (heuristicFact != null) {
|
||||
addFact(uuid, name, heuristicFact);
|
||||
} else {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
static String extractHeuristicFact(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
String lower = text.toLowerCase(Locale.ROOT).trim();
|
||||
String[] triggers = {
|
||||
"minha base", "meu spawn", "minha casa", "estou construindo",
|
||||
"meu plano", "meu objetivo", "sou especialista em", "moro em"
|
||||
};
|
||||
for (String trigger : triggers) {
|
||||
int idx = lower.indexOf(trigger);
|
||||
if (idx >= 0) {
|
||||
String candidate = text.substring(idx).trim();
|
||||
candidate = candidate.replaceAll("[?!.]+$", "").trim();
|
||||
if (candidate.length() > 60) {
|
||||
candidate = candidate.substring(0, 60) + "…";
|
||||
}
|
||||
if (candidate.length() >= 8) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void addFact(UUID uuid, String name, String fact) {
|
||||
@@ -250,7 +295,15 @@ final class PlayerMemory {
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
/** Flushes any pending background writes to disk (useful for shutdown or tests). */
|
||||
void flush() {
|
||||
try {
|
||||
io.submit(() -> {}).get(2, TimeUnit.SECONDS);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private YamlConfiguration buildYaml() {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
synchronized (profiles) {
|
||||
for (Map.Entry<UUID, Profile> entry : profiles.entrySet()) {
|
||||
@@ -265,6 +318,21 @@ final class PlayerMemory {
|
||||
yaml.set(key + ".atualizado_em", p.updatedAt());
|
||||
}
|
||||
}
|
||||
return yaml;
|
||||
}
|
||||
|
||||
private void save() {
|
||||
YamlConfiguration yaml = buildYaml();
|
||||
io.execute(() -> {
|
||||
try {
|
||||
yaml.save(file);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void saveSync() {
|
||||
YamlConfiguration yaml = buildYaml();
|
||||
try {
|
||||
yaml.save(file);
|
||||
} catch (Exception e) {
|
||||
|
||||
Reference in New Issue
Block a user