diff --git a/codegen-examples/examples/swebench_agent_run/local_run.ipynb b/codegen-examples/examples/swebench_agent_run/local_run.ipynb index f2f73c922..54d845c98 100644 --- a/codegen-examples/examples/swebench_agent_run/local_run.ipynb +++ b/codegen-examples/examples/swebench_agent_run/local_run.ipynb @@ -32,7 +32,7 @@ "metadata": {}, "outputs": [], "source": [ - "await run_eval(use_existing_preds=None, dataset=\"lite\", length=5, repo=\"django/django\", num_workers=10, model=\"claude-3-7-sonnet-latest\")" + "await run_eval(use_existing_preds=None, dataset=\"lite\", length=20, repo=\"django/django\", num_workers=10, model=\"claude-3-7-sonnet-latest\")" ] }, { diff --git a/src/codegen/agents/data.py b/src/codegen/agents/data.py index fab2283da..6ac9b1d81 100644 --- a/src/codegen/agents/data.py +++ b/src/codegen/agents/data.py @@ -52,7 +52,6 @@ class ToolMessageData(BaseMessage): tool_name: Optional[str] = None tool_response: Optional[str] = None tool_id: Optional[str] = None - status: Optional[str] = None @dataclass diff --git a/src/codegen/agents/tracer.py b/src/codegen/agents/tracer.py index ef711b9e9..816835c41 100644 --- a/src/codegen/agents/tracer.py +++ b/src/codegen/agents/tracer.py @@ -71,14 +71,7 @@ def extract_structured_data(self, chunk: dict[str, Any]) -> Optional[BaseMessage tool_calls = [ToolCall(name=tc.get("name"), arguments=tc.get("arguments"), id=tc.get("id")) for tc in tool_calls_data] return AssistantMessage(type=message_type, content=content, tool_calls=tool_calls) elif message_type == "tool": - return ToolMessageData( - type=message_type, - content=content, - tool_name=getattr(latest_message, "name", None), - tool_response=getattr(latest_message, "artifact", content), - tool_id=getattr(latest_message, "tool_call_id", None), - status=getattr(latest_message, "status", None), - ) + return ToolMessageData(type=message_type, content=content, tool_name=getattr(latest_message, "name", None), tool_response=content, tool_id=getattr(latest_message, "tool_call_id", None)) elif message_type == "function": return FunctionMessageData(type=message_type, content=content) else: diff --git a/src/codegen/extensions/langchain/graph.py b/src/codegen/extensions/langchain/graph.py index 22a49a78d..2987f6863 100644 --- a/src/codegen/extensions/langchain/graph.py +++ b/src/codegen/extensions/langchain/graph.py @@ -100,7 +100,7 @@ def reasoner(self, state: GraphState) -> dict[str, Any]: messages.append(HumanMessage(content=query)) result = self.model.invoke([self.system_message, *messages]) - if isinstance(result, AIMessage) and not result.tool_calls: + if isinstance(result, AIMessage): updated_messages = [*messages, result] return {"messages": updated_messages, "final_answer": result.content} @@ -455,7 +455,7 @@ def get_field_descriptions(tool_obj): return f"Error: Could not identify the tool you're trying to use.\n\nAvailable tools:\n{available_tools}\n\nPlease use one of the available tools with the correct parameters." # For other types of errors - return f"Error executing tool: {exception!s}\n\nPlease check your tool usage and try again with the correct parameters." + return f"Error executing tool: {error_msg}\n\nPlease check your tool usage and try again with the correct parameters." # Add nodes builder.add_node("reasoner", self.reasoner, retry=retry_policy) diff --git a/src/codegen/extensions/langchain/tools.py b/src/codegen/extensions/langchain/tools.py index e800a9c78..9b7acc2e4 100644 --- a/src/codegen/extensions/langchain/tools.py +++ b/src/codegen/extensions/langchain/tools.py @@ -1,10 +1,8 @@ """Langchain tools for workspace operations.""" from collections.abc import Callable -from typing import Annotated, ClassVar, Literal, Optional +from typing import ClassVar, Literal -from langchain_core.messages import ToolMessage -from langchain_core.tools import InjectedToolCallId from langchain_core.tools.base import BaseTool from pydantic import BaseModel, Field @@ -54,11 +52,10 @@ class ViewFileInput(BaseModel): """Input for viewing a file.""" filepath: str = Field(..., description="Path to the file relative to workspace root") - start_line: Optional[int] = Field(None, description="Starting line number to view (1-indexed, inclusive)") - end_line: Optional[int] = Field(None, description="Ending line number to view (1-indexed, inclusive)") - max_lines: Optional[int] = Field(None, description="Maximum number of lines to view at once, defaults to 250") - line_numbers: Optional[bool] = Field(True, description="If True, add line numbers to the content (1-indexed)") - tool_call_id: Annotated[str, InjectedToolCallId] + start_line: int | None = Field(None, description="Starting line number to view (1-indexed, inclusive)") + end_line: int | None = Field(None, description="Ending line number to view (1-indexed, inclusive)") + max_lines: int | None = Field(None, description="Maximum number of lines to view at once, defaults to 250") + line_numbers: bool | None = Field(True, description="If True, add line numbers to the content (1-indexed)") class ViewFileTool(BaseTool): @@ -76,13 +73,12 @@ def __init__(self, codebase: Codebase) -> None: def _run( self, - tool_call_id: str, filepath: str, - start_line: Optional[int] = None, - end_line: Optional[int] = None, - max_lines: Optional[int] = None, - line_numbers: Optional[bool] = True, - ) -> ToolMessage: + start_line: int | None = None, + end_line: int | None = None, + max_lines: int | None = None, + line_numbers: bool | None = True, + ) -> str: result = view_file( self.codebase, filepath, @@ -92,7 +88,7 @@ def _run( max_lines=max_lines if max_lines is not None else 250, ) - return result.render(tool_call_id) + return result.render() class ListDirectoryInput(BaseModel): @@ -100,7 +96,6 @@ class ListDirectoryInput(BaseModel): dirpath: str = Field(default="./", description="Path to directory relative to workspace root") depth: int = Field(default=1, description="How deep to traverse. Use -1 for unlimited depth.") - tool_call_id: Annotated[str, InjectedToolCallId] class ListDirectoryTool(BaseTool): @@ -114,9 +109,9 @@ class ListDirectoryTool(BaseTool): def __init__(self, codebase: Codebase) -> None: super().__init__(codebase=codebase) - def _run(self, tool_call_id: str, dirpath: str = "./", depth: int = 1) -> ToolMessage: + def _run(self, dirpath: str = "./", depth: int = 1) -> str: result = list_directory(self.codebase, dirpath, depth) - return result.render(tool_call_id) + return result.render() class SearchInput(BaseModel): @@ -131,7 +126,6 @@ class SearchInput(BaseModel): page: int = Field(default=1, description="Page number to return (1-based, default: 1)") files_per_page: int = Field(default=10, description="Number of files to return per page (default: 10)") use_regex: bool = Field(default=False, description="Whether to treat query as a regex pattern (default: False)") - tool_call_id: Annotated[str, InjectedToolCallId] class SearchTool(BaseTool): @@ -145,9 +139,9 @@ class SearchTool(BaseTool): def __init__(self, codebase: Codebase) -> None: super().__init__(codebase=codebase) - def _run(self, tool_call_id: str, query: str, file_extensions: Optional[list[str]] = None, page: int = 1, files_per_page: int = 10, use_regex: bool = False) -> ToolMessage: + def _run(self, query: str, file_extensions: list[str] | None = None, page: int = 1, files_per_page: int = 10, use_regex: bool = False) -> str: result = search(self.codebase, query, file_extensions=file_extensions, page=page, files_per_page=files_per_page, use_regex=use_regex) - return result.render(tool_call_id) + return result.render() class EditFileInput(BaseModel): @@ -155,7 +149,6 @@ class EditFileInput(BaseModel): filepath: str = Field(..., description="Path to the file to edit") content: str = Field(..., description="New content for the file") - tool_call_id: Annotated[str, InjectedToolCallId] class EditFileTool(BaseTool): @@ -188,9 +181,9 @@ class EditFileTool(BaseTool): def __init__(self, codebase: Codebase) -> None: super().__init__(codebase=codebase) - def _run(self, filepath: str, content: str, tool_call_id: str) -> str: + def _run(self, filepath: str, content: str) -> str: result = edit_file(self.codebase, filepath, content) - return result.render(tool_call_id) + return result.render() class CreateFileInput(BaseModel): @@ -347,7 +340,6 @@ class SemanticEditInput(BaseModel): edit_content: str = Field(..., description=FILE_EDIT_PROMPT) start: int = Field(default=1, description="Starting line number (1-indexed, inclusive). Default is 1.") end: int = Field(default=-1, description="Ending line number (1-indexed, inclusive). Default is -1 (end of file).") - tool_call_id: Annotated[str, InjectedToolCallId] class SemanticEditTool(BaseTool): @@ -361,10 +353,10 @@ class SemanticEditTool(BaseTool): def __init__(self, codebase: Codebase) -> None: super().__init__(codebase=codebase) - def _run(self, filepath: str, tool_call_id: str, edit_content: str, start: int = 1, end: int = -1) -> ToolMessage: + def _run(self, filepath: str, edit_content: str, start: int = 1, end: int = -1) -> str: # Create the the draft editor mini llm result = semantic_edit(self.codebase, filepath, edit_content, start=start, end=end) - return result.render(tool_call_id) + return result.render() class RenameFileInput(BaseModel): diff --git a/src/codegen/extensions/tools/edit_file.py b/src/codegen/extensions/tools/edit_file.py index de0e42cbf..13ba35951 100644 --- a/src/codegen/extensions/tools/edit_file.py +++ b/src/codegen/extensions/tools/edit_file.py @@ -1,8 +1,7 @@ """Tool for editing file contents.""" -from typing import TYPE_CHECKING, ClassVar, Optional +from typing import ClassVar -from langchain_core.messages import ToolMessage from pydantic import Field from codegen.sdk.core.codebase import Codebase @@ -10,9 +9,6 @@ from .observation import Observation from .replacement_edit import generate_diff -if TYPE_CHECKING: - from .tool_output_types import EditFileArtifacts - class EditFileObservation(Observation): """Response from editing a file.""" @@ -20,34 +16,17 @@ class EditFileObservation(Observation): filepath: str = Field( description="Path to the edited file", ) - diff: Optional[str] = Field( - default=None, + diff: str = Field( description="Unified diff showing the changes made", ) str_template: ClassVar[str] = "Edited file {filepath}" - def render(self, tool_call_id: str) -> ToolMessage: + def render(self) -> str: """Render edit results in a clean format.""" - if self.status == "error": - artifacts_error: EditFileArtifacts = {"filepath": self.filepath, "error": self.error} - return ToolMessage( - content=f"[ERROR EDITING FILE]: {self.filepath}: {self.error}", - status=self.status, - tool_name="edit_file", - artifact=artifacts_error, - tool_call_id=tool_call_id, - ) - - artifacts_success: EditFileArtifacts = {"filepath": self.filepath, "diff": self.diff} - - return ToolMessage( - content=f"""[EDIT FILE]: {self.filepath}\n\n{self.diff}""", - status=self.status, - tool_name="edit_file", - artifact=artifacts_success, - tool_call_id=tool_call_id, - ) + return f"""[EDIT FILE]: {self.filepath} + +{self.diff}""" def edit_file(codebase: Codebase, filepath: str, new_content: str) -> EditFileObservation: diff --git a/src/codegen/extensions/tools/list_directory.py b/src/codegen/extensions/tools/list_directory.py index 285e3b62d..357f303ca 100644 --- a/src/codegen/extensions/tools/list_directory.py +++ b/src/codegen/extensions/tools/list_directory.py @@ -2,14 +2,13 @@ from typing import ClassVar -from langchain_core.messages import ToolMessage from pydantic import Field -from codegen.extensions.tools.observation import Observation -from codegen.extensions.tools.tool_output_types import ListDirectoryArtifacts from codegen.sdk.core.codebase import Codebase from codegen.sdk.core.directory import Directory +from .observation import Observation + class DirectoryInfo(Observation): """Information about a directory.""" @@ -32,14 +31,6 @@ class DirectoryInfo(Observation): default=False, description="Whether this is a leaf node (at max depth)", ) - depth: int = Field( - default=0, - description="Current depth in the tree", - ) - max_depth: int = Field( - default=1, - description="Maximum depth allowed", - ) str_template: ClassVar[str] = "Directory {path} ({file_count} files, {dir_count} subdirs)" @@ -50,7 +41,7 @@ def _get_details(self) -> dict[str, int]: "dir_count": len(self.subdirectories), } - def render_as_string(self) -> str: + def render(self) -> str: """Render directory listing as a file tree.""" lines = [ f"[LIST DIRECTORY]: {self.path}", @@ -106,26 +97,6 @@ def build_tree(items: list[tuple[str, bool, "DirectoryInfo | None"]], prefix: st return "\n".join(lines) - def to_artifacts(self) -> ListDirectoryArtifacts: - """Convert directory info to artifacts for UI.""" - artifacts: ListDirectoryArtifacts = { - "dirpath": self.path, - "name": self.name, - "is_leaf": self.is_leaf, - "depth": self.depth, - "max_depth": self.max_depth, - } - - if self.files is not None: - artifacts["files"] = self.files - artifacts["file_paths"] = [f"{self.path}/{f}" for f in self.files] - - if self.subdirectories: - artifacts["subdirs"] = [d.name for d in self.subdirectories] - artifacts["subdir_paths"] = [d.path for d in self.subdirectories] - - return artifacts - class ListDirectoryObservation(Observation): """Response from listing directory contents.""" @@ -136,29 +107,9 @@ class ListDirectoryObservation(Observation): str_template: ClassVar[str] = "{directory_info}" - def render(self, tool_call_id: str) -> ToolMessage: - """Render directory listing with artifacts for UI.""" - if self.status == "error": - error_artifacts: ListDirectoryArtifacts = { - "dirpath": self.directory_info.path, - "name": self.directory_info.name, - "error": self.error, - } - return ToolMessage( - content=f"[ERROR LISTING DIRECTORY]: {self.directory_info.path}: {self.error}", - status=self.status, - tool_name="list_directory", - artifact=error_artifacts, - tool_call_id=tool_call_id, - ) - - return ToolMessage( - content=self.directory_info.render_as_string(), - status=self.status, - tool_name="list_directory", - artifact=self.directory_info.to_artifacts(), - tool_call_id=tool_call_id, - ) + def render(self) -> str: + """Render directory listing.""" + return self.directory_info.render() def list_directory(codebase: Codebase, path: str = "./", depth: int = 2) -> ListDirectoryObservation: @@ -185,7 +136,7 @@ def list_directory(codebase: Codebase, path: str = "./", depth: int = 2) -> List ), ) - def get_directory_info(dir_obj: Directory, current_depth: int, max_depth: int) -> DirectoryInfo: + def get_directory_info(dir_obj: Directory, current_depth: int) -> DirectoryInfo: """Helper function to get directory info recursively.""" # Get direct files (always include files unless at max depth) all_files = [] @@ -200,7 +151,7 @@ def get_directory_info(dir_obj: Directory, current_depth: int, max_depth: int) - 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, max_depth)) + subdirs.append(get_directory_info(subdir, new_depth)) else: # At max depth, return a leaf node subdirs.append( @@ -210,8 +161,6 @@ def get_directory_info(dir_obj: Directory, current_depth: int, max_depth: int) - path=subdir.dirpath, files=None, # Don't include files at max depth is_leaf=True, - depth=current_depth, - max_depth=max_depth, ) ) @@ -221,11 +170,9 @@ def get_directory_info(dir_obj: Directory, current_depth: int, max_depth: int) - path=dir_obj.dirpath, files=sorted(all_files), subdirectories=subdirs, - depth=current_depth, - max_depth=max_depth, ) - dir_info = get_directory_info(directory, depth, depth) + dir_info = get_directory_info(directory, depth) return ListDirectoryObservation( status="success", directory_info=dir_info, diff --git a/src/codegen/extensions/tools/observation.py b/src/codegen/extensions/tools/observation.py index 487c1bdfe..512b10117 100644 --- a/src/codegen/extensions/tools/observation.py +++ b/src/codegen/extensions/tools/observation.py @@ -3,7 +3,6 @@ import json from typing import Any, ClassVar, Optional -from langchain_core.messages import ToolMessage from pydantic import BaseModel, Field @@ -38,47 +37,13 @@ def __str__(self) -> str: """Get string representation of the observation.""" if self.status == "error": return f"Error: {self.error}" - return self.render_as_string() + details = self._get_details() + return self.render() def __repr__(self) -> str: """Get detailed string representation of the observation.""" return f"{self.__class__.__name__}({self.model_dump_json()})" - def render_as_string(self) -> str: - """Render the observation as a string. - - This is used for string representation and as the content field - in the ToolMessage. Subclasses can override this to customize - their string output format. - """ + def render(self) -> str: + """Render the observation as a string.""" return json.dumps(self.model_dump(), indent=2) - - def render(self, tool_call_id: Optional[str] = None) -> ToolMessage | str: - """Render the observation as a ToolMessage or string. - - Args: - tool_call_id: Optional[str] = None - If provided, return a ToolMessage. - If None, return a string representation. - - Returns: - ToolMessage or str containing the observation content and metadata. - For error cases, includes error information in artifacts. - """ - if tool_call_id is None: - return self.render_as_string() - - # Get content first in case render_as_string has side effects - content = self.render_as_string() - - if self.status == "error": - return ToolMessage( - content=content, - status=self.status, - tool_call_id=tool_call_id, - ) - - return ToolMessage( - content=content, - status=self.status, - tool_call_id=tool_call_id, - ) diff --git a/src/codegen/extensions/tools/search.py b/src/codegen/extensions/tools/search.py index b8fce05d0..2a347c133 100644 --- a/src/codegen/extensions/tools/search.py +++ b/src/codegen/extensions/tools/search.py @@ -11,11 +11,8 @@ import subprocess from typing import ClassVar -from langchain_core.messages import ToolMessage from pydantic import Field -from codegen.extensions.tools.tool_output_types import SearchArtifacts -from codegen.extensions.tools.tool_output_types import SearchMatch as SearchMatchDict from codegen.sdk.core.codebase import Codebase from .observation import Observation @@ -37,18 +34,10 @@ class SearchMatch(Observation): ) str_template: ClassVar[str] = "Line {line_number}: {match}" - def render_as_string(self) -> str: + def render(self) -> str: """Render match in a VSCode-like format.""" return f"{self.line_number:>4}: {self.line}" - def to_dict(self) -> SearchMatchDict: - """Convert to SearchMatch TypedDict format.""" - return { - "line_number": self.line_number, - "line": self.line, - "match": self.match, - } - class SearchFileResult(Observation): """Search results for a single file.""" @@ -62,13 +51,13 @@ class SearchFileResult(Observation): str_template: ClassVar[str] = "{filepath}: {match_count} matches" - def render_as_string(self) -> str: + def render(self) -> str: """Render file results in a VSCode-like format.""" lines = [ f"📄 {self.filepath}", ] for match in self.matches: - lines.append(match.render_as_string()) + lines.append(match.render()) return "\n".join(lines) def _get_details(self) -> dict[str, str | int]: @@ -100,47 +89,11 @@ class SearchObservation(Observation): str_template: ClassVar[str] = "Found {total_files} files with matches for '{query}' (page {page}/{total_pages})" - def render(self, tool_call_id: str) -> ToolMessage: - """Render search results in a VSCode-like format. - - Args: - tool_call_id: ID of the tool call that triggered this search - - Returns: - ToolMessage containing search results or error - """ - # Prepare artifacts dictionary with default values - artifacts: SearchArtifacts = { - "query": self.query, - "error": self.error if self.status == "error" else None, - "matches": [], # List[SearchMatchDict] - match data as TypedDict - "file_paths": [], # List[str] - file paths with matches - "page": self.page, - "total_pages": self.total_pages if self.status == "success" else 0, - "total_files": self.total_files if self.status == "success" else 0, - "files_per_page": self.files_per_page, - } - - # Handle error case early + def render(self) -> str: + """Render search results in a VSCode-like format.""" if self.status == "error": - return ToolMessage( - content=f"[SEARCH ERROR]: {self.error}", - status=self.status, - tool_name="search", - tool_call_id=tool_call_id, - artifact=artifacts, - ) + return f"[SEARCH ERROR]: {self.error}" - # Build matches and file paths for success case - for result in self.results: - artifacts["file_paths"].append(result.filepath) - for match in result.matches: - # Convert match to SearchMatchDict format - match_dict = match.to_dict() - match_dict["filepath"] = result.filepath - artifacts["matches"].append(match_dict) - - # Build content lines lines = [ f"[SEARCH RESULTS]: {self.query}", f"Found {self.total_files} files with matches (showing page {self.page} of {self.total_pages})", @@ -149,23 +102,16 @@ def render(self, tool_call_id: str) -> ToolMessage: if not self.results: lines.append("No matches found") - else: - # Add results with blank lines between files - for result in self.results: - lines.append(result.render_as_string()) - lines.append("") # Add blank line between files - - # Add pagination info if there are multiple pages - if self.total_pages > 1: - lines.append(f"Page {self.page}/{self.total_pages} (use page parameter to see more results)") - - return ToolMessage( - content="\n".join(lines), - status=self.status, - tool_name="search", - tool_call_id=tool_call_id, - artifact=artifacts, - ) + return "\n".join(lines) + + for result in self.results: + lines.append(result.render()) + lines.append("") # Add blank line between files + + if self.total_pages > 1: + lines.append(f"Page {self.page}/{self.total_pages} (use page parameter to see more results)") + + return "\n".join(lines) def _search_with_ripgrep( diff --git a/src/codegen/extensions/tools/semantic_edit.py b/src/codegen/extensions/tools/semantic_edit.py index 32a10031f..91f35083d 100644 --- a/src/codegen/extensions/tools/semantic_edit.py +++ b/src/codegen/extensions/tools/semantic_edit.py @@ -2,9 +2,8 @@ import difflib import re -from typing import TYPE_CHECKING, ClassVar, Optional +from typing import ClassVar, Optional -from langchain_core.messages import ToolMessage from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from pydantic import Field @@ -16,9 +15,6 @@ from .semantic_edit_prompts import _HUMAN_PROMPT_DRAFT_EDITOR, COMMANDER_SYSTEM_PROMPT from .view_file import add_line_numbers -if TYPE_CHECKING: - from .tool_output_types import SemanticEditArtifacts - class SemanticEditObservation(Observation): """Response from making semantic edits to a file.""" @@ -28,55 +24,19 @@ class SemanticEditObservation(Observation): ) diff: Optional[str] = Field( default=None, - description="Unified diff of changes made to the file", + description="Unified diff showing the changes made", ) new_content: Optional[str] = Field( default=None, - description="New content of the file with line numbers after edits", + description="New content with line numbers", ) line_count: Optional[int] = Field( default=None, - description="Total number of lines in the edited file", + description="Total number of lines in file", ) str_template: ClassVar[str] = "Edited file {filepath}" - def render(self, tool_call_id: str) -> ToolMessage: - """Render the observation as a ToolMessage. - - Args: - tool_call_id: ID of the tool call that triggered this edit - - Returns: - ToolMessage containing edit results or error - """ - # Prepare artifacts dictionary with default values - artifacts: SemanticEditArtifacts = { - "filepath": self.filepath, - "diff": self.diff, - "new_content": self.new_content, - "line_count": self.line_count, - "error": self.error if self.status == "error" else None, - } - - # Handle error case early - if self.status == "error": - return ToolMessage( - content=f"[EDIT ERROR]: {self.error}", - status=self.status, - tool_name="semantic_edit", - tool_call_id=tool_call_id, - artifact=artifacts, - ) - - return ToolMessage( - content=self.render_as_string(), - status=self.status, - tool_name="semantic_edit", - tool_call_id=tool_call_id, - artifact=artifacts, - ) - def generate_diff(original: str, modified: str) -> str: """Generate a unified diff between two strings. diff --git a/src/codegen/extensions/tools/tool_output_types.py b/src/codegen/extensions/tools/tool_output_types.py deleted file mode 100644 index 88ac887b1..000000000 --- a/src/codegen/extensions/tools/tool_output_types.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Type definitions for tool outputs.""" - -from typing import Optional, TypedDict - - -class EditFileArtifacts(TypedDict, total=False): - """Artifacts for edit file operations. - - All fields are optional to support both success and error cases. - """ - - filepath: str # Path to the edited file - diff: Optional[str] # Diff of changes made to the file - error: Optional[str] # Error message (only present on error) - - -class ViewFileArtifacts(TypedDict, total=False): - """Artifacts for view file operations. - - All fields are optional to support both success and error cases. - Includes metadata useful for UI logging and pagination. - """ - - filepath: str # Path to the viewed file - start_line: Optional[int] # Starting line number viewed - end_line: Optional[int] # Ending line number viewed - total_lines: Optional[int] # Total number of lines in file - has_more: Optional[bool] # Whether there are more lines to view - max_lines_per_page: Optional[int] # Maximum lines that can be viewed at once - file_size: Optional[int] # Size of file in bytes - error: Optional[str] # Error message (only present on error) - - -class ListDirectoryArtifacts(TypedDict, total=False): - """Artifacts for directory listing operations. - - All fields are optional to support both success and error cases. - Includes metadata useful for UI tree view and navigation. - """ - - dirpath: str # Full path to the directory - name: str # Name of the directory - files: Optional[list[str]] # List of files in this directory - file_paths: Optional[list[str]] # Full paths to files in this directory - subdirs: Optional[list[str]] # List of subdirectory names - subdir_paths: Optional[list[str]] # Full paths to subdirectories - is_leaf: Optional[bool] # Whether this is a leaf node (at max depth) - depth: Optional[int] # Current depth in the tree - max_depth: Optional[int] # Maximum depth allowed - error: Optional[str] # Error message (only present on error) - - -class SearchMatch(TypedDict, total=False): - """Information about a single search match.""" - - filepath: str # Path to the file containing the match - line_number: int # 1-based line number of the match - line: str # The full line containing the match - match: str # The specific text that matched - - -class SearchArtifacts(TypedDict, total=False): - """Artifacts for search operations. - - All fields are optional to support both success and error cases. - Includes metadata useful for UI search results and navigation. - """ - - query: str # Search query that was used - page: int # Current page number (1-based) - total_pages: int # Total number of pages available - total_files: int # Total number of files with matches - files_per_page: int # Number of files shown per page - matches: list[SearchMatch] # List of matches with file paths and line numbers - file_paths: list[str] # List of files containing matches - error: Optional[str] # Error message (only present on error) - - -class SemanticEditArtifacts(TypedDict, total=False): - """Artifacts for semantic edit operations. - - All fields are optional to support both success and error cases. - Includes metadata useful for UI diff view and file content. - """ - - filepath: str # Path to the edited file - diff: Optional[str] # Unified diff of changes made to the file - new_content: Optional[str] # New content of the file after edits - line_count: Optional[int] # Total number of lines in the edited file - error: Optional[str] # Error message (only present on error) diff --git a/src/codegen/extensions/tools/view_file.py b/src/codegen/extensions/tools/view_file.py index 6a5b43f6a..88bb4bc28 100644 --- a/src/codegen/extensions/tools/view_file.py +++ b/src/codegen/extensions/tools/view_file.py @@ -1,17 +1,13 @@ """Tool for viewing file contents and metadata.""" -from typing import TYPE_CHECKING, ClassVar, Optional +from typing import ClassVar, Optional -from langchain_core.messages import ToolMessage from pydantic import Field from codegen.sdk.core.codebase import Codebase from .observation import Observation -if TYPE_CHECKING: - from .tool_output_types import ViewFileArtifacts - class ViewFileObservation(Observation): """Response from viewing a file.""" @@ -45,30 +41,8 @@ class ViewFileObservation(Observation): str_template: ClassVar[str] = "File {filepath} (showing lines {start_line}-{end_line} of {line_count})" - def render(self, tool_call_id: str) -> ToolMessage: + def render(self) -> str: """Render the file view with pagination information if applicable.""" - if self.status == "error": - error_artifacts: ViewFileArtifacts = {"filepath": self.filepath} - return ToolMessage( - content=f"[ERROR VIEWING FILE]: {self.filepath}: {self.error}", - status=self.status, - tool_call_id=tool_call_id, - tool_name="view_file", - artifact=error_artifacts, - additional_kwargs={ - "error": self.error, - }, - ) - - success_artifacts: ViewFileArtifacts = { - "filepath": self.filepath, - "start_line": self.start_line, - "end_line": self.end_line, - "total_lines": self.line_count, - "has_more": self.has_more, - "max_lines_per_page": self.max_lines_per_page, - } - header = f"[VIEW FILE]: {self.filepath}" if self.line_count is not None: header += f" ({self.line_count} lines total)" @@ -78,13 +52,10 @@ def render(self, tool_call_id: str) -> ToolMessage: if self.has_more: header += f" (more lines available, max {self.max_lines_per_page} lines per page)" - return ToolMessage( - content=f"{header}\n\n{self.content}" if self.content else f"{header}\n", - status=self.status, - tool_name="view_file", - tool_call_id=tool_call_id, - artifact=success_artifacts, - ) + if not self.content: + return f"{header}\n" + + return f"{header}\n\n{self.content}" def add_line_numbers(content: str) -> str: @@ -121,12 +92,10 @@ def view_file( """ try: file = codebase.get_file(filepath) - except ValueError: return ViewFileObservation( status="error", - error=f"""File not found: {filepath}. Please use full filepath relative to workspace root. -Ensure that this is indeed the correct filepath, else keep searching to find the correct fullpath.""", + error=f"File not found: {filepath}. Please use full filepath relative to workspace root.", filepath=filepath, content="", line_count=0, diff --git a/tests/unit/codegen/extensions/test_tools.py b/tests/unit/codegen/extensions/test_tools.py index 6b02f4120..b7c728d74 100644 --- a/tests/unit/codegen/extensions/test_tools.py +++ b/tests/unit/codegen/extensions/test_tools.py @@ -225,14 +225,14 @@ def test_list_directory(codebase): core_dir = next(d for d in src_dir.subdirectories if d.name == "core") # Verify rendered output has proper tree structure - rendered = result.render(tool_call_id="test") + rendered = result.render() print(rendered) expected_tree = """ └── src/ ├── main.py ├── utils.py └── core/""" - assert expected_tree in rendered.content.strip() + assert expected_tree in rendered.strip() def test_edit_file(codebase):