diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c38b1e..044e313 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,10 @@ add_qtc_plugin(QodeAssist widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp QuickRefactorHandler.hpp QuickRefactorHandler.cpp + tools/ToolsFactory.hpp tools/ToolsFactory.cpp + tools/ReadProjectFileByNameTool.hpp tools/ReadProjectFileByNameTool.cpp + tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp + tools/ToolHandler.hpp tools/ToolHandler.cpp ) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) diff --git a/llmcore/BaseTool.cpp b/llmcore/BaseTool.cpp new file mode 100644 index 0000000..bd4a62e --- /dev/null +++ b/llmcore/BaseTool.cpp @@ -0,0 +1,88 @@ +/* + * 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 "BaseTool.hpp" + +namespace QodeAssist::LLMCore { + +BaseTool::BaseTool(QObject *parent) + : QObject(parent) +{} + +QJsonObject BaseTool::getDefinition(ToolSchemaFormat format) const +{ + QJsonObject properties; + QJsonObject filenameProperty; + filenameProperty["type"] = "string"; + filenameProperty["description"] = "The filename or relative path to read"; + properties["filename"] = filenameProperty; + + QJsonObject definition; + definition["type"] = "object"; + definition["properties"] = properties; + + QJsonArray required; + required.append("filename"); + definition["required"] = required; + + switch (format) { + case LLMCore::ToolSchemaFormat::OpenAI: + definition = customizeForOpenAI(definition); + break; + case LLMCore::ToolSchemaFormat::Claude: + definition = customizeForClaude(definition); + break; + case LLMCore::ToolSchemaFormat::Ollama: + definition = customizeForOllama(definition); + break; + } + + return definition; +} + +QJsonObject BaseTool::customizeForOpenAI(const QJsonObject &baseDefinition) const +{ + QJsonObject function; + function["name"] = name(); + function["description"] = description(); + function["parameters"] = baseDefinition; + + QJsonObject tool; + tool["type"] = "function"; + tool["function"] = function; + + return tool; +} + +QJsonObject BaseTool::customizeForClaude(const QJsonObject &baseDefinition) const +{ + QJsonObject tool; + tool["name"] = name(); + tool["description"] = description(); + tool["input_schema"] = baseDefinition; + + return tool; +} + +QJsonObject BaseTool::customizeForOllama(const QJsonObject &baseDefinition) const +{ + return customizeForOpenAI(baseDefinition); +} + +} // namespace QodeAssist::LLMCore diff --git a/llmcore/ITool.hpp b/llmcore/BaseTool.hpp similarity index 60% rename from llmcore/ITool.hpp rename to llmcore/BaseTool.hpp index ebe893b..6a4c6fd 100644 --- a/llmcore/ITool.hpp +++ b/llmcore/BaseTool.hpp @@ -19,31 +19,33 @@ #pragma once +#include +#include #include #include #include namespace QodeAssist::LLMCore { -class ITool : public QObject +enum class ToolSchemaFormat { OpenAI, Claude, Ollama }; + +class BaseTool : public QObject { Q_OBJECT - public: - explicit ITool(QObject *parent = nullptr) - : QObject(parent) - {} - virtual ~ITool() = default; + explicit BaseTool(QObject *parent = nullptr); + ~BaseTool() override = default; virtual QString name() const = 0; virtual QString description() const = 0; - virtual QJsonObject getDefinition() const = 0; - virtual QString execute(const QJsonObject &input = QJsonObject()) = 0; + virtual QJsonObject getDefinition(ToolSchemaFormat format) const; -signals: - void toolStarted(const QString &toolName); - void toolCompleted(const QString &toolName, const QString &result); - void toolFailed(const QString &toolName, const QString &error); + virtual QFuture executeAsync(const QJsonObject &input = QJsonObject()) = 0; + +protected: + virtual QJsonObject customizeForOpenAI(const QJsonObject &baseDefinition) const; + virtual QJsonObject customizeForClaude(const QJsonObject &baseDefinition) const; + virtual QJsonObject customizeForOllama(const QJsonObject &baseDefinition) const; }; } // namespace QodeAssist::LLMCore diff --git a/llmcore/CMakeLists.txt b/llmcore/CMakeLists.txt index 272a44b..7b6d847 100644 --- a/llmcore/CMakeLists.txt +++ b/llmcore/CMakeLists.txt @@ -17,8 +17,8 @@ add_library(LLMCore STATIC HttpClient.hpp HttpClient.cpp DataBuffers.hpp SSEBuffer.hpp SSEBuffer.cpp - ITool.hpp - IToolsFactory.hpp + BaseTool.hpp + BaseTool.cpp ) target_link_libraries(LLMCore diff --git a/tools/ReadProjectFileByNameTool.cpp b/tools/ReadProjectFileByNameTool.cpp new file mode 100644 index 0000000..7776ebc --- /dev/null +++ b/tools/ReadProjectFileByNameTool.cpp @@ -0,0 +1,135 @@ +/* + * 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 "ReadProjectFileByNameTool.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QodeAssist::Tools { + +ReadProjectFileByNameTool::ReadProjectFileByNameTool(QObject *parent) + : BaseTool(parent) +{} + +QString ReadProjectFileByNameTool::name() const +{ + return "read_project_file_by_name"; +} + +QString ReadProjectFileByNameTool::description() const +{ + return "Read the content of a specific file from the current project by providing its filename " + "or " + "relative path. This tool searches for files within the project scope and supports:\n" + "- Exact filename match (e.g., 'main.cpp')\n" + "- Relative path from project root (e.g., 'src/utils/helper.cpp')\n" + "- Partial path matching (e.g., 'utils/helper.cpp')\n" + "- Case-insensitive filename search as fallback\n" + "Input parameter: 'filename' - the name or path of the file to read.\n" + "Use this when you need to examine specific project files that are not currently open " + "in the editor."; +} + +QFuture ReadProjectFileByNameTool::executeAsync(const QJsonObject &input) +{ + return QtConcurrent::run([this, input]() -> QString { + QString filename = input["filename"].toString(); + if (filename.isEmpty()) { + QString error = "Error: filename parameter is required"; + throw std::invalid_argument(error.toStdString()); + } + + QString filePath = findFileInProject(filename); + if (filePath.isEmpty()) { + QString error = QString("Error: File '%1' not found").arg(filename); + throw std::runtime_error(error.toStdString()); + } + + QString content = readFileContent(filePath); + if (content.isNull()) { + QString error = QString("Error: Could not read file '%1'").arg(filePath); + throw std::runtime_error(error.toStdString()); + } + + QString result = QString("File: %1\n\nContent:\n%2").arg(filePath, content); + return result; + }); +} + +QString ReadProjectFileByNameTool::findFileInProject(const QString &fileName) const +{ + auto project = ProjectExplorer::ProjectManager::startupProject(); + if (!project) { + LOG_MESSAGE("No startup project found"); + return QString(); + } + + Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles); + + for (const auto &projectFile : projectFiles) { + QFileInfo fileInfo(projectFile.path()); + if (fileInfo.fileName() == fileName) { + return projectFile.path(); + } + } + + for (const auto &projectFile : projectFiles) { + if (projectFile.endsWith(fileName)) { + return projectFile.path(); + } + } + + for (const auto &projectFile : projectFiles) { + QFileInfo fileInfo(projectFile.path()); + if (fileInfo.fileName().contains(fileName, Qt::CaseInsensitive)) { + return projectFile.path(); + } + } + + return QString(); +} + +QString ReadProjectFileByNameTool::readFileContent(const QString &filePath) const +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + LOG_MESSAGE(QString("Could not open file: %1").arg(filePath)); + return QString(); + } + + QTextStream stream(&file); + stream.setAutoDetectUnicode(true); + QString content = stream.readAll(); + + file.close(); + return content; +} + +} // namespace QodeAssist::Tools diff --git a/tools/ReadProjectFileByNameTool.hpp b/tools/ReadProjectFileByNameTool.hpp new file mode 100644 index 0000000..8f40798 --- /dev/null +++ b/tools/ReadProjectFileByNameTool.hpp @@ -0,0 +1,41 @@ +/* + * 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 . + */ + +#pragma once + +#include "llmcore/BaseTool.hpp" + +namespace QodeAssist::Tools { + +class ReadProjectFileByNameTool : public LLMCore::BaseTool +{ + Q_OBJECT +public: + explicit ReadProjectFileByNameTool(QObject *parent = nullptr); + + QString name() const override; + QString description() const override; + QFuture executeAsync(const QJsonObject &input = QJsonObject()) override; + +private: + QString findFileInProject(const QString &fileName) const; + QString readFileContent(const QString &filePath) const; +}; + +} // namespace QodeAssist::Tools diff --git a/tools/ReadVisibleFilesTool.cpp b/tools/ReadVisibleFilesTool.cpp new file mode 100644 index 0000000..035120b --- /dev/null +++ b/tools/ReadVisibleFilesTool.cpp @@ -0,0 +1,86 @@ +/* + * 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 "ReadVisibleFilesTool.hpp" + +#include +#include +#include +#include +#include +#include + +namespace QodeAssist::Tools { + +ReadVisibleFilesTool::ReadVisibleFilesTool(QObject *parent) + : BaseTool(parent) +{} + +QString ReadVisibleFilesTool::name() const +{ + return "read_visible_files"; +} + +QString ReadVisibleFilesTool::description() const +{ + return "Read the content of all currently visible files in editor tabs. " + "Returns content from all open tabs that are currently visible, including unsaved " + "changes. " + "No parameters required."; +} + +QFuture ReadVisibleFilesTool::executeAsync(const QJsonObject &input) +{ + Q_UNUSED(input) + + return QtConcurrent::run([this]() -> QString { + auto editors = Core::EditorManager::visibleEditors(); + + if (editors.isEmpty()) { + QString error = "Error: No visible files in the editor"; + throw std::runtime_error(error.toStdString()); + } + + QStringList results; + + for (auto editor : editors) { + if (!editor || !editor->document()) { + continue; + } + + QString filePath = editor->document()->filePath().toFSPathString(); + QByteArray contentBytes = editor->document()->contents(); + QString fileContent = QString::fromUtf8(contentBytes); + + QString fileResult; + if (fileContent.isEmpty()) { + fileResult + = QString("File: %1\n\nThe file is empty or could not be read").arg(filePath); + } else { + fileResult = QString("File: %1\n\nContent:\n%2").arg(filePath, fileContent); + } + + results.append(fileResult); + } + + return results.join("\n\n" + QString(80, '=') + "\n\n"); + }); +} + +} // namespace QodeAssist::Tools diff --git a/llmcore/IToolsFactory.hpp b/tools/ReadVisibleFilesTool.hpp similarity index 66% rename from llmcore/IToolsFactory.hpp rename to tools/ReadVisibleFilesTool.hpp index 8f47c13..584e99d 100644 --- a/llmcore/IToolsFactory.hpp +++ b/tools/ReadVisibleFilesTool.hpp @@ -19,20 +19,19 @@ #pragma once -#include -#include +#include "llmcore/BaseTool.hpp" -#include "ITool.hpp" +namespace QodeAssist::Tools { -namespace QodeAssist::LLMCore { - -class IToolsFactory +class ReadVisibleFilesTool : public LLMCore::BaseTool { + Q_OBJECT public: - virtual ~IToolsFactory() = default; - virtual QList getAvailableTools() const = 0; - virtual ITool *getToolByName(const QString &name) const = 0; - virtual QJsonArray getToolsDefinitions() const = 0; + explicit ReadVisibleFilesTool(QObject *parent = nullptr); + + QString name() const override; + QString description() const override; + QFuture executeAsync(const QJsonObject &input = QJsonObject()) override; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::Tools diff --git a/tools/ToolHandler.cpp b/tools/ToolHandler.cpp new file mode 100644 index 0000000..e16c887 --- /dev/null +++ b/tools/ToolHandler.cpp @@ -0,0 +1,106 @@ +/* + * 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 "ToolHandler.hpp" + +#include +#include +#include + +#include "logger/Logger.hpp" + +namespace QodeAssist::Tools { + +ToolHandler::ToolHandler(QObject *parent) + : QObject(parent) +{} + +QFuture ToolHandler::executeToolAsync( + const QString &requestId, + const QString &toolId, + LLMCore::BaseTool *tool, + const QJsonObject &input) +{ + if (!tool) { + return QtConcurrent::run([]() -> QString { throw std::runtime_error("Tool is null"); }); + } + + auto execution = new ToolExecution(); + execution->requestId = requestId; + execution->toolId = toolId; + execution->toolName = tool->name(); + execution->watcher = new QFutureWatcher(this); + + connect(execution->watcher, &QFutureWatcher::finished, this, [this, toolId]() { + onToolExecutionFinished(toolId); + }); + + LOG_MESSAGE(QString("Starting tool execution: %1 (ID: %2)").arg(tool->name(), toolId)); + + auto future = tool->executeAsync(input); + execution->watcher->setFuture(future); + m_activeExecutions.insert(toolId, execution); + + return future; +} + +void ToolHandler::cleanupRequest(const QString &requestId) +{ + auto it = m_activeExecutions.begin(); + while (it != m_activeExecutions.end()) { + if (it.value()->requestId == requestId) { + auto execution = it.value(); + LOG_MESSAGE( + QString("Canceling tool %1 for request %2").arg(execution->toolName, requestId)); + + if (execution->watcher) { + execution->watcher->cancel(); + execution->watcher->deleteLater(); + } + delete execution; + it = m_activeExecutions.erase(it); + } else { + ++it; + } + } +} + +void ToolHandler::onToolExecutionFinished(const QString &toolId) +{ + if (!m_activeExecutions.contains(toolId)) { + return; + } + + auto execution = m_activeExecutions.take(toolId); + + try { + QString result = execution->watcher->result(); + LOG_MESSAGE(QString("Tool %1 completed").arg(execution->toolName)); + emit toolCompleted(execution->requestId, execution->toolId, result); + } catch (const std::exception &e) { + QString error = QString::fromStdString(e.what()); + LOG_MESSAGE(QString("Tool %1 failed: %2").arg(execution->toolName, error)); + emit toolFailed(execution->requestId, execution->toolId, error); + } + + execution->watcher->deleteLater(); + delete execution; +} + +} // namespace QodeAssist::Tools diff --git a/tools/ToolHandler.hpp b/tools/ToolHandler.hpp new file mode 100644 index 0000000..c8b2367 --- /dev/null +++ b/tools/ToolHandler.hpp @@ -0,0 +1,65 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace QodeAssist::Tools { + +class ToolHandler : public QObject +{ + Q_OBJECT + +public: + explicit ToolHandler(QObject *parent = nullptr); + + QFuture executeToolAsync( + const QString &requestId, + const QString &toolId, + LLMCore::BaseTool *tool, + const QJsonObject &input); + + void cleanupRequest(const QString &requestId); + +signals: + void toolCompleted(const QString &requestId, const QString &toolId, const QString &result); + void toolFailed(const QString &requestId, const QString &toolId, const QString &error); + +private: + struct ToolExecution + { + QString requestId; + QString toolId; + QString toolName; + QFutureWatcher *watcher; + }; + + QHash m_activeExecutions; // toolId -> execution + + void onToolExecutionFinished(const QString &toolId); +}; + +} // namespace QodeAssist::Tools diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp new file mode 100644 index 0000000..50e98c7 --- /dev/null +++ b/tools/ToolsFactory.cpp @@ -0,0 +1,83 @@ +/* + * 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 "ToolsFactory.hpp" + +#include "logger/Logger.hpp" +#include +#include + +#include "ReadVisibleFilesTool.hpp" +#include "ReadProjectFileByNameTool.hpp" + +namespace QodeAssist::Tools { + +ToolsFactory::ToolsFactory(QObject *parent) + : QObject(parent) +{ + registerTools(); +} + +void ToolsFactory::registerTools() +{ + registerTool(new ReadVisibleFilesTool(this)); + registerTool(new ReadProjectFileByNameTool(this)); + + LOG_MESSAGE(QString("Registered %1 tools").arg(m_tools.size())); +} + +void ToolsFactory::registerTool(LLMCore::BaseTool *tool) +{ + if (!tool) { + LOG_MESSAGE("Warning: Attempted to register null tool"); + return; + } + + const QString toolName = tool->name(); + if (m_tools.contains(toolName)) { + LOG_MESSAGE(QString("Warning: Tool '%1' already registered, replacing").arg(toolName)); + } + + m_tools.insert(toolName, tool); +} + +QList ToolsFactory::getAvailableTools() const +{ + return m_tools.values(); +} + +LLMCore::BaseTool *ToolsFactory::getToolByName(const QString &name) const +{ + return m_tools.value(name, nullptr); +} + +QJsonArray ToolsFactory::getToolsDefinitions(LLMCore::ToolSchemaFormat format) const +{ + QJsonArray toolsArray; + + for (auto it = m_tools.constBegin(); it != m_tools.constEnd(); ++it) { + if (it.value()) { + toolsArray.append(it.value()->getDefinition(format)); + } + } + + return toolsArray; +} + +} // namespace QodeAssist::Tools diff --git a/tools/ToolsFactory.hpp b/tools/ToolsFactory.hpp new file mode 100644 index 0000000..14663a0 --- /dev/null +++ b/tools/ToolsFactory.hpp @@ -0,0 +1,45 @@ +/* + * 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 . + */ + +#pragma once + +#include + +#include + +namespace QodeAssist::Tools { + +class ToolsFactory : public QObject +{ + Q_OBJECT +public: + ToolsFactory(QObject *parent = nullptr); + ~ToolsFactory() override = default; + + QList getAvailableTools() const; + LLMCore::BaseTool *getToolByName(const QString &name) const; + QJsonArray getToolsDefinitions(LLMCore::ToolSchemaFormat format) const; + +private: + void registerTools(); + void registerTool(LLMCore::BaseTool *tool); + + QHash m_tools; +}; +} // namespace QodeAssist::Tools