Skip to content

Commit 04edd28

Browse files
- Made the InteractiveFileEditor the only editor that can create and edit text files
- Made the only not interactive editor specif for binary files, so it won't be picked for source files - Added more logic in InteractiveFileEditor so that if the mode is not interactive, it false back to createBinaryFile
1 parent 1604985 commit 04edd28

8 files changed

Lines changed: 68 additions & 28 deletions

File tree

src/main/java/io/github/jeddict/ai/agent/AbstractTool.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.github.jeddict.ai.agent;
1717

1818
import dev.langchain4j.exception.ToolExecutionException;
19+
import io.github.jeddict.ai.lang.InteractionMode;
1920
import io.github.jeddict.ai.lang.JeddictBrainListener;
2021
import java.io.BufferedReader;
2122
import java.io.File;
@@ -45,6 +46,8 @@ public abstract class AbstractTool {
4546

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

49+
protected InteractionMode interaction = InteractionMode.ASK;
50+
4851
// TODO: add comment
4952
private Optional<UnaryOperator<String>> humanInTheMiddle = Optional.empty();
5053

@@ -87,7 +90,7 @@ public void checkPath(final String path) throws ToolExecutionException {
8790
"trying to reach a file outside the project folder");
8891
}
8992
}
90-
93+
9194
public Path fullPath(final String path) {
9295
return basepath.resolve(path).normalize();
9396
}
@@ -153,4 +156,12 @@ protected String runCommand(final String command, final String label) {
153156
}
154157
return fullLog.toString();
155158
}
159+
160+
public void interaction(final InteractionMode interaction) {
161+
this.interaction = interaction;
162+
}
163+
164+
public InteractionMode interaction() {
165+
return this.interaction;
166+
}
156167
}

