Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/core/src/tools/read-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
}));

vi.mock('../utils/pdf.js', async (importOriginal) => {
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
const actual = await importOriginal<typeof import('../utils/pdf.js')>();
return {
...actual,
getPDFPageCount: async () => 31,
isPdftotextAvailable: async () => true,
};
});

describe('ReadFileTool', () => {
let tempRootDir: string;
let tool: ReadFileTool;
Expand Down Expand Up @@ -715,6 +724,37 @@ describe('ReadFileTool', () => {
return invocation.execute(abortSignal);
}

it('returns a short error when a text-only model reads a large PDF without pages', async () => {
const pdfPath = path.join(tempRootDir, 'large.pdf');
await fsp.writeFile(pdfPath, Buffer.alloc(2 * 1024 * 1024));
const textOnlyConfig = {
getFileService: () => new FileDiscoveryService(tempRootDir),
getFileSystemService: () => new StandardFileSystemService(),
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
storage: {
getProjectTempDir: () => path.join(tempRootDir, '.temp'),
getProjectDir: () => path.join(tempRootDir, '.project'),
getUserSkillsDirs: () => [
path.join(os.homedir(), '.qwen', 'skills'),
],
},
getTruncateToolOutputThreshold: () => 2500,
getTruncateToolOutputLines: () => 500,
getContentGeneratorConfig: () => ({ modalities: {} }),
getFileReadCache: () => fileReadCache,
getFileReadCacheDisabled: () => false,
} as unknown as Config;
const textOnlyTool = new ReadFileTool(textOnlyConfig);

const result = await read({ file_path: pdfPath }, textOnlyTool);

expect(result.error?.type).toBe(ToolErrorType.FILE_TOO_LARGE);
expect(String(result.llmContent).length).toBeLessThan(1000);
expect(result.llmContent).toContain('has 31 pages');
expect(result.llmContent).toContain("Use the 'pages' parameter");
});

it('returns the file_unchanged placeholder on a second full Read of an unchanged text file', async () => {
const filePath = path.join(tempRootDir, 'note.txt');
await fsp.writeFile(filePath, 'hello world', 'utf-8');
Expand Down
13 changes: 6 additions & 7 deletions packages/core/src/tools/read-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
getSpecificMimeType,
isCacheableReadResult,
} from '../utils/fileUtils.js';
import { parsePDFPageRange } from '../utils/pdf.js';
import { parsePDFPageRange, PDF_MAX_PAGES_PER_READ } from '../utils/pdf.js';
import type { Config } from '../config/config.js';
import { FileOperation } from '../telemetry/metrics.js';
import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js';
Expand Down Expand Up @@ -54,8 +54,7 @@ export interface ReadFileToolParams {

/**
* For PDF files, the page range to extract as text (e.g. "1-5", "3", "10-20").
* Pages are 1-indexed. Max 20 pages per request. Open-ended ranges like "3-"
* are not supported.
* Pages are 1-indexed. Open-ended ranges like "3-" are not supported.
*/
pages?: string;
}
Expand Down Expand Up @@ -394,7 +393,7 @@ export class ReadFileTool extends BaseDeclarativeTool<
super(
ReadFileTool.Name,
ToolDisplayNames.READ_FILE,
`Reads and returns the content of a specified file. The file_path argument MUST be an absolute path. Always construct it by combining the project root with the file's relative path (e.g. project root '/path/to/project/' + relative 'foo/bar.txt' = '/path/to/project/foo/bar.txt'). If the user provides a relative path, resolve it against the project root first. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), PDF files, and Jupyter notebooks (.ipynb). For text files, it can read specific line ranges. For PDF files, use the 'pages' parameter to extract specific page ranges as text (e.g. '1-5'). Max 20 pages per request. This tool can read Jupyter notebooks (.ipynb) and returns structured cell content with outputs.`,
`Reads and returns the content of a specified file. The file_path argument MUST be an absolute path. Always construct it by combining the project root with the file's relative path (e.g. project root '/path/to/project/' + relative 'foo/bar.txt' = '/path/to/project/foo/bar.txt'). If the user provides a relative path, resolve it against the project root first. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), PDF files, and Jupyter notebooks (.ipynb). For text files, it can read specific line ranges. For PDF files, use the 'pages' parameter to extract specific page ranges as text (e.g. '1-5'). Max ${PDF_MAX_PAGES_PER_READ} pages per request. Large PDFs cannot be read all at once when the model does not support native PDF input; retry with narrower page ranges if the tool reports a PDF is too large. This tool can read Jupyter notebooks (.ipynb) and returns structured cell content with outputs.`,
Kind.Read,
{
properties: {
Expand All @@ -415,7 +414,7 @@ export class ReadFileTool extends BaseDeclarativeTool<
},
pages: {
description:
"Optional: For PDF files, the page range to extract as text (e.g., '1-5', '3', '10-20'). Pages are 1-indexed. Max 20 pages per request. Open-ended ranges like '3-' are not supported. When provided, PDF content is extracted as text regardless of model capabilities.",
`Optional: For PDF files, the page range to extract as text (e.g., '1-5', '3', '10-20'). Pages are 1-indexed. Max ${PDF_MAX_PAGES_PER_READ} pages per request. Open-ended ranges like '3-' are not supported. Use this for large PDFs or when the model does not support native PDF input.`,
type: 'string',
},
},
Expand Down Expand Up @@ -471,9 +470,9 @@ export class ReadFileTool extends BaseDeclarativeTool<
return `Invalid pages parameter: '${params.pages}'. Use formats like '5' or '1-10'.`;
}
if (parsed.lastPage === Infinity) {
return `Open-ended page ranges (e.g. '3-') are not supported; specify an explicit end page within the 20-page limit (e.g. '3-22').`;
return `Open-ended page ranges (e.g. '3-') are not supported; specify an explicit end page within the ${PDF_MAX_PAGES_PER_READ}-page limit (e.g. '3-22').`;
}
const maxPages = 20;
const maxPages = PDF_MAX_PAGES_PER_READ;
if (parsed.lastPage - parsed.firstPage + 1 > maxPages) {
return `Pages range exceeds maximum of ${maxPages} pages per request.`;
}
Expand Down
Loading
Loading