Skip to content

Commit a8b5aca

Browse files
authored
fix: better tool output rendering (#562)
1 parent d3a1dab commit a8b5aca

File tree

6 files changed

+219
-124
lines changed

6 files changed

+219
-124
lines changed

src/codegen/extensions/langchain/agent.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
RevealSymbolTool,
2525
SearchTool,
2626
SemanticEditTool,
27-
SemanticSearchTool,
2827
ViewFileTool,
2928
)
3029

@@ -70,8 +69,8 @@ def create_codebase_agent(
7069
MoveSymbolTool(codebase),
7170
RevealSymbolTool(codebase),
7271
SemanticEditTool(codebase),
73-
SemanticSearchTool(codebase),
7472
ReplacementEditTool(codebase),
73+
# SemanticSearchTool(codebase),
7574
# =====[ Github Integration ]=====
7675
# Enable Github integration
7776
# GithubCreatePRTool(codebase),

src/codegen/extensions/tools/edit_file.py

Lines changed: 24 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from codegen.sdk.core.codebase import Codebase
88

99
from .observation import Observation
10-
from .view_file import ViewFileObservation, view_file
10+
from .replacement_edit import generate_diff
1111

1212

1313
class EditFileObservation(Observation):
@@ -16,23 +16,26 @@ class EditFileObservation(Observation):
1616
filepath: str = Field(
1717
description="Path to the edited file",
1818
)
19-
file_info: ViewFileObservation = Field(
20-
description="Information about the edited file",
19+
diff: str = Field(
20+
description="Unified diff showing the changes made",
2121
)
2222

2323
str_template: ClassVar[str] = "Edited file {filepath}"
2424

25+
def render(self) -> str:
26+
"""Render edit results in a clean format."""
27+
return f"""[EDIT FILE]: {self.filepath}
2528
26-
def edit_file(codebase: Codebase, filepath: str, content: str) -> EditFileObservation:
27-
"""Edit a file by replacing its entire content.
29+
{self.diff}"""
30+
31+
32+
def edit_file(codebase: Codebase, filepath: str, new_content: str) -> EditFileObservation:
33+
"""Edit the contents of a file.
2834
2935
Args:
3036
codebase: The codebase to operate on
31-
filepath: Path to the file to edit
32-
content: New content for the file
33-
34-
Returns:
35-
EditFileObservation containing updated file state, or error if file not found
37+
filepath: Path to the file relative to workspace root
38+
new_content: New content for the file
3639
"""
3740
try:
3841
file = codebase.get_file(filepath)
@@ -41,52 +44,18 @@ def edit_file(codebase: Codebase, filepath: str, content: str) -> EditFileObserv
4144
status="error",
4245
error=f"File not found: {filepath}",
4346
filepath=filepath,
44-
file_info=ViewFileObservation(
45-
status="error",
46-
error=f"File not found: {filepath}",
47-
filepath=filepath,
48-
content="",
49-
line_count=0,
50-
),
51-
)
52-
53-
if file is None:
54-
return EditFileObservation(
55-
status="error",
56-
error=f"File not found: {filepath}",
57-
filepath=filepath,
58-
file_info=ViewFileObservation(
59-
status="error",
60-
error=f"File not found: {filepath}",
61-
filepath=filepath,
62-
content="",
63-
line_count=0,
64-
),
47+
diff="",
6548
)
6649

67-
try:
68-
file.edit(content)
69-
codebase.commit()
50+
# Generate diff before making changes
51+
diff = generate_diff(file.content, new_content)
7052

71-
# Get updated file info using view_file
72-
file_info = view_file(codebase, filepath)
53+
# Apply the edit
54+
file.edit(new_content)
55+
codebase.commit()
7356

74-
return EditFileObservation(
75-
status="success",
76-
filepath=filepath,
77-
file_info=file_info,
78-
)
79-
80-
except Exception as e:
81-
return EditFileObservation(
82-
status="error",
83-
error=f"Failed to edit file: {e!s}",
84-
filepath=filepath,
85-
file_info=ViewFileObservation(
86-
status="error",
87-
error=f"Failed to edit file: {e!s}",
88-
filepath=filepath,
89-
content="",
90-
line_count=0,
91-
),
92-
)
57+
return EditFileObservation(
58+
status="success",
59+
filepath=filepath,
60+
diff=diff,
61+
)
Lines changed: 120 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,144 @@
11
"""Tool for listing directory contents."""
22

3-
from typing import ClassVar, Union
3+
from typing import ClassVar
44

5-
from pydantic import BaseModel, Field
5+
from pydantic import Field
66

77
from codegen.sdk.core.codebase import Codebase
88
from codegen.sdk.core.directory import Directory
99

1010
from .observation import Observation
1111

1212

13-
class DirectoryInfo(BaseModel):
13+
class DirectoryInfo(Observation):
1414
"""Information about a directory."""
1515

16-
name: str = Field(description="Name of the directory")
17-
path: str = Field(description="Full path to the directory")
18-
files: list[str] = Field(description="List of files in this directory")
19-
subdirectories: list[Union[str, "DirectoryInfo"]] = Field(
20-
description="List of subdirectories (either names or full DirectoryInfo objects depending on depth)",
16+
name: str = Field(
17+
description="Name of the directory",
18+
)
19+
path: str = Field(
20+
description="Full path to the directory",
21+
)
22+
files: list[str] | None = Field(
23+
default=None,
24+
description="List of files in this directory (None if at max depth)",
25+
)
26+
subdirectories: list["DirectoryInfo"] = Field(
27+
default_factory=list,
28+
description="List of subdirectories",
29+
)
30+
is_leaf: bool = Field(
31+
default=False,
32+
description="Whether this is a leaf node (at max depth)",
2133
)
2234

35+
str_template: ClassVar[str] = "Directory {path} ({file_count} files, {dir_count} subdirs)"
36+
37+
def _get_details(self) -> dict[str, int]:
38+
"""Get details for string representation."""
39+
return {
40+
"file_count": len(self.files or []),
41+
"dir_count": len(self.subdirectories),
42+
}
43+
44+
def render(self) -> str:
45+
"""Render directory listing as a file tree."""
46+
lines = [
47+
f"[LIST DIRECTORY]: {self.path}",
48+
"",
49+
]
50+
51+
def add_tree_item(name: str, prefix: str = "", is_last: bool = False) -> tuple[str, str]:
52+
"""Helper to format a tree item with proper prefix."""
53+
marker = "└── " if is_last else "├── "
54+
indent = " " if is_last else "│ "
55+
return prefix + marker + name, prefix + indent
56+
57+
def build_tree(items: list[tuple[str, bool, "DirectoryInfo | None"]], prefix: str = "") -> list[str]:
58+
"""Recursively build tree with proper indentation."""
59+
if not items:
60+
return []
61+
62+
result = []
63+
for i, (name, is_dir, dir_info) in enumerate(items):
64+
is_last = i == len(items) - 1
65+
line, new_prefix = add_tree_item(name, prefix, is_last)
66+
result.append(line)
67+
68+
# If this is a directory and not a leaf node, show its contents
69+
if dir_info and not dir_info.is_leaf:
70+
subitems = []
71+
# Add files first
72+
if dir_info.files:
73+
for f in sorted(dir_info.files):
74+
subitems.append((f, False, None))
75+
# Then add subdirectories
76+
for d in dir_info.subdirectories:
77+
subitems.append((d.name + "/", True, d))
78+
79+
result.extend(build_tree(subitems, new_prefix))
80+
81+
return result
82+
83+
# Sort files and directories
84+
items = []
85+
if self.files:
86+
for f in sorted(self.files):
87+
items.append((f, False, None))
88+
for d in self.subdirectories:
89+
items.append((d.name + "/", True, d))
90+
91+
if not items:
92+
lines.append("(empty directory)")
93+
return "\n".join(lines)
94+
95+
# Generate tree
96+
lines.extend(build_tree(items))
97+
98+
return "\n".join(lines)
99+
23100

24101
class ListDirectoryObservation(Observation):
25102
"""Response from listing directory contents."""
26103

27-
path: str = Field(description="Path to the listed directory")
28-
directory_info: DirectoryInfo = Field(description="Information about the directory and its contents")
29-
depth: int = Field(description="How deep the directory traversal went")
104+
directory_info: DirectoryInfo = Field(
105+
description="Information about the directory",
106+
)
107+
108+
str_template: ClassVar[str] = "{directory_info}"
30109

31-
str_template: ClassVar[str] = "Listed contents of {path} (depth={depth})"
110+
def render(self) -> str:
111+
"""Render directory listing."""
112+
return self.directory_info.render()
32113

33114

34-
def list_directory(codebase: Codebase, dirpath: str = "./", depth: int = 1) -> ListDirectoryObservation:
115+
def list_directory(codebase: Codebase, path: str = "./", depth: int = 2) -> ListDirectoryObservation:
35116
"""List contents of a directory.
36117
37118
Args:
38119
codebase: The codebase to operate on
39-
dirpath: Path to directory relative to workspace root
120+
path: Path to directory relative to workspace root
40121
depth: How deep to traverse the directory tree. Default is 1 (immediate children only).
41122
Use -1 for unlimited depth.
42-
43-
Returns:
44-
ListDirectoryObservation containing directory contents and metadata
45123
"""
46124
try:
47-
directory = codebase.get_directory(dirpath)
125+
directory = codebase.get_directory(path)
48126
except ValueError:
49127
return ListDirectoryObservation(
50128
status="error",
51-
error=f"Directory not found: {dirpath}",
52-
path=dirpath,
129+
error=f"Directory not found: {path}",
53130
directory_info=DirectoryInfo(
54-
name="",
55-
path=dirpath,
131+
status="error",
132+
name=path.split("/")[-1],
133+
path=path,
56134
files=[],
57135
subdirectories=[],
58136
),
59-
depth=depth,
60-
)
61-
62-
if not directory:
63-
return ListDirectoryObservation(
64-
status="error",
65-
error=f"Directory not found: {dirpath}",
66-
path=dirpath,
67-
directory_info=DirectoryInfo(
68-
name="",
69-
path=dirpath,
70-
files=[],
71-
subdirectories=[],
72-
),
73-
depth=depth,
74137
)
75138

76139
def get_directory_info(dir_obj: Directory, current_depth: int) -> DirectoryInfo:
77140
"""Helper function to get directory info recursively."""
78-
# Get direct files
141+
# Get direct files (always include files unless at max depth)
79142
all_files = []
80143
for file in dir_obj.files:
81144
if file.directory == dir_obj:
@@ -86,38 +149,32 @@ def get_directory_info(dir_obj: Directory, current_depth: int) -> DirectoryInfo:
86149
for subdir in dir_obj.subdirectories:
87150
# Only include direct descendants
88151
if subdir.parent == dir_obj:
89-
if current_depth != 1:
152+
if current_depth > 1 or current_depth == -1:
153+
# For deeper traversal, get full directory info
90154
new_depth = current_depth - 1 if current_depth > 1 else -1
91155
subdirs.append(get_directory_info(subdir, new_depth))
92156
else:
93-
# At max depth, just include name
94-
subdirs.append(subdir.name)
157+
# At max depth, return a leaf node
158+
subdirs.append(
159+
DirectoryInfo(
160+
status="success",
161+
name=subdir.name,
162+
path=subdir.dirpath,
163+
files=None, # Don't include files at max depth
164+
is_leaf=True,
165+
)
166+
)
95167

96168
return DirectoryInfo(
169+
status="success",
97170
name=dir_obj.name,
98171
path=dir_obj.dirpath,
99-
files=all_files,
172+
files=sorted(all_files),
100173
subdirectories=subdirs,
101174
)
102175

103-
try:
104-
directory_info = get_directory_info(directory, depth)
105-
return ListDirectoryObservation(
106-
status="success",
107-
path=dirpath,
108-
directory_info=directory_info,
109-
depth=depth,
110-
)
111-
except Exception as e:
112-
return ListDirectoryObservation(
113-
status="error",
114-
error=f"Failed to list directory: {e!s}",
115-
path=dirpath,
116-
directory_info=DirectoryInfo(
117-
name="",
118-
path=dirpath,
119-
files=[],
120-
subdirectories=[],
121-
),
122-
depth=depth,
123-
)
176+
dir_info = get_directory_info(directory, depth)
177+
return ListDirectoryObservation(
178+
status="success",
179+
directory_info=dir_info,
180+
)

0 commit comments

Comments
 (0)