src/main/java/io/github/jeddict/ai/agent/FileSystemTools.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,13 +381,13 @@ public String replaceFileContent(
381381
* @param content optional content to write into the file
382382
* @return a status message
383383
*/
384-
@Tool("Create a new file at the given path with optional content with no user interaction")
384+
@Tool("Create a new binary file at the given path with optional content. Not for text files like code source, text, json, etc.")
385385
@ToolPolicy(READWRITE)
386-
public String createFile(
386+
public String createBinaryFile(
387387
@P("the pathname of the file to create")
388388
final String path,
389389
@P("the file content")
390-
final String content
390+
final byte[] content
391391
) throws ToolExecutionException {
392392
progress("📄 Creating file " + path);
393393

@@ -402,7 +402,9 @@ public String createFile(
402402
}
403403

404404
Files.createDirectories(filePath.getParent());
405-
Files.writeString(filePath, content != null ? content : "");
405+
if (content != null) {
406+
Files.write(filePath, content);
407+
}
406408

407409
progress("✅ File created: " + path);
408410
return "File created";

src/main/java/io/github/jeddict/ai/agent/InteractiveFileEditor.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.github.jeddict.ai.components.AssistantChat;
2323
import io.github.jeddict.ai.components.diff.DiffPane;
2424
import io.github.jeddict.ai.components.diff.DiffPaneController;
25+
import io.github.jeddict.ai.lang.InteractionMode;
2526
import java.io.IOException;
2627
import java.util.concurrent.CountDownLatch;
2728
import java.util.concurrent.atomic.AtomicBoolean;
@@ -67,6 +68,22 @@ public String editFile(
6768
if (content == null) {
6869
throw new ToolExecutionException("path can not be null");
6970
}
71+
72+
//
73+
// If the tool is invoched in iteraction modes different than INTERACTIVE,
74+
// create the file right away with the createBinaryTool. The design is
75+
// not great but it is what we can do given current Jeddict and langchain4j
76+
// design.
77+
//
78+
if (interaction != InteractionMode.INTERACTIVE) {
79+
try {
80+
final FileSystemTools delegate = new FileSystemTools(basedir);
81+
return delegate.createBinaryFile(path, content.getBytes());
82+
} catch (IOException x) {
83+
throw new ToolExecutionException("error in getting the content: " + x.getMessage());
84+
}
85+
}
86+
7087
progress("∆ Editing " + path);
7188

7289
checkPath(path);

src/main/java/io/github/jeddict/ai/hints/AssistantChatManager.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
import io.github.jeddict.ai.components.CustomScrollBarUI;
5454
import static io.github.jeddict.ai.components.MarkdownPane.getHtmlWrapWidth;
5555
import io.github.jeddict.ai.lang.InteractionMode;
56-
import static io.github.jeddict.ai.lang.InteractionMode.INTERACTIVE;
5756
import io.github.jeddict.ai.lang.JeddictBrain;
5857
import io.github.jeddict.ai.lang.JeddictBrainListener;
5958
import io.github.jeddict.ai.response.TextBlock;
@@ -963,9 +962,7 @@ private List<AbstractTool> buildToolsList(
963962
//
964963
// Tools for interactive mode
965964
//
966-
if (mode == INTERACTIVE) {
967-
toolsList.add(new InteractiveFileEditor(basedir, ac));
968-
}
965+
toolsList.add(new InteractiveFileEditor(basedir, ac));
969966

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

999996
//
1000-
// The handler wants to know about tool execution
997+
// The handler wants to know about tool execution.
998+
// The tools want to know about interaction mode
1001999
//
1002-
toolsList.forEach((tool) -> tool.addListener(listener));
1000+
toolsList.forEach((tool) -> {
1001+
tool.addListener(listener);
1002+
tool.interaction(mode);
1003+
});
10031004

10041005
return toolsList;
10051006
} catch (IOException x) {

src/test/java/io/github/jeddict/ai/agent/AbstractToolTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.github.jeddict.ai.test.TestBase;
55
import io.github.jeddict.ai.test.DummyTool;
66
import io.github.jeddict.ai.lang.DummyJeddictBrainListener;
7+
import io.github.jeddict.ai.lang.InteractionMode;
78
import java.io.IOException;
89
import java.util.function.UnaryOperator;
910
import org.junit.jupiter.api.Test;
@@ -130,4 +131,14 @@ public void checkPath_raises_exception_with_not_child_path() throws IOException
130131
.hasMessageStartingWith("trying to reach a file");;
131132
}
132133

134+
@Test
135+
public void set_interactive_mode() throws IOException{
136+
final DummyTool tool = new DummyTool(projectDir);
137+
138+
then(tool.interaction()).isEqualTo(InteractionMode.ASK); // default
139+
140+
tool.interaction(InteractionMode.INTERACTIVE);
141+
then(tool.interaction()).isEqualTo(InteractionMode.INTERACTIVE);
142+
}
143+
133144
}

src/test/java/io/github/jeddict/ai/agent/FileSystemToolsTest.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,16 @@ public void searchInFile_outside_project_dir() {
8787
}
8888

8989
@Test
90-
public void createFile_with_and_without_existing_file() throws Exception {
90+
public void createBinaryFile_with_and_without_existing_file() throws Exception {
9191
final String path = "folder/newfile.txt";
9292
final String content = "Sample content.";
9393

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

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

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

@@ -105,14 +105,14 @@ public void createFile_with_and_without_existing_file() throws Exception {
105105
}
106106

107107
@Test
108-
public void createFile_outside_project_dir() {
108+
public void createBinaryFile_outside_project_dir() {
109109
//
110110
// absolute path
111111
//
112112
final String abs = HOME.resolve("jeddict-config.json").toAbsolutePath().toString();
113113
final String content = "Sample content.";
114114

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

117117
//
118118
// relative path
@@ -121,7 +121,7 @@ public void createFile_outside_project_dir() {
121121

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

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

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

256256
//

src/test/java/io/github/jeddict/ai/agent/pair/HackerWithoutToolsTest.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import dev.langchain4j.service.AiServices;
2727
import io.github.jeddict.ai.agent.AbstractTool;
2828
import io.github.jeddict.ai.agent.FileSystemTools;
29+
import io.github.jeddict.ai.agent.InteractiveFileEditor;
2930
import io.github.jeddict.ai.agent.UtilTools;
3031
import static io.github.jeddict.ai.agent.pair.HackerWithToolsTest.GLOBAL_RULES;
3132
import static io.github.jeddict.ai.agent.pair.HackerWithToolsTest.PROJECT_INFO;
@@ -52,10 +53,6 @@
5253
*/
5354
public class HackerWithoutToolsTest {
5455

55-
private static final String KEY_NAME = "name";
56-
private static final String KEY_DESCRIPTION = "description";
57-
private static final String KEY_ARGUMENTS = "arguments";
58-
5956
@TempDir
6057
Path basedir;
6158

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

385382
@Test
386383
public void create_calculator_application() throws Exception {
387-
final FileSystemTools tools = new FileSystemTools(basedir.toAbsolutePath().toString());
388-
389-
final HackerWithoutTools hacker = new HackerWithoutTools(MODEL, BUILDER, List.of(tools));
384+
final HackerWithoutTools hacker = new HackerWithoutTools(MODEL, BUILDER, List.of(
385+
new FileSystemTools(basedir.toAbsolutePath().toString()),
386+
new InteractiveFileEditor(basedir.toAbsolutePath().toString(), null)
387+
));
390388

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

src/test/resources/mocks/create calculator.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Let's start by creating the necessary directories and files.
1818
---
1919

2020
### Action 2: Create the Maven `pom.xml` file
21-
```tool:createFile
21+
```tool:editFile
2222
"path": "calculator/pom.xml",
2323
"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>"
2424
```
@@ -40,7 +40,7 @@ Let's start by creating the necessary directories and files.
4040
---
4141

4242
### Action 5: Create the main calculator class `CalculatorApp.java`
43-
```tool:createFile
43+
```tool:editFile
4444
"path": "calculator/src/main/java/com/example/calculator/CalculatorApp.java",
4545
"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}"
4646
```
@@ -55,7 +55,7 @@ Let's start by creating the necessary directories and files.
5555
---
5656

5757
### Action 7: Create a `README.md` file for documentation
58-
```tool:createFile
58+
```tool:editFile
5959
"path": "calculator/README.md",
6060
"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"
6161
```

0 commit comments

Comments
 (0)