feat(api): major refactor from TCP socket to HTTP REST via Spring Boot

This commit introduces a large-scale refactor that replaces the existing TCP-based API with a Spring Boot-powered HTTP REST architecture. Due to the size and scope of this change, only essential structural notes are included below.

High-level changes:
- Replaced TCP-based communication with RESTful endpoints
- Introduced Spring Boot for API handling and configuration
- Refactored internal core logic to support REST architecture

New/Updated API components:
- `APIApplication.java`: Main Spring Boot entry point
- `MessageController.java`: Handles LLM-related queries
- `ToolController.java`: Handles adding/removing tools
- `NewToolRequest.java` / `NewToolResponse.java`: Data models for tool addition
- `NewQueryResponseHook.java`: Webhook handler for LLM query results
- `WebhookError.java`: Model for reporting webhook errors
- `EnableIfNotDisplay.java`: Conditional configuration for TTY context
- Other supporting classes (e.g., `ToolArgument`, `ToolRequest`)

Core changes:
- `Core.java`: Removed deprecated `addFunctionTool`, added `removeTool`
- `LaunchOptions.java`: Added `notDisplay` flag for headless operation
- `OllamaObject.java`: Implements tool removal logic

Launcher/display changes:
- `Launcher.java`: Starts `APIApplication` if not in TTY mode
- `Display.java`: Integrates REST API contextually with TTY display

NOTE: Several classes are included but not yet fully utilized; these are placeholders for upcoming features (e.g., `MessageResponse`, `ToolRequest`).

BREAKING CHANGE: This refactors removes all TCP-based API code and replaces it with HTTP REST using Spring Boot. Any clients or modules depending on the old TCP interface will need to be updated.
This commit is contained in:
2025-05-24 18:11:58 +02:00
parent e7cedb7f08
commit 8e44c11385
31 changed files with 771 additions and 452 deletions

1
.gitignore vendored
View File

@@ -46,3 +46,4 @@ bin/
/logs/ /logs/
data data
/cache/ /cache/
/run/

View File

