File size: 5,122 Bytes
40e575e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'path';
import { SchemaValidator } from '../utils/schemaValidator.js';
import { makeRelative, shortenPath } from '../utils/paths.js';
import { BaseTool, ToolResult } from './tools.js';
import { isWithinRoot, processSingleFileContent } from '../utils/fileUtils.js';
import { Config } from '../config/config.js';
import { getSpecificMimeType } from '../utils/fileUtils.js';
import {
recordFileOperationMetric,
FileOperation,
} from '../telemetry/metrics.js';
/**
* Parameters for the ReadFile tool
*/
export interface ReadFileToolParams {
/**
* The absolute path to the file to read
*/
absolute_path: string;
/**
* The line number to start reading from (optional)
*/
offset?: number;
/**
* The number of lines to read (optional)
*/
limit?: number;
}
/**
* Implementation of the ReadFile tool logic
*/
export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> {
static readonly Name: string = 'read_file';
constructor(
private rootDirectory: string,
private config: Config,
) {
super(
ReadFileTool.Name,
'ReadFile',
'Reads and returns the content of a specified file from the local filesystem. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges.',
{
properties: {
absolute_path: {
description:
"The absolute path to the file to read (e.g., '/home/user/project/file.txt'). Relative paths are not supported. You must provide an absolute path.",
type: 'string',
pattern: '^/',
},
offset: {
description:
"Optional: For text files, the 0-based line number to start reading from. Requires 'limit' to be set. Use for paginating through large files.",
type: 'number',
},
limit: {
description:
"Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible, up to a default limit).",
type: 'number',
},
},
required: ['absolute_path'],
type: 'object',
},
);
this.rootDirectory = path.resolve(rootDirectory);
}
validateToolParams(params: ReadFileToolParams): string | null {
if (
this.schema.parameters &&
!SchemaValidator.validate(
this.schema.parameters as Record<string, unknown>,
params,
)
) {
return 'Parameters failed schema validation.';
}
const filePath = params.absolute_path;
if (!path.isAbsolute(filePath)) {
return `File path must be absolute, but was relative: ${filePath}. You must provide an absolute path.`;
}
if (!isWithinRoot(filePath, this.rootDirectory)) {
return `File path must be within the root directory (${this.rootDirectory}): ${filePath}`;
}
if (params.offset !== undefined && params.offset < 0) {
return 'Offset must be a non-negative number';
}
if (params.limit !== undefined && params.limit <= 0) {
return 'Limit must be a positive number';
}
const fileService = this.config.getFileService();
if (fileService.shouldGeminiIgnoreFile(params.absolute_path)) {
const relativePath = makeRelative(
params.absolute_path,
this.rootDirectory,
);
return `File path '${shortenPath(relativePath)}' is ignored by .geminiignore pattern(s).`;
}
return null;
}
getDescription(params: ReadFileToolParams): string {
if (
!params ||
typeof params.absolute_path !== 'string' ||
params.absolute_path.trim() === ''
) {
return `Path unavailable`;
}
const relativePath = makeRelative(params.absolute_path, this.rootDirectory);
return shortenPath(relativePath);
}
async execute(
params: ReadFileToolParams,
_signal: AbortSignal,
): Promise<ToolResult> {
const validationError = this.validateToolParams(params);
if (validationError) {
return {
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
returnDisplay: validationError,
};
}
const result = await processSingleFileContent(
params.absolute_path,
this.rootDirectory,
params.offset,
params.limit,
);
if (result.error) {
return {
llmContent: result.error, // The detailed error for LLM
returnDisplay: result.returnDisplay, // User-friendly error
};
}
const lines =
typeof result.llmContent === 'string'
? result.llmContent.split('\n').length
: undefined;
const mimetype = getSpecificMimeType(params.absolute_path);
recordFileOperationMetric(
this.config,
FileOperation.READ,
lines,
mimetype,
path.extname(params.absolute_path),
);
return {
llmContent: result.llmContent,
returnDisplay: result.returnDisplay,
};
}
}
|