mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-20 00:12:50 +00:00
209 lines
6 KiB
Python
209 lines
6 KiB
Python
"""fs utils"""
|
|
|
|
from typing import Literal
|
|
|
|
from ...schema import TruncationResult
|
|
|
|
# Default limits for output truncation
|
|
DEFAULT_MAX_LINES = 1000 # Maximum lines to keep for tail truncation
|
|
DEFAULT_MAX_BYTES = 30 * 1024 # Maximum bytes to keep (30KB)
|
|
|
|
# Find tool limits
|
|
FIND_MAX_LINES = 2000 # Maximum lines for find output
|
|
FIND_MAX_BYTES = 50 * 1024 # 50KB for find output
|
|
|
|
# Grep tool limits
|
|
GREP_MAX_LINE_LENGTH = 500 # Maximum line length for grep output
|
|
|
|
|
|
def format_size(num_bytes: int) -> str:
|
|
"""Format byte size in human-readable format.
|
|
|
|
Args:
|
|
num_bytes: Number of bytes
|
|
|
|
Returns:
|
|
Formatted string (e.g., "1.5KB", "2.3MB")
|
|
"""
|
|
if num_bytes < 1024:
|
|
return f"{num_bytes}B"
|
|
elif num_bytes < 1024 * 1024:
|
|
return f"{num_bytes / 1024:.1f}KB"
|
|
else:
|
|
return f"{num_bytes / (1024 * 1024):.1f}MB"
|
|
|
|
|
|
def truncate_line(text: str, max_length: int = GREP_MAX_LINE_LENGTH) -> tuple[str, bool]:
|
|
"""Truncate a single line if it exceeds max length.
|
|
|
|
Args:
|
|
text: Line text
|
|
max_length: Maximum line length
|
|
|
|
Returns:
|
|
Tuple of (truncated_text, was_truncated)
|
|
"""
|
|
if len(text) <= max_length:
|
|
return text, False
|
|
return text[:max_length] + "...", True
|
|
|
|
|
|
def truncate_tail(
|
|
text: str,
|
|
max_lines: int = DEFAULT_MAX_LINES,
|
|
max_bytes: int = DEFAULT_MAX_BYTES,
|
|
) -> TruncationResult:
|
|
"""Truncate text to keep only the tail (last portion).
|
|
|
|
Keeps the last N lines or M bytes, whichever is hit first.
|
|
This is useful for command outputs where the end is most relevant.
|
|
|
|
Args:
|
|
text: The text to truncate
|
|
max_lines: Maximum number of lines to keep
|
|
max_bytes: Maximum bytes to keep
|
|
|
|
Returns:
|
|
TruncationResult with truncated content and metadata
|
|
"""
|
|
if not text:
|
|
return TruncationResult(
|
|
content="",
|
|
truncated=False,
|
|
total_lines=0,
|
|
output_lines=0,
|
|
total_bytes=0,
|
|
output_bytes=0,
|
|
)
|
|
|
|
total_bytes = len(text.encode("utf-8"))
|
|
lines = text.split("\n")
|
|
total_lines = len(lines)
|
|
|
|
# Check if we need to truncate
|
|
if total_lines <= max_lines and total_bytes <= max_bytes:
|
|
return TruncationResult(
|
|
content=text,
|
|
truncated=False,
|
|
total_lines=total_lines,
|
|
output_lines=total_lines,
|
|
total_bytes=total_bytes,
|
|
output_bytes=total_bytes,
|
|
)
|
|
|
|
# Keep last N lines
|
|
kept_lines = lines[-max_lines:] if total_lines > max_lines else lines
|
|
truncated_by: Literal["lines", "bytes"] = "lines" if total_lines > max_lines else "bytes"
|
|
|
|
# Check byte limit on kept lines
|
|
kept_text = "\n".join(kept_lines)
|
|
kept_bytes = len(kept_text.encode("utf-8"))
|
|
|
|
# If still over byte limit, truncate further
|
|
last_line_partial = False
|
|
if kept_bytes > max_bytes:
|
|
truncated_by = "bytes"
|
|
# Keep truncating from the start until under byte limit
|
|
while kept_lines and len("\n".join(kept_lines).encode("utf-8")) > max_bytes:
|
|
kept_lines.pop(0)
|
|
|
|
# If still over (single line > max_bytes), truncate the line itself
|
|
if kept_lines and len("\n".join(kept_lines).encode("utf-8")) > max_bytes:
|
|
last_line = kept_lines[-1]
|
|
# Binary search to find how much of last line fits
|
|
encoded = last_line.encode("utf-8")
|
|
if len(encoded) > max_bytes:
|
|
last_line_partial = True
|
|
# Take last max_bytes of the line
|
|
kept_lines[-1] = encoded[-max_bytes:].decode("utf-8", errors="ignore")
|
|
|
|
kept_text = "\n".join(kept_lines)
|
|
kept_bytes = len(kept_text.encode("utf-8"))
|
|
|
|
return TruncationResult(
|
|
content=kept_text,
|
|
truncated=True,
|
|
total_lines=total_lines,
|
|
output_lines=len(kept_lines),
|
|
total_bytes=total_bytes,
|
|
output_bytes=kept_bytes,
|
|
truncated_by=truncated_by,
|
|
last_line_partial=last_line_partial,
|
|
)
|
|
|
|
|
|
def truncate_head(
|
|
text: str,
|
|
max_lines: int = FIND_MAX_LINES,
|
|
max_bytes: int = FIND_MAX_BYTES,
|
|
) -> TruncationResult:
|
|
"""Truncate text to keep only the head (first portion).
|
|
|
|
Keeps the first N lines or M bytes, whichever is hit first.
|
|
Suitable for file reads where you want to see the beginning.
|
|
|
|
Args:
|
|
text: The text to truncate
|
|
max_lines: Maximum number of lines to keep
|
|
max_bytes: Maximum bytes to keep
|
|
|
|
Returns:
|
|
TruncationResult with truncated content and metadata
|
|
"""
|
|
if not text:
|
|
return TruncationResult(
|
|
content="",
|
|
truncated=False,
|
|
total_lines=0,
|
|
output_lines=0,
|
|
total_bytes=0,
|
|
output_bytes=0,
|
|
)
|
|
|
|
total_bytes = len(text.encode("utf-8"))
|
|
lines = text.split("\n")
|
|
total_lines = len(lines)
|
|
|
|
# Check if no truncation needed
|
|
if total_lines <= max_lines and total_bytes <= max_bytes:
|
|
return TruncationResult(
|
|
content=text,
|
|
truncated=False,
|
|
total_lines=total_lines,
|
|
output_lines=total_lines,
|
|
total_bytes=total_bytes,
|
|
output_bytes=total_bytes,
|
|
)
|
|
|
|
# Collect complete lines that fit
|
|
kept_lines = []
|
|
kept_bytes = 0
|
|
truncated_by: Literal["lines", "bytes"] = "lines"
|
|
|
|
for i, line in enumerate(lines):
|
|
if i >= max_lines:
|
|
truncated_by = "lines"
|
|
break
|
|
|
|
# Calculate bytes for this line (+1 for newline except first line)
|
|
line_bytes = len(line.encode("utf-8")) + (1 if i > 0 else 0)
|
|
|
|
if kept_bytes + line_bytes > max_bytes:
|
|
truncated_by = "bytes"
|
|
break
|
|
|
|
kept_lines.append(line)
|
|
kept_bytes += line_bytes
|
|
|
|
kept_text = "\n".join(kept_lines)
|
|
final_bytes = len(kept_text.encode("utf-8"))
|
|
|
|
return TruncationResult(
|
|
content=kept_text,
|
|
truncated=True,
|
|
total_lines=total_lines,
|
|
output_lines=len(kept_lines),
|
|
total_bytes=total_bytes,
|
|
output_bytes=final_bytes,
|
|
truncated_by=truncated_by,
|
|
)
|