@@ -1,5 +1,8 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
} }
group = 'me.zacharias' group = 'me.zacharias'
@@ -7,6 +10,15 @@ version = '1.0-SNAPSHOT'
dependencies { dependencies {
implementation project(":Core") implementation project(":Core")
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
//implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//runtimeOnly('org.springframework.boot:spring-boot-starter-web')
} }
test { test {
@@ -15,6 +27,6 @@ test {
jar{ jar{
manifest { manifest {
attributes 'Main-Class': 'me.zacharias.char.api.APIServer' attributes 'Main-Class': 'me.zacharias.chat.api.APIApplication'
} }
} }

View File

@@ -0,0 +1,127 @@
package me.zacharias.chat.api;
import me.zacharias.chat.api.payload.request.NewQurryResponceHook;
import me.zacharias.chat.api.payload.request.NewToolRequest;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.GlobalObjects;
import me.zacharias.chat.core.PrintMessageHandler;
import me.zacharias.chat.ollama.OllamaObject;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@SpringBootApplication
public class APIApplication {
public static void start(){
ConfigurableApplicationContext ctx = SpringApplication.run(APIApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(APIApplication.class, args);
}
private static APIApplication instance;
private final Map<String, APITool> tools = new HashMap<>();
private final ArrayList<QuerryResponceEndpoint> querryResponceEndpoints = new ArrayList<>();
private final Core core;
WebClient webClient = WebClient.create();
public APIApplication()
{
if(instance != null)
throw new IllegalStateException("APIApplication is already running!");
instance = this;
if(GlobalObjects.getObject("core") instanceof Core coreInstance) {
this.core = coreInstance;
} else {
this.core = new Core(new PrintMessageHandler() {
@Override
public void printMessage(String message) {
synchronized (querryResponceEndpoints) {
querryResponceEndpoints.forEach(endpoint -> {
webClient.post().uri(endpoint.getUrl()).bodyValue(message).retrieve()
.onStatus(
status -> status.is4xxClientError() || status.is5xxServerError() || status.isError(),
clientResponse -> {
return clientResponse.bodyToMono(String.class).flatMap(body -> {
endpoint.sendError("Failed to send message for qurry response");
querryResponceEndpoints.remove(endpoint);
return Mono.error(new RuntimeException("Webhook call failed with status: " + clientResponse.statusCode()));
});
})
.toBodilessEntity()
.doOnError(e -> {
})
.subscribe();
});
}
}
@Override
public boolean color() {
return false;
}
});
core.setOllamaObject(OllamaObject.builder()
.setModel("qwen3:8b")
.keep_alive(10)
.build());
}
}
public static APIApplication getInstance() {
return instance;
}
public boolean addTool(String name, NewToolRequest request) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Tool name cannot be null or empty");
}
if (tools.containsKey(name)) {
return false; // Tool with this name already exists
}
APITool tool = new APITool(request);
tools.put(name, tool);
core.addTool(tool, Core.Source.API);
return true;
}
public boolean removeTool(String name) {
if(name == null || name.isEmpty()) {
throw new IllegalArgumentException("Tool name cannot be null or empty");
}
if (!tools.containsKey(name)) {
return false; // Tool with this name does not exist
}
APITool tool = tools.remove(name);
core.removeTool(tool.name());
return true;
}
public void addQurryResponseHook(NewQurryResponceHook request) {
if (request == null || request.getUrl() == null || request.getUrl().isEmpty()) {
throw new IllegalArgumentException("Request and URL cannot be null or empty");
}
QuerryResponceEndpoint endpoint = new QuerryResponceEndpoint(request.getUrl(), request.getErrorUrl());
synchronized (querryResponceEndpoints) {
querryResponceEndpoints.add(endpoint);
}
}
public Core getCore() {
return core;
}
}

View File

@@ -1,8 +0,0 @@
package me.zacharias.chat.api;
public enum APICodes {
CREDENTIALS,
ERROR,
API,
SUCCESS,
}

View File

@@ -1,33 +0,0 @@
package me.zacharias.chat.api;
/**
* API Endpoints
* see <a href="https://server.4zellen.se:3000/Zacharias/chat_thing/wiki/API-Docs">API Docs</a>
*/
public enum APIEndpoints {
// API request endpoints
/**
* Requests a tool to be added to the environment
*/
ADD_TOOL,
/**
* Requests a query to be executed on the environment
*/
QUERY,
// API response endpoints
/**
* Response to a tool request containing the Ollama response
*/
RESPONSE,
/**
* Response to use a tool defined by the API Client
*/
USE_TOOL,
// API endpoints that doesn't require credentials even if the server has them required
/**
* Returns info about the server
*/
INFO
}

View File

@@ -1,19 +0,0 @@
package me.zacharias.chat.api;
public enum APIErrorCodes {
/**
* If the format is not a JSON or not having the expected JSON keys present
*/
FORMAT_ERROR,
/**
* An error with credentials<br>
* Can be that the credentials is not matching or other credentials related issue
*/
CREDENTIALS_ERROR,
/**
* An error describing that the APIEndpoint is unavailable, invalid, or related issue
*/
API_ERROR,
}

View File

@@ -0,0 +1,92 @@
package me.zacharias.chat.api;
import me.zacharias.chat.api.payload.ToolArgument;
import me.zacharias.chat.api.payload.request.NewToolRequest;
import me.zacharias.chat.api.payload.webhook.responce.APIToolResponse;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
public class APITool extends OllamaFunctionTool {
/**
* The name of the tool.
*/
private final String name;
/**
* The description of the tool.
*/
private final String description;
/**
* The arguments this tool will take.
*/
private final ToolArgument[] arguments;
/**
* The URL this api will call when the tool is used.
*/
private final String requestUrl;
RestTemplate restTemplate = new RestTemplate();
public APITool(NewToolRequest request) {
super();
this.name = request.getName();
this.description = request.getDescription();
this.arguments = request.getArguments();
this.requestUrl = request.getRequestUrl();
if (this.name == null || this.name.isEmpty()) {
throw new IllegalArgumentException("Tool name cannot be null or empty");
}
}
@Override
public String name() {
return name;
}
@Override
public String description() {
return description;
}
@Override
public OllamaPerameter parameters() {
OllamaPerameter.OllamaPerameterBuilder parameter = OllamaPerameter.builder();
for (ToolArgument argument : arguments) {
parameter.addProperty(argument.getName(), argument.getType(), argument.getDescription(), argument.isRequired());
}
return parameter.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(requestUrl);
for (OllamaFunctionArgument arg : args) {
builder.queryParam(arg.argument(), arg.value());
}
String url = builder.toUriString();
ResponseEntity<APIToolResponse> response = restTemplate.getForEntity(url, APIToolResponse.class);
if (response.getStatusCode().is2xxSuccessful()){
if (response.getBody() == null) {
throw new OllamaToolErrorException(name, "Response body is null");
}
if(response.getBody().getError() != null && !response.getBody().getError().isEmpty())
throw new OllamaToolErrorException(name, response.getBody().getError());
return new OllamaToolRespnce(name, response.getBody().getResponse());
}
else {
if(response.getBody() == null) {
throw new OllamaToolErrorException(name, "Failed to call API: " + response.getStatusCode() + " - Response body is null");
}
throw new OllamaToolErrorException(name, "Failed to call API: " + response.getStatusCode() + " - " + response.getBody().getError());
}
}
}

View File

@@ -0,0 +1,31 @@
package me.zacharias.chat.api;
import me.zacharias.chat.api.payload.webhook.WebhookError;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
public class QuerryResponceEndpoint {
String url;
String errorUrl;
public QuerryResponceEndpoint(String url, String errorUrl) {
this.url = url;
this.errorUrl = errorUrl;
}
public String getUrl() {
return url;
}
public void sendError(String error){
WebClient.create().post()
.uri(errorUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new WebhookError(url, error))
.retrieve()
.toBodilessEntity()
.doOnError(iggnore -> {})
.subscribe();
}
}

View File

@@ -1,4 +0,0 @@
package me.zacharias.chat.api.client;
public class APIClient {
}

View File

@@ -0,0 +1,13 @@
package me.zacharias.chat.api.condations;
import me.zacharias.chat.core.LaunchOptions;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class EnableIfNotDisplay implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return LaunchOptions.getInstance().isNotDisplay();
}
}

