Skip to content

fix: better tool output rendering #562

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

Merged
merged 10 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/codegen/extensions/langchain/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
RevealSymbolTool,
SearchTool,
SemanticEditTool,
SemanticSearchTool,
ViewFileTool,
)

Expand Down Expand Up @@ -70,8 +69,8 @@
MoveSymbolTool(codebase),
RevealSymbolTool(codebase),
SemanticEditTool(codebase),
SemanticSearchTool(codebase),
ReplacementEditTool(codebase),
# SemanticSearchTool(codebase),
# =====[ Github Integration ]=====
# Enable Github integration
# GithubCreatePRTool(codebase),
Expand Down Expand Up @@ -160,7 +159,7 @@

# Wrap with message history
return RunnableWithMessageHistory(
agent_executor,

Check failure on line 162 in src/codegen/extensions/langchain/agent.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "RunnableWithMessageHistory" has incompatible type "AgentExecutor"; expected "Runnable[Sequence[BaseMessage] | dict[str, Any], str | BaseMessage | Sequence[BaseMessage] | dict[str, Any]] | Runnable[PromptValue | str | Sequence[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]], BaseMessage | str]" [arg-type]
lambda session_id: message_history,
input_messages_key="input",
history_messages_key="chat_history",
Expand Down Expand Up @@ -222,7 +221,7 @@

# Wrap with message history
return RunnableWithMessageHistory(
agent_executor,

Check failure on line 224 in src/codegen/extensions/langchain/agent.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "RunnableWithMessageHistory" has incompatible type "AgentExecutor"; expected "Runnable[Sequence[BaseMessage] | dict[str, Any], str | BaseMessage | Sequence[BaseMessage] | dict[str, Any]] | Runnable[PromptValue | str | Sequence[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]], BaseMessage | str]" [arg-type]
lambda session_id: message_history,
input_messages_key="input",
history_messages_key="chat_history",
Expand Down Expand Up @@ -278,7 +277,7 @@

# Wrap with message history
return RunnableWithMessageHistory(
agent_executor,

Check failure on line 280 in src/codegen/extensions/langchain/agent.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "RunnableWithMessageHistory" has incompatible type "AgentExecutor"; expected "Runnable[Sequence[BaseMessage] | dict[str, Any], str | BaseMessage | Sequence[BaseMessage] | dict[str, Any]] | Runnable[PromptValue | str | Sequence[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]], BaseMessage | str]" [arg-type]
lambda session_id: message_history,
input_messages_key="input",
history_messages_key="chat_history",
Expand Down
79 changes: 24 additions & 55 deletions src/codegen/extensions/tools/edit_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from codegen import Codebase

from .observation import Observation
from .view_file import ViewFileObservation, view_file
from .replacement_edit import generate_diff


class EditFileObservation(Observation):
Expand All @@ -16,23 +16,26 @@ class EditFileObservation(Observation):
filepath: str = Field(
description="Path to the edited file",
)
file_info: ViewFileObservation = Field(
description="Information about the edited file",
diff: str = Field(
description="Unified diff showing the changes made",
)

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

def render(self) -> str:
"""Render edit results in a clean format."""
return f"""[EDIT FILE]: {self.filepath}

def edit_file(codebase: Codebase, filepath: str, content: str) -> EditFileObservation:
"""Edit a file by replacing its entire content.
{self.diff}"""


def edit_file(codebase: Codebase, filepath: str, new_content: str) -> EditFileObservation:
"""Edit the contents of a file.

Args:
codebase: The codebase to operate on
filepath: Path to the file to edit
content: New content for the file

Returns:
EditFileObservation containing updated file state, or error if file not found
filepath: Path to the file relative to workspace root
new_content: New content for the file
"""
try:
file = codebase.get_file(filepath)
Expand All @@ -41,52 +44,18 @@ def edit_file(codebase: Codebase, filepath: str, content: str) -> EditFileObserv
status="error",
error=f"File not found: {filepath}",
filepath=filepath,
file_info=ViewFileObservation(
status="error",
error=f"File not found: {filepath}",
filepath=filepath,
content="",
line_count=0,
),
)

if file is None:
return EditFileObservation(
status="error",
error=f"File not found: {filepath}",
filepath=filepath,
file_info=ViewFileObservation(
status="error",
error=f"File not found: {filepath}",
filepath=filepath,
content="",
line_count=0,
),
diff="",
)

try:
file.edit(content)
codebase.commit()
# Generate diff before making changes
diff = generate_diff(file.content, new_content)

# Get updated file info using view_file
file_info = view_file(codebase, filepath)
# Apply the edit
file.edit(new_content)
codebase.commit()

return EditFileObservation(
status="success",
filepath=filepath,
file_info=file_info,
)

except Exception as e:
return EditFileObservation(
status="error",
error=f"Failed to edit file: {e!s}",
filepath=filepath,
file_info=ViewFileObservation(
status="error",
error=f"Failed to edit file: {e!s}",
filepath=filepath,
content="",
line_count=0,
),
)
return EditFileObservation(
status="success",
filepath=filepath,
diff=diff,
)
183 changes: 120 additions & 63 deletions src/codegen/extensions/tools/list_directory.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,144 @@
"""Tool for listing directory contents."""

from typing import ClassVar, Union
from typing import ClassVar

from pydantic import BaseModel, Field
from pydantic import Field

from codegen import Codebase
from codegen.sdk.core.directory import Directory

from .observation import Observation


class DirectoryInfo(BaseModel):
class DirectoryInfo(Observation):
"""Information about a directory."""

name: str = Field(description="Name of the directory")
path: str = Field(description="Full path to the directory")
files: list[str] = Field(description="List of files in this directory")
subdirectories: list[Union[str, "DirectoryInfo"]] = Field(
description="List of subdirectories (either names or full DirectoryInfo objects depending on depth)",
name: str = Field(
description="Name of the directory",
)
path: str = Field(
description="Full path to the directory",
)
files: list[str] | None = Field(
default=None,
description="List of files in this directory (None if at max depth)",
)
subdirectories: list["DirectoryInfo"] = Field(
default_factory=list,
description="List of subdirectories",
)
is_leaf: bool = Field(
default=False,
description="Whether this is a leaf node (at max depth)",
)

str_template: ClassVar[str] = "Directory {path} ({file_count} files, {dir_count} subdirs)"

def _get_details(self) -> dict[str, int]:
"""Get details for string representation."""
return {
"file_count": len(self.files or []),
"dir_count": len(self.subdirectories),
}

def render(self) -> str:
"""Render directory listing as a file tree."""
lines = [
f"[LIST DIRECTORY]: {self.path}",
"",
]

def add_tree_item(name: str, prefix: str = "", is_last: bool = False) -> tuple[str, str]:
"""Helper to format a tree item with proper prefix."""
marker = "└── " if is_last else "├── "
indent = " " if is_last else "│ "
return prefix + marker + name, prefix + indent

def build_tree(items: list[tuple[str, bool, "DirectoryInfo | None"]], prefix: str = "") -> list[str]:
"""Recursively build tree with proper indentation."""
if not items:
return []

result = []
for i, (name, is_dir, dir_info) in enumerate(items):
is_last = i == len(items) - 1
line, new_prefix = add_tree_item(name, prefix, is_last)
result.append(line)

# If this is a directory and not a leaf node, show its contents
if dir_info and not dir_info.is_leaf:
subitems = []
# Add files first
if dir_info.files:
for f in sorted(dir_info.files):
subitems.append((f, False, None))
# Then add subdirectories
for d in dir_info.subdirectories:
subitems.append((d.name + "/", True, d))

Check failure on line 77 in src/codegen/extensions/tools/list_directory.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "append" of "list" has incompatible type "tuple[str, bool, DirectoryInfo]"; expected "tuple[str, bool, None]" [arg-type]

result.extend(build_tree(subitems, new_prefix))

Check failure on line 79 in src/codegen/extensions/tools/list_directory.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "build_tree" has incompatible type "list[tuple[str, bool, None]]"; expected "list[tuple[str, bool, DirectoryInfo | None]]" [arg-type]

return result

# Sort files and directories
items = []
if self.files:
for f in sorted(self.files):
items.append((f, False, None))
for d in self.subdirectories:
items.append((d.name + "/", True, d))

Check failure on line 89 in src/codegen/extensions/tools/list_directory.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "append" of "list" has incompatible type "tuple[str, bool, DirectoryInfo]"; expected "tuple[str, bool, None]" [arg-type]

if not items:
lines.append("(empty directory)")
return "\n".join(lines)

# Generate tree
lines.extend(build_tree(items))

Check failure on line 96 in src/codegen/extensions/tools/list_directory.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "build_tree" has incompatible type "list[tuple[str, bool, None]]"; expected "list[tuple[str, bool, DirectoryInfo | None]]" [arg-type]

return "\n".join(lines)


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

path: str = Field(description="Path to the listed directory")
directory_info: DirectoryInfo = Field(description="Information about the directory and its contents")
depth: int = Field(description="How deep the directory traversal went")
directory_info: DirectoryInfo = Field(
description="Information about the directory",
)

str_template: ClassVar[str] = "{directory_info}"

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


def list_directory(codebase: Codebase, dirpath: str = "./", depth: int = 1) -> ListDirectoryObservation:
def list_directory(codebase: Codebase, path: str = "./", depth: int = 2) -> ListDirectoryObservation:
"""List contents of a directory.

Args:
codebase: The codebase to operate on
dirpath: Path to directory relative to workspace root
path: Path to directory relative to workspace root
depth: How deep to traverse the directory tree. Default is 1 (immediate children only).
Use -1 for unlimited depth.

Returns:
ListDirectoryObservation containing directory contents and metadata
"""
try:
directory = codebase.get_directory(dirpath)
directory = codebase.get_directory(path)
except ValueError:
return ListDirectoryObservation(
status="error",
error=f"Directory not found: {dirpath}",
path=dirpath,
error=f"Directory not found: {path}",
directory_info=DirectoryInfo(
name="",
path=dirpath,
status="error",
name=path.split("/")[-1],
path=path,
files=[],
subdirectories=[],
),
depth=depth,
)

if not directory:
return ListDirectoryObservation(
status="error",
error=f"Directory not found: {dirpath}",
path=dirpath,
directory_info=DirectoryInfo(
name="",
path=dirpath,
files=[],
subdirectories=[],
),
depth=depth,
)

def get_directory_info(dir_obj: Directory, current_depth: int) -> DirectoryInfo:
"""Helper function to get directory info recursively."""
# Get direct files
# Get direct files (always include files unless at max depth)
all_files = []
for file in dir_obj.files:
if file.directory == dir_obj:
Expand All @@ -86,38 +149,32 @@
for subdir in dir_obj.subdirectories:
# Only include direct descendants
if subdir.parent == dir_obj:
if current_depth != 1:
if current_depth > 1 or current_depth == -1:
# For deeper traversal, get full directory info
new_depth = current_depth - 1 if current_depth > 1 else -1
subdirs.append(get_directory_info(subdir, new_depth))
else:
# At max depth, just include name
subdirs.append(subdir.name)
# At max depth, return a leaf node
subdirs.append(
DirectoryInfo(
status="success",
name=subdir.name,
path=subdir.dirpath,
files=None, # Don't include files at max depth
is_leaf=True,
)
)

return DirectoryInfo(
status="success",
name=dir_obj.name,
path=dir_obj.dirpath,
files=all_files,
files=sorted(all_files),
subdirectories=subdirs,
)

try:
directory_info = get_directory_info(directory, depth)
return ListDirectoryObservation(
status="success",
path=dirpath,
directory_info=directory_info,
depth=depth,
)
except Exception as e:
return ListDirectoryObservation(
status="error",
error=f"Failed to list directory: {e!s}",
path=dirpath,
directory_info=DirectoryInfo(
name="",
path=dirpath,
files=[],
subdirectories=[],
),
depth=depth,
)
dir_info = get_directory_info(directory, depth)

Check failure on line 176 in src/codegen/extensions/tools/list_directory.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "get_directory_info" has incompatible type "Directory[Any, Any, Any, Any, Any, Any, Any] | None"; expected "Directory[Any, Any, Any, Any, Any, Any, Any]" [arg-type]
return ListDirectoryObservation(
status="success",
directory_info=dir_info,
)
Loading
Loading