Compare commits
4 commits
1dcf09614e
...
111e74ca6f
Author | SHA1 | Date | |
---|---|---|---|
111e74ca6f | |||
983c108eed | |||
b23600e216 | |||
66d8967a9d |
20 changed files with 500 additions and 226 deletions
157
LICENSE.md
Normal file
157
LICENSE.md
Normal file
|
@ -0,0 +1,157 @@
|
|||
# GNU LESSER GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates the
|
||||
terms and conditions of version 3 of the GNU General Public License,
|
||||
supplemented by the additional permissions listed below.
|
||||
|
||||
## 0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the
|
||||
GNU General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License, other
|
||||
than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
## 1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
## 2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
- a) under this License, provided that you make a good faith effort
|
||||
to ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
- b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
## 3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from a
|
||||
header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
- a) Give prominent notice with each copy of the object code that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
- b) Accompany the object code with a copy of the GNU GPL and this
|
||||
license document.
|
||||
|
||||
## 4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that, taken
|
||||
together, effectively do not restrict modification of the portions of
|
||||
the Library contained in the Combined Work and reverse engineering for
|
||||
debugging such modifications, if you also do each of the following:
|
||||
|
||||
- a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
- b) Accompany the Combined Work with a copy of the GNU GPL and this
|
||||
license document.
|
||||
- c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
- d) Do one of the following:
|
||||
- 0) Convey the Minimal Corresponding Source under the terms of
|
||||
this License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
- 1) Use a suitable shared library mechanism for linking with
|
||||
the Library. A suitable mechanism is one that (a) uses at run
|
||||
time a copy of the Library already present on the user's
|
||||
computer system, and (b) will operate properly with a modified
|
||||
version of the Library that is interface-compatible with the
|
||||
Linked Version.
|
||||
- e) Provide Installation Information, but only if you would
|
||||
otherwise be required to provide such information under section 6
|
||||
of the GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the Application
|
||||
with a modified version of the Linked Version. (If you use option
|
||||
4d0, the Installation Information must accompany the Minimal
|
||||
Corresponding Source and Corresponding Application Code. If you
|
||||
use option 4d1, you must provide the Installation Information in
|
||||
the manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.)
|
||||
|
||||
## 5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the Library
|
||||
side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
- a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities, conveyed under the terms of this License.
|
||||
- b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
## 6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
as you received it specifies that a certain numbered version of the
|
||||
GNU Lesser General Public License "or any later version" applies to
|
||||
it, you have the option of following the terms and conditions either
|
||||
of that published version or of any later version published by the
|
||||
Free Software Foundation. If the Library as you received it does not
|
||||
specify a version number of the GNU Lesser General Public License, you
|
||||
may choose any version of the GNU Lesser General Public License ever
|
||||
published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
30
pom.xml
30
pom.xml
|
@ -31,4 +31,34 @@
|
|||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>m724</id>
|
||||
<url>https://git.m724.eu/api/packages/Minecon724/maven</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>m724</id>
|
||||
<url>https://git.m724.eu/api/packages/Minecon724/maven</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-release-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<configuration>
|
||||
<allowTimestampedSnapshots>true</allowTimestampedSnapshots>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<scm>
|
||||
<developerConnection>scm:git:git@git.m724.eu:Minecon724/chatapi.git</developerConnection>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
</project>
|
|
@ -1,35 +1,60 @@
|
|||
package eu.m724;
|
||||
package eu.m724.chatapi;
|
||||
|
||||
import eu.m724.chat.Chat;
|
||||
import eu.m724.chat.ChatEvent;
|
||||
import eu.m724.chat.ChatMessage;
|
||||
import eu.m724.example.OaiSource;
|
||||
import eu.m724.source.ChatResponse;
|
||||
import eu.m724.source.ChatSource;
|
||||
import eu.m724.source.option.Option;
|
||||
import eu.m724.chatapi.chat.Chat;
|
||||
import eu.m724.chatapi.chat.ChatEvent;
|
||||
import eu.m724.chatapi.chat.ChatMessage;
|
||||
import eu.m724.chatapi.example.OaiSource;
|
||||
import eu.m724.chatapi.source.ChatResponse;
|
||||
import eu.m724.chatapi.source.ChatSource;
|
||||
import eu.m724.chatapi.source.exception.HttpException;
|
||||
import eu.m724.chatapi.source.option.Option;
|
||||
import eu.m724.chatapi.source.option.Options;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Scanner;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Main {
|
||||
class Main {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
//ChatSource source = new ExampleSource();
|
||||
//source.options().setValue("name", "nekalakininahappenenawiwanatin");
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
ChatSource source = new OaiSource(System.getenv("API_KEY"));
|
||||
String apiKey = System.getenv("API_KEY");
|
||||
boolean complainedApiKey = false;
|
||||
|
||||
if (apiKey == null) {
|
||||
System.out.print("\nAPI Key: ");
|
||||
apiKey = scanner.nextLine();
|
||||
if (apiKey.isBlank()) {
|
||||
System.out.println("Wrong");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Pattern.matches("sk-proj-.*?(?:\\s|$)", apiKey)) {
|
||||
System.out.println("This key looks invalid");
|
||||
complainedApiKey = true;
|
||||
}
|
||||
|
||||
ChatSource source = new OaiSource(apiKey);
|
||||
source.options().setValue("model", "chatgpt-4o-latest");
|
||||
|
||||
Chat chat = new Chat("Speak in super wuper uwu wanguage.");
|
||||
|
||||
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");
|
||||
System.out.printf("Source: \033[1m%s\033[0m %s (%d) by %s\n", source.info().name(), source.info().versionName(), source.info().version(), source.info().author());
|
||||
System.out.println("\nWelcome to CHUT chat. Say something after the \033[1m>\033[0m, or type \033[1m:help\033[0m to see available commands");
|
||||
System.out.println("Working directory: " + System.getProperty("user.dir"));
|
||||
System.out.printf("\033[1m%s\033[0m %s by %s (%s v%d)\n", source.info().name(), source.info().versionName(), source.info().author(), source.getClass().getName(), source.info().version());
|
||||
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
String prompt;
|
||||
|
||||
while (true) {
|
||||
System.out.print("\n> ");
|
||||
String prompt = scanner.nextLine();
|
||||
|
||||
try {
|
||||
prompt = scanner.nextLine();
|
||||
} catch (NoSuchElementException e) {
|
||||
System.out.println("Exiting");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!prompt.startsWith(":")) {
|
||||
chat.messages.add(new ChatMessage(false, prompt));
|
||||
|
@ -40,13 +65,17 @@ public class Main {
|
|||
do {
|
||||
token = chatResponse.eventQueue().take();
|
||||
|
||||
if (token.finishReason() != "error") {
|
||||
if (!"error".equals(token.finishReason())) { // this looks bad but at least idea doesn't nag me
|
||||
if (token.text() != null) {
|
||||
System.out.print(i++ % 2 == 1 ? "\033[1m" : "\033[0m");
|
||||
System.out.print(token.text());
|
||||
}
|
||||
} else {
|
||||
System.out.print("Error: " + token.error().toString());
|
||||
if (complainedApiKey && token.error() instanceof HttpException && ((HttpException)token.error()).statusCode == 401) {
|
||||
System.out.print("\nTold you");
|
||||
complainedApiKey = false;
|
||||
}
|
||||
}
|
||||
} while (token.finishReason() == null);
|
||||
|
||||
|
@ -86,18 +115,20 @@ public class Main {
|
|||
case "options":
|
||||
case "opt":
|
||||
case "o":
|
||||
Options options = source.options();
|
||||
|
||||
boolean shouldShowOptions = parts.length < 3;
|
||||
boolean optionExists = parts.length > 1 && Arrays.asList(source.options().keys()).contains(parts[1]);
|
||||
boolean optionExists = parts.length > 1 && source.options().getKeys().contains(parts[1]);
|
||||
boolean complainNoOption = parts.length > 1 && !optionExists;
|
||||
String chosenOption = parts.length > 1 ? parts[1] : null;
|
||||
|
||||
if (!shouldShowOptions) {
|
||||
Option<?> option = source.options().getOptions().get(parts[1]);
|
||||
if (option != null) {
|
||||
try {
|
||||
Option<?> option = options.getOption(parts[1]);
|
||||
Object value = option.fromString(parts[2]);
|
||||
option.setValue(value);
|
||||
System.out.printf("Set %s to %s\n", option.label, option.getValue());
|
||||
} else {
|
||||
} catch (NoSuchElementException e) {
|
||||
shouldShowOptions = true;
|
||||
System.out.printf("Unknown option \"%s\". ", parts[1]);
|
||||
}
|
||||
|
@ -108,16 +139,16 @@ public class Main {
|
|||
System.out.printf("Unknown option \"%s\". ", chosenOption);
|
||||
System.out.println("Available options:");
|
||||
|
||||
for (Map.Entry<String, Option<?>> entry : source.options().getOptions().entrySet()) {
|
||||
String value = entry.getValue().getValue().toString() + " (" + entry.getValue().getType().getName() + ")";
|
||||
for (Option<?> option : options.getOptions()) {
|
||||
String value = option.toString() + " (" + option.getType().getName() + ")";
|
||||
|
||||
if (entry.getKey().equals(chosenOption)) {
|
||||
System.out.printf("\033[1m%s (%s) = %s\033[0m\n", entry.getValue().label, entry.getKey(), value);
|
||||
if (option.id.equals(chosenOption)) {
|
||||
System.out.printf("\033[1m%s (%s) = %s\033[0m\n", option.label, option.id, value);
|
||||
} else {
|
||||
if (entry.getValue().label.toLowerCase().contains("key")) {
|
||||
if (option.label.toLowerCase().contains("key")) {
|
||||
value = "(looks confidential, specify to see)";
|
||||
}
|
||||
System.out.printf("%s (%s) = %s\n", entry.getValue().label, entry.getKey(), value);
|
||||
System.out.printf("%s (%s) = %s\n", option.label, option.id, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,50 +182,5 @@ public class Main {
|
|||
}
|
||||
System.out.print("\033[0m");
|
||||
}
|
||||
/*
|
||||
ChatResponse chatResponse = source.ask(chat);
|
||||
|
||||
// I was thinking about integrating this into ChatMessage
|
||||
List<String> tokens = new ArrayList<>();
|
||||
List<Long> delays = new ArrayList<>();
|
||||
|
||||
System.out.printf("%s has %d options: %s\n\n",
|
||||
source.getClass().getName(),
|
||||
source.options().count(),
|
||||
source.options().getOptions().values().stream().map(o -> "%s (%s)".formatted(o.label, o.getType().getName())).collect(Collectors.joining(", "))
|
||||
);
|
||||
|
||||
System.out.println("Streaming response now\n");
|
||||
ChatEvent token;
|
||||
|
||||
// usually finish reason will be alongside a token but this is simpler
|
||||
while ((token = chatResponse.eventQueue().take()).finishReason() == null) {
|
||||
System.out.print(token.text());
|
||||
tokens.add(token.text());
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
delays.add(now);
|
||||
}
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.printf("Tokens: %d\n", tokens.size());
|
||||
|
||||
long time = delays.getFirst();
|
||||
System.out.printf("\"%s\"", tokens.getFirst());
|
||||
|
||||
for (int i = 1; i < tokens.size(); i++) {
|
||||
System.out.print(i % 2 == 1 ? "\033[1m" : "\033[0m");
|
||||
System.out.printf(" + %dms + \"%s\"", delays.get(i) - time, tokens.get(i).replace("\n", "\\n"));
|
||||
time = delays.get(i);
|
||||
|
||||
if (i % 5 == 0 && i != tokens.size() - 1) {
|
||||
System.out.println(" +\033[0m");
|
||||
}
|
||||
}
|
||||
System.out.println("\033[0m\n");
|
||||
|
||||
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());*/
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.chat;
|
||||
package eu.m724.chatapi.chat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.chat;
|
||||
package eu.m724.chatapi.chat;
|
||||
|
||||
public record ChatEvent(
|
||||
String text,
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.chat;
|
||||
package eu.m724.chatapi.chat;
|
||||
|
||||
public record ChatMessage(boolean assistant, String text) {
|
||||
public static ChatMessage assistant(String text) {
|
|
@ -1,13 +1,13 @@
|
|||
package eu.m724.example
|
||||
package eu.m724.chatapi.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.option.Options
|
||||
import eu.m724.source.option.StringOption
|
||||
import eu.m724.chatapi.chat.Chat
|
||||
import eu.m724.chatapi.chat.ChatEvent
|
||||
import eu.m724.chatapi.chat.ChatMessage
|
||||
import eu.m724.chatapi.source.ChatResponse
|
||||
import eu.m724.chatapi.source.ChatSource
|
||||
import eu.m724.chatapi.source.ChatSourceInfo
|
||||
import eu.m724.chatapi.source.option.Options
|
||||
import eu.m724.chatapi.source.option.StringOption
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
|
@ -1,19 +1,22 @@
|
|||
package eu.m724.example
|
||||
package eu.m724.chatapi.example
|
||||
|
||||
import eu.m724.chat.Chat
|
||||
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.DoubleOption
|
||||
import eu.m724.source.option.Options
|
||||
import eu.m724.source.option.StringOption
|
||||
import eu.m724.chatapi.chat.Chat
|
||||
import eu.m724.chatapi.source.ChatResponse
|
||||
import eu.m724.chatapi.source.ChatSource
|
||||
import eu.m724.chatapi.source.ChatSourceInfo
|
||||
import eu.m724.chatapi.source.exception.HttpException
|
||||
import eu.m724.chatapi.source.impl.StreamingChatResponse
|
||||
import eu.m724.chatapi.source.option.DoubleOption
|
||||
import eu.m724.chatapi.source.option.Options
|
||||
import eu.m724.chatapi.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.stream.Collectors
|
||||
|
||||
// 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 {
|
||||
|
@ -29,7 +32,8 @@ class OaiSource implements ChatSource {
|
|||
private def options = new Options(
|
||||
new StringOption("apiKey", "API key", apiKey),
|
||||
new StringOption("model", "Model", "gpt-4o-mini"),
|
||||
new DoubleOption("temperature", "Temperature", 1.1)
|
||||
new DoubleOption("temperature", "Temperature", 1.1),
|
||||
new DoubleOption("presence_penalty", "Presence penalty", 0.1)
|
||||
)
|
||||
|
||||
@Override
|
||||
|
@ -47,8 +51,8 @@ class OaiSource implements ChatSource {
|
|||
def apiKey = options.getStringValue("apiKey") // TODO handle null
|
||||
def requestBody = new JSONObject()
|
||||
.put("model", options.getStringValue("model"))
|
||||
.put("temperature", options.getOptionValue("temperature", Double::class))
|
||||
.put("presence_penalty", 0.1)
|
||||
.put("temperature", options.getDoubleValue("temperature"))
|
||||
.put("presence_penalty", options.getDoubleValue("presence_penalty"))
|
||||
.put("stream", true)
|
||||
.put("messages", formatChat(chat)).toString()
|
||||
|
||||
|
@ -61,16 +65,25 @@ class OaiSource implements ChatSource {
|
|||
|
||||
def client = HttpClient.newHttpClient()
|
||||
|
||||
def chatResponse = new SimpleChatResponse()
|
||||
def chatResponse = new StreamingChatResponse()
|
||||
|
||||
def response = client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
|
||||
|
||||
|
||||
response.thenAccept {
|
||||
Exception exception = null
|
||||
//System.out.println(it.statusCode());
|
||||
|
||||
if (it.statusCode() != 200) {
|
||||
exception = new Exception("Non 200 status code: %d".formatted(it.statusCode()))
|
||||
String message = "Unknown";
|
||||
|
||||
try {
|
||||
String body = it.body().collect(Collectors.joining());
|
||||
JSONObject json = new JSONObject(body);
|
||||
message = json.getJSONObject("error").getString("message")
|
||||
} catch (Exception ignored) { }
|
||||
|
||||
chatResponse.error(new HttpException(it.statusCode(), message));
|
||||
return;
|
||||
}
|
||||
|
||||
it.body().forEach {
|
||||
|
@ -95,17 +108,6 @@ class OaiSource implements ChatSource {
|
|||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
if (exception != null) {
|
||||
chatResponse.completeExceptionally(exception)
|
||||
} else {
|
||||
def json = new JSONObject(it.body())
|
||||
//System.out.println(json)
|
||||
def completion = json.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content")
|
||||
chatResponse.complete(completion)
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
return chatResponse
|
|
@ -1,7 +1,7 @@
|
|||
package eu.m724.source;
|
||||
package eu.m724.chatapi.source;
|
||||
|
||||
import eu.m724.chat.ChatEvent;
|
||||
import eu.m724.chat.ChatMessage;
|
||||
import eu.m724.chatapi.chat.ChatEvent;
|
||||
import eu.m724.chatapi.chat.ChatMessage;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
@ -10,6 +10,7 @@ public interface ChatResponse {
|
|||
/**
|
||||
* is this response streaming
|
||||
* if it's not, the queue will get one element that is the whole response
|
||||
* I think about replacing with an abstract class and put this in the constructor
|
||||
*
|
||||
* @return is this response streaming
|
||||
*/
|
|
@ -1,7 +1,7 @@
|
|||
package eu.m724.source;
|
||||
package eu.m724.chatapi.source;
|
||||
|
||||
import eu.m724.chat.Chat;
|
||||
import eu.m724.source.option.Options;
|
||||
import eu.m724.chatapi.chat.Chat;
|
||||
import eu.m724.chatapi.source.option.Options;
|
||||
|
||||
public interface ChatSource {
|
||||
/**
|
||||
|
@ -32,26 +32,6 @@ public interface ChatSource {
|
|||
default ChatResponse ask(Chat chat) {
|
||||
ChatResponse chatResponse = onAsked(chat);
|
||||
|
||||
/* if (chatResponse.streaming()) {
|
||||
StringBuilder total = new StringBuilder();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
ChatEvent event;
|
||||
while (true) {
|
||||
try {
|
||||
event = chatResponse.eventQueue().take();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
total.append(event.text());
|
||||
if (event.finishReason() != null) {
|
||||
chatResponse.message().complete(new ChatMessage(true, total.toString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
} this was a draft I'm keeping because I might change my mind */
|
||||
|
||||
// TODO make sure it works in parallel
|
||||
chatResponse.message().thenAccept(message -> {
|
||||
if (message != null) chat.addMessage(message);
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.source;
|
||||
package eu.m724.chatapi.source;
|
||||
|
||||
public record ChatSourceInfo(String name, String author, String versionName, int version) {
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package eu.m724.chatapi.source.exception;
|
||||
|
||||
// TODO rename?
|
||||
public class HttpException extends Exception {
|
||||
public final int statusCode;
|
||||
|
||||
public HttpException(int statusCode, String message) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
package eu.m724.source;
|
||||
package eu.m724.chatapi.source.impl;
|
||||
|
||||
import eu.m724.chat.ChatEvent;
|
||||
import eu.m724.chat.ChatMessage;
|
||||
import eu.m724.chatapi.chat.ChatEvent;
|
||||
import eu.m724.chatapi.chat.ChatMessage;
|
||||
import eu.m724.chatapi.source.ChatResponse;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
@ -31,8 +32,12 @@ public class NonStreamingChatResponse implements ChatResponse {
|
|||
try {
|
||||
eventQueue.put(ChatEvent.of(content, "stop"));
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e); // TODO probably should handle this
|
||||
// I don't know what this exception means
|
||||
// and I don't think how will it cause me problems
|
||||
// so ignoring it for now
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
message.complete(new ChatMessage(true, content));
|
||||
|
||||
return true;
|
||||
|
@ -44,8 +49,10 @@ public class NonStreamingChatResponse implements ChatResponse {
|
|||
try {
|
||||
eventQueue.put(ChatEvent.of(throwable));
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e); // TODO probably should handle this
|
||||
// again
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
message.complete(null);
|
||||
|
||||
return true;
|
|
@ -1,31 +1,33 @@
|
|||
package eu.m724.source;
|
||||
package eu.m724.chatapi.source.impl;
|
||||
|
||||
import eu.m724.chat.ChatEvent;
|
||||
import eu.m724.chat.ChatMessage;
|
||||
import eu.m724.chatapi.chat.ChatEvent;
|
||||
import eu.m724.chatapi.chat.ChatMessage;
|
||||
import eu.m724.chatapi.source.ChatResponse;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
public class SimpleChatResponse implements ChatResponse {
|
||||
public class StreamingChatResponse implements ChatResponse {
|
||||
private final boolean streaming;
|
||||
private final LinkedBlockingQueue<ChatEvent> eventQueue;
|
||||
private final CompletableFuture<ChatMessage> message;
|
||||
|
||||
private String total = "";
|
||||
|
||||
public SimpleChatResponse() {
|
||||
public StreamingChatResponse() {
|
||||
this.streaming = true;
|
||||
this.eventQueue = new LinkedBlockingQueue<>();
|
||||
this.message = new CompletableFuture<>();
|
||||
}
|
||||
|
||||
public void put(String token, String finishReason) {
|
||||
//System.out.println(System.currentTimeMillis());
|
||||
|
||||
try {
|
||||
eventQueue.put(ChatEvent.of(token, finishReason));
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e); // TODO I don't know what this is
|
||||
// I don't know what this exception means
|
||||
// and I don't think how will it cause me problems
|
||||
// so ignoring it for now
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (token != null) {
|
||||
|
@ -45,6 +47,19 @@ public class SimpleChatResponse implements ChatResponse {
|
|||
put(null, finishReason);
|
||||
}
|
||||
|
||||
public void error(Throwable throwable) {
|
||||
try {
|
||||
eventQueue.put(ChatEvent.of(throwable));
|
||||
} catch (InterruptedException e) {
|
||||
// again
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
message.complete(ChatMessage.assistant(total));
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@Override
|
||||
public boolean streaming() {
|
||||
return streaming;
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.source.option;
|
||||
package eu.m724.chatapi.source.option;
|
||||
|
||||
public class DoubleOption extends Option<Double> {
|
||||
private double minValue = Double.MIN_VALUE;
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.source.option;
|
||||
package eu.m724.chatapi.source.option;
|
||||
|
||||
public class NumberOption extends Option<Integer> {
|
||||
private int minValue = Integer.MIN_VALUE;
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.source.option;
|
||||
package eu.m724.chatapi.source.option;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
|
@ -17,6 +17,10 @@ public abstract class Option<T> {
|
|||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the value of this option
|
||||
* @return the value
|
||||
*/
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
@ -28,12 +32,16 @@ public abstract class Option<T> {
|
|||
* @throws ClassCastException if type is wrong
|
||||
*/
|
||||
public void setValue(Object valueObject) {
|
||||
if (valueObject == null) {
|
||||
this.value = null;
|
||||
} else {
|
||||
Class<T> type = getType();
|
||||
|
||||
if (!type.isInstance(valueObject)) {
|
||||
throw new ClassCastException("Invalid type %s, expected %s".formatted(valueObject.getClass().getName(), getType().getName()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
T value = (T) valueObject;
|
||||
|
||||
if (!isValid(value))
|
||||
|
@ -41,25 +49,27 @@ public abstract class Option<T> {
|
|||
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Class<T> getType() {
|
||||
return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
|
||||
}
|
||||
|
||||
// TODO I'm not a fan of that, probably should address the warnings
|
||||
|
||||
/**
|
||||
* checks if the option is valid given current constraints
|
||||
* checks if a value respects current constraints
|
||||
*
|
||||
* @param value the checked value
|
||||
* @return is it valid
|
||||
*/
|
||||
abstract boolean isValid(T value);
|
||||
|
||||
/**
|
||||
* convert a string to the type of this option
|
||||
* TODO fix english
|
||||
* @param text a text representation of a value
|
||||
* @return a value in an acceptable type
|
||||
* parse a string as {@link T}
|
||||
*
|
||||
* @param text {@link T} but text
|
||||
* @return a value as {@link T}
|
||||
* @throws IllegalArgumentException if the text cannot be converted (to {@link T})
|
||||
*/
|
||||
public abstract T fromString(String text);
|
||||
public abstract T fromString(String text) throws IllegalArgumentException;
|
||||
}
|
127
src/main/java/eu/m724/chatapi/source/option/Options.java
Normal file
127
src/main/java/eu/m724/chatapi/source/option/Options.java
Normal file
|
@ -0,0 +1,127 @@
|
|||
package eu.m724.chatapi.source.option;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Options {
|
||||
private final Map<String, Option<?>> options;
|
||||
|
||||
public Options(Option<?>... options) {
|
||||
this.options = Arrays.stream(options).collect(
|
||||
Collectors.toMap(o -> o.id, Function.identity())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* how many options
|
||||
* @return the amount of options
|
||||
*/
|
||||
public int optionsCount() {
|
||||
return options.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* get all keys, also called "option ids"
|
||||
* @return the keys
|
||||
*/
|
||||
public Set<String> getKeys() {
|
||||
return options.keySet();
|
||||
}
|
||||
|
||||
public Collection<Option<?>> getOptions() {
|
||||
return options.values();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* get option by id
|
||||
* @param id the option id
|
||||
* @return the option
|
||||
* @throws NoSuchElementException if no option with such id
|
||||
*/
|
||||
public Option<?> getOption(String id) {
|
||||
Option<?> option = options.get(id);
|
||||
|
||||
if (option == null) {
|
||||
throw new NoSuchElementException("No option with ID: " + id);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an option value as an {@link Object}
|
||||
*
|
||||
* @param id option id
|
||||
* @return value as object
|
||||
* @see #getOptionValue(String, Class)
|
||||
*/
|
||||
public Object getOptionValue(String id) {
|
||||
return getOption(id).getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* return an option value as {@link T}
|
||||
* you can achieve the same effect by casting {@link #getDoubleValue(String)}
|
||||
*
|
||||
* @param id option id
|
||||
* @param type the type to cast to
|
||||
* @return the option as {@link T}
|
||||
* @param <T> the same as {@link .type}
|
||||
* @see #getOptionValue(String)
|
||||
*/
|
||||
public <T> T getOptionValue(String id, Class<T> type) {
|
||||
Object value = getOptionValue(id);
|
||||
|
||||
if (!type.isInstance(value)) {
|
||||
throw new ClassCastException("Option \"%s\" is of type \"%s\". You wanted it as \"%s\".".formatted(id, value.getClass().getName(), type.getName()));
|
||||
}
|
||||
|
||||
return type.cast(value);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
public String getStringValue(String id) {
|
||||
return getOptionValue(id, String.class);
|
||||
}
|
||||
|
||||
public Integer getIntegerValue(String id) {
|
||||
return getOptionValue(id, Integer.class);
|
||||
}
|
||||
|
||||
public Double getDoubleValue(String id) {
|
||||
return getOptionValue(id, Double.class);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* Set an option's value, hoping you passed the correct type
|
||||
*
|
||||
* @param id the option id
|
||||
* @param value the value
|
||||
* @throws IllegalArgumentException if value doesn't fit constraints
|
||||
* @throws ClassCastException if type is wrong
|
||||
* @throws NoSuchElementException if no option with this id
|
||||
*/
|
||||
public void setValue(String id, Object value) {
|
||||
getOption(id).setValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse {@code text} to an option's type
|
||||
*
|
||||
* @param id the option id
|
||||
* @param text the value as text
|
||||
* @throws IllegalArgumentException if value doesn't fit constraints, or can't be parsed from {@code text}
|
||||
* @throws ClassCastException if type is wrong
|
||||
* @throws NoSuchElementException if no option with this id
|
||||
*/
|
||||
public void setStringValue(String id, String text) {
|
||||
Option<?> option = getOption(id);
|
||||
option.setValue(option.fromString(text));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.source.option;
|
||||
package eu.m724.chatapi.source.option;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package eu.m724.source.option;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Options {
|
||||
private final Map<String, Option<?>> options;
|
||||
|
||||
public Options(Option<?>... options) {
|
||||
this.options = Arrays.stream(options).collect(
|
||||
Collectors.toMap(o -> o.id, Function.identity())
|
||||
);
|
||||
}
|
||||
|
||||
public int count() {
|
||||
return options.size();
|
||||
}
|
||||
|
||||
public String[] keys() {
|
||||
return options.keySet().toArray(String[]::new); // TODO I should think whether it's a good idea to cast when I don't have to
|
||||
}
|
||||
|
||||
public Map<String, Option<?>> getOptions() {
|
||||
return options;
|
||||
} // TODO remove that
|
||||
|
||||
public Option<?> getOption(String id) {
|
||||
return options.get(id);
|
||||
}
|
||||
|
||||
public <T> Object getOptionValue(String id, T type) {
|
||||
return (T) options.get(id).getValue();
|
||||
}
|
||||
|
||||
public String getStringValue(String id) {
|
||||
return (String) getOptionValue(id, String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* set a value of an option
|
||||
* @param id the option id
|
||||
* @param value the value
|
||||
* @throws IllegalArgumentException if value doesn't fit constraints
|
||||
* @throws ClassCastException if type is wrong
|
||||
* @throws NullPointerException if no such option
|
||||
*/
|
||||
public void setValue(String id, Object value) {
|
||||
options.get(id).setValue(value);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue