C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 2m18s
C++ / clang-tidy (pull_request) Failing after 2m7s
JS/TS / fmt (pull_request) Failing after 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m38s
Rust / fmt (pull_request) Successful in 26s
Rust / clippy (pull_request) Successful in 47s
152 lines
4.3 KiB
C++
152 lines
4.3 KiB
C++
#include "filetool.hpp"
|
|
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QJsonArray>
|
|
|
|
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<void(const QJsonObject&)> 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
|