Added My Anime List API wrapper

for documentation on MAL API check out the [API docs](https://myanimelist.net/apiconfig/references/api/v2)

Display:Display.java
- Switched the AI model to `qwen3:8b`
- some temporary test changes
- switched to using static references when supposed too

MALAPITool
- Module for handling the MAL API

MALAPITool:README.md
- Some general information about the tools

Started adding some base systems for finding API endpoints, will later be added to the API module for the ability to add plugins

.gitignore
- Added so all data folder are ignored. So submodules data folder from testing aren't added to git

Core:OllamaObject.java
- switched location of messages.json from the static location of `./cache/` to the dynamic location of `${Core.DATA_DIR}/messages.json`

Core:OllamaPerameter.java
- Added ENUM and ARRAY values. Incidentally also discover that Ollama supports more than just STRING, INT, and BOOLEAN ad parameters

Core:Core.java
- Added precluding day to the name of logging files, now using the format dd_HH-mm-ss
This commit is contained in:
2025-05-23 15:24:58 +02:00
parent 58c23c5897
commit 5c3efa1376
19 changed files with 678 additions and 18 deletions

17
MALAPITool/README.md Normal file
View File

@@ -0,0 +1,17 @@
# MAL API wrapper
## Important notes
### Scope
This wrapper is not fully implemented, some endpoints are missing, as well as no user-specific endpoints are added.
### Rate limits
I'm yet to enforce the rate limit, due to in there the Ollama backend won't be fast enough, but this is subject to change
### API Key Required
You must provide your own API key
Store the key in `${DATE_DIR}/malapi.json` using the following format:
```jsonc
{
"client_id": "<client_id>",
"client_secret": "<client_secret>"// This is for the moment not used but shuld till be added
}
```

View File

@@ -0,0 +1,108 @@
package me.zacharias.chat.mal.api;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.ollama.*;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
public class MALAPITool {
public final String Client_ID;
public final String Client_Secret;
public final String BaseURL = "https://api.myanimelist.net/v2";
public final OllamaFunctionTools MALAPITools;
public MALAPITool() {
super();
try {
JSONObject obj = new JSONObject(Files.readString(Path.of(Core.DATA_DIR + "/malapi.json")));
this.Client_ID = obj.getString("client_id");
this.Client_Secret = obj.getString("client_secret");
} catch (IOException e) {
throw new RuntimeException(e);
}
OllamaFunctionTools.OllamaFunctionToolsBuilder builder = new OllamaFunctionTools.OllamaFunctionToolsBuilder();
try(ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("me.zacharias.chat.mal.api.endpoints").scan()) {
ClassInfoList endpoints = scanResult.getClassesWithAnnotation(MALEndpoint.class.getName());
for (ClassInfo classInfo : endpoints) {
Class<?> clazz = classInfo.loadClass();
if (OllamaFunctionTool.class.isAssignableFrom(clazz)) {
//System.out.println("Found endpoint: " + clazz.getName() + " With constructor: " + clazz.getDeclaredConstructors().f.getName() + " and arguments: " + Arrays.toString(clazz.getDeclaredConstructor().getParameterTypes()));
MALEndpointTool tool = (MALEndpointTool) clazz.getDeclaredConstructor(MALAPITool.class).newInstance(MALAPITool.this);
builder.addTool(tool, Core.Source.INTERNAL);
}
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
this.MALAPITools = builder.build();
}
public OllamaFunctionTools getOllamaTools() {
return this.MALAPITools;
}
public JSONObject APIRequest(String endpoint, HashMap<String, String> params) {
try {
String urlParams = ParameterStringBuilder.getParamsString(params);
URL url = new URI(this.BaseURL + endpoint + "?" + urlParams).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("X-MAL-CLIENT-ID", this.Client_ID);
connection.setDoOutput(true);
connection.setConnectTimeout(80 * 1000);
int responseCode = connection.getResponseCode();
// HTTP_OK or 200 response code generally means that the server ran successfully without any errors
StringBuilder response = new StringBuilder();
// Read response content
// connection.getInputStream() purpose is to obtain an input stream for reading the server's response.
try (
BufferedReader reader = new BufferedReader( new InputStreamReader( connection.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line); // Adds every line to response till the end of file.
}
}
if (responseCode == HttpURLConnection.HTTP_OK) {
connection.disconnect();
return new JSONObject(response.toString());
}
else {
connection.disconnect();
System.err.println("Error: HTTP Response code - " + responseCode + "\n"+response.toString());
throw new RuntimeException("HTTP Response code - " + responseCode);
}
}catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,11 @@
package me.zacharias.chat.mal.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MALEndpoint {
}

View File

@@ -0,0 +1,11 @@
package me.zacharias.chat.mal.api;
import me.zacharias.chat.ollama.OllamaFunctionTool;
public abstract class MALEndpointTool extends OllamaFunctionTool {
protected MALAPITool MALAPIToolInstance;
public MALEndpointTool(MALAPITool malAPITool) {
super();
this.MALAPIToolInstance = malAPITool;
}
}

View File

@@ -0,0 +1,25 @@
package me.zacharias.chat.mal.api;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class ParameterStringBuilder {
public static String getParamsString(Map<String, String> params)
throws UnsupportedEncodingException {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
result.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
result.append("&");
}
String resultString = result.toString();
return !resultString.isEmpty()
? resultString.substring(0, resultString.length() - 1)
: resultString;
}
}

View File

@@ -0,0 +1,86 @@
package me.zacharias.chat.mal.api.endpoints;
import me.zacharias.chat.mal.api.MALAPITool;
import me.zacharias.chat.mal.api.MALEndpoint;
import me.zacharias.chat.mal.api.MALEndpointTool;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
@MALEndpoint
public class GetAnimeDetails extends MALEndpointTool {
public GetAnimeDetails(MALAPITool malAPITool) {
super(malAPITool);
}
@Override
public String name() {
return "get_anime_details";
}
@Override
public String description() {
return "Gets the details of an anime from the MAL API";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The id of the anime", true)
.addProperty("fields", OllamaPerameter.OllamaPerameterBuilder.Type.ARRAY, "The fields to return, defaults to [\"id\", \"title\", \"synopsis\", \"genres\"]")
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
int id = -1;
String[] fields = null;
for (OllamaFunctionArgument arg : args) {
switch (arg.argument()) {
case "id":
id = (Integer) arg.value();
break;
case "fields":
JSONArray jsonArray = (JSONArray) arg.value();
for (int i = 0; i < jsonArray.length(); i++) {
if (jsonArray.get(i) instanceof String) {
if (fields == null) {
fields = new String[jsonArray.length()];
}
fields[i] = jsonArray.getString(i);
} else {
throw new OllamaToolErrorException(this.name(), "The fields must be a string array");
}
}
break;
}
}
if (id == -1) {
throw new OllamaToolErrorException(this.name(), "The id must be specified");
}
if (fields == null) {
fields = new String[]{
"id",
"title",
"synopsis",
"genres"
};
}
HashMap<String, String> map = new HashMap<>();
map.put("fields", String.join(",", fields));
JSONObject obj = MALAPIToolInstance.APIRequest("/anime/"+id, map);
return new OllamaToolRespnce(this.name(), obj.toString());
}
}

View File

@@ -0,0 +1,88 @@
package me.zacharias.chat.mal.api.endpoints;
import me.zacharias.chat.mal.api.MALAPITool;
import me.zacharias.chat.mal.api.MALEndpoint;
import me.zacharias.chat.mal.api.MALEndpointTool;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
@MALEndpoint
public class GetAnimeList extends MALEndpointTool {
public GetAnimeList(MALAPITool malAPITool) {
super(malAPITool);
}
@Override
public String name() {
return "get_anime_list";
}
@Override
public String description() {
return "Gets a list of anime from the MAL API";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("query", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The query to search for",true)
.addProperty("offset", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The offset to start from")
.addProperty("fields", OllamaPerameter.OllamaPerameterBuilder.Type.ARRAY, "The fields to return")
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
String query = "";
int offset = 0;
String[] fields = null;
for (OllamaFunctionArgument arg : args) {
switch (arg.argument()) {
case "query":
query = (String)arg.value();
break;
case "offset":
offset = (Integer) arg.value();
break;
case "fields":
JSONArray jsonArray = (JSONArray) arg.value();
if(!jsonArray.isEmpty()) {
fields = new String[jsonArray.length()];
for (int i = 0; i < jsonArray.length(); i++) {
if (jsonArray.get(i) instanceof String) {
fields[i] = jsonArray.getString(i);
} else {
throw new OllamaToolErrorException(this.name(), "The fields must be a string array");
}
}
}
break;
}
}
if (query.isEmpty()) {
throw new OllamaToolErrorException(this.name(), "Query cannot be empty");
}
HashMap<String, String> params = new HashMap<>();
params.put("q", query);
if (offset > 0) {
params.put("offset", String.valueOf(offset));
}
if (fields != null) {
params.put("fields", String.join(",", fields));
}
JSONObject response = MALAPIToolInstance.APIRequest("/anime", params);
return new OllamaToolRespnce(this.name(), response.toString());
}
}

View File

@@ -0,0 +1,85 @@
package me.zacharias.chat.mal.api.endpoints;
import me.zacharias.chat.mal.api.MALAPITool;
import me.zacharias.chat.mal.api.MALEndpoint;
import me.zacharias.chat.mal.api.MALEndpointTool;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.HashMap;
@MALEndpoint
public class GetManagDetails extends MALEndpointTool {
public GetManagDetails(MALAPITool malAPITool) {
super(malAPITool);
}
@Override
public String name() {
return "get_manag_details";
}
@Override
public String description() {
return "Gets the details of an manga from the MAL API";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The id of the manga", true)
.addProperty("fields", OllamaPerameter.OllamaPerameterBuilder.Type.ARRAY, "The fields to return, defaults to [\"id\", \"title\", \"synopsis\", \"genres\"]")
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
int id = -1;
String[] fields = null;
for (OllamaFunctionArgument arg : args) {
switch (arg.argument()) {
case "id":
id = (Integer) arg.value();
break;
case "fields":
JSONArray jsonArray = (JSONArray) arg.value();
for (int i = 0; i < jsonArray.length(); i++) {
if (jsonArray.get(i) instanceof String) {
if (fields == null) {
fields = new String[jsonArray.length()];
}
fields[i] = jsonArray.getString(i);
} else {
throw new OllamaToolErrorException(this.name(), "The fields must be a string array");
}
}
break;
}
}
if (id == -1) {
throw new OllamaToolErrorException(this.name(), "The id must be specified");
}
if (fields == null) {
fields = new String[]{
"id",
"title",
"synopsis",
"genres"
};
}
HashMap<String, String> map = new HashMap<>();
map.put("fields", String.join(",", fields));
JSONObject obj = MALAPIToolInstance.APIRequest("/manga/"+id, map);
return new OllamaToolRespnce(this.name(), obj.toString());
}
}

View File

@@ -0,0 +1,87 @@
package me.zacharias.chat.mal.api.endpoints;
import me.zacharias.chat.mal.api.MALAPITool;
import me.zacharias.chat.mal.api.MALEndpoint;
import me.zacharias.chat.mal.api.MALEndpointTool;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.HashMap;
@MALEndpoint
public class GetMangaList extends MALEndpointTool {
public GetMangaList(MALAPITool malAPITool) {
super(malAPITool);
}
@Override
public String name() {
return "get_manga_list";
}
@Override
public String description() {
return "Gets a list of manga from the MAL API";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("query", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The query to search for",true)
.addProperty("offset", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The offset to start from")
.addProperty("fields", OllamaPerameter.OllamaPerameterBuilder.Type.ARRAY, "The fields to return")
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
String query = "";
int offset = 0;
String[] fields = null;
for (OllamaFunctionArgument arg : args) {
switch (arg.argument()) {
case "query":
query = (String)arg.value();
break;
case "offset":
offset = (Integer) arg.value();
break;
case "fields":
JSONArray jsonArray = (JSONArray) arg.value();
if(!jsonArray.isEmpty()) {
fields = new String[jsonArray.length()];
for (int i = 0; i < jsonArray.length(); i++) {
if (jsonArray.get(i) instanceof String) {
fields[i] = jsonArray.getString(i);
} else {
throw new OllamaToolErrorException(this.name(), "The fields must be a string array");
}
}
}
break;
}
}
if (query.isEmpty()) {
throw new OllamaToolErrorException(this.name(), "Query cannot be empty");
}
HashMap<String, String> params = new HashMap<>();
params.put("q", query);
if (offset > 0) {
params.put("offset", String.valueOf(offset));
}
if (fields != null) {
params.put("fields", String.join(",", fields));
}
JSONObject response = MALAPIToolInstance.APIRequest("/manga", params);
return new OllamaToolRespnce(this.name(), response.toString());
}
}