Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/main/java/io/github/jeddict/ai/agent/AbstractTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.github.jeddict.ai.agent;

import dev.langchain4j.exception.ToolExecutionException;
import io.github.jeddict.ai.lang.InteractionMode;
import io.github.jeddict.ai.lang.JeddictBrainListener;
import java.io.BufferedReader;
import java.io.File;
Expand Down Expand Up @@ -45,6 +46,8 @@ public abstract class AbstractTool {

private final List<JeddictBrainListener> listeners = new CopyOnWriteArrayList<>();

protected InteractionMode interaction = InteractionMode.ASK;

// TODO: add comment
private Optional<UnaryOperator<String>> humanInTheMiddle = Optional.empty();

Expand Down Expand Up @@ -87,7 +90,7 @@ public void checkPath(final String path) throws ToolExecutionException {
"trying to reach a file outside the project folder");
}
}

public Path fullPath(final String path) {
return basepath.resolve(path).normalize();
}
Expand Down Expand Up @@ -153,4 +156,12 @@ protected String runCommand(final String command, final String label) {
}
return fullLog.toString();
}

public void interaction(final InteractionMode interaction) {
this.interaction = interaction;
}

public InteractionMode interaction() {
return this.interaction;
}
}
10 changes: 6 additions & 4 deletions src/main/java/io/github/jeddict/ai/agent/FileSystemTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,13 @@ public String replaceFileContent(
* @param content optional content to write into the file
* @return a status message
*/
@Tool("Create a new file at the given path with optional content with no user interaction")
@Tool("Create a new binary file at the given path with optional content. Not for text files like code source, text, json, etc.")
@ToolPolicy(READWRITE)
public String createFile(
public String createBinaryFile(
@P("the pathname of the file to create")
final String path,
@P("the file content")
final String content
final byte[] content
) throws ToolExecutionException {
progress("📄 Creating file " + path);

Expand All @@ -402,7 +402,9 @@ public String createFile(
}

Files.createDirectories(filePath.getParent());
Files.writeString(filePath, content != null ? content : "");
if (content != null) {
Files.write(filePath, content);
}

progress("✅ File created: " + path);
return "File created";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.github.jeddict.ai.components.AssistantChat;
import io.github.jeddict.ai.components.diff.DiffPane;
import io.github.jeddict.ai.components.diff.DiffPaneController;
import io.github.jeddict.ai.lang.InteractionMode;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -67,6 +68,22 @@ public String editFile(
if (content == null) {
throw new ToolExecutionException("path can not be null");
}

//
// If the tool is invoched in iteraction modes different than INTERACTIVE,
// create the file right away with the createBinaryTool. The design is
// not great but it is what we can do given current Jeddict and langchain4j
// design.
//
if (interaction != InteractionMode.INTERACTIVE) {
try {
final FileSystemTools delegate = new FileSystemTools(basedir);
return delegate.createBinaryFile(path, content.getBytes());
} catch (IOException x) {
throw new ToolExecutionException("error in getting the content: " + x.getMessage());
}
}

progress("∆ Editing " + path);

checkPath(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
import io.github.jeddict.ai.components.CustomScrollBarUI;
import static io.github.jeddict.ai.components.MarkdownPane.getHtmlWrapWidth;
import io.github.jeddict.ai.lang.InteractionMode;
import static io.github.jeddict.ai.lang.InteractionMode.INTERACTIVE;
import io.github.jeddict.ai.lang.JeddictBrain;
import io.github.jeddict.ai.lang.JeddictBrainListener;
import io.github.jeddict.ai.response.TextBlock;
Expand Down Expand Up @@ -963,9 +962,7 @@ private List<AbstractTool> buildToolsList(
//
// Tools for interactive mode
//
if (mode == INTERACTIVE) {
toolsList.add(new InteractiveFileEditor(basedir, ac));
}
toolsList.add(new InteractiveFileEditor(basedir, ac));

//
// Tools commmon to both AGENT and INTERACTIVE mode
Expand Down Expand Up @@ -997,9 +994,13 @@ private List<AbstractTool> buildToolsList(
toolsList.add(new RefactoringTools(basedir));

//
// The handler wants to know about tool execution
// The handler wants to know about tool execution.
// The tools want to know about interaction mode
//
toolsList.forEach((tool) -> tool.addListener(listener));
toolsList.forEach((tool) -> {
tool.addListener(listener);
tool.interaction(mode);
});

return toolsList;
} catch (IOException x) {
Expand Down
11 changes: 11 additions & 0 deletions src/test/java/io/github/jeddict/ai/agent/AbstractToolTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.jeddict.ai.test.TestBase;
import io.github.jeddict.ai.test.DummyTool;
import io.github.jeddict.ai.lang.DummyJeddictBrainListener;
import io.github.jeddict.ai.lang.InteractionMode;
import java.io.IOException;
import java.util.function.UnaryOperator;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -130,4 +131,14 @@ public void checkPath_raises_exception_with_not_child_path() throws IOException
.hasMessageStartingWith("trying to reach a file");;
}

@Test
public void set_interactive_mode() throws IOException{
final DummyTool tool = new DummyTool(projectDir);

then(tool.interaction()).isEqualTo(InteractionMode.ASK); // default

tool.interaction(InteractionMode.INTERACTIVE);
then(tool.interaction()).isEqualTo(InteractionMode.INTERACTIVE);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,16 @@ public void searchInFile_outside_project_dir() {
}

@Test
public void createFile_with_and_without_existing_file() throws Exception {
public void createBinaryFile_with_and_without_existing_file() throws Exception {
final String path = "folder/newfile.txt";
final String content = "Sample content.";

then(tools.createFile(path, content)).isEqualTo("File created");
then(tools.createBinaryFile(path, content.getBytes())).isEqualTo("File created");

thenProgressContains(listener.collector.get(0), "\n📄 Creating file " + path);

listener.collector.clear();
thenThrownBy(() -> tools.createFile(path, content))
thenThrownBy(() -> tools.createBinaryFile(path, content.getBytes()))
.isInstanceOf(ToolExecutionException.class)
.hasMessage("❌ " + path + " already exists");

Expand All @@ -105,14 +105,14 @@ public void createFile_with_and_without_existing_file() throws Exception {
}

@Test
public void createFile_outside_project_dir() {
public void createBinaryFile_outside_project_dir() {
//
// absolute path
//
final String abs = HOME.resolve("jeddict-config.json").toAbsolutePath().toString();
final String content = "Sample content.";

thenTriedFileOutsideProjectFolder(() -> tools.createFile(abs, content));
thenTriedFileOutsideProjectFolder(() -> tools.createBinaryFile(abs, content.getBytes()));

//
// relative path
Expand All @@ -121,7 +121,7 @@ public void createFile_outside_project_dir() {

final String rel = projectDir + File.separator + "../outside.txt";

thenTriedFileOutsideProjectFolder(() -> tools.createFile(rel, content));
thenTriedFileOutsideProjectFolder(() -> tools.createBinaryFile(rel, content.getBytes()));

thenProgressContains(listener.collector.get(0), "\n📄 Creating file " + rel);
}
Expand Down Expand Up @@ -250,7 +250,7 @@ public void readFileLines_success_and_failure() throws Exception {
//
final String path = "folder/multiline.txt";
final String content = "line one\nline two\nline three\nline four\nline five";
tools.createFile(path, content);
tools.createBinaryFile(path, content.getBytes());
listener.collector.clear();

//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import dev.langchain4j.service.AiServices;
import io.github.jeddict.ai.agent.AbstractTool;
import io.github.jeddict.ai.agent.FileSystemTools;
import io.github.jeddict.ai.agent.InteractiveFileEditor;
import io.github.jeddict.ai.agent.UtilTools;
import static io.github.jeddict.ai.agent.pair.HackerWithToolsTest.GLOBAL_RULES;
import static io.github.jeddict.ai.agent.pair.HackerWithToolsTest.PROJECT_INFO;
Expand All @@ -52,10 +53,6 @@
*/
public class HackerWithoutToolsTest {

private static final String KEY_NAME = "name";
private static final String KEY_DESCRIPTION = "description";
private static final String KEY_ARGUMENTS = "arguments";

@TempDir
Path basedir;

Expand Down Expand Up @@ -384,9 +381,10 @@ public void send_error_on_tool_execution_exception() throws Exception {

@Test
public void create_calculator_application() throws Exception {
final FileSystemTools tools = new FileSystemTools(basedir.toAbsolutePath().toString());

final HackerWithoutTools hacker = new HackerWithoutTools(MODEL, BUILDER, List.of(tools));
final HackerWithoutTools hacker = new HackerWithoutTools(MODEL, BUILDER, List.of(
new FileSystemTools(basedir.toAbsolutePath().toString()),
new InteractiveFileEditor(basedir.toAbsolutePath().toString(), null)
));

hacker.hack("use mock 'create calculator.txt'");

Expand Down
6 changes: 3 additions & 3 deletions src/test/resources/mocks/create calculator.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Let's start by creating the necessary directories and files.
---

### Action 2: Create the Maven `pom.xml` file
```tool:createFile
```tool:editFile
"path": "calculator/pom.xml",
"content": "<?xml version=\"\\\"1.0\\\" encoding=\"\\\"UTF-8\\\"?>\\n<project xmlns=\"\\\"http://maven.apache.org/POM/4.0.0\\\"\\n xmlns:xsi=\"\\\"http://www.w3.org/2001/XMLSchema-instance\\\"\\n xsi:schemaLocation=\"\\\"http://maven.apache.org/POM/4.0.0\\\" \\\"http://maven.apache.org/xsd/maven-4.0.0.xsd\\\"\">\\n <modelVersion>4.0.0</modelVersion>\\n\\n <groupId>com.example</groupId>\\n <artifactId>calculator</artifactId>\\n <version>1.0-SNAPSHOT</version>\\n <packaging>jar</packaging>\\n <properties>\\n <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\\n <maven.compiler.source>1.8</maven.compiler.source>\\n <maven.compiler.target>1.8</maven.compiler.target>\\n </properties>\\n\\n <dependencies>\\n <!-- Swing for GUI -->\\n <dependency>\\n <groupId>javax.swing</groupId>\\n <artifactId>swing</artifactId>\\n <version>1.0</version>\\n </dependency>\\n </dependencies>\\n <build>\\n <plugins>\\n <plugin>\\n <groupId>org.apache.maven.plugins</groupId>\\n <artifactId>maven-compiler-plugin</artifactId>\\n <version>3.8.1</version>\\n <configuration>\\n <source>1.8</source>\\n <target>1.8</target>\\n </configuration>\\n </plugin>\\n </plugins>\\n </build>\\n</project>"
```
Expand All @@ -40,7 +40,7 @@ Let's start by creating the necessary directories and files.
---

### Action 5: Create the main calculator class `CalculatorApp.java`
```tool:createFile
```tool:editFile
"path": "calculator/src/main/java/com/example/calculator/CalculatorApp.java",
"content": "package com.example.calculator;\\n\\nimport javax.swing.*;\\nimport java.awt.*;\\nimport java.awt.event.ActionEvent;\\nimport java.awt.event.ActionListener;\\n\\npublic class CalculatorApp {\\n public static void main(String[] args) {\\n SwingUtilities.invokeLater(() -> {\\n CalculatorFrame frame = new CalculatorFrame();\\n frame.setVisible(true);\\n });\\n }\\n}\\n\\nclass CalculatorFrame extends JFrame {\\n private JTextField display;\\n private String currentInput = \\\"\\\";\\n private double firstNumber = 0;\\n private String operation = null;\\n\\n public CalculatorFrame() {\\n setTitle(\\\"Calculator\\\");\\n setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\\n setSize(300, 400);\\n setLayout(new BorderLayout());\\n\\n // Create display\\n display = new JTextField();\\n display.setEditable(false);\\n display.setHorizontalAlignment(JTextField.RIGHT);\\n display.setFont(new Font(\\\"Arial\\\", Font.PLAIN, 24));\\n add(display, BorderLayout.NORTH);\\n\\n // Create buttons panel\\n JPanel buttonPanel = new JPanel();\\n buttonPanel.setLayout(new GridLayout(5, 4, 5, 5));\\n\\n // Button labels\\n String[] buttonLabels = {\\n \\\"7\\\", \\\"8\\\", \\\"9\\\", \\\"/\\\",\\n \\\"4\\\", \\\"5\\\", \\\"6\\\", \\\"*\\\",\\n \\\"1\\\", \\\"2\\\", \\\"3\\\", \\\"-\\\",\\n \\\"0\\\", \\\".\\\", \\\"=\\\", \\\"+\\\",\\n \\\"C\\\", \\\"CE\\\", \\\"(\\\", \\\")\\\"\\n };\\n\\n // Add buttons\\n for (String label : buttonLabels) {\\n JButton button = new JButton(label);\\n button.addActionListener(new ButtonClickListener());\\n buttonPanel.add(button);\\n }\\n\\n add(buttonPanel, BorderLayout.CENTER);\\n }\\n\\n private class ButtonClickListener implements ActionListener {\\n @Override\\n public void actionPerformed(ActionEvent e) {\\n String command = e.getActionCommand();\\n\\n if (command.matches(\\\"[0-9]\\\")) {\\n currentInput += command;\\n display.setText(currentInput);\\n } else if (command.equals(\\\".\\\")) {\\n if (!currentInput.contains(\\\".\\\")) {\\n currentInput += command;\\n display.setText(currentInput);\\n }\\n } else if (command.equals(\\\"=\\\")) {\\n if (operation != null) {\\n double secondNumber = Double.parseDouble(currentInput);\\n switch (operation) {\\n case \\\"+\\\": firstNumber += secondNumber; break;\\n case \\\"-\\\": firstNumber -= secondNumber; break;\\n case \\\"*\\\": firstNumber *= secondNumber; break;\\n case \\\"/\\\": firstNumber /= secondNumber; break;\\n }\\n display.setText(String.valueOf(firstNumber));\\n operation = null;\\n currentInput = \\\"\\\";\\n }\\n } else if (command.equals(\\\"C\\\")) {\\n currentInput = \\\"\\\";\\n firstNumber = 0;\\n operation = null;\\n display.setText(currentInput);\\n } else if (command.equals(\\\"CE\\\")) {\\n currentInput = \\\"\\\";\\n display.setText(currentInput);\\n } else if (command.matches(\\\"[+\\\\-*/]\\\")) {\\n if (!currentInput.isEmpty()) {\\n firstNumber = Double.parseDouble(currentInput);\\n operation = command;\\n currentInput = \\\"\\\";\\n }\\n } else if (command.equals(\\\"(\\\") || command.equals(\\\")\\\")) {\\n currentInput += command;\\n display.setText(currentInput);\\n }\\n }\\n }\\n}"
```
Expand All @@ -55,7 +55,7 @@ Let's start by creating the necessary directories and files.
---

### Action 7: Create a `README.md` file for documentation
```tool:createFile
```tool:editFile
"path": "calculator/README.md",
"content": "# Calculator Application\\n\\nA simple Java Swing calculator application.\\n\\n## How to Run\\n\\n1. Navigate to the project directory:\\n ```bash\\n cd /tmp/calculator\\n ```\\n\\n2. Compile and run the application using Maven:\\n ```bash\\n mvn clean compile exec:java -Dexec.mainClass=\\\"com.example.calculator.CalculatorApp\\\"\\n ```\\n\\n## Features\\n- Basic arithmetic operations: +, -, *, /\\n- Clear (C) and Clear Entry (CE) functionality\\n- Parentheses support\\n"
```
Expand Down
Loading