Skip to content

Commit

Permalink
Merge pull request #35 from stoerr/feature/runchatfromcommandline
Browse files Browse the repository at this point in the history
Add pmcodevgpt : run a chat with the CoDeveloper engine on the command line
  • Loading branch information
stoerr authored Sep 20, 2024
2 parents 5358f39 + 6e905db commit 7e5ba2b
Show file tree
Hide file tree
Showing 22 changed files with 714 additions and 67 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ and have it execute (e.g. build and test)
actions locally to support you in your development processes? Then this might be for you. The CoDeveloperGPTengine
provide the actions for a OpenAI [GPT](https://openai.com/blog/introducing-gpts)
for read or even write access to the files in the local directory it is started in.
It can also work as a [ChatGPT plugin](https://openai.com/blog/chatgpt-plugins).
It can also work as a [ChatGPT plugin](https://openai.com/blog/chatgpt-plugins) (OK, that's rather obsolete now) and
as a chat on the command line with the chatgpt script from my
[ChatGPT Toolsuite](https://github.com/stoerr/chatGPTtools).

In contrast to other approaches like [AutoGPT](https://github.com/Significant-Gravitas/AutoGPT) this is not meant to
autonomously execute extensive changes (which would likely require a lot of prompt engineering), but to enable the
Expand Down
27 changes: 27 additions & 0 deletions bin/pmcodevgpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# "Poor mans" co developer engine: run it with the chatgpt script from https://github.com/stoerr/chatGPTtools

# start the co-developer-gpt-engine.jar in the directory this script is placed, following links
progfile=$(realpath $0)
progdir=$(dirname "$progfile")

JAVA=java
# if jenv is in the path, we use the version that is set for the directory this script is in,
# since the current dir could use some ridiculously low version.
if which jenv >/dev/null 2>&1; then
JAVA=$(cd $progdir; jenv which java)
fi

$JAVA -jar "$progdir/co-developer-gpt-engine.jar" -w -q &
pid=$!
trap "kill $pid" EXIT

sleep 2

ARGS=""
# if $* doesn't contain -cr or -ca we add -cr to ARGS
if [[ ! "$*" =~ -cr ]] && [[ ! "$*" =~ -ca ]]; then
ARGS="-cr"
fi

chatgpt -tf <($JAVA -jar "$progdir/co-developer-gpt-engine.jar" --aitoolsdef) $ARGS "$@"
14 changes: 14 additions & 0 deletions project-bin/generate_chatgpt_script_toolsdefinition.prompt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Transform the retrieved OpenAI tools definition file into the following format:

[
{
"function": {
// here comes the function definition of the tool
},
"commandline: [
"curl", "-X", "POST", "-d", "@-", "http://localhost:3002/executetool"
],
"stdin": "$toolcall"
},
... // more tools
]
4 changes: 4 additions & 0 deletions project-bin/generate_openai_toolsdefinition.prompt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Print an OpenAI tools definition file that describes the requests specified in the retrieved OpenAPI YAML.
Set "strict" to false. Include the descriptions unmodified.
Take care that the "required" attribute contains exactly the required attributes.
If the request is a POST with requestBody, then the body content should be contained in a parameter "requestBody".
9 changes: 9 additions & 0 deletions project-bin/generate_openai_toolsdefinition.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# generates a json schema for using the engine within other tools
scriptdir=$(dirname "$(realpath $0)")
cd "$(dirname $0)/../"
aigenpipeline -wvf -o src/main/resources/static/codeveloperengine-toolsdefinition.json \
-p $scriptdir/generate_openai_toolsdefinition.prompt src/test/resources/test-expected/codeveloperengine.yaml

aigenpipeline -wvf -o src/main/resources/static/codeveloperengine-chatgptscript-toolsdefinition.json \
-p $scriptdir/generate_chatgpt_script_toolsdefinition.prompt src/main/resources/static/codeveloperengine-toolsdefinition.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public abstract class AbstractPluginAction extends HttpServlet {
*/
public static final Pattern BINARY_FILES_PATTERN = Pattern.compile("(?i).*\\.(gif|png|mov|jpg|jpeg|mp4|mp3|pdf|zip|gz|tgz|tar|jar|class|war|ear|exe|dll|so|o|a|lib|bin|dat|dmg|iso)");

private final transient Gson gson = new Gson();
protected final transient Gson gson = new Gson();

/**
* Logs an error and sends it to ChatGPT, always throws {@link ExecutionAbortedException}.
Expand All @@ -62,10 +62,9 @@ protected static ExecutionAbortedException sendError(HttpServletResponse respons
protected static Stream<Path> findMatchingFiles(
boolean suppressMessage,
HttpServletResponse response, Path path, Pattern filePathPattern,
Pattern grepPattern, boolean recursive) {
Pattern grepPattern, boolean recursive, boolean listDirectories) {
boolean haveFilePathPattern = filePathPattern != null && !filePathPattern.pattern().isEmpty();
List<Path> result = new ArrayList<>();
boolean returnDirectories = !recursive;
try {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
Expand All @@ -74,7 +73,7 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th
(!recursive && !dir.equals(path))) {
return FileVisitResult.SKIP_SUBTREE;
}
if (returnDirectories) {
if (listDirectories || !recursive) {
result.add(dir);
}
return super.preVisitDirectory(dir, attrs);
Expand All @@ -83,7 +82,7 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
FileVisitResult res = super.visitFile(file, attrs);
if (!isIgnored(file)) {
if (!isIgnored(file) && !listDirectories) {
result.add(file);
}
return res;
Expand All @@ -95,11 +94,15 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO

List<Path> matchingFiles = result.stream()
.filter(p -> !haveFilePathPattern || filePathPattern.matcher(p.toString()).find())
.filter(f -> returnDirectories || Files.isRegularFile(f))
.filter(f -> listDirectories && Files.isDirectory(f) || Files.isRegularFile(f))
.collect(toList());
if (matchingFiles.isEmpty() && !suppressMessage) {
String similarFilesMessage = getSimilarFilesMessage(response, path, filePathPattern != null ? filePathPattern.toString() : "");
throw sendError(response, 404, "No files found matching filePathRegex: " + filePathPattern + "\n\n" + similarFilesMessage);
if (filePathPattern != null) {
String similarFilesMessage = getSimilarFilesMessage(response, path, filePathPattern != null ? filePathPattern.toString() : "", listDirectories);
throw sendError(response, 404, "No files found matching filePathRegex: " + filePathPattern + "\n\n" + similarFilesMessage);
} else {
throw sendError(response, 404, "No files found in " + path);
}
}
Collections.sort(matchingFiles); // make it deterministic.

Expand Down Expand Up @@ -184,7 +187,7 @@ protected Path getPath(HttpServletRequest request, HttpServletResponse response,
if (mustExist && !Files.exists(resolved)) {
String message = "Path " + path + " does not exist! Try to list files with /listFiles to find the right path.";
String filename = resolved.getFileName().toString();
String similarFilesMessage = getSimilarFilesMessage(response, CoDeveloperEngine.currentDir, filename);
String similarFilesMessage = getSimilarFilesMessage(response, CoDeveloperEngine.currentDir, filename, false);
if (!similarFilesMessage.isEmpty()) {
message += "\n\n" + similarFilesMessage;
}
Expand All @@ -193,17 +196,17 @@ protected Path getPath(HttpServletRequest request, HttpServletResponse response,
return resolved;
}

protected static String getSimilarFilesMessage(HttpServletResponse response, Path path, String filename) {
protected static String getSimilarFilesMessage(HttpServletResponse response, Path path, String filename, boolean listDirectories) {
String similarFilesMessage = "";
List<Path> matchingFiles = findMatchingFiles(true, response, path, null, null, true)
List<Path> matchingFiles = findMatchingFiles(true, response, path, null, null, true, listDirectories)
.collect(toList());
List<String> files = matchingFiles.stream()
.map(p -> CoDeveloperEngine.currentDir.relativize(p).toString())
.map(CoDeveloperEngine::canonicalName)
.filter(p -> p.contains("/" + filename))
.limit(5)
.collect(toList());
matchingFiles.stream()
.map(p -> CoDeveloperEngine.currentDir.relativize(p).toString())
.map(CoDeveloperEngine::canonicalName)
.map(p -> Pair.of(p, StringUtils.getFuzzyDistance(p, filename, Locale.getDefault())))
.map(p -> Pair.of(p.getLeft(), -p.getRight()))
.sorted(Comparator.comparingDouble(Pair::getRight))
Expand Down Expand Up @@ -243,10 +246,6 @@ protected String getBodyParameter(HttpServletResponse response, String json, Str
return parameterValue;
}

protected String mappedFilename(Path path) {
return CoDeveloperEngine.currentDir.relativize(path).toString();
}

protected String abbreviate(String s, int max) {
if (s == null || s.length() <= max) {
return s;
Expand Down
65 changes: 42 additions & 23 deletions src/main/java/net/stoerr/chatgpt/codevengine/CoDeveloperEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
Expand All @@ -23,9 +24,7 @@
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.apache.pdfbox.io.IOUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.FilterHolder;
Expand Down Expand Up @@ -58,8 +57,6 @@ public class CoDeveloperEngine {
*/
public static final Pattern OVERRIDE_IGNORE_PATTERN = Pattern.compile(".*/.github/.*|.*/.content.xml|(.*/)?\\.chatgpt.*.md|.*\\.htaccess");

// private static final Gson GSON = new Gson();

static Path currentDir = Paths.get(".").normalize().toAbsolutePath();

private static int port;
Expand Down Expand Up @@ -113,24 +110,21 @@ public class CoDeveloperEngine {
chain.doFilter(rawRequest, rawResponse);
};

private static final RequestLog requestlog = new RequestLog() {
@Override
public void log(Request request, Response response) {
TbUtils.logInfo("Remote address: " + request.getRemoteAddr());
TbUtils.logInfo("Remote host: " + request.getRemoteHost());
TbUtils.logInfo("Remote port: " + request.getRemotePort());
TbUtils.logInfo("Requestlog: " + request.getMethod() + " " + request.getRequestURL() + (request.getQueryString() != null && !request.getQueryString().isEmpty() ? "?" + request.getQueryString() : "") + " " + response.getStatus());
// list all request headers
for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements(); ) {
String header = e.nextElement();
TbUtils.logInfo("Request header: " + header + ": " + request.getHeader(header));
}
// list all response headers
for (String header : response.getHeaderNames()) {
TbUtils.logInfo("Response header: " + header + ": " + response.getHeader(header));
}
TbUtils.logInfo("");
private static final RequestLog requestlog = (request, response) -> {
TbUtils.logInfo("Remote address: " + request.getRemoteAddr());
TbUtils.logInfo("Remote host: " + request.getRemoteHost());
TbUtils.logInfo("Remote port: " + request.getRemotePort());
TbUtils.logInfo("Requestlog: " + request.getMethod() + " " + request.getRequestURL() + (request.getQueryString() != null && !request.getQueryString().isEmpty() ? "?" + request.getQueryString() : "") + " " + response.getStatus());
// list all request headers
for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements(); ) {
String header = e.nextElement();
TbUtils.logInfo("Request header: " + header + ": " + request.getHeader(header));
}
// list all response headers
for (String header : response.getHeaderNames()) {
TbUtils.logInfo("Response header: " + header + ": " + response.getHeader(header));
}
TbUtils.logInfo("");
};

private static void addHandler(AbstractPluginAction handler) {
Expand All @@ -148,6 +142,7 @@ protected static void initServlets() {
addHandler(new ListFilesAction());
addHandler(new ReadFileAction());
addHandler(new GrepAction());
addHandler(new ExecuteOpenAIToolCallAction(HANDLERS));
if (writingEnabled) {
addHandler(new WriteFileAction());
ExecuteExternalAction executeExternalAction = new ExecuteExternalAction();
Expand Down Expand Up @@ -228,9 +223,19 @@ private static String getMainUrl(HttpServletRequest request) {
}

public static void main(String[] args) throws Exception {
TbUtils.logVersion();
if (args.length == 1 && args[0].equals("--aitoolsdef")) {
// hidden option to output a description of the operations for use with my chatgpt script to stdout
try (InputStream in = CoDeveloperEngine.class.getResourceAsStream("/static/codeveloperengine-chatgptscript-toolsdefinition.json")) {
IOUtils.copy(in, System.out);
}
return;
}

parseOptions(args);
try {
parseOptions(args);
} finally {
TbUtils.logVersion();
}
server = new Server(new InetSocketAddress("127.0.0.1", port));
context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
Expand Down Expand Up @@ -262,6 +267,7 @@ private static void parseOptions(String[] args) {
options.addOption("h", "help", false, "Display this help message");
options.addOption("g", "globalconfigdir", true, "Directory for global configuration (default: ~/.cgptcodeveloperglobal/");
options.addOption("l", "local", false, "Only use local configuration via options - ignore any global configuration");
options.addOption("q", "quiet", false, "Suppress info level output");

CommandLineParser parser = new DefaultParser();

Expand Down Expand Up @@ -293,6 +299,10 @@ private static void parseOptions(String[] args) {
userGlobalConfigDir = null;
ignoreGlobalConfig = true;
}

if (cmd.hasOption("q")) {
TbUtils.setQuiet(true);
}
} catch (ParseException e) {
TbUtils.logError("Error parsing command line options: " + e);
System.exit(1);
Expand All @@ -307,4 +317,13 @@ public static void execute(Runnable runnable) {
server.getThreadPool().execute(runnable);
}

public static final String canonicalName(Path path) {
if (!path.toAbsolutePath().startsWith(currentDir.toAbsolutePath())) {
throw new IllegalArgumentException("Bug: trying to return file not in current dir - " + path);
}
String file = currentDir.relativize(path).toString();
file = file.isEmpty() ? "." : file;
return Files.isDirectory(path) ? file + "/" : file;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package net.stoerr.chatgpt.codevengine;

import static net.stoerr.chatgpt.codevengine.TbUtils.logInfo;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;

/**
* Executes an OpenAI tool call coming in as JSON - for usage outside ChatGPT.
*/
public class ExecuteOpenAIToolCallAction extends AbstractPluginAction {

private final Map<String, AbstractPluginAction> handlers;

public ExecuteOpenAIToolCallAction(Map<String, AbstractPluginAction> handlers) {
this.handlers = handlers;
}

@Override
public String getUrl() {
return "/executetool";
}

/**
* This is not registered in the yaml description since it's not a normal action, but rather
* distributes to actions.
*/
@Override
public String openApiDescription() {
return "";
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BufferedReader reader = req.getReader();
String json = reader.lines().collect(Collectors.joining(System.lineSeparator()));
logInfo("Received tool call:\n" + json);
String name = getBodyParameter(resp, json, "name", true);
String arguments = getBodyParameter(resp, json, "arguments", true);
Map<String, Object> parsedArguments = gson.fromJson(arguments, Map.class);
logInfo("Executing tool call: " + name + " " + parsedArguments);
Object requestBody = parsedArguments.get("requestBody");
String body = requestBody != null ? gson.toJson(requestBody) : null;
if (StringUtils.isNotBlank(body)) logInfo("Body: " + body);
AbstractPluginAction handler = handlers.get("/" + name);
if (null == handler) {
sendError(resp, HttpServletResponse.SC_BAD_REQUEST, "No handler for tool call: " + name);
return;
}
// call handler with a request that has parsedArguments as parameters and body as request body (JSON request)
HttpServletRequest requestWrapper = new HttpServletRequestWrapper(req) {
@Override
public String getParameter(String name) {
Object value = parsedArguments.get(name);
if (value == null) return null;
if (value instanceof String) return (String) value;
if (value instanceof Double) {
// check whether it's an integer
double d = (Double) value;
if (Math.abs(d - Math.round(d)) < 0.001) return "" + Math.round(d);
}
return String.valueOf(value);
}

@Override
public BufferedReader getReader() throws IOException {
return body != null ? new BufferedReader(new StringReader(body)) : null;
}

@Override
public String getMethod() {
return requestBody != null ? "POST" : "GET";
}
};
try {
handler.service(requestWrapper, resp);
} catch (ExecutionAbortedException e) {
// is already sufficiently handled. Just ignore.
} catch (ServletException | IOException | RuntimeException e) {
TbUtils.logError("Error executing tool call: " + name + "\n" + arguments);
TbUtils.logStacktrace(e);
throw e;
}
}

}
Loading

0 comments on commit 7e5ba2b

Please sign in to comment.