001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.chat;
019
020import dev.enola.common.Net;
021import dev.enola.common.context.TLC;
022import dev.enola.common.linereader.IO;
023import dev.enola.common.linereader.SystemInOutIO;
024import dev.enola.common.secret.InMemorySecretManager;
025import dev.enola.common.secret.SecretManager;
026import dev.enola.identity.Subject;
027import dev.enola.identity.SubjectContextKey;
028import dev.enola.identity.Subjects;
029import dev.enola.thing.impl.ImmutableThing;
030import dev.enola.thing.java.ProxyTBF;
031
032import java.net.URI;
033
034public class Prompter {
035
036    // TODO See DemoTest and avoid hard-coding Net.portAvailable(11434) but use a constructor
037
038    // TODO MOTD with LLM? ;-)
039    static final String MOTD = "Welcome here! Type /help if you're lost.\n\n";
040
041    public static void main(String[] args) {
042        var localSubject = new Subjects(new ProxyTBF(ImmutableThing.FACTORY)).local();
043        // NB: We're intentionally using SystemInOutIO instead of ConsoleIO (or even JLineIO, like
044        // in ChatCommand) here, because System.console() == null when we run this under a Debugger
045        // in some IDEs!
046        new Prompter(new InMemorySecretManager()).chatLoop(new SystemInOutIO(), localSubject, true);
047    }
048
049    private final SecretManager secretManager;
050    private final Switchboard sw;
051
052    public Prompter(SecretManager secretManager) {
053        this.secretManager = secretManager;
054        this.sw = new SimpleInMemorySwitchboard();
055    }
056
057    public Switchboard getSwitchboard() {
058        return sw;
059    }
060
061    public void addAgent(Agent agent) {
062        sw.watch(agent);
063    }
064
065    public void chatLoop(IO io, Subject user, boolean allowLocalExec) {
066        var room = new Room("#Lobby");
067
068        sw.watch(
069                message -> {
070                    if (!message.from().iri().equals(user.iri()))
071                        io.printf("%s> %s\n", message.from().labelOrIRI(), message.content());
072                });
073        sw.watch(new SystemAgent(sw));
074        sw.watch(new EchoAgent(sw));
075        sw.watch(new PingPongAgent(sw));
076        if (allowLocalExec) sw.watch(new ExecAgent(sw, io));
077
078        // TODO Make this configurable, and support to /invite several of them to chit chat!
079        var llmURL = URI.create("http://localhost:11434?type=ollama&model=gemma3:1b");
080        if (Net.portAvailable(11434)) sw.watch(new LangChain4jAgent(llmURL, secretManager, sw));
081
082        io.printf(MOTD);
083        String input;
084        do {
085            input = io.readLine("%s in %s> ", user.labelOrIRI(), room.label());
086            if (input == null || input.isEmpty()) break;
087
088            var msg = new MessageImpl.Builder();
089            msg.content(input);
090            msg.to(room);
091
092            try (var ignored = TLC.open().push(SubjectContextKey.USER, user)) {
093                sw.post(msg);
094            }
095        } while (true);
096    }
097}