package me.zacharias.chat.display; 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.*; import com.github.dockerjava.api.model.BuildResponseItem; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.Statistics; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientBuilder; import me.zacharias.chat.core.Core; import me.zacharias.chat.core.Pair; 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.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; import static me.zacharias.chat.core.Core.writeLog; /** * A tool that runs python code. * This is a wrapper around a docker container. * This is partly meant as a proof of concept, but also as a way to run python code while keeping the executed code in a secure environment. */ public class PythonRunner extends OllamaFunctionTool { /** * The DockerClient instance. */ private DockerClient dockerClient; /** * The Core instance. */ private Core core; /** * The ServerSocket instance. */ private ServerSocket serverSocket; /** * Creates a new instance of PythonRunner. * @param core The Core instance */ public PythonRunner(Core core) { this.core = core; try { serverSocket = new ServerSocket(6050); Thread thread = new Thread(() -> { while (true) { try { Socket socket = serverSocket.accept(); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String inputLine = in.readLine(); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); try { JSONObject data = new JSONObject(inputLine); List> list = core.getFuntionTools().stream().filter(funtionTool -> funtionTool.getKey().name().equalsIgnoreCase(data.optString("function", ""))).toList(); if (list.isEmpty()) { out.write(new JSONObject().put("error", "Function don't exist").toString()); out.newLine(); out.flush(); out.close(); in.close(); socket.close(); continue; } ArrayList args = new ArrayList<>(); for (Object o : data.optJSONArray("arguments", new JSONArray())) { if (o instanceof JSONObject obj) { if(obj.has("value") && !obj.isNull("value")) { OllamaFunctionArgument arg = new OllamaFunctionArgument(obj.getString("name"), obj.getString("value")); args.add(arg); } } } out.write(list.getFirst().getKey().function(args.toArray(new OllamaFunctionArgument[0])).getResponse()); out.newLine(); out.flush(); out.close(); in.close(); socket.close(); } catch (Exception e) { } } catch (Exception e) { } } }); thread.start(); }catch (Exception e) { e.printStackTrace(); } 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") .addProperty("libs", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "A space separated list of pip packages needed") .build(); } @Override public OllamaToolRespnce function(OllamaFunctionArgument... args) { if(args.length == 0) { throw new OllamaToolErrorException(name(), "Missing code argument"); } String name = null; String code = null; String libs = null; for(OllamaFunctionArgument arg : args) { if(arg.argument().equals("name") && !arg.value().equals("")) { name = (String) arg.value(); if(!name.endsWith(".py")) { name += ".py"; } } else if (arg.argument().equals("code") && !arg.value().equals("")) { code = (String) arg.value(); } else if(arg.argument().equals("libs") && !arg.value().equals("")) { libs = (String) arg.value(); } } if(name == null) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] encodedhash = digest.digest(String.valueOf(args[0].value()).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); code = "from external_tools import *\n\n"+code; if(!pythonFile.exists()) { try { BufferedWriter writer = new BufferedWriter(new FileWriter(pythonFile)); writer.write(code); writer.close(); }catch(IOException e) {} } File f = new File("./pythonFiles", "external_tools.py"); try { String external_tools = generateExternalTools(); BufferedWriter bw = new BufferedWriter(new FileWriter(f)); bw.write(external_tools.replace("\\n", "\n")); bw.flush(); bw.close(); }catch(IOException e) {} try { ArrayList pythonArgs = new ArrayList<>(); pythonArgs.add("/bin/bash"); StringBuilder cmd = new StringBuilder(); cmd.append("set -e\n\npacman --noconfirm -Sy > /dev/null\npacman --noconfirm -S python python-pip > /dev/null\nmkdir pythonRun > /dev/null\npython -m venv ./pythonRun > /dev/null\ncp external_tools.py ./pythonRun > /dev/null\ncp ").append(name).append(" ./pythonRun > /dev/null\ncd pythonRun > /dev/null\nsource ./bin/activate > /dev/null\n"); if(libs != null && !libs.isEmpty()) { cmd.append("pip install "); for(String lib : libs.split(" ")) { cmd.append(lib).append(" "); } cmd.append(" > /dev/null\n"); } cmd.append("python ").append(name);//.append(" exit"); pythonArgs.add("-c"); pythonArgs.add(cmd.toString()); StringBuilder fullCmd = new StringBuilder(); for(String arg : pythonArgs) { fullCmd.append(arg).append(" "); } File program = new File("./cache", "cmd.sh"); if(program.exists()){ program.delete(); } program.createNewFile(); BufferedWriter bw = new BufferedWriter(new FileWriter(program)); bw.write(cmd.toString()); bw.flush(); bw.close(); String containerId = dockerClient.createContainerCmd("archlinux").withCmd("/bin/bash","./cmd.sh").exec().getId(); dockerClient.copyArchiveToContainerCmd(containerId) .withHostResource(pythonFile.getPath()) .exec(); dockerClient.copyArchiveToContainerCmd(containerId) .withHostResource(f.getPath()) .exec(); dockerClient.copyArchiveToContainerCmd(containerId) .withHostResource(program.getPath()) .exec(); //InputStream stdin = new ByteArrayInputStream(fullCmd.toString().getBytes(StandardCharsets.UTF_8)); dockerClient.startContainerCmd(containerId).exec(); //dockerClient.attachContainerCmd(containerId).withStdIn(stdin).exec(null); //dockerClient.execCreateCmd(containerId).withCmd(fullCmd.toString()).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 (isRunning(containerId)); 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()); } catch (Exception e) { throw new OllamaToolErrorException(name(), "Docker unavalible"); } } /** * Generates the external_tools.py file.
* This is meant to provide the python code with all ExternalTools defined in the OllamaObject. * @return The generated external_tools.py file */ private String generateExternalTools() { StringBuilder code = new StringBuilder(); code.append(""" import socket import json HOST = "host.docker.internal" PORT = 6050 def connect(data): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) data = data + "\\n" s.sendall(data.encode("utf-8")) responce = s.recv(4096) return responce.decode("utf-8") """); for(Pair funtionTool : core.getFuntionTools()) { OllamaFunctionTool tool = funtionTool.getKey(); String name = tool.name(); code.append("def ").append(name).append("("); ArrayList args = new ArrayList<>(); boolean first = true; try { for (String argName : tool.parameters().getProperties().keySet()) { args.add(argName); if (!first) { code.append(", "); } code.append(argName); if (Arrays.stream(tool.parameters().getRequired()).noneMatch(required -> required.equals(argName))) { code.append("=None"); } first = false; } }catch (Exception e) {} code.append("):\n"); code.append(" data = {\"function\":\"").append(tool.name()).append("\""); if(args.size() > 0) { code.append(",\"arguments\":["); } first = true; for(String str : args) { if(!first) { code.append(", "); } code.append("{\"name\": \"").append(str).append("\", \"value\": ").append(str).append("}"); first = false; } if(args.size() > 0) { code.append("]"); } code.append("}\n"); code.append(" return json.loads(connect(json.dumps(data)))\n\n"); } return code.toString(); } /** * Checks if a Docker Container is running * @param containerId The ID of the Container * @return a boolean weather it's running or not */ public boolean isRunning(String containerId) { InspectContainerResponse cmd = dockerClient.inspectContainerCmd(containerId).exec(); return Boolean.TRUE.equals(cmd.getState().getRunning()); } /** * A Helper class to get the logs from a docker container. */ 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); /** * Creates a new instance of {@link GetContainerLog} * @param dockerClient The DockerClient instance * @param containerId The container id */ public GetContainerLog(DockerClient dockerClient, String containerId) { this.dockerClient = dockerClient; this.containerId = containerId; this.lastLogTime = (int) (System.currentTimeMillis() / 1000); } /** * Gets the logs of the container. * @return The logs of the container */ 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(new String(item.getPayload()).trim()); } }).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; } } }