Skip to content

Commit f510673

Browse files
feat✨: add new tools for CLI plotting, codebase search, file search, grep, and directory listing
1 parent 2f79ef7 commit f510673

File tree

5 files changed

+359
-0
lines changed

5 files changed

+359
-0
lines changed

agent_loop/tools/cli_plot.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import plotext as plt
2+
import time
3+
4+
tool_definition = {
5+
"name": "cli_plot",
6+
"description": (
7+
"This is the default plotting and charting tool."
8+
"Render advanced terminal charts using plotext. Use this to visualize data in the terminal, for charts, graphs, plots, etc. that have not to be saved to a file."
9+
"Supports line, scatter, bar, histogram, datetime, log-scales, subplots, styling, and streaming."
10+
),
11+
"input_schema": {
12+
"type": "object",
13+
"properties": {
14+
"data": {
15+
"type": "array",
16+
"items": {
17+
"type": "object",
18+
"properties": {
19+
"x": {"type": "array", "items": {"type": ["number", "string"]}},
20+
"y": {"type": "array", "items": {"type": "number"}},
21+
"label": {"type": "string"},
22+
"type": {
23+
"type": "string",
24+
"enum": ["line", "scatter", "bar", "hist"],
25+
},
26+
"yaxis": {"type": "string", "enum": ["left", "right"]},
27+
"color": {"type": "string"},
28+
"marker": {"type": "string"},
29+
"fill": {"type": "boolean"},
30+
},
31+
"required": ["y", "type"],
32+
},
33+
"description": "List of series to plot.",
34+
},
35+
"style": {
36+
"type": "object",
37+
"properties": {
38+
"width": {"type": "integer"},
39+
"height": {"type": "integer"},
40+
"title": {"type": "string"},
41+
"xlabel": {"type": "string"},
42+
"ylabel": {"type": "array", "items": {"type": "string"}},
43+
"grid": {"type": "array", "items": {"type": "boolean"}},
44+
"xscale": {"type": "string"},
45+
"yscale": {"type": "string"},
46+
},
47+
"description": "Styling options for the plot.",
48+
},
49+
"stream": {
50+
"type": "object",
51+
"properties": {
52+
"frames": {"type": "integer"},
53+
"interval": {"type": "number"},
54+
"clear_terminal": {"type": "boolean"},
55+
},
56+
"description": "Streaming mode configuration.",
57+
},
58+
},
59+
"required": ["data"],
60+
},
61+
}
62+
63+
64+
def handle_call(input_data):
65+
try:
66+
plt.clear_figure()
67+
data_series = input_data["data"]
68+
style = input_data.get("style", {})
69+
stream_cfg = input_data.get("stream")
70+
71+
# Styling
72+
if style.get("width") and style.get("height"):
73+
plt.plotsize(style["width"], style["height"])
74+
if style.get("title"):
75+
plt.title(style["title"])
76+
if style.get("xlabel"):
77+
plt.xlabel(style["xlabel"])
78+
if style.get("ylabel"):
79+
ylabels = style["ylabel"]
80+
plt.ylabel(ylabels[0], *(ylabels[1:] if len(ylabels) > 1 else []))
81+
if style.get("grid"):
82+
plt.grid(*style["grid"])
83+
if style.get("xscale"):
84+
plt.xscale(style["xscale"])
85+
if style.get("yscale"):
86+
plt.yscale(style["yscale"])
87+
88+
# Plot data
89+
for series in data_series:
90+
func = {
91+
"line": plt.plot,
92+
"scatter": plt.scatter,
93+
"bar": plt.bar,
94+
"hist": plt.hist,
95+
}[series["type"]]
96+
97+
args = []
98+
if "x" in series:
99+
args.append(series["x"])
100+
args.append(series["y"])
101+
102+
kwargs = {}
103+
for opt in ("label", "yaxis", "color", "marker"):
104+
if series.get(opt):
105+
kwargs[opt] = series[opt]
106+
if series.get("fill"):
107+
kwargs["fillx" if series["type"] in ("line", "scatter") else "fill"] = (
108+
True
109+
)
110+
111+
func(*args, **kwargs)
112+
113+
# Streaming mode
114+
if stream_cfg:
115+
frames = stream_cfg.get("frames", 1)
116+
interval = stream_cfg.get("interval", 0.1)
117+
clear_term = stream_cfg.get("clear_terminal", True)
118+
119+
for _ in range(frames):
120+
if clear_term:
121+
plt.clear_terminal()
122+
plt.show()
123+
time.sleep(interval)
124+
plt.clear_data()
125+
else:
126+
plt.show()
127+
128+
return {"status": "Rendered successfully."}
129+
130+
except Exception as e:
131+
return {"error": f"Plotting failed: {e}"}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import subprocess
2+
3+
tool_definition = {
4+
"name": "codebase_search",
5+
"description": (
6+
"Find snippets of code from the codebase most relevant to the search query. "
7+
"This is a semantic search tool, so the query should ask for something semantically matching what is needed. "
8+
"Use target_directories to scope the search. Reuse the user's exact query unless there's a clear reason not to."
9+
),
10+
"input_schema": {
11+
"type": "object",
12+
"properties": {
13+
"query": {
14+
"type": "string",
15+
"description": "The search query to find relevant code. Reuse the user's exact wording.",
16+
},
17+
"target_directories": {
18+
"type": "array",
19+
"items": {"type": "string"},
20+
"description": "Glob patterns for directories to search over.",
21+
},
22+
"explanation": {
23+
"type": "string",
24+
"description": "Why the agent is performing this semantic search and how it helps.",
25+
},
26+
},
27+
"required": ["query"],
28+
},
29+
}
30+
31+
32+
def handle_call(input_data):
33+
query = input_data["query"]
34+
target_dirs = input_data.get("target_directories", ["."])
35+
36+
try:
37+
args = [
38+
"semantic-code-search",
39+
"--query",
40+
query,
41+
]
42+
43+
for directory in target_dirs:
44+
args.extend(["--dir", directory])
45+
46+
result = subprocess.run(args, capture_output=True, text=True, timeout=20)
47+
48+
if result.returncode != 0:
49+
return {"error": result.stderr.strip(), "exit_code": result.returncode}
50+
51+
return {
52+
"query": query,
53+
"output": result.stdout.strip(),
54+
"exit_code": result.returncode,
55+
}
56+
57+
except Exception as e:
58+
return {"error": f"Execution error: {e}"}

