Compare commits
No commits in common. "111e74ca6fea83ec4280b7baf4e26ae5db633528" and "1dcf09614e86849f41c05ab5c89cfef9726f786c" have entirely different histories.
111e74ca6f
...
1dcf09614e
20 changed files with 226 additions and 500 deletions
157
LICENSE.md
157
LICENSE.md
|
@ -1,157 +0,0 @@
|
||||||
# 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,34 +31,4 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
</project>
|
|
@ -1,60 +1,35 @@
|
||||||
package eu.m724.chatapi;
|
package eu.m724;
|
||||||
|
|
||||||
import eu.m724.chatapi.chat.Chat;
|
import eu.m724.chat.Chat;
|
||||||
import eu.m724.chatapi.chat.ChatEvent;
|
import eu.m724.chat.ChatEvent;
|
||||||
import eu.m724.chatapi.chat.ChatMessage;
|
import eu.m724.chat.ChatMessage;
|
||||||
import eu.m724.chatapi.example.OaiSource;
|
import eu.m724.example.OaiSource;
|
||||||
import eu.m724.chatapi.source.ChatResponse;
|
import eu.m724.source.ChatResponse;
|
||||||
import eu.m724.chatapi.source.ChatSource;
|
import eu.m724.source.ChatSource;
|
||||||
import eu.m724.chatapi.source.exception.HttpException;
|
import eu.m724.source.option.Option;
|
||||||
import eu.m724.chatapi.source.option.Option;
|
|
||||||
import eu.m724.chatapi.source.option.Options;
|
|
||||||
|
|
||||||
import java.util.NoSuchElementException;
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
class Main {
|
public class Main {
|
||||||
public static void main(String[] args) throws InterruptedException {
|
public static void main(String[] args) throws InterruptedException {
|
||||||
Scanner scanner = new Scanner(System.in);
|
//ChatSource source = new ExampleSource();
|
||||||
|
//source.options().setValue("name", "nekalakininahappenenawiwanatin");
|
||||||
|
|
||||||
String apiKey = System.getenv("API_KEY");
|
ChatSource source = new OaiSource(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");
|
source.options().setValue("model", "chatgpt-4o-latest");
|
||||||
|
|
||||||
Chat chat = new Chat("Speak in super wuper uwu wanguage.");
|
Chat chat = new Chat("Speak in super wuper uwu wanguage.");
|
||||||
|
|
||||||
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("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.println("Working directory: " + System.getProperty("user.dir"));
|
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.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());
|
|
||||||
|
|
||||||
String prompt;
|
Scanner scanner = new Scanner(System.in);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
System.out.print("\n> ");
|
System.out.print("\n> ");
|
||||||
|
String prompt = scanner.nextLine();
|
||||||
try {
|
|
||||||
prompt = scanner.nextLine();
|
|
||||||
} catch (NoSuchElementException e) {
|
|
||||||
System.out.println("Exiting");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prompt.startsWith(":")) {
|
if (!prompt.startsWith(":")) {
|
||||||
chat.messages.add(new ChatMessage(false, prompt));
|
chat.messages.add(new ChatMessage(false, prompt));
|
||||||
|
@ -65,17 +40,13 @@ class Main {
|
||||||
do {
|
do {
|
||||||
token = chatResponse.eventQueue().take();
|
token = chatResponse.eventQueue().take();
|
||||||
|
|
||||||
if (!"error".equals(token.finishReason())) { // this looks bad but at least idea doesn't nag me
|
if (token.finishReason() != "error") {
|
||||||
if (token.text() != null) {
|
if (token.text() != null) {
|
||||||
System.out.print(i++ % 2 == 1 ? "\033[1m" : "\033[0m");
|
System.out.print(i++ % 2 == 1 ? "\033[1m" : "\033[0m");
|
||||||
System.out.print(token.text());
|
System.out.print(token.text());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
System.out.print("Error: " + token.error().toString());
|
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);
|
} while (token.finishReason() == null);
|
||||||
|
|
||||||
|
@ -115,20 +86,18 @@ class Main {
|
||||||
case "options":
|
case "options":
|
||||||
case "opt":
|
case "opt":
|
||||||
case "o":
|
case "o":
|
||||||
Options options = source.options();
|
|
||||||
|
|
||||||
boolean shouldShowOptions = parts.length < 3;
|
boolean shouldShowOptions = parts.length < 3;
|
||||||
boolean optionExists = parts.length > 1 && source.options().getKeys().contains(parts[1]);
|
boolean optionExists = parts.length > 1 && Arrays.asList(source.options().keys()).contains(parts[1]);
|
||||||
boolean complainNoOption = parts.length > 1 && !optionExists;
|
boolean complainNoOption = parts.length > 1 && !optionExists;
|
||||||
String chosenOption = parts.length > 1 ? parts[1] : null;
|
String chosenOption = parts.length > 1 ? parts[1] : null;
|
||||||
|
|
||||||
if (!shouldShowOptions) {
|
if (!shouldShowOptions) {
|
||||||
try {
|
Option<?> option = source.options().getOptions().get(parts[1]);
|
||||||
Option<?> option = options.getOption(parts[1]);
|
if (option != null) {
|
||||||
Object value = option.fromString(parts[2]);
|
Object value = option.fromString(parts[2]);
|
||||||
option.setValue(value);
|
option.setValue(value);
|
||||||
System.out.printf("Set %s to %s\n", option.label, option.getValue());
|
System.out.printf("Set %s to %s\n", option.label, option.getValue());
|
||||||
} catch (NoSuchElementException e) {
|
} else {
|
||||||
shouldShowOptions = true;
|
shouldShowOptions = true;
|
||||||
System.out.printf("Unknown option \"%s\". ", parts[1]);
|
System.out.printf("Unknown option \"%s\". ", parts[1]);
|
||||||
}
|
}
|
||||||
|
@ -139,16 +108,16 @@ class Main {
|
||||||
System.out.printf("Unknown option \"%s\". ", chosenOption);
|
System.out.printf("Unknown option \"%s\". ", chosenOption);
|
||||||
System.out.println("Available options:");
|
System.out.println("Available options:");
|
||||||
|
|
||||||
for (Option<?> option : options.getOptions()) {
|
for (Map.Entry<String, Option<?>> entry : source.options().getOptions().entrySet()) {
|
||||||
String value = option.toString() + " (" + option.getType().getName() + ")";
|
String value = entry.getValue().getValue().toString() + " (" + entry.getValue().getType().getName() + ")";
|
||||||
|
|
||||||
if (option.id.equals(chosenOption)) {
|
if (entry.getKey().equals(chosenOption)) {
|
||||||
System.out.printf("\033[1m%s (%s) = %s\033[0m\n", option.label, option.id, value);
|
System.out.printf("\033[1m%s (%s) = %s\033[0m\n", entry.getValue().label, entry.getKey(), value);
|
||||||
} else {
|
} else {
|
||||||
if (option.label.toLowerCase().contains("key")) {
|
if (entry.getValue().label.toLowerCase().contains("key")) {
|
||||||
value = "(looks confidential, specify to see)";
|
value = "(looks confidential, specify to see)";
|
||||||
}
|
}
|
||||||
System.out.printf("%s (%s) = %s\n", option.label, option.id, value);
|
System.out.printf("%s (%s) = %s\n", entry.getValue().label, entry.getKey(), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,5 +151,50 @@ class Main {
|
||||||
}
|
}
|
||||||
System.out.print("\033[0m");
|
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.chatapi.chat;
|
package eu.m724.chat;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapi.chat;
|
package eu.m724.chat;
|
||||||
|
|
||||||
public record ChatEvent(
|
public record ChatEvent(
|
||||||
String text,
|
String text,
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapi.chat;
|
package eu.m724.chat;
|
||||||
|
|
||||||
public record ChatMessage(boolean assistant, String text) {
|
public record ChatMessage(boolean assistant, String text) {
|
||||||
public static ChatMessage assistant(String text) {
|
public static ChatMessage assistant(String text) {
|
|
@ -1,11 +0,0 @@
|
||||||
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,127 +0,0 @@
|
||||||
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,13 +1,13 @@
|
||||||
package eu.m724.chatapi.example
|
package eu.m724.example
|
||||||
|
|
||||||
import eu.m724.chatapi.chat.Chat
|
import eu.m724.chat.Chat
|
||||||
import eu.m724.chatapi.chat.ChatEvent
|
import eu.m724.chat.ChatEvent
|
||||||
import eu.m724.chatapi.chat.ChatMessage
|
import eu.m724.chat.ChatMessage
|
||||||
import eu.m724.chatapi.source.ChatResponse
|
import eu.m724.source.ChatResponse
|
||||||
import eu.m724.chatapi.source.ChatSource
|
import eu.m724.source.ChatSource
|
||||||
import eu.m724.chatapi.source.ChatSourceInfo
|
import eu.m724.source.ChatSourceInfo
|
||||||
import eu.m724.chatapi.source.option.Options
|
import eu.m724.source.option.Options
|
||||||
import eu.m724.chatapi.source.option.StringOption
|
import eu.m724.source.option.StringOption
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
|
@ -1,22 +1,19 @@
|
||||||
package eu.m724.chatapi.example
|
package eu.m724.example
|
||||||
|
|
||||||
import eu.m724.chatapi.chat.Chat
|
import eu.m724.chat.Chat
|
||||||
import eu.m724.chatapi.source.ChatResponse
|
import eu.m724.source.ChatResponse
|
||||||
import eu.m724.chatapi.source.ChatSource
|
import eu.m724.source.ChatSource
|
||||||
import eu.m724.chatapi.source.ChatSourceInfo
|
import eu.m724.source.ChatSourceInfo
|
||||||
import eu.m724.chatapi.source.exception.HttpException
|
import eu.m724.source.SimpleChatResponse
|
||||||
import eu.m724.chatapi.source.impl.StreamingChatResponse
|
import eu.m724.source.option.DoubleOption
|
||||||
import eu.m724.chatapi.source.option.DoubleOption
|
import eu.m724.source.option.Options
|
||||||
import eu.m724.chatapi.source.option.Options
|
import eu.m724.source.option.StringOption
|
||||||
import eu.m724.chatapi.source.option.StringOption
|
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
import java.net.http.HttpClient
|
import java.net.http.HttpClient
|
||||||
import java.net.http.HttpRequest
|
import java.net.http.HttpRequest
|
||||||
import java.net.http.HttpResponse
|
import java.net.http.HttpResponse
|
||||||
import java.util.stream.Collectors
|
|
||||||
|
|
||||||
// for now let's not focus on readability
|
// 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
|
// this is more about find out what is common and should be included in the common api
|
||||||
class OaiSource implements ChatSource {
|
class OaiSource implements ChatSource {
|
||||||
|
@ -32,8 +29,7 @@ class OaiSource implements ChatSource {
|
||||||
private def options = new Options(
|
private def options = new Options(
|
||||||
new StringOption("apiKey", "API key", apiKey),
|
new StringOption("apiKey", "API key", apiKey),
|
||||||
new StringOption("model", "Model", "gpt-4o-mini"),
|
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
|
@Override
|
||||||
|
@ -51,8 +47,8 @@ class OaiSource implements ChatSource {
|
||||||
def apiKey = options.getStringValue("apiKey") // TODO handle null
|
def apiKey = options.getStringValue("apiKey") // TODO handle null
|
||||||
def requestBody = new JSONObject()
|
def requestBody = new JSONObject()
|
||||||
.put("model", options.getStringValue("model"))
|
.put("model", options.getStringValue("model"))
|
||||||
.put("temperature", options.getDoubleValue("temperature"))
|
.put("temperature", options.getOptionValue("temperature", Double::class))
|
||||||
.put("presence_penalty", options.getDoubleValue("presence_penalty"))
|
.put("presence_penalty", 0.1)
|
||||||
.put("stream", true)
|
.put("stream", true)
|
||||||
.put("messages", formatChat(chat)).toString()
|
.put("messages", formatChat(chat)).toString()
|
||||||
|
|
||||||
|
@ -65,25 +61,16 @@ class OaiSource implements ChatSource {
|
||||||
|
|
||||||
def client = HttpClient.newHttpClient()
|
def client = HttpClient.newHttpClient()
|
||||||
|
|
||||||
def chatResponse = new StreamingChatResponse()
|
def chatResponse = new SimpleChatResponse()
|
||||||
|
|
||||||
def response = client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
|
def response = client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
|
||||||
|
|
||||||
|
|
||||||
response.thenAccept {
|
response.thenAccept {
|
||||||
Exception exception = null
|
Exception exception = null
|
||||||
//System.out.println(it.statusCode());
|
|
||||||
|
|
||||||
if (it.statusCode() != 200) {
|
if (it.statusCode() != 200) {
|
||||||
String message = "Unknown";
|
exception = new Exception("Non 200 status code: %d".formatted(it.statusCode()))
|
||||||
|
|
||||||
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 {
|
it.body().forEach {
|
||||||
|
@ -108,6 +95,17 @@ 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
|
return chatResponse
|
|
@ -1,7 +1,7 @@
|
||||||
package eu.m724.chatapi.source;
|
package eu.m724.source;
|
||||||
|
|
||||||
import eu.m724.chatapi.chat.ChatEvent;
|
import eu.m724.chat.ChatEvent;
|
||||||
import eu.m724.chatapi.chat.ChatMessage;
|
import eu.m724.chat.ChatMessage;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
@ -10,7 +10,6 @@ public interface ChatResponse {
|
||||||
/**
|
/**
|
||||||
* is this response streaming
|
* is this response streaming
|
||||||
* if it's not, the queue will get one element that is the whole response
|
* 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
|
* @return is this response streaming
|
||||||
*/
|
*/
|
|
@ -1,7 +1,7 @@
|
||||||
package eu.m724.chatapi.source;
|
package eu.m724.source;
|
||||||
|
|
||||||
import eu.m724.chatapi.chat.Chat;
|
import eu.m724.chat.Chat;
|
||||||
import eu.m724.chatapi.source.option.Options;
|
import eu.m724.source.option.Options;
|
||||||
|
|
||||||
public interface ChatSource {
|
public interface ChatSource {
|
||||||
/**
|
/**
|
||||||
|
@ -32,6 +32,26 @@ public interface ChatSource {
|
||||||
default ChatResponse ask(Chat chat) {
|
default ChatResponse ask(Chat chat) {
|
||||||
ChatResponse chatResponse = onAsked(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 -> {
|
chatResponse.message().thenAccept(message -> {
|
||||||
if (message != null) chat.addMessage(message);
|
if (message != null) chat.addMessage(message);
|
||||||
});
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapi.source;
|
package eu.m724.source;
|
||||||
|
|
||||||
public record ChatSourceInfo(String name, String author, String versionName, int version) {
|
public record ChatSourceInfo(String name, String author, String versionName, int version) {
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
package eu.m724.chatapi.source.impl;
|
package eu.m724.source;
|
||||||
|
|
||||||
import eu.m724.chatapi.chat.ChatEvent;
|
import eu.m724.chat.ChatEvent;
|
||||||
import eu.m724.chatapi.chat.ChatMessage;
|
import eu.m724.chat.ChatMessage;
|
||||||
import eu.m724.chatapi.source.ChatResponse;
|
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
@ -32,12 +31,8 @@ public class NonStreamingChatResponse implements ChatResponse {
|
||||||
try {
|
try {
|
||||||
eventQueue.put(ChatEvent.of(content, "stop"));
|
eventQueue.put(ChatEvent.of(content, "stop"));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// I don't know what this exception means
|
throw new RuntimeException(e); // TODO probably should handle this
|
||||||
// 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));
|
message.complete(new ChatMessage(true, content));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -49,10 +44,8 @@ public class NonStreamingChatResponse implements ChatResponse {
|
||||||
try {
|
try {
|
||||||
eventQueue.put(ChatEvent.of(throwable));
|
eventQueue.put(ChatEvent.of(throwable));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// again
|
throw new RuntimeException(e); // TODO probably should handle this
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message.complete(null);
|
message.complete(null);
|
||||||
|
|
||||||
return true;
|
return true;
|
|
@ -1,33 +1,31 @@
|
||||||
package eu.m724.chatapi.source.impl;
|
package eu.m724.source;
|
||||||
|
|
||||||
import eu.m724.chatapi.chat.ChatEvent;
|
import eu.m724.chat.ChatEvent;
|
||||||
import eu.m724.chatapi.chat.ChatMessage;
|
import eu.m724.chat.ChatMessage;
|
||||||
import eu.m724.chatapi.source.ChatResponse;
|
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
public class StreamingChatResponse implements ChatResponse {
|
public class SimpleChatResponse implements ChatResponse {
|
||||||
private final boolean streaming;
|
private final boolean streaming;
|
||||||
private final LinkedBlockingQueue<ChatEvent> eventQueue;
|
private final LinkedBlockingQueue<ChatEvent> eventQueue;
|
||||||
private final CompletableFuture<ChatMessage> message;
|
private final CompletableFuture<ChatMessage> message;
|
||||||
|
|
||||||
private String total = "";
|
private String total = "";
|
||||||
|
|
||||||
public StreamingChatResponse() {
|
public SimpleChatResponse() {
|
||||||
this.streaming = true;
|
this.streaming = true;
|
||||||
this.eventQueue = new LinkedBlockingQueue<>();
|
this.eventQueue = new LinkedBlockingQueue<>();
|
||||||
this.message = new CompletableFuture<>();
|
this.message = new CompletableFuture<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void put(String token, String finishReason) {
|
public void put(String token, String finishReason) {
|
||||||
|
//System.out.println(System.currentTimeMillis());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
eventQueue.put(ChatEvent.of(token, finishReason));
|
eventQueue.put(ChatEvent.of(token, finishReason));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// I don't know what this exception means
|
throw new RuntimeException(e); // TODO I don't know what this is
|
||||||
// and I don't think how will it cause me problems
|
|
||||||
// so ignoring it for now
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
|
@ -47,19 +45,6 @@ public class StreamingChatResponse implements ChatResponse {
|
||||||
put(null, finishReason);
|
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
|
@Override
|
||||||
public boolean streaming() {
|
public boolean streaming() {
|
||||||
return streaming;
|
return streaming;
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapi.source.option;
|
package eu.m724.source.option;
|
||||||
|
|
||||||
public class DoubleOption extends Option<Double> {
|
public class DoubleOption extends Option<Double> {
|
||||||
private double minValue = Double.MIN_VALUE;
|
private double minValue = Double.MIN_VALUE;
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapi.source.option;
|
package eu.m724.source.option;
|
||||||
|
|
||||||
public class NumberOption extends Option<Integer> {
|
public class NumberOption extends Option<Integer> {
|
||||||
private int minValue = Integer.MIN_VALUE;
|
private int minValue = Integer.MIN_VALUE;
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapi.source.option;
|
package eu.m724.source.option;
|
||||||
|
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
|
||||||
|
@ -17,10 +17,6 @@ public abstract class Option<T> {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* gets the value of this option
|
|
||||||
* @return the value
|
|
||||||
*/
|
|
||||||
public T getValue() {
|
public T getValue() {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -32,44 +28,38 @@ public abstract class Option<T> {
|
||||||
* @throws ClassCastException if type is wrong
|
* @throws ClassCastException if type is wrong
|
||||||
*/
|
*/
|
||||||
public void setValue(Object valueObject) {
|
public void setValue(Object valueObject) {
|
||||||
if (valueObject == null) {
|
Class<T> type = getType();
|
||||||
this.value = null;
|
|
||||||
} else {
|
|
||||||
Class<T> type = getType();
|
|
||||||
|
|
||||||
if (!type.isInstance(valueObject)) {
|
if (!type.isInstance(valueObject)) {
|
||||||
throw new ClassCastException("Invalid type %s, expected %s".formatted(valueObject.getClass().getName(), getType().getName()));
|
throw new ClassCastException("Invalid type %s, expected %s".formatted(valueObject.getClass().getName(), getType().getName()));
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
T value = (T) valueObject;
|
|
||||||
|
|
||||||
if (!isValid(value))
|
|
||||||
throw new IllegalArgumentException("Value invalid");
|
|
||||||
|
|
||||||
this.value = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T value = (T) valueObject;
|
||||||
|
|
||||||
|
if (!isValid(value))
|
||||||
|
throw new IllegalArgumentException("Value invalid");
|
||||||
|
|
||||||
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public Class<T> getType() {
|
public Class<T> getType() {
|
||||||
return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
|
return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO I'm not a fan of that, probably should address the warnings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if a value respects current constraints
|
* checks if the option is valid given current constraints
|
||||||
*
|
|
||||||
* @param value the checked value
|
* @param value the checked value
|
||||||
* @return is it valid
|
* @return is it valid
|
||||||
*/
|
*/
|
||||||
abstract boolean isValid(T value);
|
abstract boolean isValid(T value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* parse a string as {@link T}
|
* convert a string to the type of this option
|
||||||
*
|
* TODO fix english
|
||||||
* @param text {@link T} but text
|
* @param text a text representation of a value
|
||||||
* @return a value as {@link T}
|
* @return a value in an acceptable type
|
||||||
* @throws IllegalArgumentException if the text cannot be converted (to {@link T})
|
|
||||||
*/
|
*/
|
||||||
public abstract T fromString(String text) throws IllegalArgumentException;
|
public abstract T fromString(String text);
|
||||||
}
|
}
|
52
src/main/java/eu/m724/source/option/Options.java
Normal file
52
src/main/java/eu/m724/source/option/Options.java
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapi.source.option;
|
package eu.m724.source.option;
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
Loading…
Reference in a new issue