View File

@@ -0,0 +1,58 @@
package me.zacharias.chat.api.controllers;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import me.zacharias.chat.api.APIApplication;
import me.zacharias.chat.api.condations.EnableIfNotDisplay;
import me.zacharias.chat.api.payload.request.NewQurryResponceHook;
import me.zacharias.chat.ollama.OllamaMessage;
import me.zacharias.chat.ollama.OllamaMessageRole;
import org.springframework.context.annotation.Conditional;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Conditional(EnableIfNotDisplay.class)
@RestController
@RequestMapping("/v1/message")
public class Message {
private final APIApplication apiApplication;
public Message(APIApplication apiApplication) {
this.apiApplication = apiApplication;
}
@GetMapping("/query")
public ResponseEntity<String> query(@RequestParam(name = "query", required = true) String query) {
if (query == null || query.isEmpty()) {
return ResponseEntity.badRequest().body("Query cannot be null or empty");
}
Thread t = new Thread(() -> {
apiApplication.getCore().getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, query));
apiApplication.getCore().handleResponce(apiApplication.getCore().qurryOllama());
});
t.start();
return ResponseEntity.ok("Query received");
}
@PostMapping("/addResponseHook")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Response hook added successfully"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid request data"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "Internal server error")
})
public ResponseEntity<String> addResponseHook(@RequestBody NewQurryResponceHook request) {
if (request.getUrl() == null || request.getUrl().isEmpty()) {
return ResponseEntity.badRequest().body("URL cannot be null or empty");
}
try {
apiApplication.addQurryResponseHook(request);
return ResponseEntity.ok("Response hook added successfully: " + request.getUrl());
}catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body("Invalid request data: " + e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(500).body("Internal server error: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,40 @@
package me.zacharias.chat.api.controllers;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import me.zacharias.chat.api.APIApplication;
import me.zacharias.chat.api.payload.request.NewToolRequest;
import me.zacharias.chat.api.payload.response.NewToolResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/v1/tool")
public class Tool {
APIApplication application;
public Tool(APIApplication application) {
this.application = application;
}
/**
* Adds a new tool to the application.
*
* @param request The request containing the tool details.
* @return A response entity containing the tool details or an error message.
*/
@PostMapping("addTool")
@ApiResponse(responseCode = "200", description = "Tool added successfully", content = @io.swagger.v3.oas.annotations.media.Content(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = NewToolResponse.class)))
@ApiResponse(responseCode = "400", description = "Tool already exists", content = @io.swagger.v3.oas.annotations.media.Content(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = String.class)))
public ResponseEntity<?> addTool(@RequestBody NewToolRequest request) {
if(application.addTool(request.getName(), request)) {
String[] arguments = new String[request.getArguments().length];
for(int i = 0; i < arguments.length; i++) {
arguments[i] = request.getArguments()[i].getName()+":("+request.getArguments()[i].getType()+")";
}
return ResponseEntity.ok(new NewToolResponse(request.getName(), request.getDescription(), arguments));
} else {
return new ResponseEntity<>("Tool already exists", null, 400);
}
}
}

View File

@@ -0,0 +1,19 @@
package me.zacharias.chat.api.payload;
public class MessageResponce {
private final String message;
private final ToolRequest[] tool;
public MessageResponce(String message, ToolRequest[] tool) {
this.message = message;
this.tool = tool;
}
public String getMessage() {
return message;
}
public ToolRequest[] getTool() {
return tool;
}
}

View File

@@ -0,0 +1,33 @@
package me.zacharias.chat.api.payload;
import me.zacharias.chat.ollama.OllamaPerameter;
public class ToolArgument {
String name;
String description;
OllamaPerameter.OllamaPerameterBuilder.Type type;
boolean required;
public ToolArgument(String name, String description, OllamaPerameter.OllamaPerameterBuilder.Type type, boolean required) {
this.name = name;
this.description = description;
this.type = type;
this.required = required;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public OllamaPerameter.OllamaPerameterBuilder.Type getType() {
return type;
}
public boolean isRequired() {
return required;
}
}

View File

@@ -0,0 +1,19 @@
package me.zacharias.chat.api.payload;
public class ToolRequest {
private final String name;
private final String[] args;
public ToolRequest(String name, String[] args) {
this.name = name;
this.args = args;
}
public String getName() {
return name;
}
public String[] getArgs() {
return args;
}
}

View File

@@ -0,0 +1,45 @@
package me.zacharias.chat.api.payload.request;
import io.swagger.v3.oas.annotations.media.Schema;
public class NewQurryResponceHook {
/**
* The URL to send the response to.
*/
@Schema(description = "The URL to send the response to.", example = "https://example.com/webhook")
private final String url;
/**
* The URL to send errors to.
*/
@Schema(description = "The URL to send errors to.", example = "https://example.com/error-webhook")
private final String errorUrl;
/**
* Constructor for NewQurryResponceHook.
*
* @param url The URL to send the response to.
* @param errorUrl The URL to send errors to.
*/
public NewQurryResponceHook(String url, String errorUrl) {
this.url = url;
this.errorUrl = errorUrl;
}
/**
* Gets the URL to send the response to.
*
* @return The URL for the response.
*/
public String getUrl() {
return url;
}
/**
* Gets the URL to send errors to.
*
* @return The URL for errors.
*/
public String getErrorUrl() {
return errorUrl;
}
}

View File

@@ -0,0 +1,69 @@
package me.zacharias.chat.api.payload.request;
import me.zacharias.chat.api.payload.ToolArgument;
public class NewToolRequest {
/**
* The name of the tool.
*/
private String name;
/**
* The description of the tool.
*/
private String description;
/**
* The arguments this tool will take.
*/
private ToolArgument[] arguments;
/**
* The URL this api will call when the tool is used.
*/
private String requestUrl;
/**
* Constructor for NewToolRequest.
*
* @param name The name of the tool.
* @param description The description of the tool.
* @param arguments The arguments this tool will take.
* @param requestUrl The URL this api will call when the tool is used.
*/
public NewToolRequest(String name, String description, ToolArgument[] arguments, String requestUrl) {
this.name = name;
this.description = description;
this.arguments = arguments;
this.requestUrl = requestUrl;
}
/**
*
* @return the name of the tool.
*/
public String getName() {
return name;
}
/**
*
* @return the description of the tool.
*/
public String getDescription() {
return description;
}
/**
*
* @return the arguments this tool will take.
*/
public ToolArgument[] getArguments() {
return arguments;
}
/**
*
* @return the URL this api will call when the tool is used.
*/
public String getRequestUrl() {
return requestUrl;
}
}

View File

@@ -0,0 +1,44 @@
package me.zacharias.chat.api.payload.response;
public class NewToolResponse {
private String name;
private String description;
private String[] arguments;
/**
* Constructor for NewToolResponse.
*
* @param name The name of the tool.
* @param description The description of the tool.
* @param arguments The arguments this tool will take.
*/
public NewToolResponse(String name, String description, String[] arguments) {
this.name = name;
this.description = description;
this.arguments = arguments;
}
/**
*
* @return the name of the tool.
*/
public String getName() {
return name;
}
/**
*
* @return the description of the tool.
*/
public String getDescription() {
return description;
}
/**
*
* @return the arguments this tool will take.
*/
public String[] getArguments() {
return arguments;
}
}

View File

@@ -0,0 +1,31 @@
package me.zacharias.chat.api.payload.webhook;
public class WebhookError {
private final String originalUrl;
private final String errorMessage;
private final int statusCode;
private final String timestamp;
public WebhookError(String url, String error) {
this.originalUrl = url;
this.errorMessage = error;
this.statusCode = 500; // Default status code for errors
this.timestamp = java.time.Instant.now().toString(); // Current timestamp in ISO-8601 format
}
public String getOriginalUrl() {
return originalUrl;
}
public String getErrorMessage() {
return errorMessage;
}
public int getStatusCode() {
return statusCode;
}
public String getTimestamp() {
return timestamp;
}
}

View File

@@ -0,0 +1,35 @@
package me.zacharias.chat.api.payload.webhook.responce;
public class APIToolResponse {
private final String response;
private final String error;
/**
* Constructor for APIToolResponse.
*
* @param response The response from the API.
* @param error The error message if any.
*/
public APIToolResponse(String response, String error) {
this.response = response;
this.error = error;
}
/**
* Gets the response from the API.
*
* @return The response from the API.
*/
public String getResponse() {
return response;
}
/**
* Gets the error message if any.
*
* @return The error message.
*/
public String getError() {
return error;
}
}

View File

@@ -1,329 +0,0 @@
package me.zacharias.chat.api.server;
import me.zacharias.chat.api.APICodes;
import me.zacharias.chat.api.APIEndpoints;
import me.zacharias.chat.api.APIErrorCodes;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.LaunchOptions;
import me.zacharias.chat.core.PrintMessageHandler;
import me.zacharias.chat.ollama.OllamaObject;
import org.json.JSONObject;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.Objects;
import java.util.Scanner;
import static me.zacharias.chat.api.APIErrorCodes.*;
/**
* The API server.<br>
* This class is responsible for handling the server socket and the clients for the API.<br>
* It also handles the output redirection and the message printing.<br>
* And the Registration of Tools on the {@link Core} object.
*/
public class APIServer {
/**
* The list of clients connected to the server.
*/
ArrayList<Client> clientsList = new ArrayList<>();
/**
* The server socket.
*/
ServerSocket serverSocket;
Thread clientAcceptThread = new Thread(() -> {
while (true) {
try{
Socket s = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter out = new PrintWriter(s.getOutputStream(), true);
JSONObject outObj = new JSONObject();
if(in.ready()) {
String line = in.readLine();
JSONObject obj;
try{
obj = new JSONObject(line);
}catch(Exception e){
errorOutClient(out,"Invalid JSON object", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
if(!obj.has("code"))
{
errorOutClient(out, "Missing APICode", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
if(obj.getEnum(APICodes.class, "code") != APICodes.API)
{
errorOutClient(out, "Invalid APICode", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
if(!obj.has("api"))
{
errorOutClient(out, "Missing API", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
JSONObject apiObj = obj.getJSONObject("api");
if(!apiObj.has("code"))
{
errorOutClient(out, "Missing APIEndpoint", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
APIEndpoints apiEndpoint;
if((apiEndpoint = apiObj.getEnum(APIEndpoints.class, "code")) != APIEndpoints.INFO)
{
errorOutClient(out, "APIEndpoint \""+apiEndpoint+"\" requires setup", API_ERROR);
s.shutdownOutput();
s.close();
continue;
}
outObj.put("code", APICodes.API);
JSONObject endpointObj = new JSONObject();
JSONObject infoObj = new JSONObject();
infoObj.put("requires_credentials", LaunchOptions.getInstance().isServerCredentialsEnabled());
endpointObj.put("message", infoObj);
outObj.put("api", endpointObj);
out.println(outObj);
s.shutdownOutput();
s.close();
continue;
}
if(LaunchOptions.getInstance().isServerCredentialsEnabled())
{
outObj.put("code", APICodes.CREDENTIALS);
out.println(outObj);
String line = in.readLine();
JSONObject lineObj;
try{
lineObj = new JSONObject(line);
}catch(Exception e)
{
errorOutClient(out, "Missing credentials or Incorrect format", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
if(!lineObj.has("code"))
{
errorOutClient(out, "Missing APICode", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
if(!(lineObj.getEnum(APICodes.class, "code") == APICodes.CREDENTIALS))
{
errorOutClient(out, "Missing Credentials", CREDENTIALS_ERROR);
s.shutdownOutput();
s.close();
continue;
}
if(!lineObj.has("credentials"))
{
errorOutClient(out, "Missing Credentials", FORMAT_ERROR);
s.shutdownOutput();
s.close();
continue;
}
if(!Objects.equals(lineObj.getString("credentials"), LaunchOptions.getInstance().getServerCredentials()))
{
errorOutClient(out, "Invalid Credentials", CREDENTIALS_ERROR);
s.shutdownOutput();
s.close();
continue;
}
outObj = new JSONObject();
outObj.put("code", APICodes.SUCCESS);
out.println(outObj);
clientsList.add(new Client(s));
continue;
}
else
{
outObj.put("code", APICodes.SUCCESS);
out.println(outObj);
clientsList.add(new Client(s));
continue;
}
}catch(Exception e) {
}
}
});
/**
* The output stream for the server.
*/
PrintStream dataOut;
/**
* The message handler for the server.
*/
PrintMessageHandler printMessageHandler = new PrintMessageHandler() {
@Override
public void printMessage(String message) {
synchronized (clientsList) {
for (Client client : clientsList) {
boolean success = client.sendMessage(message);
if (!success) {
clientsList.remove(client);
continue;
}
}
}
}
@Override
public boolean color() {
return false;
}
};
/**
* The core object for the server.
*/
Core core = new Core(printMessageHandler);
/**
* Options for this is expected to be passed through the {@link LaunchOptions#instance} object.<br>
* Used objects:<br>
* - {@link LaunchOptions#autoAccept}<br>
* - {@link LaunchOptions#redirectOutput}<br>
* - {@link LaunchOptions#port}<br>
*/
public APIServer() {
LaunchOptions options = LaunchOptions.getInstance();
String redirectedOutput = options.getRedirectOutput();
int port = options.getPort();
core.setOllamaObject(OllamaObject.builder()
.setModel("llama3-AI")
.keep_alive(10)
//.stream(false)
.build());
if (redirectedOutput != null && !Paths.get(redirectedOutput).toFile().getParentFile().exists()) {
System.out.println("Failed to be able to open the redirected output file due to missing directory");
}
else {
redirectedOutput = "./out.txt";
}
File f = new File(redirectedOutput);
try {
if (f.exists()) {
System.out.println("Output already exists");
System.out.print("Overwrite the existing output file? [y/N]: ");
Scanner sc = new Scanner(System.in);
char c;
if (options.isAutoAccept()) {
c = 'y';
} else {
String s = sc.nextLine();
c = s.isBlank() ? 'n' : s.charAt(0);
}
if (Character.toLowerCase(c) == 'y') {
f.delete();
f.createNewFile();
dataOut = new PrintStream(new FileOutputStream(f), true);
} else {
System.out.print("Rename existing output file? [y/N]: ");
String s = sc.nextLine();
c = s.isBlank() ? 'n' : s.charAt(0);
if (c == 'y') {
System.out.println("New file name for [" + f.getName() + "]: ");
File newFile = new File(f.getParentFile(), sc.nextLine().trim());
if (f.renameTo(newFile)) {
System.out.println("Old file placed in [" + newFile.getPath() + "]");
f = new File(redirectedOutput);
} else {
System.out.println("Failed to rename file. Proceeding with appending data.");
}
if (f.exists()) {
f.delete();
}
f.createNewFile();
dataOut = new PrintStream(new FileOutputStream(f), true);
} else {
System.out.print("Appending new data to [" + f.getPath() + "]");
dataOut = new PrintStream(new FileOutputStream(f, true), true);
Date date = new Date();
dataOut.println("\n\nNew instance started at: " + date.toString() + "\n");
}
}
sc.close();
} else {
f.createNewFile();
dataOut = new PrintStream(new FileOutputStream(f, true), true);
}
}
catch (Exception e) {
e.printStackTrace();
System.exit(1);
return;
}
System.out.println("Redirecting output to [" + redirectedOutput + "]");
System.out.println("Starting server on port [" + port + "]");
try{
serverSocket = new ServerSocket(port);
}
catch(Exception e){
System.out.println("Failed to start server on port [" + port + "]");
System.exit(1);
return;
}
//serverSocket = new ServerSocket(port);
}
private void errorOutClient(PrintWriter writer, String errorMessage, APIErrorCodes apiErrorCodes) {
JSONObject outObj = new JSONObject();
outObj.put("code", APICodes.ERROR);
JSONObject errObj = new JSONObject();
errObj.put("code", apiErrorCodes);
errObj.put("message", errorMessage);
outObj.put("error", errObj);
writer.println(outObj);
}
}

View File

@@ -1,38 +0,0 @@
package me.zacharias.chat.api.server;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.net.Socket;
/**
* The client object for the API server.
*/
public class Client {
/**
* The socket for the client.
*/
Socket socket;
/**
* Creates a new client object.
* @param socket the socket for the client.
*/
public Client(Socket socket) {
this.socket = socket;
}
/**
* Returnes true unless if the method failes
* @param message the message to send to the client.
* @return true if the message was sent successfully, false otherwise.
*/
public boolean sendMessage(String message) {
try(BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
out.write(message+"\r\n");
return true;
}catch (Exception e) {
return false;
}
}
}

View File

@@ -274,14 +274,16 @@ public class Core {
return ollamaObject; return ollamaObject;
} }
/** public void removeTool(String name) {
* Adds a new tool to the System Pair<OllamaFunctionTool, String> funtionTool = funtionTools.stream().filter(tool -> tool.getKey().name().equalsIgnoreCase(name)).findFirst().orElse(null);
* @deprecated Use {@link Core#addTool(OllamaFunctionTool, String)} instead funtionTools.remove(funtionTool);
* @param funtionTool The tool to add if(funtionTool.getKey() == null) {
*/ // This should never happens... So if it does, Shit hit the fan
@Deprecated Exception e = new IllegalArgumentException("Function tool with name '"+name+"' does not exist");
public void addFuntionTool(OllamaFunctionTool funtionTool) { e.printStackTrace();
funtionTools.add(new Pair<>(funtionTool, "External")); System.exit(1);
}
ollamaObject.removeTool(funtionTool.getKey());
} }
/** /**
@@ -306,8 +308,10 @@ public class Core {
connection.setDoOutput(true); connection.setDoOutput(true);
connection.setConnectTimeout(80*1000); connection.setConnectTimeout(80*1000);
String ollamaObjectString = ollamaObject.toString();
try(DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { try(DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.writeBytes(ollamaObject.toString()); wr.writeBytes(ollamaObjectString);
wr.flush(); wr.flush();
} }

View File

@@ -0,0 +1,29 @@
package me.zacharias.chat.core;
import java.util.HashMap;
import java.util.Map;
public class GlobalObjects {
private static final Map<String, Object> objects = new HashMap<>();
public static void addObject(String name, Object object) {
if (name == null || object == null) {
throw new IllegalArgumentException("Name and object cannot be null");
}
objects.put(name, object);
}
public static Object getObject(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
return objects.get(name);
}
public static boolean removeObject(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
return objects.remove(name) != null;
}
}

View File

@@ -19,6 +19,7 @@ public class LaunchOptions {
private boolean autoAccept; private boolean autoAccept;
private boolean serverMode; private boolean serverMode;
private boolean serverCredentialsEnabled; private boolean serverCredentialsEnabled;
private boolean notDisplay = true;
private int port = 39075; private int port = 39075;
private String redirectOutput; private String redirectOutput;
private String serverCredentials; private String serverCredentials;
@@ -143,4 +144,20 @@ public class LaunchOptions {
public void setServerCredentials(String serverCredentials) { public void setServerCredentials(String serverCredentials) {
this.serverCredentials = serverCredentials; this.serverCredentials = serverCredentials;
} }
/**
* Gets if the program is running in display mode.
* @return a boolean indicating if the program is running in display mode.
*/
public boolean isNotDisplay() {
return notDisplay;
}
/**
* Sets if the program is running in display mode.
* @param notDisplay a boolean indicating if the program is running in display mode.
*/
public void setNotDisplay(boolean notDisplay) {
this.notDisplay = notDisplay;
}
} }

View File

@@ -135,6 +135,10 @@ public class OllamaObject {
tools.add(tool); tools.add(tool);
} }
public void removeTool(OllamaTool tool) {
tools.remove(tool);
}
/** /**
* Gets the format of the Ollama Object. * Gets the format of the Ollama Object.
* @return The format of the Ollama Object * @return The format of the Ollama Object

View File

@@ -8,6 +8,7 @@ version = '1.0-SNAPSHOT'
dependencies { dependencies {
implementation project(":Core") implementation project(":Core")
implementation project(":MALAPITool") implementation project(":MALAPITool")
implementation project(":API")
} }
test { test {

View File

@@ -1,5 +1,6 @@
package me.zacharias.chat.display; package me.zacharias.chat.display;
import me.zacharias.chat.api.APIApplication;
import me.zacharias.chat.core.Core; import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.Pair; import me.zacharias.chat.core.Pair;
import me.zacharias.chat.core.PrintMessageHandler; import me.zacharias.chat.core.PrintMessageHandler;
@@ -56,6 +57,8 @@ public class Display {
//core.addTool(new PythonRunner(core), Core.Source.INTERNAL); //core.addTool(new PythonRunner(core), Core.Source.INTERNAL);
core.addTools(new MALAPITool().getOllamaTools()); core.addTools(new MALAPITool().getOllamaTools());
APIApplication.start();
//core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "Have a nice tone and use formal wording")); //core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "Have a nice tone and use formal wording"));
writeLog("Creating base OllamaObject with model: "+core.getOllamaObject().getModel()); writeLog("Creating base OllamaObject with model: "+core.getOllamaObject().getModel());

View File

@@ -10,7 +10,7 @@ While the primary goal is providing a somewhat more modular frontend for Ollama,
simple examples are in the Display module where i gave it the ability to access python through docker and get the current date and time both with the [OllamaFunctionTool](https://server.4zellen.se:3000/Zacharias/chat_thing/src/branch/master/Core/src/main/java/me/zacharias/chat/ollama/OllamaFuntionTool.java) thru my Ollama framework simple examples are in the Display module where i gave it the ability to access python through docker and get the current date and time both with the [OllamaFunctionTool](https://server.4zellen.se:3000/Zacharias/chat_thing/src/branch/master/Core/src/main/java/me/zacharias/chat/ollama/OllamaFuntionTool.java) thru my Ollama framework
## API ## API
The documentation for the API is available at the gitea wiki under [API docs](https://server.4zellen.se:3000/Zacharias/chat_thing/wiki/API-Docs) The documentation for the API is available at the gitea wiki under [API docs](https://server.4zellen.se:3000/Chat_things/NeuroDock/wiki/API-Docs)
## How to run? ## How to run?
To run you need to build the `launcher` module<br> To run you need to build the `launcher` module<br>
@@ -19,7 +19,7 @@ This will put the `launcher-1.0-all.jar`(or similar, based on the version) in `.
`$ java -jar launcher-1.0-all.jar`<br> `$ java -jar launcher-1.0-all.jar`<br>
you can use the `-h` argument to get a help message. you can use the `-h` argument to get a help message.
``` ```
Launch options for AI_chat Launch options for NeuroChat:
-h --help Provides this help message -h --help Provides this help message
-s --server Starts the application as API server -s --server Starts the application as API server
-p --port Provides the port number that the API server should use, defaults to 39075 -p --port Provides the port number that the API server should use, defaults to 39075
@@ -36,7 +36,7 @@ If you only want to build a specific module, you can use the same command as lis
- Core: `$ ./gradlew :Core:shadowJar` - Core: `$ ./gradlew :Core:shadowJar`
- However, this one is kinda useless unless you want to directly implement the system into your application - However, this one is kinda useless unless you want to directly implement the system into your application
- MALAPITool: `$ ./gradlew :MALAPITool:shadowJar` - MALAPITool: `$ ./gradlew :MALAPITool:shadowJar`
- Please read MALAPITool [README.md](https://server.4zellen.se:3000/Chat_things/chat_thing/src/branch/master/MALAPITool/README.md) - Please read MALAPITool [README.md](https://server.4zellen.se:3000/Chat_things/NeuroDock/src/branch/master/MALAPITool/README.md)
- Launcher: `$ ./gradlew :launcher:shadowJar` - Launcher: `$ ./gradlew :launcher:shadowJar`
- Depends on `API`, `Display` and `Core` - Depends on `API`, `Display` and `Core`
- This is the main module that runs the application and starts the API server - This is the main module that runs the application and starts the API server

View File

@@ -1,5 +1,8 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
} }
group = 'me.zacharias' group = 'me.zacharias'
@@ -9,6 +12,16 @@ dependencies {
implementation project(":Display") implementation project(":Display")
implementation project(":API") implementation project(":API")
implementation project(":Core") implementation project(":Core")
afterEvaluate {
boolean hasAPIDependency = configurations.implementation.dependencies.any { it.name.contains('API') }
if (hasAPIDependency) {
dependencies.add("implementation", 'org.springframework.boot:spring-boot-starter-web')
dependencies.add("implementation", 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0')
dependencies.add("testImplementation", 'org.springframework.boot:spring-boot-starter-test')
}
}
} }
jar{ jar{

View File

@@ -1,9 +1,12 @@
package me.zacharias.chat.launcher; package me.zacharias.chat.launcher;
import me.zacharias.chat.api.server.APIServer; //import me.zacharias.chat.api.APIApplication;
import me.zacharias.chat.api.APIApplication;
import me.zacharias.chat.core.LaunchOptions; import me.zacharias.chat.core.LaunchOptions;
import me.zacharias.chat.display.Display; import me.zacharias.chat.display.Display;
import java.lang.reflect.Method;
/** /**
* The launcher for AI_chat.<br> * The launcher for AI_chat.<br>
* This is the main class of the application and is responsible for handling the command line arguments. * This is the main class of the application and is responsible for handling the command line arguments.
@@ -49,7 +52,7 @@ public class Launcher {
} }
case "--help", "-h" -> { case "--help", "-h" -> {
System.out.println(""" System.out.println("""
Launch options for AI_chat Launch options for NeuroChat:
-h --help Provides this help message -h --help Provides this help message
-s --server Starts the application as API server -s --server Starts the application as API server
-p --port Provides the port number that the API server should use, defaults to 39075 -p --port Provides the port number that the API server should use, defaults to 39075
@@ -84,9 +87,16 @@ public class Launcher {
if (options.isServerMode()) { if (options.isServerMode()) {
System.out.println("Starting in API mode..."); System.out.println("Starting in API mode...");
new APIServer(); try {
APIApplication.start();
}catch (Exception e)
{
System.out.println("Failed to start API server: " + e.getMessage());
e.printStackTrace();
}
} else { } else {
System.out.println("Starting in Display mode..."); System.out.println("Starting in Display mode...");
options.setNotDisplay(false);
new Display(); new Display();
} }
} }