#include "filetool.hpp" #include #include #include namespace ZShell::llm { namespace { QJsonObject makeOutput(const QString& text) { QJsonObject obj; obj[QStringLiteral("output")] = text; return obj; } QJsonObject makeError(const QString& message) { QJsonObject obj; obj[QStringLiteral("error")] = message; return obj; } } // namespace FileReadTool::FileReadTool(QObject* parent) : LlmTool(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 stored under /tmp/zshell-llm/webfetch/ 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 QString& toolCallId, const QJsonObject& args, std::function done) { Q_UNUSED(toolCallId); const QString path = args[QStringLiteral("path")].toString().trimmed(); if (path.isEmpty()) { done(makeError(QStringLiteral("Missing required argument 'path'"))); return; } const int offset = qMax(0, args[QStringLiteral("offset")].toInt(0)); const int budget = inlineBudgetChars(); 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(makeError(QStringLiteral("Path is a directory, not a " "file: %1") .arg(path))); } else { done(makeError(QStringLiteral("Cannot open file: %1").arg(path))); } return; } const qint64 fileSize = file.size(); if (fileSize == 0) { file.close(); done(makeOutput(QString())); return; } if (offset >= fileSize) { file.close(); done(makeError(QStringLiteral("Offset %1 is beyond the end of the " "file (%2 bytes)") .arg(offset) .arg(fileSize))); return; } if (!file.seek(offset)) { file.close(); done(makeError(QStringLiteral("Cannot seek in file: %1").arg(path))); return; } // UTF-8 uses at most 4 bytes per character, so this many bytes always // cover `limit` characters. const qint64 wanted = qint64(limit) * 4 + 16; const QByteArray chunk = file.read(qMin(wanted, fileSize - offset)); file.close(); if (chunk.contains('\0')) { done(makeError(QStringLiteral("Binary files are not supported: %1") .arg(path))); return; } const QString content = QString::fromUtf8(chunk); if (content.size() <= limit) { done(makeOutput(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(makeOutput(output)); } } // namespace ZShell::llm