agent_loop/tools/file_search.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import subprocess
2+
3+
tool_definition = {
4+
"name": "file_search",
5+
"description": (
6+
"Fast file search based on fuzzy matching against file path. "
7+
"Use if you know part of the file path but don't know where it's located exactly. "
8+
"Response will be capped to 10 results. Make your query more specific if need to filter results further."
9+
),
10+
"input_schema": {
11+
"type": "object",
12+
"properties": {
13+
"query": {"type": "string", "description": "Fuzzy filename to search for"},
14+
"explanation": {
15+
"type": "string",
16+
"description": "Why the agent is performing this file search and how it helps.",
17+
},
18+
},
19+
"required": ["query", "explanation"],
20+
},
21+
}
22+
23+
24+
def handle_call(input_data):
25+
query = input_data["query"]
26+
27+
try:
28+
result = subprocess.run(
29+
["fdfind", query], capture_output=True, text=True, timeout=10
30+
)
31+
32+
if result.returncode not in [0, 1]: # 0: found, 1: not found
33+
return {"error": result.stderr.strip(), "exit_code": result.returncode}
34+
35+
matches = result.stdout.strip().splitlines()[:10]
36+
37+
return {"query": query, "matches": matches, "exit_code": result.returncode}
38+
39+
except Exception as e:
40+
return {"error": f"Execution error: {e}"}

