Skip to content

Commit 0618158

Browse files
add new InsertAfterSubstringTool
Unfortunately LLMs can't count line numbers so when they insert new 'Patch' lines, they make mistakes easily. This new tool should make the 'Patch' line addition more deterministic. Example: ``` "input": { "file": "/git-repos/RHEL-107890/apr/apr.spec", "insert_after_substring": "Patch4: apr-1.7.2-autoconf.patch", "new_string": "Patch5: RHEL-107890.patch" }, "output": "Successfully inserted the specified text into /git-repos/RHEL-107890/apr/apr.spec" ``` Signed-off-by: Tomas Tomecek <[email protected]> Assisted-by: Cursor(Claude) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 37931a3 commit 0618158

File tree

4 files changed

+96
-3
lines changed

4 files changed

+96
-3
lines changed

beeai/agents/backport_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from observability import setup_observability
3434
from tools.commands import RunShellCommandTool
3535
from tools.specfile import AddChangelogEntryTool, BumpReleaseTool
36-
from tools.text import CreateTool, InsertTool, StrReplaceTool, ViewTool
36+
from tools.text import CreateTool, InsertAfterSubstringTool, InsertTool, StrReplaceTool, ViewTool
3737
from tools.wicked_git import GitLogSearchTool, GitPatchCreationTool, GitPreparePackageSources
3838
from triage_agent import BackportData, ErrorData
3939
from utils import check_subprocess, get_agent_execution_config, mcp_tools, render_prompt
@@ -113,6 +113,7 @@ def create_backport_agent(_: list[Tool], run_shell_command_options: dict[str, An
113113
CreateTool(),
114114
ViewTool(),
115115
InsertTool(),
116+
InsertAfterSubstringTool(),
116117
StrReplaceTool(),
117118
GitPatchCreationTool(),
118119
GitLogSearchTool(),

beeai/agents/rebase_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from observability import setup_observability
3333
from tools.commands import RunShellCommandTool
3434
from tools.specfile import AddChangelogEntryTool
35-
from tools.text import CreateTool, InsertTool, StrReplaceTool, ViewTool
35+
from tools.text import CreateTool, InsertAfterSubstringTool, InsertTool, StrReplaceTool, ViewTool
3636
from triage_agent import RebaseData, ErrorData
3737
from utils import get_agent_execution_config, mcp_tools, render_prompt, run_tool
3838

@@ -127,6 +127,7 @@ def create_rebase_agent(mcp_tools: list[Tool], run_shell_command_options: dict[s
127127
CreateTool(),
128128
ViewTool(),
129129
InsertTool(),
130+
InsertAfterSubstringTool(),
130131
StrReplaceTool(),
131132
AddChangelogEntryTool(),
132133
] + [t for t in mcp_tools if t.name == "upload_sources"],

beeai/agents/tests/unit/test_tools.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import subprocess
33
from textwrap import dedent
44

5+
from beeai_framework.tools import ToolError
56
import pytest
67
from flexmock import flexmock
78
from specfile import specfile
@@ -26,6 +27,8 @@
2627
from tools.text import (
2728
CreateTool,
2829
CreateToolInput,
30+
InsertAfterSubstringTool,
31+
InsertAfterSubstringToolInput,
2932
ViewTool,
3033
ViewToolInput,
3134
InsertTool,
@@ -265,6 +268,49 @@ async def test_insert(line, content, tmp_path):
265268
assert test_file.read_text() == content
266269

267270

271+
@pytest.mark.parametrize(
272+
"insert_after_substring, final_content",
273+
[
274+
(
275+
"Line 1",
276+
"Line 1\nInserted line\nLine 2\nLine 3\n",
277+
),
278+
(
279+
"Line 2",
280+
"Line 1\nLine 2\nInserted line\nLine 3\n",
281+
),
282+
(
283+
"Line 3",
284+
"Line 1\nLine 2\nLine 3\nInserted line\n",
285+
),
286+
],
287+
)
288+
@pytest.mark.asyncio
289+
async def test_insert_after_substring(insert_after_substring, final_content, tmp_path):
290+
test_file = tmp_path / "test.txt"
291+
test_file.write_text("Line 1\nLine 2\nLine 3\n")
292+
tool = InsertAfterSubstringTool()
293+
output = await tool.run(
294+
input=InsertAfterSubstringToolInput(file=test_file, insert_after_substring=insert_after_substring, new_string="Inserted line")
295+
).middleware(GlobalTrajectoryMiddleware(pretty=True))
296+
result = output.result
297+
assert result.startswith("Successfully")
298+
assert test_file.read_text() == final_content
299+
300+
301+
@pytest.mark.asyncio
302+
async def test_insert_after_substring_missing(tmp_path):
303+
test_file = tmp_path / "test.txt"
304+
test_file.write_text("Line 1\nLine 2\nLine 3\n")
305+
tool = InsertAfterSubstringTool()
306+
with pytest.raises(ToolError) as e:
307+
await tool.run(
308+
input=InsertAfterSubstringToolInput(file=test_file, insert_after_substring="Line 4", new_string="Inserted line")
309+
).middleware(GlobalTrajectoryMiddleware(pretty=True))
310+
result = e.value.message
311+
assert "No insertion was done because the specified substring wasn't present" in result
312+
313+
268314
@pytest.mark.asyncio
269315
async def test_str_replace(tmp_path):
270316
test_file = tmp_path / "test.txt"

beeai/agents/tools/text.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class InsertToolInput(BaseModel):
8989
class InsertTool(Tool[InsertToolInput, ToolRunOptions, StringToolOutput]):
9090
name = "insert"
9191
description = """
92-
Inserts the specified text at a specific location in a file.
92+
Inserts the specified text at a specific location in a file
9393
"""
9494
input_schema = InsertToolInput
9595

@@ -111,6 +111,51 @@ async def _run(
111111
return StringToolOutput(result=f"Successfully inserted the specified text into {tool_input.file}")
112112

113113

114+
class InsertAfterSubstringToolInput(BaseModel):
115+
file: AbsolutePath = Field(description="Absolute path to a file to edit")
116+
insert_after_substring: str = Field(description="Substring to insert the text after")
117+
new_string: str = Field(description="Text to insert")
118+
119+
120+
class InsertAfterSubstringTool(Tool[InsertAfterSubstringToolInput, ToolRunOptions, StringToolOutput]):
121+
name = "insert_after_substring"
122+
description = """
123+
Inserts the provided text new_string on a new line after the first
124+
occurrence of the specified substring insert_after_substring. The insertion
125+
happens only once.
126+
"""
127+
input_schema = InsertAfterSubstringToolInput
128+
129+
def _create_emitter(self) -> Emitter:
130+
return Emitter.root().child(
131+
namespace=["tool", "text", self.name],
132+
creator=self,
133+
)
134+
135+
async def _run(
136+
self, tool_input: InsertAfterSubstringToolInput, options: ToolRunOptions | None, context: RunContext
137+
) -> StringToolOutput:
138+
if not tool_input.insert_after_substring:
139+
raise ToolError("No insertion was done because the specified substring wasn't provided")
140+
try:
141+
content = await asyncio.to_thread(tool_input.file.read_text)
142+
if tool_input.insert_after_substring not in content:
143+
raise ToolError("No insertion was done because the specified substring wasn't present")
144+
await asyncio.to_thread(
145+
tool_input.file.write_text,
146+
content.replace(
147+
tool_input.insert_after_substring,
148+
tool_input.insert_after_substring + "\n" + tool_input.new_string,
149+
1 # Replace only the first occurrence, 'count' kw introduced in Python 3.13
150+
)
151+
)
152+
except ToolError:
153+
raise
154+
except Exception as e:
155+
raise ToolError(f"Failed to insert text: {e}") from e
156+
return StringToolOutput(result=f"Successfully inserted the specified text into {tool_input.file}")
157+
158+
114159
class StrReplaceToolInput(BaseModel):
115160
file: AbsolutePath = Field(description="Absolute path to a file to edit")
116161
old_string: str = Field(

0 commit comments

Comments
 (0)