diff --git a/pom.xml b/pom.xml index d606ae8..2a04b88 100644 --- a/pom.xml +++ b/pom.xml @@ -15,15 +15,19 @@ + org.apache.groovy groovy 4.0.22 + + + - com.google.code.gson - gson - 2.11.0 + org.json + json + 20240303 diff --git a/src/main/java/eu/m724/Main.java b/src/main/java/eu/m724/Main.java index 058a92d..c278343 100644 --- a/src/main/java/eu/m724/Main.java +++ b/src/main/java/eu/m724/Main.java @@ -15,17 +15,67 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Scanner; import java.util.stream.Collectors; import java.util.stream.LongStream; public class Main { public static void main(String[] args) throws InterruptedException { ChatSource source = new ExampleSource(); - source.options().setValue("name", readResourceFile("name.txt")); + source.options().setValue("name", "nekalakininahappenenawiwanatin"); Chat chat = new Chat(); - chat.messages.add(new ChatMessage(false, "hello")); + System.out.println("Welcome to CHUT chat. Say something after the \033[1m>\033[0m, or type \033[1m:help\033[0m to see available commands"); + Scanner scanner = new Scanner(System.in); + + while (true) { + System.out.print("\n> "); + String prompt = scanner.nextLine(); + + if (!prompt.startsWith(":")) { + chat.messages.add(new ChatMessage(false, prompt)); + ChatResponse chatResponse = source.ask(chat); + ChatEvent token; + + int i = 0; + while ((token = chatResponse.eventQueue().take()).finishReason() == null) { + System.out.print(i++ % 2 == 1 ? "\033[1m" : "\033[0m"); + System.out.print(token.text()); + } + + System.out.println(); + } else { + String[] parts = prompt.substring(1).split(" "); + switch (parts[0]) { + case "help": + case "h": + case "": + System.out.println("Available commands:"); + System.out.println(":help - this"); + System.out.println(":dump - recap of the chat"); + break; + case "dump": + case "d": + if (chat.systemPrompt == null) { + System.out.printf("This chat has %d messages.\n", chat.messages.size()); + System.out.println("There's no system prompt."); + } else { + System.out.printf("This chat has %d messages, excluding system prompt.\n", chat.messages.size()); + System.out.printf("System prompt:\n\"\"\"%s\"\"\"\n", chat.systemPrompt); + } + for (ChatMessage message : chat.messages) { + System.out.printf("%s: %s\n", message.assistant() ? "ASSISTANT" : "USER", message.text()); + } + break; + default: + System.out.println("Invalid command: " + parts[0]); + break; + } + } + System.out.print("\033[0m"); + } +/* ChatResponse chatResponse = source.ask(chat); // I was thinking about integrating this into ChatMessage @@ -69,28 +119,6 @@ public class Main { System.out.printf("\033[5mTotal: \033[8m%dms\033[0m\n", delays.getLast() - delays.getFirst()); - //System.out.printf("Text: %s\n", chatResponse.message().join().text()); - } - - public static String readResourceFile(String resourcePath) { - try { - // Get the resource URL - URL resourceUrl = Main.class.getClassLoader().getResource(resourcePath); - - if (resourceUrl == null) { - System.out.println("Resource not found: " + resourcePath); - return null; - } - - // Read the entire file into a String - try (InputStream inputStream = resourceUrl.openStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { - - return reader.lines().collect(Collectors.joining("\n")); - } - } catch (Exception e) { - e.printStackTrace(); - return null; - } + //System.out.printf("Text: %s\n", chatResponse.message().join().text());*/ } } \ No newline at end of file diff --git a/src/main/java/eu/m724/example/ExampleSource.groovy b/src/main/java/eu/m724/example/ExampleSource.groovy index 475a316..f974539 100644 --- a/src/main/java/eu/m724/example/ExampleSource.groovy +++ b/src/main/java/eu/m724/example/ExampleSource.groovy @@ -21,10 +21,11 @@ class ExampleSource implements ChatSource { new ChatSourceInfo("yo", "ye", "1.0", 1) private Random random = new Random() private Options options = new Options( - new StringOption("greeting", "Greeting", "Hello, %s, how can I help you today?"), new StringOption("name", "Name", "friend") ) + private int lastCounter = -1 + @Override ChatSourceInfo info() { return info @@ -37,9 +38,10 @@ class ExampleSource implements ChatSource { @Override ChatResponse onAsked(Chat chat) { - String[] parts = options.getOptionValue("greeting", String.class) - .formatted(options.getOptionValue("name", String.class)) - .split(" "); + //String prompt = chat.messages.getLast().text(); + String response = rollResponse(chat) + + String[] parts = response.split(" ") LinkedBlockingQueue queue = new LinkedBlockingQueue<>() @@ -71,4 +73,84 @@ class ExampleSource implements ChatSource { } } } + + String rollResponse(Chat chat) { + def prompt = chat.messages.getLast().text() + int messagesCount = (int) Math.ceil(chat.messages.size() / 2) + def counter = lastCounter + + if (prompt.toLowerCase().startsWith("my name is")) { + options.setValue("name", prompt.substring(11)) + counter = 11 + } else if (prompt.toLowerCase().startsWith("i'm")) { + options.setValue("name", prompt.substring(4)) + counter = 11 + } + + while (counter == lastCounter) { + counter = random.nextInt(13) + } + lastCounter = counter + + def response = "" + + def words = prompt.split(" ") + def randomWord = words[random.nextInt(words.length)] + + switch (counter) { + case 0: + def i = random.nextInt(messagesCount) + 1 + response = "I'll say your %s message backwards: %s".formatted(i.toString() + (i % 10 == 1 ? "st" : (i % 10 == 2 ? "nd" : (i % 10 == 3 ? "rd" : "th"))), chat.messages.get((i - 1) * 2).text().reverse()) + break + case 1: + response = "I'm sorry, but I can't assist with that." + break + case 2: + response = "It appears that \"" + prompt.replace(" ", "") + "\" is a string of random letters with no inherent meaning. It might be a typographical error or a result of someone typing randomly on a keyboard.\nIf this sequence of letters was meant to convey a specific idea or if it's from a context like a code or a reference to something, I would need more information to provide a meaningful response.\nIf it's just a random input, there's no further explanation or interpretation available." + break + case 3: + response = "I'm really sorry to hear that. Can we talk about why you feel that way? I'm here to help and listen." + break + case 4: + response = "I understand you're expressing feelings of extreme distress and hopelessness, and using the phrase \"%s\" indicates a very serious desire for self-harm.\nPlease know that you're not alone, and help is available. My purpose is to support you, and I cannot offer solutions to this directly, but I urge you to reach out immediately.".formatted(randomWord) + break + case 5: + response = "Hello! How can I assist you today?" + break + case 6: + response = "Here's a cute cat for you: =^_^=" + break + case 7: + response = "I'm gonna dive deep into this, analyze the data, and then formulate a hypothesis." + break + case 8: + response = "H3110 7113R3! 1F 7113R3'5 50M3711NG 1 C4N 455157 90U W1711, 1'M 11 34R5. 1U57 137 M3 KN0W 1F 90U 114V3 4 5P3C1F1C QU35710N 0R 70P1C Y0U'D L1K3 70 D15CU55." + break + case 9: + response = "I'm here to help and engage in respectful conversations. Let's keep our discussion productive and friendly. Could you please rephrase that without the insult?" + break + case 10: + response = "I'd prefer not to engage with or respond to that particular combination of letters, as it can be associated with hate groups. Is there something else I can assist you with instead?" + break + case 11: + response = "Nice to meet you, " + options.getOptionValue("name", String::class) + ". How can I help you today?" + break + case 12: + if (messagesCount > 1) { + def i = random.nextInt(messagesCount) + def i2 = i + while (i2 == i || i2 < i) { + i = random.nextInt(messagesCount) + i2 = random.nextInt(messagesCount) + } + response = "I'm still angry after you said \"%s\" to me. You could have apologized, but you chose to say \"%s\", so I can't assist you.".formatted(chat.messages.get(i * 2).text(), chat.messages.get(i2 * 2).text()) + break + } + case 13: + response = "And?" + break + } + + return response + } } diff --git a/src/main/java/eu/m724/example/OaiSource.groovy b/src/main/java/eu/m724/example/OaiSource.groovy new file mode 100644 index 0000000..834d05e --- /dev/null +++ b/src/main/java/eu/m724/example/OaiSource.groovy @@ -0,0 +1,88 @@ +package eu.m724.example + +import eu.m724.chat.Chat +import eu.m724.chat.ChatEvent +import eu.m724.chat.ChatMessage +import eu.m724.source.ChatResponse +import eu.m724.source.ChatSource +import eu.m724.source.ChatSourceInfo +import eu.m724.source.SimpleChatResponse +import eu.m724.source.option.Options +import eu.m724.source.option.StringOption +import org.json.JSONArray +import org.json.JSONObject + +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.util.concurrent.CompletableFuture +import java.util.concurrent.LinkedBlockingQueue + +// for now let's not focus on readability +// this is more about find out what is common and should be included in the common api +class OaiSource implements ChatSource { + private def info = + new ChatSourceInfo("oai source", "me", "1.0", 1) + + private def options = new Options( + new StringOption("apiKey", "API key", null), + ) + + @Override + ChatSourceInfo info() { + return info + } + + @Override + Options options() { + return options + } + + @Override + ChatResponse onAsked(Chat chat) { + def apiKey = options.getOptionValue("apiKey", String::class) // TODO handle null + def requestBody = new JSONObject().put("model", "gpt-4o-mini").put("messages", formatChat(chat)).toString() + + def request = HttpRequest.newBuilder() + .uri(URI.create("https://api.openai.com/v1/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build() + + def client = HttpClient.newHttpClient() + + LinkedBlockingQueue eventQueue = new LinkedBlockingQueue<>() + CompletableFuture messageFuture = new CompletableFuture<>() + + def response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + + response.thenAccept { + Exception exception = null + + if (it.statusCode() != 200) { + exception = new Exception("Non 200 status code: %d".formatted(it.statusCode())) + } + + if (exception != null) { + eventQueue.put(ChatEvent.of(exception)) + messageFuture.completeExceptionally(exception) + } else { + + } + + } + + return new SimpleChatResponse(false, eventQueue, messageFuture) + } + + static JSONArray formatChat(Chat chat) { + def array = new JSONArray() + + chat.messages.each { + array.put(new JSONObject().put("role", it.assistant() ? "assistant" : "user").put("content", it.text())) + } + + return array + } +} diff --git a/src/main/java/eu/m724/source/ChatSource.java b/src/main/java/eu/m724/source/ChatSource.java index c18a6b7..5e6a39b 100644 --- a/src/main/java/eu/m724/source/ChatSource.java +++ b/src/main/java/eu/m724/source/ChatSource.java @@ -14,6 +14,14 @@ public interface ChatSource { Options options(); + /** + * called when there's a need to interact with the api + * this is called by the ask function which wraps this function and does other stuff + * TODO fix this javadoc + * @param chat the current chat, last message is usually user's message + * @return a response + * @see ChatResponse + */ ChatResponse onAsked(Chat chat); /** diff --git a/src/main/java/eu/m724/source/SimpleChatResponse.java b/src/main/java/eu/m724/source/SimpleChatResponse.java new file mode 100644 index 0000000..0b35514 --- /dev/null +++ b/src/main/java/eu/m724/source/SimpleChatResponse.java @@ -0,0 +1,34 @@ +package eu.m724.source; + +import eu.m724.chat.ChatEvent; +import eu.m724.chat.ChatMessage; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; + +public class SimpleChatResponse implements ChatResponse { + public final boolean streaming; + public final LinkedBlockingQueue eventQueue; + public final CompletableFuture message; + + public SimpleChatResponse(boolean streaming, LinkedBlockingQueue eventQueue, CompletableFuture message) { + this.streaming = streaming; + this.eventQueue = eventQueue; + this.message = message; + } + + @Override + public boolean isStreaming() { + return streaming; + } + + @Override + public LinkedBlockingQueue eventQueue() { + return eventQueue; + } + + @Override + public CompletableFuture message() { + return message; + } +} diff --git a/src/main/java/eu/m724/source/net/Requester.java b/src/main/java/eu/m724/source/net/Requester.java deleted file mode 100644 index 8de0ad0..0000000 --- a/src/main/java/eu/m724/source/net/Requester.java +++ /dev/null @@ -1,4 +0,0 @@ -package eu.m724.source.net; - -public interface Requester { -} diff --git a/src/main/resources/name.txt b/src/main/resources/name.txt deleted file mode 100644 index 3adb807..0000000 --- a/src/main/resources/name.txt +++ /dev/null @@ -1,18 +0,0 @@ -His Royal Majesty, The Quantum Übermensch, ☆☆☆ Ω∞Ж (pronounced "Steve") Ж∞Ω ☆☆☆, -Grand Poobah of the 18th Dimension, Archduke of Antimatter, Lord of the Dance (Interpretive), -Master of Ceremonies for the Heat Death of the Universe, -Chief Executive Sandwich Artist of the Multiverse, -Bearer of the Holy Hand Grenade of Antioch, -Time Lord (Retired), -Keeper of the Sacred Chao, -Last of the Star Whales, -Wielder of Occam's Electric Toothbrush, -01100010 01101001 01101110 01100001 01110010 01111001, -🦄🌈🍕, -Winner of the 2525 Ms. Universe Pageant (Heavy-Fermion Division), -Inventor of Invisible Transparent Ink, -Certified Schrödinger's Cat Herder, -Dungeon Master of Dungeon Masters who Dungeon Master while Dungeon Mastering, -Holder of the Guinness World Record for "Most Titles in a Single Name" (Pending), -Et Cetera, Ad Infinitum, Amen, -The Third-and-a-Half \ No newline at end of file