agent_loop/tools/grep.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import subprocess
2+
import shlex
3+
4+
tool_definition = {
5+
"name": "grep_search",
6+
"description": (
7+
"Search for exact strings or regular expressions using `grep`. "
8+
"This tool is ideal when the agent knows exactly what to look for (e.g., symbols, patterns, or keywords in files). "
9+
"Use include/exclude filters to narrow down scope."
10+
),
11+
"input_schema": {
12+
"type": "object",
13+
"properties": {
14+
"query": {
15+
"type": "string",
16+
"description": "Regex or literal pattern to search for.",
17+
},
18+
"include_pattern": {
19+
"type": "string",
20+
"description": "Glob pattern for files to include, e.g. '*.py'",
21+
},
22+
"exclude_pattern": {
23+
"type": "string",
24+
"description": "Glob pattern for files to exclude, e.g. '*test*'",
25+
},
26+
"case_sensitive": {
27+
"type": "boolean",
28+
"description": "Case-sensitive search or not.",
29+
},
30+
"explanation": {
31+
"type": "string",
32+
"description": "Why the agent is performing this search.",
33+
},
34+
"directory": {
35+
"type": "string",
36+
"description": "Base directory to search in (default: current directory)",
37+
"default": ".",
38+
},
39+
},
40+
"required": ["query"],
41+
},
42+
}
43+
44+
45+
def handle_call(input_data):
46+
query = input_data["query"]
47+
include = input_data.get("include_pattern")
48+
exclude = input_data.get("exclude_pattern")
49+
case_sensitive = input_data.get("case_sensitive", True)
50+
directory = input_data.get("directory", ".")
51+
52+
# Base grep command
53+
cmd = ["grep", "-r", "--line-number", "--color=never"]
54+
55+
if not case_sensitive:
56+
cmd.append("-i")
57+
58+
if include:
59+
cmd.extend(["--include", include])
60+
61+
if exclude:
62+
cmd.extend(["--exclude", exclude])
63+
64+
cmd.extend([shlex.quote(query), shlex.quote(directory)])
65+
66+
try:
67+
result = subprocess.run(
68+
" ".join(cmd), shell=True, capture_output=True, text=True, timeout=15
69+
)
70+
output = result.stdout.strip()
71+
if result.returncode == 0 or output:
72+
return {
73+
"matches": output.splitlines()[:50],
74+
"exit_code": result.returncode,
75+
"stderr": result.stderr,
76+
}
77+
else:
78+
return {
79+
"matches": [],
80+
"exit_code": result.returncode,
81+
"stderr": result.stderr,
82+
}
83+
except Exception as e:
84+
return {"error": f"Execution error: {e}"}

agent_loop/tools/list_dir.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
3+
tool_definition = {
4+
"name": "list_dir",
5+
"description": (
6+
"List the contents of a directory. The quick tool to use for discovery, "
7+
"before using more targeted tools like semantic search or file reading. "
8+
"Useful to try to understand the file structure before diving deeper into specific files."
9+
),
10+
"input_schema": {
11+
"type": "object",
12+
"properties": {
13+
"relative_workspace_path": {
14+
"type": "string",
15+
"description": "Path to list contents of, relative to the workspace root.",
16+
},
17+
"explanation": {
18+
"type": "string",
19+
"description": "Why the agent is listing this directory and how it helps.",
20+
},
21+
},
22+
"required": ["relative_workspace_path"],
23+
},
24+
}
25+
26+
27+
def handle_call(input_data):
28+
path = input_data["relative_workspace_path"]
29+
30+
try:
31+
entries = os.listdir(path)
32+
entries_info = []
33+
for entry in sorted(entries):
34+
full_path = os.path.join(path, entry)
35+
if os.path.isdir(full_path):
36+
entry_type = "directory"
37+
elif os.path.isfile(full_path):
38+
entry_type = "file"
39+
else:
40+
entry_type = "other"
41+
entries_info.append({"name": entry, "type": entry_type})
42+
43+
return {"path": path, "contents": entries_info}
44+
45+
except Exception as e:
46+
return {"error": f"Could not list directory '{path}': {e}"}

0 commit comments

Comments
 (0)