"""Read file tool."""
from pydantic import BaseModel, Field
from ..bot import BotContext
from .base import ToolResult
from .constants import MAX_FILE_READ_CHARS
from .registry import folder_bot, get_services
[docs]
class ReadFileRequest(BaseModel, frozen=True):
"""Request for reading a file."""
path: str = Field(
description="File path relative to the root folder (e.g., 'notes/todo.md')"
)
[docs]
@folder_bot.tool(
name="read_file",
request_type=ReadFileRequest,
response_type=ToolResult,
)
async def read_file(
request: ReadFileRequest, _context: BotContext | None = None
) -> ToolResult:
"""Read the contents of a specific file.
The path must be relative to the root folder.
Use list_files first to discover available files.
"""
services = get_services(_context)
if services is None:
return ToolResult(content="Services not available", is_error=True)
file_path = services.validate_path(request.path)
if file_path is None:
return ToolResult(content="Invalid path: access denied", is_error=True)
if not file_path.exists():
return ToolResult(content=f"File not found: {request.path}", is_error=True)
if not file_path.is_file():
return ToolResult(content=f"Not a file: {request.path}", is_error=True)
rel_path = str(file_path.relative_to(services.root))
if not services.is_file_allowed(rel_path):
return ToolResult(content=f"File not accessible: {request.path}", is_error=True)
try:
content = file_path.read_text(encoding="utf-8", errors="replace")
if len(content) > MAX_FILE_READ_CHARS:
content = (
content[:MAX_FILE_READ_CHARS]
+ f"\n\n[Truncated at {MAX_FILE_READ_CHARS} characters]"
)
return ToolResult(content=content)
except Exception as e:
return ToolResult(content=f"Error reading file: {e}", is_error=True)