172 lines
5.0 KiB
TypeScript
172 lines
5.0 KiB
TypeScript
/**
|
|
* @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,
|
|
};
|
|
}
|
|
}
|