#include "filetool.hpp" #include #include #include namespace ZShell::llm { FileReadTool::FileReadTool(ToolRegistry* registry, QObject* parent) : LlmTool(registry, parent) {} QString FileReadTool::name() const { return QStringLiteral("readfile"); } QString FileReadTool::description() const { return QStringLiteral( "Read a text file from the local filesystem and return part of " "its content. At most a limited number of characters is returned " "per call; use offset and limit to page through larger files. " "Binary files are not supported. Full output of webfetch calls " "is saved in the sandbox directory and can be read this way. " "This tool is read-only."); } QJsonObject FileReadTool::parameters() const { QJsonObject path; path[QStringLiteral("type")] = QStringLiteral("string"); path[QStringLiteral("description")] = QStringLiteral("Path of the file to read"); QJsonObject offset; offset[QStringLiteral("type")] = QStringLiteral("integer"); offset[QStringLiteral("minimum")] = 0; offset[QStringLiteral("description")] = QStringLiteral("Byte offset to start reading from. Defaults to 0."); QJsonObject limit; limit[QStringLiteral("type")] = QStringLiteral("integer"); limit[QStringLiteral("minimum")] = 1; limit[QStringLiteral("description")] = QStringLiteral( "Maximum number of characters to return. Defaults " "to a value that fits the model's context window."); QJsonObject properties; properties[QStringLiteral("path")] = path; properties[QStringLiteral("offset")] = offset; properties[QStringLiteral("limit")] = limit; QJsonObject schema; schema[QStringLiteral("type")] = QStringLiteral("object"); schema[QStringLiteral("properties")] = properties; QJsonArray required; required.append(QStringLiteral("path")); schema[QStringLiteral("required")] = required; return schema; } void FileReadTool::execute( const ToolContext& context, const QJsonObject& args, ToolDone done) { const QString path = args[QStringLiteral("path")].toString().trimmed(); if (path.isEmpty()) { done({false, QStringLiteral("Missing required argument 'path'")}); return; } const int offset = qMax(0, args[QStringLiteral("offset")].toInt(0)); const int budget = context.outputBudgetChars > 0 ? context.outputBudgetChars : registry()->outputBudgetChars(1); const int limit = qBound(1, args[QStringLiteral("limit")].toInt(budget), budget); QFile file(path); if (!file.open(QIODevice::ReadOnly)) { const QFileInfo info(path); if (info.exists() && info.isDir()) { done( {false, QStringLiteral("Path is a directory, not a file: %1") .arg(path)}); } else { done({false, QStringLiteral("Cannot open file: %1").arg(path)}); } return; } const qint64 fileSize = file.size(); if (fileSize == 0) { file.close(); done({true, QString()}); return; } if (offset >= fileSize) { file.close(); done( {false, QStringLiteral("Offset %1 is beyond the end of the file (%2 bytes)") .arg(offset) .arg(fileSize)}); return; } if (!file.seek(offset)) { file.close(); done({false, QStringLiteral("Cannot seek in file: %1").arg(path)}); return; } const qint64 wanted = qint64(limit) * 4 + 16; const QByteArray chunk = file.read(qMin(wanted, fileSize - offset)); file.close(); if (chunk.contains('\0')) { done( {false, QStringLiteral("Binary files are not supported: %1").arg(path)}); return; } const QString content = QString::fromUtf8(chunk); if (content.size() <= limit) { done({true, content}); return; } const QString head = content.left(limit); const int nextOffset = offset + head.toUtf8().size(); const QString output = head + QStringLiteral( "\n\n[... truncated: showing %1 characters (bytes %2-%3 of a " "%4 byte file). Continue with offset=%5.]") .arg(limit) .arg(offset) .arg(nextOffset) .arg(fileSize) .arg(nextOffset); done({true, output}); } } // namespace ZShell::llm