qwen-code/packages/core/src/tools/read-file.ts

183 lines
5.1 KiB
TypeScript
Raw Normal View History

/**
* @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 {
BaseDeclarativeTool,
Icon,
ToolInvocation,
ToolLocation,
ToolResult,
} from './tools.js';
import { Type } from '@google/genai';
import {
processSingleFileContent,
getSpecificMimeType,
} from '../utils/fileUtils.js';
import { Config } from '../config/config.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;
}
class ReadFileToolInvocation
implements ToolInvocation<ReadFileToolParams, ToolResult>
{
constructor(
private config: Config,
public params: ReadFileToolParams,
) {}
getDescription(): string {
const relativePath = makeRelative(
this.params.absolute_path,
this.config.getTargetDir(),
);
return shortenPath(relativePath);
}
toolLocations(): ToolLocation[] {
return [{ path: this.params.absolute_path, line: this.params.offset }];
}
shouldConfirmExecute(): Promise<false> {
return Promise.resolve(false);
}
async execute(): Promise<ToolResult> {
const result = await processSingleFileContent(
this.params.absolute_path,
this.config.getTargetDir(),
this.params.offset,
this.params.limit,
);
if (result.error) {
return {
llmContent: result.error, // The detailed error for LLM
returnDisplay: result.returnDisplay || 'Error reading file', // User-friendly error
};
}
const lines =
typeof result.llmContent === 'string'
? result.llmContent.split('\n').length
: undefined;
const mimetype = getSpecificMimeType(this.params.absolute_path);
recordFileOperationMetric(
this.config,
FileOperation.READ,
lines,
mimetype,
path.extname(this.params.absolute_path),
);
return {
llmContent: result.llmContent || '',
returnDisplay: result.returnDisplay || '',
};
}
}
/**
* Implementation of the ReadFile tool logic
*/
export class ReadFileTool extends BaseDeclarativeTool<
ReadFileToolParams,
ToolResult
> {
static readonly Name: string = 'read_file';
constructor(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.',
Icon.FileSearch,
{
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: Type.STRING,
},
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: 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: Type.NUMBER,
},
},
required: ['absolute_path'],
type: Type.OBJECT,
},
);
}
protected validateToolParams(params: ReadFileToolParams): string | null {
const errors = SchemaValidator.validate(this.schema.parameters, params);
if (errors) {
return errors;
}
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.`;
}
const workspaceContext = this.config.getWorkspaceContext();
if (!workspaceContext.isPathWithinWorkspace(filePath)) {
const directories = workspaceContext.getDirectories();
return `File path must be within one of the workspace directories: ${directories.join(', ')}`;
}
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)) {
return `File path '${filePath}' is ignored by .geminiignore pattern(s).`;
}
return null;
}
protected createInvocation(
params: ReadFileToolParams,
): ToolInvocation<ReadFileToolParams, ToolResult> {
return new ReadFileToolInvocation(this.config, params);
}
}