commit 6935e938a3abb21000bf17854adaace4d7f7e2bb Author: Zacharias Date: Thu Feb 20 18:00:16 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88233f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +/pythonFiles/ +/messages/ +/logs/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..2a65317 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..32cf4db --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9ac6b9b --- /dev/null +++ b/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' + id 'com.gradleup.shadow' version '9.0.0-beta7' +} + +group = 'me.zacharias' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.json:json:20250107") + implementation("com.github.docker-java:docker-java:3.4.1") +} + +test { + useJUnitPlatform() +} + +jar{ + manifest { + attributes 'Main-Class': 'me.zacharias.chat.Main' + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4854ea6 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Feb 20 00:10:50 CET 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a8ed29d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'AI-test' + diff --git a/src/main/java/me/zacharias/chat/Main.java b/src/main/java/me/zacharias/chat/Main.java new file mode 100644 index 0000000..0aea3f9 --- /dev/null +++ b/src/main/java/me/zacharias/chat/Main.java @@ -0,0 +1,320 @@ +package me.zacharias.chat; + +import me.zacharias.chat.ollama.*; +import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.*; +import java.net.*; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class Main { + private String ollamaIP = "localhost";//"192.168.5.184"; + private int ollamaPort = 11434; + private URL url; + + private static File logFile = new File("./logs/latest.log"); + private static BufferedWriter logWriter; + private ScheduledExecutorService scheduler; + + private OllamaObject ollamaObject; + private ArrayList funtionTools; + + { + File dir = new File("./logs/"); + if (!dir.exists()) { + dir.mkdir(); + } + dir = new File("./pythonFiles/"); + if (!dir.exists()) { + dir.mkdir(); + } + dir = new File("./messages"); + if (!dir.exists()) { + dir.mkdir(); + } + + try { + url = new URI("http://"+ollamaIP+":"+ollamaPort+"/api/chat").toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + try { + if (logFile.exists()) { + BufferedReader br = new BufferedReader(new FileReader(logFile)); + String line = br.readLine(); + br.close(); + if (line != null) { + String date = line.substring(0, line.indexOf(">")).replaceAll("[/:]", "-"); + logFile.renameTo(new File(logFile.getParentFile(), date + ".log")); + logFile = new File("./logs/latest.log"); + } + else { + System.out.println("Exisitng log file is empty, overwriting it!"); + logFile.delete(); + } + logFile.createNewFile(); + } + logWriter = new BufferedWriter(new FileWriter(logFile)); + }catch (IOException e) { + throw new RuntimeException(e); + } + + this.scheduler = Executors.newScheduledThreadPool(1); + + scheduler.scheduleAtFixedRate(() -> { + try { + logWriter.flush(); + //System.out.println("Buffer flushed to file."); + } catch (IOException e) { + e.printStackTrace(); + } + }, 0, 3, TimeUnit.MINUTES); + } + + public static void main(String[] args) { + new Main(); + } + + public Main() + { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + scheduler.shutdownNow(); + try { + logWriter.flush(); + logWriter.close(); + + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH-mm-ss"); + + File messagesFile = new File("./messages/"+now.format(formatter)+".txt"); + + BufferedWriter messagesWriter = new BufferedWriter(new FileWriter(messagesFile)); + + JSONArray messages = new JSONArray(); + + for(OllamaMessage message : ollamaObject.getMessages()) { + messages.put(new JSONObject(message.toString())); + } + + messagesWriter.write(messages.toString()); + messagesWriter.close(); + + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + + ollamaObject = OllamaObject.builder() + .setModel("llama3-AI") + .keep_alive(10) + .addTool(new TimeTool()) + .addTool(new PythonRunner()) + .stream(false) + .build(); + + writeLog("Creating base OllamaObject with model: "+ollamaObject.getModel()); + + funtionTools = new ArrayList<>(); + + System.out.println("Installed tools"); + writeLog("Tools installed in this instance"); + + for(OllamaTool tool : ollamaObject.getTools()) + { + if(tool instanceof OllamaFuntionTool funtion) + { + StringBuilder args = new StringBuilder(); + OllamaPerameter perameter = funtion.parameters(); + if(perameter != null) { + JSONObject obj = perameter.getProperties(); + for (String name : obj.keySet()) { + args.append(args.toString().isBlank() ? "" : ", ").append(obj.getJSONObject(name).getString("type")).append(Arrays.stream(perameter.getRequired()).anyMatch(str -> str.equalsIgnoreCase(name)) ? "" : "?").append(" ").append(name); + } + } + + System.out.println("> Function: "+funtion.name()+"("+args+")"); + writeLog("Function: "+funtion.name()+"("+args+")"); + funtionTools.add(funtion); + } + } + + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + + System.out.println("Message Trsnscription:"); + try { + while (true) { + System.out.print("> "); + StringBuilder message = new StringBuilder(br.readLine()); + while(br.ready()) + { + message.append("\n").append(br.readLine()); + } + if(message.toString().startsWith("/")) + { + switch (message.substring(1)) { + case "help": + System.out.print(""" + Available commands: + /help Prints this help message. + /bye Exits the program. + /write Flushes the current log stream to file. + """); + break; + case "bye": + writeLog("Exiting program..."); + System.out.println("Bye!"); + System.exit(0); + return; + case "write": + logWriter.flush(); + break; + default: + System.out.println("Unknown command: "+message); + } + } + else + { + writeLog("User: "+message); + ollamaObject.addMessage(new OllamaMessage(OllamaMessageRole.USER, message.toString())); + //System.out.println(ollamaObject.toString()); + handleResponce(qurryOllama(ollamaObject)); + } + } + }catch (Exception e) { + e.printStackTrace(); + System.out.println("Exiting due to exception"); + System.exit(-1); + } + } + + public JSONObject qurryOllama(OllamaObject request) + { + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + connection.setConnectTimeout(80*1000); + + try(DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { + wr.writeBytes(request.toString()); + wr.flush(); + } + + 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.out.println("Error: HTTP Response code - " + responseCode + "\n"+response.toString()); + throw new RuntimeException("HTTP Response code - " + responseCode); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void handleResponce(JSONObject responce) + { + //System.out.println("Responce: "+responce); + if(responce != null) { + writeLog("Raw responce: "+responce.toString()); + if(responce.getJSONObject("message").has("tool_calls")) + { + JSONArray calls = responce.getJSONObject("message").getJSONArray("tool_calls"); + for(Object call : calls) + { + if(call instanceof JSONObject jsonObject) + { + if(jsonObject.has("function")) + { + JSONObject function = jsonObject.getJSONObject("function"); + List functions = funtionTools.stream().filter(func -> func.name().equalsIgnoreCase(function.getString("name"))).toList(); + + if(functions.isEmpty()) { + ollamaObject.addMessage(new OllamaToolError("Function '"+function.getString("name")+"' does not exist")); + System.out.println(">> \u001b[31mTried funtion call "+function.getString("name")+" but failed to find it.\u001b[0m"); + writeLog("Failed function call to "+function.getString("name")); + } + else { + + OllamaFuntionTool func = functions.getFirst(); + + ArrayList argumentArrayList = new ArrayList<>(); + + JSONObject arguments = function.getJSONObject("arguments"); + + for (String key : arguments.keySet()) { + argumentArrayList.add(new OllamaFunctionArgument(key, arguments.get(key))); + } + + try { + OllamaToolRespnce function1 = func.function(argumentArrayList.toArray(new OllamaFunctionArgument[0])); + ollamaObject.addMessage(function1); + System.out.println(">> \u001b[34mCall " + func.name() + "\u001b[0m"); + writeLog("Successfully function call " + func.name() + " output: " + function1.getResponse()); + } catch (OllamaToolErrorException e) { + ollamaObject.addMessage(new OllamaToolError(e.getMessage())); + System.out.println(">> \u001b[31mTried funtion call " + func.name() + " but failed due to " + e.getError() + "\u001b[0m"); + writeLog(e.getMessage()); + } + } + } + } + } + if(responce.getJSONObject("message").has("content") && !responce.getJSONObject("message").getString("content").isBlank()) + { + System.out.println(">> \u001b[32m"+responce.getJSONObject("message").getString("content")+"\u001b[0m"); + writeLog("Response content: "+responce.getJSONObject("message").getString("content")); + ollamaObject.addMessage(new OllamaMessage(OllamaMessageRole.ASSISTANT, responce.getJSONObject("message").getString("content"))); + } + handleResponce(qurryOllama(ollamaObject)); + } + else if(responce.getJSONObject("message").has("content") && !responce.getJSONObject("message").getString("content").isBlank()) + { + System.out.println(">> \u001b[32m"+responce.getJSONObject("message").getString("content")+"\u001b[0m"); + writeLog("Response content: "+responce.getJSONObject("message").getString("content")); + ollamaObject.addMessage(new OllamaMessage(OllamaMessageRole.ASSISTANT, responce.getJSONObject("message").getString("content"))); + } + } + } + + public static void writeLog(String message) + { + try { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd%EEEE HH:mm:ss'#'SSS"); + logWriter.write(now.format(formatter) + "> " + message + "\n"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/me/zacharias/chat/PythonRunner.java b/src/main/java/me/zacharias/chat/PythonRunner.java new file mode 100644 index 0000000..b7f4890 --- /dev/null +++ b/src/main/java/me/zacharias/chat/PythonRunner.java @@ -0,0 +1,187 @@ +package me.zacharias.chat; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.async.ResultCallbackTemplate; +import com.github.dockerjava.api.command.AttachContainerCmd; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.model.Frame; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientBuilder; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.command.LogContainerResultCallback; +import me.zacharias.chat.ollama.*; +import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException; +import org.json.JSONObject; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.logging.Logger; + +import static me.zacharias.chat.Main.writeLog; + +public class PythonRunner extends OllamaFuntionTool { + DockerClient dockerClient; + public PythonRunner() { + + DefaultDockerClientConfig.Builder config + = DefaultDockerClientConfig.createDefaultConfigBuilder() + .withDockerHost("tcp://localhost:2375") + .withDockerTlsVerify(false); + dockerClient = DockerClientBuilder + .getInstance(config) + .build(); + } + + @Override + public String name() { + return "python_runner"; + } + + @Override + public String description() { + return "Runs python code"; + } + + @Override + public OllamaPerameter parameters() { + return OllamaPerameter.builder() + .addProperty("code", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The code to be executed", true) + .addProperty("name", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The name of the python code") + .build(); + } + + @Override + public OllamaToolRespnce function(OllamaFunctionArgument... args) { + if(args.length == 0) + { + throw new OllamaToolErrorException(name(), "Missing code argument"); + } + + String name = null; + String code = null; + + for(OllamaFunctionArgument arg : args) + { + if(arg.getArgument().equals("name")) + { + name = (String) arg.getValue(); + if(!name.endsWith(".py")) + { + name += ".py"; + } + } else if (arg.getArgument().equals("code")) { + code = (String) arg.getValue(); + } + } + + if(name == null) + { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] encodedhash = digest.digest(String.valueOf(args[0].getValue()).getBytes(StandardCharsets.UTF_8)); + StringBuffer hexString = new StringBuffer(); + for(byte b : encodedhash) + { + hexString.append(String.format("%02x", b)); + } + name = hexString.toString()+".py"; + }catch (Exception e) {} + } + + name = name.replace(" ", "_"); + + writeLog("Running python code `" + name + "`"); + + File pythonFile = new File("./pythonFiles", name); + + if(!pythonFile.exists()) + { + try { + BufferedWriter writer = new BufferedWriter(new FileWriter(pythonFile)); + writer.write(code); + writer.close(); + }catch(IOException e) {} + } + + String containerId = dockerClient.createContainerCmd("python").withCmd("python", name).exec().getId(); + dockerClient.copyArchiveToContainerCmd(containerId) + .withHostResource(pythonFile.getPath()) + //.withRemotePath("~/") + .exec(); + + dockerClient.startContainerCmd(containerId).exec(); + + GetContainerLog log = new GetContainerLog(dockerClient, containerId); + + List logs = new ArrayList<>(); + + do { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + logs.addAll(log.getDockerLogs()); + } + while (logs.isEmpty()); + + StringBuilder output = new StringBuilder(); + + for(String s : logs) + { + output.append(s).append("\n"); + } + + //writeLog("Result from python: " + output.toString()); + + return new OllamaToolRespnce(name(), output.toString()); + } + + public class GetContainerLog { + private DockerClient dockerClient; + private String containerId; + private int lastLogTime; + + private static String nameOfLogger = "dockertest.PrintContainerLog"; + private static Logger myLogger = Logger.getLogger(nameOfLogger); + + public GetContainerLog(DockerClient dockerClient, String containerId) { + this.dockerClient = dockerClient; + this.containerId = containerId; + this.lastLogTime = (int) (System.currentTimeMillis() / 1000); + } + + public List getDockerLogs() { + + final List logs = new ArrayList<>(); + + LogContainerCmd logContainerCmd = dockerClient.logContainerCmd(containerId); + logContainerCmd.withStdOut(true).withStdErr(true); + logContainerCmd.withSince(lastLogTime); // UNIX timestamp (integer) to filter logs. Specifying a timestamp will only output log-entries since that timestamp. + // logContainerCmd.withTail(4); // get only the last 4 log entries + + logContainerCmd.withTimestamps(true); + + try { + logContainerCmd.exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame item) { + logs.add(item.toString()); + } + }).awaitCompletion(); + } catch (InterruptedException e) { + myLogger.severe("Interrupted Exception!" + e.getMessage()); + } + + lastLogTime = (int) (System.currentTimeMillis() / 1000) + 5; // assumes at least a 5 second wait between calls to getDockerLogs + + return logs; + } + } +} diff --git a/src/main/java/me/zacharias/chat/TimeTool.java b/src/main/java/me/zacharias/chat/TimeTool.java new file mode 100644 index 0000000..4cd57c2 --- /dev/null +++ b/src/main/java/me/zacharias/chat/TimeTool.java @@ -0,0 +1,32 @@ +package me.zacharias.chat; + +import me.zacharias.chat.ollama.OllamaFunctionArgument; +import me.zacharias.chat.ollama.OllamaFuntionTool; +import me.zacharias.chat.ollama.OllamaPerameter; +import me.zacharias.chat.ollama.OllamaToolRespnce; + +import java.util.Date; + +public class TimeTool extends OllamaFuntionTool { + + @Override + public OllamaToolRespnce function(OllamaFunctionArgument... arguments) { + Date date = new Date(); + return new OllamaToolRespnce(name(), date.toString()); + } + + @Override + public String name() { + return "get_current_date"; + } + + @Override + public OllamaPerameter parameters() { + return null; + } + + @Override + public String description() { + return "Get the current date"; + } +} \ No newline at end of file diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaFunctionArgument.java b/src/main/java/me/zacharias/chat/ollama/OllamaFunctionArgument.java new file mode 100644 index 0000000..5b852a6 --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaFunctionArgument.java @@ -0,0 +1,19 @@ +package me.zacharias.chat.ollama; + +public class OllamaFunctionArgument { + private final String argument; + private final Object value; + + public OllamaFunctionArgument(String argument, Object value) { + this.argument = argument; + this.value = value; + } + + public String getArgument() { + return argument; + } + + public Object getValue() { + return value; + } +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaFuntionTool.java b/src/main/java/me/zacharias/chat/ollama/OllamaFuntionTool.java new file mode 100644 index 0000000..6675034 --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaFuntionTool.java @@ -0,0 +1,28 @@ +package me.zacharias.chat.ollama; + +import org.json.JSONObject; + +public abstract class OllamaFuntionTool implements OllamaTool { + + @Override + public String toString() { + JSONObject ret = new JSONObject(); + ret.put("tool", "function"); + + JSONObject function = new JSONObject(); + function.put("name", name()); + function.put("description", description()); + function.put("parameters", (parameters() == null? + new JSONObject() : new JSONObject(parameters().toString()))); + + ret.put("function", function); + + return ret.toString(); + } + + abstract public String name(); + abstract public String description(); + abstract public OllamaPerameter parameters(); + + abstract public OllamaToolRespnce function(OllamaFunctionArgument... args); +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaMessage.java b/src/main/java/me/zacharias/chat/ollama/OllamaMessage.java new file mode 100644 index 0000000..59eb1f4 --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaMessage.java @@ -0,0 +1,21 @@ +package me.zacharias.chat.ollama; + +import org.json.JSONObject; + +public class OllamaMessage { + OllamaMessageRole role; + String content; + + public OllamaMessage(OllamaMessageRole role, String content) { + this.role = role; + this.content = content; + } + + @Override + public String toString() { + JSONObject json = new JSONObject(); + json.put("role", role); + json.put("content", content); + return json.toString(); + } +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaMessageRole.java b/src/main/java/me/zacharias/chat/ollama/OllamaMessageRole.java new file mode 100644 index 0000000..4fd9e34 --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaMessageRole.java @@ -0,0 +1,18 @@ +package me.zacharias.chat.ollama; + +public enum OllamaMessageRole { + USER("user"), + ASSISTANT("assistant"), + TOOL("tool"), + SYSTEM("system"); + + private String role; + + OllamaMessageRole(String role) { + this.role = role; + } + + public String getRole() { + return role; + } +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaObject.java b/src/main/java/me/zacharias/chat/ollama/OllamaObject.java new file mode 100644 index 0000000..22a2e5a --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaObject.java @@ -0,0 +1,166 @@ +package me.zacharias.chat.ollama; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OllamaObject { + String model; + ArrayList messages; + OllamaTool[] tools; + JSONObject format; + Map options; + boolean stream; + String keep_alive; + + private OllamaObject(String model, ArrayList messages, OllamaTool[] tools, JSONObject format, Map options, boolean stream, String keep_alive) { + this.model = model; + this.messages = messages; + this.tools = tools; + this.format = format; + this.options = options; + this.stream = stream; + this.keep_alive = keep_alive; + } + + public String getModel() { + return model; + } + + public ArrayList getMessages() { + return messages; + } + + public OllamaTool[] getTools() { + return tools; + } + + public JSONObject getFormat() { + return format; + } + + public Map getOptions() { + return options; + } + + public boolean isStream() { + return stream; + } + + public String getKeep_alive() { + return keep_alive; + } + + public void addMessage(OllamaMessage message) { + messages.add(message); + } + + @Override + public String toString() { + JSONObject json = new JSONObject(); + + JSONArray tools = new JSONArray(); + for (OllamaTool tool : this.tools) { + tools.put(new JSONObject(tool.toString())); + } + + JSONArray messages = new JSONArray(); + for (OllamaMessage message : this.messages) { + messages.put(new JSONObject(message.toString())); + } + + json.put("model", model); + json.put("messages", messages); + json.put("tools", tools); + json.put("format", format); + json.put("options", options); + json.put("stream", stream); + json.put("keep_alive", keep_alive); + return json.toString(); + } + + public static OllamaObjectBuilder builder() + { + return new OllamaObjectBuilder(); + } + + public static class OllamaObjectBuilder { + String model; + ArrayList messages = new ArrayList<>(); + ArrayList tools = new ArrayList<>(); + JSONObject format; + Map options = new HashMap<>(); + boolean stream; + String keep_alive; + + public OllamaObjectBuilder() {} + + public OllamaObjectBuilder format(String format) { + this.format = new JSONObject(format); + return this; + } + + public OllamaObjectBuilder options(Map options) { + this.options.putAll(options); + return this; + } + + public OllamaObjectBuilder option(String key, String value) { + this.options.put(key, value); + return this; + } + + public OllamaObjectBuilder stream(boolean stream) { + this.stream = stream; + return this; + } + + public OllamaObjectBuilder keep_alive(String keep_alive) { + this.keep_alive = keep_alive; + return this; + } + + public OllamaObjectBuilder keep_alive(int minutes) { + this.keep_alive = minutes+"m"; + return this; + } + + public OllamaObjectBuilder addTool(OllamaTool tools) { + this.tools.add(tools); + return this; + } + + public OllamaObjectBuilder addTools(ArrayList tools) { + this.tools.addAll(tools); + return this; + } + + public OllamaObjectBuilder addTools(OllamaTool... tools) { + this.tools.addAll(List.of(tools)); + return this; + } + + public OllamaObjectBuilder addMessages(OllamaMessage... messages) { + this.messages.addAll(List.of(messages)); + return this; + } + + public OllamaObjectBuilder addMessage(OllamaMessage messages) { + this.messages.add(messages); + return this; + } + + public OllamaObjectBuilder setModel(String model) { + this.model = model; + return this; + } + + public OllamaObject build() { + return new OllamaObject(model, messages, tools.toArray(new OllamaTool[0]), format, options, stream, keep_alive); + } + } +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaPerameter.java b/src/main/java/me/zacharias/chat/ollama/OllamaPerameter.java new file mode 100644 index 0000000..9d10a03 --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaPerameter.java @@ -0,0 +1,116 @@ +package me.zacharias.chat.ollama; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class OllamaPerameter { + + private OllamaPerameter(JSONObject properties, String[] required) { + this.properties = properties; + this.required = required; + }; + @Override + public String toString() { + JSONObject json = new JSONObject(); + + json.put("type", "object"); + json.put("properties", properties); + json.put("required", required); + + return json.toString(); + } + + JSONObject properties; + String[] required; + + public JSONObject getProperties() { + return properties; + } + + public String[] getRequired() { + return required; + } + + public static OllamaPerameterBuilder builder() { + return new OllamaPerameterBuilder(); + } + + public static class OllamaPerameterBuilder { + Map propertyMap = new HashMap<>(); + ArrayList required = new ArrayList<>(); + + public OllamaPerameterBuilder addProperty(String name, Type type, String description) { + if(name == null || type == null || description == null) { + return this; + } + propertyMap.put(name, new Property(type.getType(), description)); + return this; + } + + public OllamaPerameterBuilder addProperty(String name, Type type, String description, boolean required) { + if(name == null || type == null || description == null) { + return this; + } + propertyMap.put(name, new Property(type.getType(), description)); + this.required.add(name); + return this; + } + + public OllamaPerameterBuilder required(String name) { + required.add(name); + return this; + } + + public OllamaPerameterBuilder removeProperty(String name) { + propertyMap.remove(name); + return this; + } + + public OllamaPerameter build() { + JSONObject properties = new JSONObject(); + for(String name : propertyMap.keySet()) { + properties.put(name, new JSONObject(propertyMap.get(name).toString())); + } + return new OllamaPerameter(properties, required.toArray(new String[0])); + } + + private class Property { + String type; + String description; + + public Property(String type, String description) { + this.type = type; + this.description = description; + } + + @Override + public String toString() { + JSONObject json = new JSONObject(); + + json.put("type", type); + json.put("description", description); + + return json.toString(); + } + } + + public enum Type { + STRING("string"), + INT("int"), + BOOLEAN("boolean"); + + private String type; + + public String getType() { + return type; + } + + Type(String type) { + this.type = type; + } + } + } +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaTool.java b/src/main/java/me/zacharias/chat/ollama/OllamaTool.java new file mode 100644 index 0000000..8108488 --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaTool.java @@ -0,0 +1,5 @@ +package me.zacharias.chat.ollama; + + +public interface OllamaTool { +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaToolError.java b/src/main/java/me/zacharias/chat/ollama/OllamaToolError.java new file mode 100644 index 0000000..6e06efd --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaToolError.java @@ -0,0 +1,15 @@ +package me.zacharias.chat.ollama; + +import org.json.JSONObject; + +public class OllamaToolError extends OllamaMessage { + String error; + public OllamaToolError(String error) { + super(OllamaMessageRole.TOOL, new JSONObject().put("error", error).toString()); + this.error = error; + } + + public String getError() { + return error; + } +} diff --git a/src/main/java/me/zacharias/chat/ollama/OllamaToolRespnce.java b/src/main/java/me/zacharias/chat/ollama/OllamaToolRespnce.java new file mode 100644 index 0000000..bd79568 --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/OllamaToolRespnce.java @@ -0,0 +1,21 @@ +package me.zacharias.chat.ollama; + +import org.json.JSONObject; + +public class OllamaToolRespnce extends OllamaMessage { + private final String tool; + private final String response; + public OllamaToolRespnce(String tool, String response) { + super(OllamaMessageRole.TOOL, new JSONObject().put("tool", tool).put("result", response).toString()); + this.tool = tool; + this.response = response; + } + + public String getTool() { + return tool; + } + + public String getResponse() { + return response; + } +} diff --git a/src/main/java/me/zacharias/chat/ollama/exceptions/OllamaToolErrorException.java b/src/main/java/me/zacharias/chat/ollama/exceptions/OllamaToolErrorException.java new file mode 100644 index 0000000..6e8ec2a --- /dev/null +++ b/src/main/java/me/zacharias/chat/ollama/exceptions/OllamaToolErrorException.java @@ -0,0 +1,21 @@ +package me.zacharias.chat.ollama.exceptions; + +import me.zacharias.chat.ollama.OllamaToolRespnce; + +public class OllamaToolErrorException extends RuntimeException { + private final String tool; + private final String error; + public OllamaToolErrorException(String tool, String error) { + super(tool + ": " + error); + this.tool = tool; + this.error = error; + } + + public String getTool() { + return tool; + } + + public String getError() { + return error; + } +} \ No newline at end of file