Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add CodeAction and QuestionClassifyNode #306

Open
wants to merge 10 commits into
base: workflow
Choose a base branch
from
31 changes: 26 additions & 5 deletions spring-ai-alibaba-graph/spring-ai-alibaba-graph-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
<description>A library for building stateful, multi-agents applications with LLMs</description>


<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<commons-exec.version>1.3</commons-exec.version>
<commons-codec.version>1.16.0</commons-codec.version>
<commons-collections4.version>4.4</commons-collections4.version>
</properties>

<dependencies>
<dependency>
Expand Down Expand Up @@ -123,6 +126,24 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons-collections4.version}</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>${commons-exec.version}</version>
</dependency>

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>


</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.alibaba.cloud.ai.graph.node.code;

import com.alibaba.cloud.ai.graph.node.code.entity.CodeBlock;
import com.alibaba.cloud.ai.graph.node.code.entity.CodeExecutionConfig;
import com.alibaba.cloud.ai.graph.node.code.entity.CodeExecutionResult;

import java.util.List;

/**
* @author HeYQ
* @since 2024-12-02 17:15
*/

public interface CodeExecutor {

/**
* Execute code blocks and return the result. This method should be implemented by the
* code executor.
* @param codeBlockList The code blocks to execute.
* @param codeExecutionConfig The configuration of the code execution.
* @return CodeExecutionResult The result of the code execution.
* @throws Exception ValueError: Errors in user inputs
*/

CodeExecutionResult executeCodeBlocks(List<CodeBlock> codeBlockList, CodeExecutionConfig codeExecutionConfig)
throws Exception;

/**
* Restart the code executor. This method should be implemented by the code executor.
* This method is called when the agent is reset.
*/
void restart();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.alibaba.cloud.ai.graph.node.code;

import com.alibaba.cloud.ai.graph.action.NodeAction;
import com.alibaba.cloud.ai.graph.node.AbstractNode;
import com.alibaba.cloud.ai.graph.node.code.entity.CodeBlock;
import com.alibaba.cloud.ai.graph.node.code.entity.CodeExecutionConfig;
import com.alibaba.cloud.ai.graph.node.code.entity.CodeExecutionResult;
import com.alibaba.cloud.ai.graph.state.NodeState;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* @author HeYQ
* @since 2024-11-28 11:47
*/

public class CodeExecutorNodeAction extends AbstractNode implements NodeAction {

private final CodeExecutor codeExecutor;

private final String codeLanguage;

private final String code;

private final CodeExecutionConfig codeExecutionConfig;

public CodeExecutorNodeAction(CodeExecutor codeExecutor, String codeLanguage, String code,
CodeExecutionConfig config) {
this.codeExecutor = codeExecutor;
this.codeLanguage = codeLanguage;
this.code = code;
this.codeExecutionConfig = config;
}

@Override
public Map<String, Object> apply(NodeState state) throws Exception {
List<CodeBlock> codeBlockList = new ArrayList<>(10);
codeBlockList.add(new CodeBlock(codeLanguage, code));
CodeExecutionResult codeExecutionResult = codeExecutor.executeCodeBlocks(codeBlockList,
this.codeExecutionConfig);
if (codeExecutionResult.exitCode() != 0) {
throw new RuntimeException("code execution failed, exit code: " + codeExecutionResult.exitCode()
+ ", logs: " + codeExecutionResult.logs());
}
return JSONObject.parseObject(codeExecutionResult.logs(), new TypeReference<Map<String, Object>>() {
});
}

public static Builder builder() {
return new Builder();
}

public static class Builder {

private CodeExecutor codeExecutor;

private String codeLanguage;

private String code;

private CodeExecutionConfig config;

public Builder() {
}

public Builder codeExecutor(CodeExecutor codeExecutor) {
this.codeExecutor = codeExecutor;
return this;
}

public Builder codeLanguage(String codeLanguage) {
this.codeLanguage = codeLanguage;
return this;
}

public Builder code(String code) {
this.code = code;
return this;
}

public Builder config(CodeExecutionConfig config) {
this.config = config;
return this;
}

public CodeExecutorNodeAction build() {
return new CodeExecutorNodeAction(codeExecutor, codeLanguage, code, config);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.alibaba.cloud.ai.graph.node.code;

import com.alibaba.cloud.ai.graph.node.code.entity.CodeBlock;
import com.alibaba.cloud.ai.graph.node.code.entity.CodeExecutionConfig;
import com.alibaba.cloud.ai.graph.node.code.entity.CodeExecutionResult;
import com.alibaba.cloud.ai.graph.utils.FileUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
* @author HeYQ
* @since 2024-12-02 17:23
*/

public class LocalCommandlineCodeExecutor implements CodeExecutor {

private static final Logger logger = LoggerFactory.getLogger(LocalCommandlineCodeExecutor.class);

@Override
public CodeExecutionResult executeCodeBlocks(List<CodeBlock> codeBlockList, CodeExecutionConfig codeExecutionConfig)
throws Exception {
StringBuilder allLogs = new StringBuilder();
CodeExecutionResult result;
for (int i = 0; i < codeBlockList.size(); i++) {
CodeBlock codeBlock = codeBlockList.get(i);
String language = codeBlock.language();
String code = codeBlock.code();
logger.info("\n>>>>>>>> EXECUTING CODE BLOCK {} (inferred language is {})...", i + 1, language);

if (Set.of("bash", "shell", "sh", "python").contains(language.toLowerCase())) {
result = executeCode(language, code, codeExecutionConfig);
}
else {
// the language is not supported, then return an error message.
result = new CodeExecutionResult(1, "unknown language " + language);
}

allLogs.append("\n").append(result.logs());
if (result.exitCode() != 0) {
return new CodeExecutionResult(result.exitCode(), allLogs.toString());
}
}
return new CodeExecutionResult(0, allLogs.toString());
}

@Override
public void restart() {

logger.warn("Restarting local command line code executor is not supported. No action is taken.");
}

public CodeExecutionResult executeCode(String language, String code, CodeExecutionConfig config) throws Exception {
if (Objects.isNull(language) || Objects.isNull(code)) {
throw new Exception("Either language or code must be provided.");
}
String workDir = config.getWorkDir();
String codeHash = DigestUtils.md5Hex(code);
String fileExt = language.startsWith("python") ? "py" : language;
String filename = String.format("tmp_code_%s.%s", codeHash, fileExt);

// write the code string to a file specified by the filename.
FileUtils.writeCodeToFile(workDir, filename, code);

CodeExecutionResult executionResult = executeCodeLocally(language, workDir, filename, config.getTimeout());

FileUtils.deleteFile(workDir, filename);
return executionResult;
}

private CodeExecutionResult executeCodeLocally(String language, String workDir, String filename, int timeout)
throws Exception {
// set up the command based on language
String executable = getExecutableForLanguage(language);
CommandLine commandLine = new CommandLine(executable);
commandLine.addArgument(filename);

// set up the execution environment
DefaultExecutor executor = new DefaultExecutor();
executor.setWorkingDirectory(new File(workDir));
executor.setExitValue(0);

// set up the streams for the output of the subprocess
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
executor.setStreamHandler(streamHandler);

// set up a watchdog to terminate the process if it exceeds the timeout
ExecuteWatchdog watchdog = new ExecuteWatchdog(TimeUnit.SECONDS.toMillis(timeout));
executor.setWatchdog(watchdog);

try {
// execute the command
executor.execute(commandLine);
// process completed before the watchdog terminated it
String output = outputStream.toString();
return new CodeExecutionResult(0, output.trim());
}
catch (ExecuteException e) {
// process finished with an exit value (possibly non-zero)
String errorOutput = errorStream.toString().replace(Path.of(workDir).toAbsolutePath() + File.separator, "");

return new CodeExecutionResult(e.getExitValue(), errorOutput.trim());
}
catch (IOException e) {
// returns a special result if the process was killed by the watchdog
throw new Exception("Error executing code.", e);
}
}

private String getExecutableForLanguage(String language) throws Exception {
return switch (language) {
case "python" -> language;
case "shell", "bash", "sh", "powershell" -> "sh";
default -> throw new Exception("Language not recognized in code execution:" + language);
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.alibaba.cloud.ai.graph.node.code.entity;

/**
* @author HeYQ
* @since 0.0.1
*/
public record CodeBlock(String language, String code) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.alibaba.cloud.ai.graph.node.code.entity;

import lombok.Builder;
import lombok.Data;

/**
* Config for the code execution.
*
* @author HeYQ
* @since 0.0.1
*/
@Data
@Builder
public class CodeExecutionConfig {

/**
* the working directory for the code execution.
*/
@Builder.Default
private String workDir = "extensions";

/**
* the docker image to use for code execution.
*/
private String docker;

/**
* the maximum execution time in seconds.
*/
@Builder.Default
private int timeout = 600;

/**
* the number of messages to look back for code execution. default value is 1, and -1
* indicates auto mode.
*/
@Builder.Default
private int lastMessagesNumber = 1;

@Builder.Default
private int codeMaxDepth = 5;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.alibaba.cloud.ai.graph.node.code.entity;

/**
* Represents the result of code execution.
*
* @param exitCode 0 if the code executes successfully.
* @param logs the error message if the code fails to execute, the stdout otherwise.
* @param extra commandLine code_file or the docker image name after container run when
* docker is used.
* @author HeYQ
* @since 0.0.1
*/
public record CodeExecutionResult(int exitCode, String logs, String extra) {

public CodeExecutionResult(int exitCode, String logs) {
this(exitCode, logs, null);
}
}
Loading