-
Notifications
You must be signed in to change notification settings - Fork 13
Factor ExecTool out of FileSystemTools #1770
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /* | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * Copyright 2025 The Enola <https://enola.dev> Authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package dev.enola.ai.adk.tool; | ||
|
|
||
| import static dev.enola.common.SuccessOrError.error; | ||
| import static dev.enola.common.SuccessOrError.success; | ||
|
|
||
| import com.google.adk.tools.Annotations; | ||
| import com.google.adk.tools.BaseTool; | ||
| import com.google.adk.tools.FunctionTool; | ||
|
|
||
| import dev.enola.common.SuccessOrError; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.io.InputStreamReader; | ||
| import java.util.Map; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| public class ExecTool { | ||
|
|
||
| // TODO Make allowed commands configurable | ||
|
|
||
| // TODO Use stuff from package dev.enola.common.exec | ||
|
|
||
| public BaseTool createTool() { | ||
| return FunctionTool.create(this, "executeCommand"); | ||
| } | ||
|
|
||
| @Annotations.Schema( | ||
| description = | ||
| "Executes a shell command and captures its standard output and standard error.") | ||
| public Map<String, String> executeCommand( | ||
| @Annotations.Schema(description = "The command to execute (e.g., 'ls -l').") | ||
| String command) { | ||
| return Tools.toMap(executeCommandHelper(command)); | ||
| } | ||
|
|
||
| private SuccessOrError<String> executeCommandHelper(String command) { | ||
| try { | ||
| ProcessBuilder pb = new ProcessBuilder("bash", "-c", command); | ||
| pb.redirectErrorStream(true); // Combine stdout and stderr | ||
| Process process = pb.start(); | ||
|
|
||
| StringBuilder output = new StringBuilder(); | ||
| try (BufferedReader reader = | ||
| new BufferedReader(new InputStreamReader(process.getInputStream()))) { | ||
| String line; | ||
| while ((line = reader.readLine()) != null) { | ||
| output.append(line).append("\n"); | ||
| } | ||
| } | ||
|
|
||
| if (!process.waitFor(10, TimeUnit.SECONDS)) { | ||
| process.destroyForcibly(); | ||
| return error("Command timed out after 10 seconds."); | ||
| } | ||
|
|
||
| int exitCode = process.exitValue(); | ||
| return success( | ||
| String.format( | ||
| "Exit Code: %d%nOutput:%n%s", exitCode, output.toString().trim())); | ||
| } catch (IOException | InterruptedException e) { | ||
| return error("Failed to execute command: " + e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -27,9 +27,7 @@ | |||||
|
|
||||||
| import dev.enola.common.SuccessOrError; | ||||||
|
|
||||||
| import java.io.BufferedReader; | ||||||
| import java.io.IOException; | ||||||
| import java.io.InputStreamReader; | ||||||
| import java.nio.file.*; | ||||||
| import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | ||||||
|
|
@@ -41,13 +39,14 @@ | |||||
| import java.util.Arrays; | ||||||
| import java.util.List; | ||||||
| import java.util.Map; | ||||||
| import java.util.concurrent.TimeUnit; | ||||||
| import java.util.regex.Pattern; | ||||||
| import java.util.stream.Collectors; | ||||||
| import java.util.stream.Stream; | ||||||
|
|
||||||
| public class FileSystemTools { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
|
|
||||||
| // TODO Enforce root(s); see "root" related TODO in McpLoader | ||||||
|
|
||||||
| public Map<String, BaseTool> createToolSet() { | ||||||
| return ImmutableMap.of( | ||||||
| "read_file", FunctionTool.create(this, "readFile"), | ||||||
|
|
@@ -56,8 +55,7 @@ public Map<String, BaseTool> createToolSet() { | |||||
| "search_files", FunctionTool.create(this, "searchFiles"), | ||||||
| "list_directory", FunctionTool.create(this, "listDirectory"), | ||||||
| "create_directory", FunctionTool.create(this, "createDirectory"), | ||||||
| "grep_file", FunctionTool.create(this, "grepFile"), | ||||||
| "execute_command", FunctionTool.create(this, "executeCommand")); | ||||||
| "grep_file", FunctionTool.create(this, "grepFile")); | ||||||
| } | ||||||
|
|
||||||
| @Schema(description = "Reads the entire content of a specified file.") | ||||||
|
|
@@ -121,14 +119,6 @@ public Map<String, String> grepFile( | |||||
| return Tools.toMap(grepFileHelper(path, pattern, context)); | ||||||
| } | ||||||
|
|
||||||
| @Schema( | ||||||
| description = | ||||||
| "Executes a shell command and captures its standard output and standard error.") | ||||||
| public Map<String, String> executeCommand( | ||||||
| @Schema(description = "The command to execute (e.g., 'ls -l').") String command) { | ||||||
| return Tools.toMap(executeCommandHelper(command)); | ||||||
| } | ||||||
|
|
||||||
| // Private Helper Methods | ||||||
| private SuccessOrError<String> readFileHelper(String pathString) { | ||||||
| try { | ||||||
|
|
@@ -275,35 +265,6 @@ private SuccessOrError<String> grepFileHelper(String pathString, String pattern, | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| private SuccessOrError<String> executeCommandHelper(String command) { | ||||||
| try { | ||||||
| ProcessBuilder pb = new ProcessBuilder("bash", "-c", command); | ||||||
| pb.redirectErrorStream(true); // Combine stdout and stderr | ||||||
| Process process = pb.start(); | ||||||
|
|
||||||
| StringBuilder output = new StringBuilder(); | ||||||
| try (BufferedReader reader = | ||||||
| new BufferedReader(new InputStreamReader(process.getInputStream()))) { | ||||||
| String line; | ||||||
| while ((line = reader.readLine()) != null) { | ||||||
| output.append(line).append("\n"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (!process.waitFor(10, TimeUnit.SECONDS)) { | ||||||
| process.destroy(); | ||||||
| return error("Command timed out after 10 seconds."); | ||||||
| } | ||||||
|
|
||||||
| int exitCode = process.exitValue(); | ||||||
| return success( | ||||||
| String.format( | ||||||
| "Exit Code: %d%nOutput:%n%s", exitCode, output.toString().trim())); | ||||||
| } catch (IOException | InterruptedException e) { | ||||||
| return error("Failed to execute command: " + e.getMessage()); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private static int sortPaths(Path a, Path b) { | ||||||
| boolean isDirectoryA = Files.isDirectory(a); | ||||||
| boolean isDirectoryB = Files.isDirectory(b); | ||||||
|
|
||||||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,10 @@ | ||
| $schema: https://enola.dev/ai/agent | ||
| model: google://?model=gemini-2.5-flash-lite | ||
| model: google://?model=gemini-2.5-flash | ||
| tools: | ||
| - modelcontextprotocol/filesystem | ||
| - read_file | ||
| - write_file | ||
| - list_directory | ||
| - edit_file | ||
| - search_files | ||
| - create_directory | ||
| - grep_file |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| $schema: https://enola.dev/ai/agent | ||
| model: google://?model=gemini-2.5-flash | ||
| # TODO prompt | ||
| instruction: | ||
| Generate a system summary using various Linux commands and format the output as a poem. | ||
| For memory, do include both total as well as used, including buff/cache. | ||
| For uptime, run it without any options, because not all versions support. | ||
| # TODO Limit allowed commands to e.g. only "uname -a", "df -h /", "free -m", "uptime" (or whatever) | ||
| tools: | ||
| - exec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The command execution timeout is hardcoded to 10 seconds. This might not be suitable for all commands; some may need more time, while for others this might be too long. It would be more flexible to make this timeout configurable, for example by passing it to the
ExecToolconstructor.