|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface ReadFileToolParams { |
|
|
|
|
|
|
|
|
|
|
|
absolute_path: string; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
offset?: number; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
limit?: number; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
returnDisplay: result.returnDisplay, |
|
|
}; |
|
|
} |
|
|
|
|
|
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, |
|
|
}; |
|
|
} |
|
|
} |
|
|
|