From dfac209c23f6f675c5db4bf008ced4b373c93bf9 Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:47:10 +0200 Subject: [PATCH] feat: Add multiply reading to read files tool --- CMakeLists.txt | 2 +- tools/ReadFileByPathTool.cpp | 202 ------------ tools/ReadFilesByPathTool.cpp | 289 ++++++++++++++++++ ...ByPathTool.hpp => ReadFilesByPathTool.hpp} | 14 +- tools/ToolsFactory.cpp | 4 +- 5 files changed, 304 insertions(+), 207 deletions(-) delete mode 100644 tools/ReadFileByPathTool.cpp create mode 100644 tools/ReadFilesByPathTool.cpp rename tools/{ReadFileByPathTool.hpp => ReadFilesByPathTool.hpp} (79%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f4c03c2..8b092b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,7 +115,7 @@ add_qtc_plugin(QodeAssist widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp QuickRefactorHandler.hpp QuickRefactorHandler.cpp tools/ToolsFactory.hpp tools/ToolsFactory.cpp - tools/ReadFileByPathTool.hpp tools/ReadFileByPathTool.cpp + tools/ReadFilesByPathTool.hpp tools/ReadFilesByPathTool.cpp tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp tools/ToolHandler.hpp tools/ToolHandler.cpp tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp diff --git a/tools/ReadFileByPathTool.cpp b/tools/ReadFileByPathTool.cpp deleted file mode 100644 index 629dee5..0000000 --- a/tools/ReadFileByPathTool.cpp +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -#include "ReadFileByPathTool.hpp" -#include "ToolExceptions.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Tools { - -ReadProjectFileByPathTool::ReadProjectFileByPathTool(QObject *parent) - : BaseTool(parent) - , m_ignoreManager(new Context::IgnoreManager(this)) -{} - -QString ReadProjectFileByPathTool::name() const -{ - return "read_project_file_by_path"; -} - -QString ReadProjectFileByPathTool::stringName() const -{ - return {"Reading project file"}; -} - -QString ReadProjectFileByPathTool::description() const -{ - return "Read content of a specific project file by its absolute path. " - "File must exist, be within project scope, and not excluded by .qodeassistignore."; -} - -QJsonObject ReadProjectFileByPathTool::getDefinition(LLMCore::ToolSchemaFormat format) const -{ - QJsonObject properties; - QJsonObject filepathProperty; - filepathProperty["type"] = "string"; - filepathProperty["description"] = "The absolute file path to read"; - properties["filepath"] = filepathProperty; - - QJsonObject definition; - definition["type"] = "object"; - definition["properties"] = properties; - - QJsonArray required; - required.append("filepath"); - definition["required"] = required; - - switch (format) { - case LLMCore::ToolSchemaFormat::OpenAI: - return customizeForOpenAI(definition); - case LLMCore::ToolSchemaFormat::Claude: - return customizeForClaude(definition); - case LLMCore::ToolSchemaFormat::Ollama: - return customizeForOllama(definition); - case LLMCore::ToolSchemaFormat::Google: - return customizeForGoogle(definition); - } - - return definition; -} - -LLMCore::ToolPermissions ReadProjectFileByPathTool::requiredPermissions() const -{ - return LLMCore::ToolPermission::FileSystemRead; -} - -QFuture ReadProjectFileByPathTool::executeAsync(const QJsonObject &input) -{ - return QtConcurrent::run([this, input]() -> QString { - QString filePath = input["filepath"].toString(); - if (filePath.isEmpty()) { - QString error = "Error: filepath parameter is required"; - throw ToolInvalidArgument(error); - } - - QFileInfo fileInfo(filePath); - LOG_MESSAGE(QString("Checking file: %1, exists: %2, isFile: %3") - .arg(filePath) - .arg(fileInfo.exists()) - .arg(fileInfo.isFile())); - - if (!fileInfo.exists() || !fileInfo.isFile()) { - QString error = QString("Error: File '%1' does not exist").arg(filePath); - throw ToolRuntimeError(error); - } - - QString canonicalPath = fileInfo.canonicalFilePath(); - LOG_MESSAGE(QString("Canonical path: %1").arg(canonicalPath)); - - bool isInProject = isFileInProject(canonicalPath); - - // Check if reading outside project is allowed - if (!isInProject) { - const auto &settings = Settings::generalSettings(); - if (!settings.allowReadOutsideProject()) { - QString error = QString("Error: File '%1' is not part of the project. " - "Enable 'Allow reading files outside project' in settings to access this file.") - .arg(filePath); - throw ToolRuntimeError(error); - } - LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath)); - } - - auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile( - Utils::FilePath::fromString(canonicalPath)) : nullptr; - if (isInProject && project && m_ignoreManager->shouldIgnore(canonicalPath, project)) { - QString error - = QString("Error: File '%1' is excluded by .qodeassistignore").arg(filePath); - throw ToolRuntimeError(error); - } - - // readFileContent throws exception if file cannot be opened - // If it returns, the file was read successfully (may be empty) - QString content = readFileContent(canonicalPath); - - // Return appropriate message for empty or non-empty files - if (content.isEmpty()) { - return QString("File: %1\n\nThe file is empty").arg(canonicalPath); - } - - return QString("File: %1\n\nContent:\n%2").arg(canonicalPath, content); - }); -} - -bool ReadProjectFileByPathTool::isFileInProject(const QString &filePath) const -{ - QList projects = ProjectExplorer::ProjectManager::projects(); - Utils::FilePath targetPath = Utils::FilePath::fromString(filePath); - - for (auto project : projects) { - if (!project) - continue; - - Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles); - for (const auto &projectFile : std::as_const(projectFiles)) { - if (projectFile == targetPath) { - return true; - } - } - - Utils::FilePath projectDir = project->projectDirectory(); - if (targetPath.isChildOf(projectDir)) { - return true; - } - } - - return false; -} - -QString ReadProjectFileByPathTool::readFileContent(const QString &filePath) const -{ - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - LOG_MESSAGE(QString("Could not open file: %1, error: %2").arg(filePath, file.errorString())); - throw ToolRuntimeError(QString("Error: Could not open file '%1': %2") - .arg(filePath, file.errorString())); - } - - QTextStream stream(&file); - stream.setAutoDetectUnicode(true); - QString content = stream.readAll(); - - file.close(); - - LOG_MESSAGE(QString("Successfully read file: %1, size: %2 bytes, isEmpty: %3") - .arg(filePath) - .arg(content.length()) - .arg(content.isEmpty())); - - // Always return valid QString (empty string for empty files) - return content; -} - -} // namespace QodeAssist::Tools diff --git a/tools/ReadFilesByPathTool.cpp b/tools/ReadFilesByPathTool.cpp new file mode 100644 index 0000000..81433e7 --- /dev/null +++ b/tools/ReadFilesByPathTool.cpp @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "ReadFilesByPathTool.hpp" +#include "ToolExceptions.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QodeAssist::Tools { + +ReadFilesByPathTool::ReadFilesByPathTool(QObject *parent) + : BaseTool(parent) + , m_ignoreManager(new Context::IgnoreManager(this)) +{} + +QString ReadFilesByPathTool::name() const +{ + return "read_files_by_path"; +} + +QString ReadFilesByPathTool::stringName() const +{ + return {"Reading file(s)"}; +} + +QString ReadFilesByPathTool::description() const +{ + return "Read content of one or multiple project files by absolute path(s). " + "Use 'filepath' for single file or 'filepaths' for multiple files (e.g., .h and .cpp). " + "Files must exist, be within project scope, and not excluded by .qodeassistignore."; +} + +QJsonObject ReadFilesByPathTool::getDefinition(LLMCore::ToolSchemaFormat format) const +{ + QJsonObject properties; + + QJsonObject filepathProperty; + filepathProperty["type"] = "string"; + filepathProperty["description"] = "The absolute file path to read (for single file)"; + properties["filepath"] = filepathProperty; + + QJsonObject filepathsProperty; + filepathsProperty["type"] = "array"; + QJsonObject itemsProperty; + itemsProperty["type"] = "string"; + filepathsProperty["items"] = itemsProperty; + filepathsProperty["description"] = "Array of absolute file paths to read (for multiple files, " + "e.g., both .h and .cpp)"; + properties["filepaths"] = filepathsProperty; + + QJsonObject definition; + definition["type"] = "object"; + definition["properties"] = properties; + definition["description"] = "Provide either 'filepath' for a single file or 'filepaths' for " + "multiple files"; + + switch (format) { + case LLMCore::ToolSchemaFormat::OpenAI: + return customizeForOpenAI(definition); + case LLMCore::ToolSchemaFormat::Claude: + return customizeForClaude(definition); + case LLMCore::ToolSchemaFormat::Ollama: + return customizeForOllama(definition); + case LLMCore::ToolSchemaFormat::Google: + return customizeForGoogle(definition); + } + + return definition; +} + +LLMCore::ToolPermissions ReadFilesByPathTool::requiredPermissions() const +{ + return LLMCore::ToolPermission::FileSystemRead; +} + +QFuture ReadFilesByPathTool::executeAsync(const QJsonObject &input) +{ + return QtConcurrent::run([this, input]() -> QString { + QStringList filePaths; + + // Check for single filepath + QString singlePath = input["filepath"].toString(); + if (!singlePath.isEmpty()) { + filePaths.append(singlePath); + } + + // Check for multiple filepaths + if (input.contains("filepaths") && input["filepaths"].isArray()) { + QJsonArray pathsArray = input["filepaths"].toArray(); + for (const auto &pathValue : pathsArray) { + QString path = pathValue.toString(); + if (!path.isEmpty()) { + filePaths.append(path); + } + } + } + + if (filePaths.isEmpty()) { + QString error = "Error: either 'filepath' or 'filepaths' parameter is required"; + throw ToolInvalidArgument(error); + } + + LOG_MESSAGE(QString("Processing %1 file(s)").arg(filePaths.size())); + + QList results; + for (const QString &filePath : filePaths) { + results.append(processFile(filePath)); + } + + return formatResults(results); + }); +} + +bool ReadFilesByPathTool::isFileInProject(const QString &filePath) const +{ + QList projects = ProjectExplorer::ProjectManager::projects(); + Utils::FilePath targetPath = Utils::FilePath::fromString(filePath); + + for (auto project : projects) { + if (!project) + continue; + + Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles); + for (const auto &projectFile : std::as_const(projectFiles)) { + if (projectFile == targetPath) { + return true; + } + } + + Utils::FilePath projectDir = project->projectDirectory(); + if (targetPath.isChildOf(projectDir)) { + return true; + } + } + + return false; +} + +QString ReadFilesByPathTool::readFileContent(const QString &filePath) const +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + LOG_MESSAGE(QString("Could not open file: %1, error: %2").arg(filePath, file.errorString())); + throw ToolRuntimeError( + QString("Error: Could not open file '%1': %2").arg(filePath, file.errorString())); + } + + QTextStream stream(&file); + stream.setAutoDetectUnicode(true); + QString content = stream.readAll(); + + file.close(); + + LOG_MESSAGE(QString("Successfully read file: %1, size: %2 bytes, isEmpty: %3") + .arg(filePath) + .arg(content.length()) + .arg(content.isEmpty())); + + return content; +} + +ReadFilesByPathTool::FileResult ReadFilesByPathTool::processFile(const QString &filePath) const +{ + FileResult result; + result.path = filePath; + result.success = false; + + try { + QFileInfo fileInfo(filePath); + LOG_MESSAGE(QString("Checking file: %1, exists: %2, isFile: %3") + .arg(filePath) + .arg(fileInfo.exists()) + .arg(fileInfo.isFile())); + + if (!fileInfo.exists() || !fileInfo.isFile()) { + result.error = QString("File does not exist"); + return result; + } + + QString canonicalPath = fileInfo.canonicalFilePath(); + LOG_MESSAGE(QString("Canonical path: %1").arg(canonicalPath)); + + bool isInProject = isFileInProject(canonicalPath); + + if (!isInProject) { + const auto &settings = Settings::generalSettings(); + if (!settings.allowReadOutsideProject()) { + result.error = QString( + "File is not part of the project. " + "Enable 'Allow reading files outside project' in settings " + "to access this file."); + return result; + } + LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath)); + } + + auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile( + Utils::FilePath::fromString(canonicalPath)) + : nullptr; + if (isInProject && project && m_ignoreManager->shouldIgnore(canonicalPath, project)) { + result.error = QString("File is excluded by .qodeassistignore"); + return result; + } + + result.content = readFileContent(canonicalPath); + result.success = true; + result.path = canonicalPath; // Use canonical path in result + + } catch (const ToolRuntimeError &e) { + result.error = e.message(); + } catch (const std::exception &e) { + result.error = QString("Unexpected error: %1").arg(e.what()); + } + + return result; +} + +QString ReadFilesByPathTool::formatResults(const QList &results) const +{ + if (results.size() == 1) { + // Single file format (backward compatibility) + const FileResult &result = results.first(); + if (!result.success) { + throw ToolRuntimeError(QString("Error: %1 - %2").arg(result.path, result.error)); + } + + if (result.content.isEmpty()) { + return QString("File: %1\n\nThe file is empty").arg(result.path); + } + + return QString("File: %1\n\nContent:\n%2").arg(result.path, result.content); + } + + // Multiple files format + QStringList output; + int successCount = 0; + + for (const FileResult &result : results) { + if (result.success) { + successCount++; + output.append(QString("=== File: %1 ===\n").arg(result.path)); + + if (result.content.isEmpty()) { + output.append("[Empty file]\n"); + } else { + output.append(result.content); + } + output.append("\n"); + } else { + output.append(QString("=== File: %1 ===\n").arg(result.path)); + output.append(QString("[Error: %1]\n\n").arg(result.error)); + } + } + + QString summary + = QString("Successfully read %1 of %2 file(s)\n\n").arg(successCount).arg(results.size()); + + return summary + output.join(""); +} + +} // namespace QodeAssist::Tools diff --git a/tools/ReadFileByPathTool.hpp b/tools/ReadFilesByPathTool.hpp similarity index 79% rename from tools/ReadFileByPathTool.hpp rename to tools/ReadFilesByPathTool.hpp index 96a2d53..5e751bc 100644 --- a/tools/ReadFileByPathTool.hpp +++ b/tools/ReadFilesByPathTool.hpp @@ -24,11 +24,11 @@ namespace QodeAssist::Tools { -class ReadProjectFileByPathTool : public LLMCore::BaseTool +class ReadFilesByPathTool : public LLMCore::BaseTool { Q_OBJECT public: - explicit ReadProjectFileByPathTool(QObject *parent = nullptr); + explicit ReadFilesByPathTool(QObject *parent = nullptr); QString name() const override; QString stringName() const override; @@ -39,8 +39,18 @@ public: QFuture executeAsync(const QJsonObject &input = QJsonObject()) override; private: + struct FileResult + { + QString path; + QString content; + bool success; + QString error; + }; + QString readFileContent(const QString &filePath) const; bool isFileInProject(const QString &filePath) const; + FileResult processFile(const QString &filePath) const; + QString formatResults(const QList &results) const; Context::IgnoreManager *m_ignoreManager; }; diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp index 3ae73b7..5cb5d6f 100644 --- a/tools/ToolsFactory.cpp +++ b/tools/ToolsFactory.cpp @@ -29,7 +29,7 @@ #include "FindSymbolTool.hpp" #include "GetIssuesListTool.hpp" #include "ListProjectFilesTool.hpp" -#include "ReadFileByPathTool.hpp" +#include "ReadFilesByPathTool.hpp" #include "ReadVisibleFilesTool.hpp" #include "SearchInProjectTool.hpp" @@ -44,7 +44,7 @@ ToolsFactory::ToolsFactory(QObject *parent) void ToolsFactory::registerTools() { registerTool(new ReadVisibleFilesTool(this)); - registerTool(new ReadProjectFileByPathTool(this)); + registerTool(new ReadFilesByPathTool(this)); registerTool(new ListProjectFilesTool(this)); registerTool(new SearchInProjectTool(this)); registerTool(new GetIssuesListTool(this));