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}