From c3eefbc3d6734421f5723ad62d57bb226d5b561b Mon Sep 17 00:00:00 2001 From: Matthew Zhou Date: Thu, 21 Aug 2025 14:29:51 -0700 Subject: [PATCH] fix: Fix 0 indexing for offset (#4086) --- letta/functions/function_sets/files.py | 4 ++-- letta/functions/types.py | 2 +- .../tool_executor/files_tool_executor.py | 22 +++++++++++-------- tests/test_sources.py | 16 +++++++------- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/letta/functions/function_sets/files.py b/letta/functions/function_sets/files.py index a311e279..1c3c72d4 100644 --- a/letta/functions/function_sets/files.py +++ b/letta/functions/function_sets/files.py @@ -21,8 +21,8 @@ async def open_files(agent_state: "AgentState", file_requests: List[FileOpenRequ Open multiple files with different view ranges: file_requests = [ - FileOpenRequest(file_name="project_utils/config.py", offset=1, length=50), # Lines 1-50 - FileOpenRequest(file_name="project_utils/main.py", offset=100, length=100), # Lines 100-199 + FileOpenRequest(file_name="project_utils/config.py", offset=0, length=50), # Lines 1-50 + FileOpenRequest(file_name="project_utils/main.py", offset=100, length=100), # Lines 101-200 FileOpenRequest(file_name="project_utils/utils.py") # Entire file ] diff --git a/letta/functions/types.py b/letta/functions/types.py index af464657..c5b45e77 100644 --- a/letta/functions/types.py +++ b/letta/functions/types.py @@ -11,7 +11,7 @@ class SearchTask(BaseModel): class FileOpenRequest(BaseModel): file_name: str = Field(description="Name of the file to open") offset: Optional[int] = Field( - default=None, description="Optional starting line number (1-indexed). If not specified, starts from beginning of file." + default=None, description="Optional offset for starting line number (0-indexed). If not specified, starts from beginning of file." ) length: Optional[int] = Field( default=None, description="Optional number of lines to view from offset (inclusive). If not specified, views to end of file." diff --git a/letta/services/tool_executor/files_tool_executor.py b/letta/services/tool_executor/files_tool_executor.py index f8cd9ff2..047a7072 100644 --- a/letta/services/tool_executor/files_tool_executor.py +++ b/letta/services/tool_executor/files_tool_executor.py @@ -151,16 +151,16 @@ class LettaFileToolExecutor(ToolExecutor): offset = file_request.offset length = file_request.length - # Convert 1-indexed offset/length to 0-indexed start/end for LineChunker + # Use 0-indexed offset/length directly for LineChunker start, end = None, None if offset is not None or length is not None: - if offset is not None and offset < 1: - raise ValueError(f"Offset for file {file_name} must be >= 1 (1-indexed), got {offset}") + if offset is not None and offset < 0: + raise ValueError(f"Offset for file {file_name} must be >= 0 (0-indexed), got {offset}") if length is not None and length < 1: raise ValueError(f"Length for file {file_name} must be >= 1, got {length}") - # Convert to 0-indexed for LineChunker - start = (offset - 1) if offset is not None else None + # Use offset directly as it's already 0-indexed + start = offset if offset is not None else None if start is not None and length is not None: end = start + length else: @@ -193,7 +193,7 @@ class LettaFileToolExecutor(ToolExecutor): visible_content=visible_content, max_files_open=agent_state.max_files_open, start_line=start + 1 if start is not None else None, # convert to 1-indexed for user display - end_line=end if end is not None else None, # end is already exclusive in slicing, so this is correct + end_line=end if end is not None else None, # end is already exclusive, shows as 1-indexed inclusive ) opened_files.append(file_name) @@ -220,10 +220,14 @@ class LettaFileToolExecutor(ToolExecutor): for req in file_requests: previous_info = format_previous_range(req.file_name) if req.offset is not None and req.length is not None: - end_line = req.offset + req.length - 1 - file_summaries.append(f"{req.file_name} (lines {req.offset}-{end_line}){previous_info}") + # Display as 1-indexed for user readability: (offset+1) to (offset+length) + start_line = req.offset + 1 + end_line = req.offset + req.length + file_summaries.append(f"{req.file_name} (lines {start_line}-{end_line}){previous_info}") elif req.offset is not None: - file_summaries.append(f"{req.file_name} (lines {req.offset}-end){previous_info}") + # Display as 1-indexed + start_line = req.offset + 1 + file_summaries.append(f"{req.file_name} (lines {start_line}-end){previous_info}") else: file_summaries.append(f"{req.file_name}{previous_info}") diff --git a/tests/test_sources.py b/tests/test_sources.py index aeedb236..905e1297 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -424,7 +424,7 @@ def test_agent_uses_open_close_file_correctly(disable_pinecone, client: LettaSDK assert initial_content_length > 10, f"Expected file content > 10 chars, got {initial_content_length}" # Ask agent to open the file for a specific range using offset/length - offset, length = 1, 5 # 1-indexed offset, 5 lines + offset, length = 0, 5 # 0-indexed offset, 5 lines print(f"Requesting agent to open file with offset={offset}, length={length}") open_response1 = client.agents.messages.create( agent_id=agent_state.id, @@ -453,7 +453,7 @@ def test_agent_uses_open_close_file_correctly(disable_pinecone, client: LettaSDK assert "5: " in old_value, f"Expected line 5 to be present, got: {old_value}" # Ask agent to open the file for a different range - offset, length = 6, 5 # Different offset, same length + offset, length = 5, 5 # Different offset, same length open_response2 = client.agents.messages.create( agent_id=agent_state.id, messages=[ @@ -482,8 +482,8 @@ def test_agent_uses_open_close_file_correctly(disable_pinecone, client: LettaSDK assert "10: " in new_value, f"Expected line 10 to be present, got: {new_value}" print(f"Comparing content ranges:") - print(f" First range (offset=1, length=5): '{old_value}'") - print(f" Second range (offset=6, length=5): '{new_value}'") + print(f" First range (offset=0, length=5): '{old_value}'") + print(f" Second range (offset=5, length=5): '{new_value}'") assert new_value != old_value, f"Different view ranges should have different content. New: '{new_value}', Old: '{old_value}'" @@ -703,7 +703,7 @@ def test_view_ranges_have_metadata(disable_pinecone, client: LettaSDKClient, age assert block.value.startswith("[Viewing file start (out of 100 lines)]") # Open a specific range using offset/length - offset = 50 # 1-indexed line 50 + offset = 49 # 0-indexed for line 50 length = 5 # 5 lines (50-54) open_response = client.agents.messages.create( agent_id=agent_state.id, @@ -960,9 +960,9 @@ def test_open_files_schema_descriptions(disable_pinecone, client: LettaSDKClient # Check that examples are included assert "Examples:" in description assert 'FileOpenRequest(file_name="project_utils/config.py")' in description - assert 'FileOpenRequest(file_name="project_utils/config.py", offset=1, length=50)' in description + assert 'FileOpenRequest(file_name="project_utils/config.py", offset=0, length=50)' in description assert "# Lines 1-50" in description - assert "# Lines 100-199" in description + assert "# Lines 101-200" in description assert "# Entire file" in description assert "close_all_others=True" in description assert "View specific portions of large files (e.g. functions or definitions)" in description @@ -1009,7 +1009,7 @@ def test_open_files_schema_descriptions(disable_pinecone, client: LettaSDKClient # Check offset field assert "offset" in file_request_properties offset_prop = file_request_properties["offset"] - expected_offset_desc = "Optional starting line number (1-indexed). If not specified, starts from beginning of file." + expected_offset_desc = "Optional offset for starting line number (0-indexed). If not specified, starts from beginning of file." assert offset_prop["description"] == expected_offset_desc assert offset_prop["type"] == "integer"