Skip to content

Commit f5eef37

Browse files
committed
sav init
1 parent 031849e commit f5eef37

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
from __future__ import annotations
2+
3+
import fnmatch
4+
import itertools
5+
import os
6+
from pathlib import Path
7+
8+
from patchwork.common.tools.tool import Tool
9+
10+
11+
class FindTool(Tool, tool_name="find_files"):
12+
def __init__(self, path: Path | str, **kwargs):
13+
self.__working_dir = Path(path).resolve()
14+
15+
def json_schema(self) -> dict:
16+
return {
17+
"name": "find_files",
18+
"description": f"""\
19+
Tool to find files in {self.__working_dir} using a pattern based on the Unix shell style.
20+
Unix shell style:
21+
* matches everything
22+
? matches any single character
23+
[seq] matches any character in seq
24+
[!seq] matches any char not in seq
25+
26+
""",
27+
"input_schema": {
28+
"type": "object",
29+
"properties": {
30+
"pattern": {
31+
"description": """\
32+
The Unix shell style pattern to match files using.
33+
34+
Unix shell style:
35+
* matches everything
36+
? matches any single character
37+
[seq] matches any character in seq
38+
[!seq] matches any char not in seq
39+
40+
Example:
41+
* '*macs' will match the file '.emacs'
42+
* '*.py' will match all files with the '.py' extension
43+
""",
44+
"type": "string",
45+
},
46+
"depth": {
47+
"description": "The maximum depth files in directories to look for. Default is 1.",
48+
"type": "integer",
49+
},
50+
"is_case_sensitive": {
51+
"description": "Whether the pattern should be case-sensitive.",
52+
"type": "boolean",
53+
},
54+
},
55+
"required": ["pattern"],
56+
},
57+
}
58+
59+
def __is_dot(self, path: Path | str) -> bool:
60+
return any(part.startswith(".") for part in path.relative_to(self.__working_dir).parts)
61+
62+
def execute(
63+
self, pattern: str | None = None, depth: int = 1, is_case_sensitive: bool = False, *args, **kwargs
64+
) -> str:
65+
if pattern is None:
66+
raise ValueError("Pattern argument is required!")
67+
68+
matcher = fnmatch.fnmatch
69+
if is_case_sensitive:
70+
matcher = fnmatch.fnmatchcase
71+
72+
file_matches = []
73+
dir_matches = []
74+
for root, dirs, files in os.walk(self.__working_dir):
75+
root_path = Path(root)
76+
if len(root_path.resolve().relative_to(self.__working_dir).parts) > depth:
77+
continue
78+
79+
for file in itertools.chain(dirs, files):
80+
file_path = root_path / file
81+
if self.__is_dot(file_path):
82+
continue
83+
84+
if file_path.is_file():
85+
list_to_append = file_matches
86+
else:
87+
list_to_append = dir_matches
88+
89+
if matcher(str(file_path), pattern):
90+
relative_file_path = file_path.relative_to(self.__working_dir)
91+
list_to_append.append(str(relative_file_path))
92+
93+
delim = "\n * "
94+
files_part = (delim + delim.join(file_matches)) if len(file_matches) > 0 else "\n No files found"
95+
dirs_part = (delim + delim.join(dir_matches)) if len(dir_matches) > 0 else "\n No directories found"
96+
return f"""\
97+
Files:{files_part}
98+
99+
Directories:{dirs_part}
100+
101+
"""
102+
103+
104+
class FindTextTool(Tool, tool_name="find_text"):
105+
__CHAR_LIMIT = 200
106+
__CHAR_LIMIT_TEXT = "<Too many characters>"
107+
108+
def __init__(self, path: Path | str, **kwargs):
109+
self.__working_dir = Path(path).resolve()
110+
111+
def json_schema(self) -> dict:
112+
return {
113+
"name": "find_text",
114+
"description": f"""\
115+
Tool to find text in a file using a pattern based on the Unix shell style.
116+
The current working directory is always {self.__working_dir}.
117+
The path provided should either be absolute or relative to the current working directory.
118+
119+
This tool will match each line of the file with the provided pattern and prints the line number and the line content.
120+
If the line contains more than {self.__CHAR_LIMIT} characters, the line content will be replaced with {self.__CHAR_LIMIT_TEXT}.
121+
""",
122+
"input_schema": {
123+
"type": "object",
124+
"properties": {
125+
"path": {
126+
"description": "The path to the file to find text in.",
127+
"type": "string",
128+
},
129+
"pattern": {
130+
"description": """\
131+
The Unix shell style pattern to match files using.
132+
133+
Unix shell style:
134+
* matches everything
135+
? matches any single character
136+
[seq] matches any character in seq
137+
[!seq] matches any char not in seq
138+
139+
Example:
140+
* '*macs' will match the file '.emacs'
141+
* '*.py' will match all files with the '.py' extension
142+
""",
143+
"type": "string",
144+
},
145+
"is_case_sensitive": {
146+
"description": "Whether the pattern should be case-sensitive.",
147+
"type": "boolean",
148+
},
149+
},
150+
"required": ["path", "pattern"],
151+
},
152+
}
153+
154+
def execute(
155+
self,
156+
path: Path | str | None = None,
157+
pattern: str | None = None,
158+
is_case_sensitive: bool = False,
159+
*args,
160+
**kwargs,
161+
) -> str:
162+
if path is None:
163+
raise ValueError("Path argument is required!")
164+
165+
if pattern is None:
166+
raise ValueError("pattern argument is required!")
167+
168+
matcher = fnmatch.fnmatch
169+
if is_case_sensitive:
170+
matcher = fnmatch.fnmatchcase
171+
172+
path = Path(path).resolve()
173+
if not path.is_relative_to(self.__working_dir):
174+
raise ValueError("Path must be relative to working dir")
175+
176+
matches = []
177+
with path.open("r") as f:
178+
for i, line in enumerate(f.readlines()):
179+
if not matcher(line, pattern):
180+
continue
181+
182+
content = f"Line {i + 1}: {line}"
183+
if len(line) > self.__CHAR_LIMIT:
184+
content = f"Line {i + 1}: {self.__CHAR_LIMIT_TEXT}"
185+
186+
matches.append(content)
187+
return f"Pattern matches found in '{path}':\n" + "\n".join(matches)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from enum import IntEnum
2+
from pathlib import Path
3+
4+
import yaml
5+
6+
from patchwork.common.utils.progress_bar import PatchflowProgressBar
7+
from patchwork.common.utils.step_typing import validate_steps_with_inputs
8+
from patchwork.logger import logger
9+
from patchwork.step import Step
10+
from patchwork.steps import (
11+
LLM,
12+
PR,
13+
CallLLM,
14+
CommitChanges,
15+
CreatePR,
16+
ExtractCode,
17+
ExtractModelResponse,
18+
ModifyCode,
19+
PreparePR,
20+
PreparePrompt,
21+
ScanSemgrep,
22+
ScanSonar, CallSQL, AgenticLLM,
23+
)
24+
25+
_DEFAULT_INPUT_FILE = Path(__file__).parent / "defaults.yml"
26+
27+
28+
class LogAnalysis(Step):
29+
def __init__(self, inputs: dict):
30+
PatchflowProgressBar(self).register_steps(
31+
CallSQL,
32+
AgenticLLM,
33+
)
34+
final_inputs = yaml.safe_load(_DEFAULT_INPUT_FILE.read_text())
35+
final_inputs.update(inputs)
36+
37+
validate_steps_with_inputs(
38+
set(final_inputs.keys()).union({""}),
39+
CallSQL,
40+
AgenticLLM,
41+
)
42+
43+
self.inputs = final_inputs
44+
45+
def run(self) -> dict:
46+
output = CallSQL(self.inputs).run()
47+
self.inputs.update(output)

patchwork/patchflows/LogAnalysis/defaults.yml

Whitespace-only changes.

0 commit comments

Comments
 (0)