Volver al índice

src/main/java/ar/com/companeros/runtime/AgentConfig.java

package ar.com.companeros.runtime;

import ar.com.companeros.decision.DecisionGateway;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.Set;

/** Configuración del operador; nunca contiene una clave de API o datos enviados al modelo. */
public final class AgentConfig {
    // Defaults de ensayo: dos jugadores y ninguna llamada paga automática.
    private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create();
    private static final Set<String> FIELDS = Set.of("schemaVersion", "populationLimit", "modelsEnabled",
        "foundationVerified", "solEnabled", "maxSessionUsd", "minAgentIntervalSeconds", "maxRequestsHour", "autoConnect", "webEnabled");
    private final Path file;
    private int populationLimit = 2;
    private boolean modelsEnabled, foundationVerified, solEnabled;
    private BigDecimal maxSessionUsd = BigDecimal.ZERO;
    private int minAgentIntervalSeconds = 60, maxRequestsHour = 30;
    private boolean autoConnect = true;
    private boolean webEnabled;
    private ResearchConfig research = ResearchConfig.defaults();

    private AgentConfig(Path file) { this.file = file; }

    public static AgentConfig load(Path configDir) {
        Path root = configDir.toAbsolutePath().normalize();
        Path legacy = root.resolve("companeros"), current = root.resolve("raps");
        try { if (Files.isDirectory(legacy) && !Files.exists(current)) Files.move(legacy, current); }
        catch (IOException failure) { throw new IllegalStateException("No se pudo migrar la configuración anterior", failure); }
        Path file = current.resolve("agents.json");
        AgentConfig config = new AgentConfig(file);
        try {
            config.research = ResearchConfig.load(current);
            config.webEnabled = config.research.enabled();
            if (!Files.exists(file)) { config.write(); return config; }
            if (Files.size(file) > 16_384) throw new IOException("Configuración fuera de límite");
            JsonObject data = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject();
            if (!FIELDS.containsAll(data.keySet()) || integer(data, "schemaVersion", 1) != 1)
                throw new IllegalArgumentException("Formato de configuración no reconocido");
            config.populationLimit = integer(data, "populationLimit", 2); validPopulation(config.populationLimit);
            config.modelsEnabled = bool(data, "modelsEnabled", false);
            config.foundationVerified = bool(data, "foundationVerified", false);
            config.solEnabled = bool(data, "solEnabled", false);
            config.autoConnect = bool(data, "autoConnect", true);
            config.maxSessionUsd = data.has("maxSessionUsd") ? data.get("maxSessionUsd").getAsBigDecimal() : BigDecimal.ZERO;
            config.minAgentIntervalSeconds = integer(data, "minAgentIntervalSeconds", 60);
            config.maxRequestsHour = integer(data, "maxRequestsHour", 30);
            config.decisionConfig(); // Valida cupos antes de habilitar cualquier proveedor.
            return config;
        } catch (IOException | RuntimeException error) {
            throw new IllegalStateException("No se pudo cargar agents.json; se conserva el archivo original", error);
        }
    }

    // Lectura y modificación limitada de población; flags del proveedor se administran en archivo.
    public synchronized int populationLimit() { return populationLimit; }
    public synchronized boolean autoConnect() { return autoConnect; }
    public synchronized boolean webEnabled() { return webEnabled; }
    public synchronized void setPopulationLimit(int value) {
        validPopulation(value); int previous = populationLimit; populationLimit = value;
        try { write(); }
        catch (IOException error) {
            populationLimit = previous;
            throw new IllegalStateException("No se pudo guardar el límite de población", error);
        }
    }
    public synchronized DecisionGateway.Config decisionConfig() {
        return new DecisionGateway.Config(modelsEnabled, foundationVerified, solEnabled, maxSessionUsd,
            maxRequestsHour, 16, 2, 2000, minAgentIntervalSeconds, Duration.ofSeconds(20), webEnabled,
            research.maxRequestsHour(), research.minAgentIntervalSeconds(), research.allowedDomains());
    }

    // Guardado atómico: rutas fijas del servicio confiable, sin parámetros de filesystem para bots.
    private void write() throws IOException {
        Files.createDirectories(file.getParent());
        JsonObject data = new JsonObject(); data.addProperty("schemaVersion", 1);
        data.addProperty("populationLimit", populationLimit); data.addProperty("modelsEnabled", modelsEnabled);
        data.addProperty("foundationVerified", foundationVerified); data.addProperty("solEnabled", solEnabled);
        data.addProperty("maxSessionUsd", maxSessionUsd); data.addProperty("minAgentIntervalSeconds", minAgentIntervalSeconds);
        data.addProperty("maxRequestsHour", maxRequestsHour); data.addProperty("autoConnect", autoConnect);
        Path temporary = file.resolveSibling("agents.json.tmp");
        Files.writeString(temporary, JSON.toJson(data) + System.lineSeparator(), StandardCharsets.UTF_8);
        try { Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); }
        catch (AtomicMoveNotSupportedException unavailable) { Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING); }
        finally { Files.deleteIfExists(temporary); }
    }
    private static void validPopulation(int value) {
        if (value != 2 && value != 4 && value != 6 && value != 10)
            throw new IllegalArgumentException("La población admite 2, 4, 6 o 10 jugadores");
    }
    private static int integer(JsonObject data, String key, int fallback) {
        if (!data.has(key)) return fallback;
        if (!data.get(key).isJsonPrimitive() || !data.get(key).getAsJsonPrimitive().isNumber())
            throw new IllegalArgumentException("Valor numérico inválido");
        return data.get(key).getAsBigDecimal().intValueExact();
    }
    private static boolean bool(JsonObject data, String key, boolean fallback) {
        if (!data.has(key)) return fallback;
        if (!data.get(key).isJsonPrimitive() || !data.get(key).getAsJsonPrimitive().isBoolean())
            throw new IllegalArgumentException("Valor booleano inválido");
        return data.get(key).getAsBoolean();
    }
}