From 10b924d78a1c520b40a447f941d5cd50e65929c9 Mon Sep 17 00:00:00 2001
From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com>
Date: Tue, 30 Sep 2025 23:19:46 +0200
Subject: [PATCH] Feat: Add Claude tools support to plugin (#231)
* feat: Add settings for handle using tools in chat
* feat: Add Claude tools support
* fix: Add ai ignore to read project files list tool
* fix: Add ai ignore to read project file by name tool
* fix: Add ai ignore to read current opened files tool
---
CMakeLists.txt | 2 +
ChatView/ClientInterface.cpp | 4 +-
LLMClientInterface.cpp | 4 +-
QuickRefactorHandler.cpp | 2 +-
llmcore/CMakeLists.txt | 1 +
llmcore/ContentBlocks.hpp | 140 +++++++++++++++++
llmcore/Provider.cpp | 5 +
llmcore/Provider.hpp | 4 +-
providers/ClaudeMessage.cpp | 162 +++++++++++++++++++
providers/ClaudeMessage.hpp | 63 ++++++++
providers/ClaudeProvider.cpp | 236 +++++++++++++++++++++++-----
providers/ClaudeProvider.hpp | 23 +++
settings/ChatAssistantSettings.cpp | 64 +++++---
settings/ChatAssistantSettings.hpp | 1 +
settings/SettingsConstants.hpp | 1 +
tools/ListProjectFilesTool.cpp | 19 ++-
tools/ListProjectFilesTool.hpp | 5 +-
tools/ReadProjectFileByNameTool.cpp | 37 ++++-
tools/ReadProjectFileByNameTool.hpp | 4 +-
tools/ReadVisibleFilesTool.cpp | 16 ++
tools/ReadVisibleFilesTool.hpp | 6 +-
tools/ToolsManager.cpp | 158 +++++++++++++++++++
tools/ToolsManager.hpp | 75 +++++++++
23 files changed, 948 insertions(+), 84 deletions(-)
create mode 100644 llmcore/ContentBlocks.hpp
create mode 100644 providers/ClaudeMessage.cpp
create mode 100644 providers/ClaudeMessage.hpp
create mode 100644 tools/ToolsManager.cpp
create mode 100644 tools/ToolsManager.hpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 56f41a6..215e42e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -117,6 +117,8 @@ add_qtc_plugin(QodeAssist
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
tools/ToolHandler.hpp tools/ToolHandler.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
+ tools/ToolsManager.hpp tools/ToolsManager.cpp
+ providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp
index 233553d..0340d7e 100644
--- a/ChatView/ClientInterface.cpp
+++ b/ChatView/ClientInterface.cpp
@@ -153,8 +153,8 @@ void ClientInterface::cancelRequest()
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
- if (ctx.provider && ctx.provider->httpClient()) {
- ctx.provider->httpClient()->cancelRequest(it.key());
+ if (ctx.provider) {
+ ctx.provider->cancelRequest(it.key());
}
}
diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp
index b8795f9..38c8677 100644
--- a/LLMClientInterface.cpp
+++ b/LLMClientInterface.cpp
@@ -117,8 +117,8 @@ void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
- if (ctx.provider && ctx.provider->httpClient()) {
- ctx.provider->httpClient()->cancelRequest(it.key());
+ if (ctx.provider) {
+ ctx.provider->cancelRequest(it.key());
}
}
diff --git a/QuickRefactorHandler.cpp b/QuickRefactorHandler.cpp
index ae8cda0..faaf8b5 100644
--- a/QuickRefactorHandler.cpp
+++ b/QuickRefactorHandler.cpp
@@ -281,7 +281,7 @@ void QuickRefactorHandler::cancelRequest()
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.key() == id) {
const RequestContext &ctx = it.value();
- ctx.provider->httpClient()->cancelRequest(id);
+ ctx.provider->cancelRequest(id);
m_activeRequests.erase(it);
break;
}
diff --git a/llmcore/CMakeLists.txt b/llmcore/CMakeLists.txt
index 7b6d847..0a3d0f2 100644
--- a/llmcore/CMakeLists.txt
+++ b/llmcore/CMakeLists.txt
@@ -19,6 +19,7 @@ add_library(LLMCore STATIC
SSEBuffer.hpp SSEBuffer.cpp
BaseTool.hpp
BaseTool.cpp
+ ContentBlocks.hpp
)
target_link_libraries(LLMCore
diff --git a/llmcore/ContentBlocks.hpp b/llmcore/ContentBlocks.hpp
new file mode 100644
index 0000000..e8ab71b
--- /dev/null
+++ b/llmcore/ContentBlocks.hpp
@@ -0,0 +1,140 @@
+/*
+ * 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::LLMCore {
+
+enum class MessageState { Building, Complete, RequiresToolExecution, Final };
+
+enum class ProviderFormat { Claude, OpenAI };
+
+class ContentBlock : public QObject
+{
+ Q_OBJECT
+public:
+ explicit ContentBlock(QObject *parent = nullptr)
+ : QObject(parent)
+ {}
+ virtual ~ContentBlock() = default;
+ virtual QString type() const = 0;
+ virtual QJsonValue toJson(ProviderFormat format) const = 0;
+};
+
+class TextContent : public ContentBlock
+{
+ Q_OBJECT
+public:
+ explicit TextContent(const QString &text = QString())
+ : ContentBlock()
+ , m_text(text)
+ {}
+
+ QString type() const override { return "text"; }
+ QString text() const { return m_text; }
+ void appendText(const QString &text) { m_text += text; }
+ void setText(const QString &text) { m_text = text; }
+
+ QJsonValue toJson(ProviderFormat format) const override
+ {
+ Q_UNUSED(format);
+ return QJsonObject{{"type", "text"}, {"text", m_text}};
+ }
+
+private:
+ QString m_text;
+};
+
+class ToolUseContent : public ContentBlock
+{
+ Q_OBJECT
+public:
+ ToolUseContent(const QString &id, const QString &name, const QJsonObject &input = QJsonObject())
+ : ContentBlock()
+ , m_id(id)
+ , m_name(name)
+ , m_input(input)
+ {}
+
+ QString type() const override { return "tool_use"; }
+ QString id() const { return m_id; }
+ QString name() const { return m_name; }
+ QJsonObject input() const { return m_input; }
+ void setInput(const QJsonObject &input) { m_input = input; }
+
+ QJsonValue toJson(ProviderFormat format) const override
+ {
+ if (format == ProviderFormat::Claude) {
+ return QJsonObject{
+ {"type", "tool_use"}, {"id", m_id}, {"name", m_name}, {"input", m_input}};
+ } else { // OpenAI
+ QJsonDocument doc(m_input);
+ return QJsonObject{
+ {"id", m_id},
+ {"type", "function"},
+ {"function",
+ QJsonObject{
+ {"name", m_name},
+ {"arguments", QString::fromUtf8(doc.toJson(QJsonDocument::Compact))}}}};
+ }
+ }
+
+private:
+ QString m_id;
+ QString m_name;
+ QJsonObject m_input;
+};
+
+class ToolResultContent : public ContentBlock
+{
+ Q_OBJECT
+public:
+ ToolResultContent(const QString &toolUseId, const QString &result)
+ : ContentBlock()
+ , m_toolUseId(toolUseId)
+ , m_result(result)
+ {}
+
+ QString type() const override { return "tool_result"; }
+ QString toolUseId() const { return m_toolUseId; }
+ QString result() const { return m_result; }
+
+ QJsonValue toJson(ProviderFormat format) const override
+ {
+ if (format == ProviderFormat::Claude) {
+ return QJsonObject{
+ {"type", "tool_result"}, {"tool_use_id", m_toolUseId}, {"content", m_result}};
+ } else { // OpenAI
+ return QJsonObject{{"role", "tool"}, {"tool_call_id", m_toolUseId}, {"content", m_result}};
+ }
+ }
+
+private:
+ QString m_toolUseId;
+ QString m_result;
+};
+
+} // namespace QodeAssist::LLMCore
diff --git a/llmcore/Provider.cpp b/llmcore/Provider.cpp
index 897875e..dbae853 100644
--- a/llmcore/Provider.cpp
+++ b/llmcore/Provider.cpp
@@ -12,6 +12,11 @@ Provider::Provider(QObject *parent)
connect(m_httpClient, &HttpClient::requestFinished, this, &Provider::onRequestFinished);
}
+void Provider::cancelRequest(const RequestID &requestId)
+{
+ m_httpClient->cancelRequest(requestId);
+}
+
HttpClient *Provider::httpClient() const
{
return m_httpClient;
diff --git a/llmcore/Provider.hpp b/llmcore/Provider.hpp
index 8e11ab5..d37107f 100644
--- a/llmcore/Provider.hpp
+++ b/llmcore/Provider.hpp
@@ -63,7 +63,9 @@ public:
virtual void sendRequest(const RequestID &requestId, const QUrl &url, const QJsonObject &payload)
= 0;
- virtual bool supportsTools() { return false; };
+ virtual bool supportsTools() const { return false; };
+
+ virtual void cancelRequest(const RequestID &requestId);
HttpClient *httpClient() const;
diff --git a/providers/ClaudeMessage.cpp b/providers/ClaudeMessage.cpp
new file mode 100644
index 0000000..0f338b0
--- /dev/null
+++ b/providers/ClaudeMessage.cpp
@@ -0,0 +1,162 @@
+/*
+ * 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 "ClaudeMessage.hpp"
+#include "logger/Logger.hpp"
+
+#include
+#include
+
+namespace QodeAssist {
+
+ClaudeMessage::ClaudeMessage(QObject *parent)
+ : QObject(parent)
+{}
+
+void ClaudeMessage::handleContentBlockStart(
+ int index, const QString &blockType, const QJsonObject &data)
+{
+ LOG_MESSAGE(QString("ClaudeMessage: handleContentBlockStart index=%1, blockType=%2")
+ .arg(index)
+ .arg(blockType));
+
+ if (blockType == "text") {
+ addCurrentContent();
+
+ } else if (blockType == "tool_use") {
+ QString toolId = data["id"].toString();
+ QString toolName = data["name"].toString();
+ QJsonObject toolInput = data["input"].toObject();
+
+ addCurrentContent(toolId, toolName, toolInput);
+ m_pendingToolInputs[index] = "";
+ }
+}
+
+void ClaudeMessage::handleContentBlockDelta(
+ int index, const QString &deltaType, const QJsonObject &delta)
+{
+ if (index >= m_currentBlocks.size()) {
+ return;
+ }
+
+ if (deltaType == "text_delta") {
+ if (auto textContent = qobject_cast(m_currentBlocks[index])) {
+ textContent->appendText(delta["text"].toString());
+ }
+
+ } else if (deltaType == "input_json_delta") {
+ QString partialJson = delta["partial_json"].toString();
+ if (m_pendingToolInputs.contains(index)) {
+ m_pendingToolInputs[index] += partialJson;
+ }
+ }
+}
+
+void ClaudeMessage::handleContentBlockStop(int index)
+{
+ if (m_pendingToolInputs.contains(index)) {
+ QString jsonInput = m_pendingToolInputs[index];
+ QJsonObject inputObject;
+
+ if (!jsonInput.isEmpty()) {
+ QJsonDocument doc = QJsonDocument::fromJson(jsonInput.toUtf8());
+ if (doc.isObject()) {
+ inputObject = doc.object();
+ }
+ }
+
+ if (index < m_currentBlocks.size()) {
+ if (auto toolContent = qobject_cast(m_currentBlocks[index])) {
+ toolContent->setInput(inputObject);
+ }
+ }
+
+ m_pendingToolInputs.remove(index);
+ }
+}
+
+void ClaudeMessage::handleStopReason(const QString &stopReason)
+{
+ m_stopReason = stopReason;
+ updateStateFromStopReason();
+}
+
+QJsonObject ClaudeMessage::toProviderFormat() const
+{
+ QJsonObject message;
+ message["role"] = "assistant";
+
+ QJsonArray content;
+ for (auto block : m_currentBlocks) {
+ content.append(block->toJson(LLMCore::ProviderFormat::Claude));
+ }
+
+ message["content"] = content;
+ return message;
+}
+
+QJsonArray ClaudeMessage::createToolResultsContent(const QHash &toolResults) const
+{
+ QJsonArray results;
+
+ for (auto toolContent : getCurrentToolUseContent()) {
+ if (toolResults.contains(toolContent->id())) {
+ auto toolResult = std::make_unique(
+ toolContent->id(), toolResults[toolContent->id()]);
+ results.append(toolResult->toJson(LLMCore::ProviderFormat::Claude));
+ }
+ }
+
+ return results;
+}
+
+QList ClaudeMessage::getCurrentToolUseContent() const
+{
+ QList toolBlocks;
+ for (auto block : m_currentBlocks) {
+ if (auto toolContent = qobject_cast(block)) {
+ toolBlocks.append(toolContent);
+ }
+ }
+ return toolBlocks;
+}
+
+void ClaudeMessage::startNewContinuation()
+{
+ LOG_MESSAGE(QString("ClaudeMessage: Starting new continuation"));
+
+ m_currentBlocks.clear();
+ m_pendingToolInputs.clear();
+ m_stopReason.clear();
+ m_state = LLMCore::MessageState::Building;
+}
+
+void ClaudeMessage::updateStateFromStopReason()
+{
+ if (m_stopReason == "tool_use" && !getCurrentToolUseContent().empty()) {
+ m_state = LLMCore::MessageState::RequiresToolExecution;
+ } else if (m_stopReason == "end_turn") {
+ m_state = LLMCore::MessageState::Final;
+ } else {
+ m_state = LLMCore::MessageState::Complete;
+ }
+}
+
+} // namespace QodeAssist
diff --git a/providers/ClaudeMessage.hpp b/providers/ClaudeMessage.hpp
new file mode 100644
index 0000000..4cf84e7
--- /dev/null
+++ b/providers/ClaudeMessage.hpp
@@ -0,0 +1,63 @@
+/*
+ * 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
+
+namespace QodeAssist {
+
+class ClaudeMessage : public QObject
+{
+ Q_OBJECT
+public:
+ explicit ClaudeMessage(QObject *parent = nullptr);
+
+ void handleContentBlockStart(int index, const QString &blockType, const QJsonObject &data);
+ void handleContentBlockDelta(int index, const QString &deltaType, const QJsonObject &delta);
+ void handleContentBlockStop(int index);
+ void handleStopReason(const QString &stopReason);
+
+ QJsonObject toProviderFormat() const;
+ QJsonArray createToolResultsContent(const QHash &toolResults) const;
+
+ LLMCore::MessageState state() const { return m_state; }
+ QList getCurrentToolUseContent() const;
+
+ void startNewContinuation();
+
+private:
+ QString m_stopReason;
+ LLMCore::MessageState m_state = LLMCore::MessageState::Building;
+ QList m_currentBlocks;
+ QHash m_pendingToolInputs;
+
+ void updateStateFromStopReason();
+
+ template
+ T *addCurrentContent(Args &&...args)
+ {
+ T *content = new T(std::forward(args)...);
+ content->setParent(this);
+ m_currentBlocks.append(content);
+ return content;
+ }
+};
+
+} // namespace QodeAssist
diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp
index 6993b5f..a7e6657 100644
--- a/providers/ClaudeProvider.cpp
+++ b/providers/ClaudeProvider.cpp
@@ -34,6 +34,17 @@
namespace QodeAssist::Providers {
+ClaudeProvider::ClaudeProvider(QObject *parent)
+ : LLMCore::Provider(parent)
+ , m_toolsManager(new Tools::ToolsManager(this))
+{
+ connect(
+ m_toolsManager,
+ &Tools::ToolsManager::toolExecutionComplete,
+ this,
+ &ClaudeProvider::onToolExecutionComplete);
+}
+
QString ClaudeProvider::name() const
{
return "Claude";
@@ -86,6 +97,15 @@ void ClaudeProvider::prepareRequest(
} else {
applyModelParams(Settings::chatAssistantSettings());
}
+
+ if (supportsTools() && type == LLMCore::RequestType::Chat
+ && Settings::chatAssistantSettings().useTools()) {
+ auto toolsDefinitions = m_toolsManager->getToolsDefinitions(Tools::ToolSchemaFormat::Claude);
+ if (!toolsDefinitions.isEmpty()) {
+ request["tools"] = toolsDefinitions;
+ LOG_MESSAGE(QString("Added %1 tools to Claude request").arg(toolsDefinitions.size()));
+ }
+ }
}
QList ClaudeProvider::getInstalledModels(const QString &baseUrl)
@@ -146,7 +166,8 @@ QList ClaudeProvider::validateRequest(const QJsonObject &request, LLMCo
{"top_p", {}},
{"top_k", {}},
{"stop", QJsonArray{}},
- {"stream", {}}};
+ {"stream", {}},
+ {"tools", {}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
@@ -174,8 +195,12 @@ LLMCore::ProviderID ClaudeProvider::providerID() const
void ClaudeProvider::sendRequest(
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
{
- m_dataBuffers[requestId].clear();
+ if (!m_messages.contains(requestId)) {
+ m_dataBuffers[requestId].clear();
+ }
+
m_requestUrls[requestId] = url;
+ m_originalRequests[requestId] = payload;
QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest);
@@ -188,52 +213,30 @@ void ClaudeProvider::sendRequest(
emit httpClient()->sendRequest(request);
}
+bool ClaudeProvider::supportsTools() const
+{
+ return true;
+}
+
+void ClaudeProvider::cancelRequest(const LLMCore::RequestID &requestId)
+{
+ LOG_MESSAGE(QString("ClaudeProvider: Cancelling request %1").arg(requestId));
+ LLMCore::Provider::cancelRequest(requestId);
+ cleanupRequest(requestId);
+}
+
void ClaudeProvider::onDataReceived(
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
{
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
QStringList lines = buffers.rawStreamBuffer.processData(data);
- QString tempResponse;
- bool isComplete = false;
-
for (const QString &line : lines) {
QJsonObject responseObj = parseEventLine(line);
if (responseObj.isEmpty())
continue;
- QString eventType = responseObj["type"].toString();
-
- if (eventType == "message_start") {
- QString messageId = responseObj["message"].toObject()["id"].toString();
- LOG_MESSAGE(QString("Claude message started: %1").arg(messageId));
-
- } else if (eventType == "content_block_delta") {
- QJsonObject delta = responseObj["delta"].toObject();
- if (delta["type"].toString() == "text_delta") {
- tempResponse += delta["text"].toString();
- }
-
- } else if (eventType == "message_delta") {
- QJsonObject delta = responseObj["delta"].toObject();
- if (delta.contains("stop_reason")) {
- isComplete = true;
- QJsonObject usage = responseObj["usage"].toObject();
- LOG_MESSAGE(QString("Tokens: input=%1, output=%2")
- .arg(usage["input_tokens"].toInt())
- .arg(usage["output_tokens"].toInt()));
- }
- }
- }
-
- if (!tempResponse.isEmpty()) {
- buffers.responseContent += tempResponse;
- emit partialResponseReceived(requestId, tempResponse);
- }
-
- if (isComplete) {
- emit fullResponseReceived(requestId, buffers.responseContent);
- m_dataBuffers.remove(requestId);
+ processStreamEvent(requestId, responseObj);
}
}
@@ -243,17 +246,164 @@ void ClaudeProvider::onRequestFinished(
if (!success) {
LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error);
- } else {
- if (m_dataBuffers.contains(requestId)) {
- const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
- if (!buffers.responseContent.isEmpty()) {
- emit fullResponseReceived(requestId, buffers.responseContent);
- }
+ cleanupRequest(requestId);
+ return;
+ }
+
+ if (m_messages.contains(requestId)) {
+ ClaudeMessage *message = m_messages[requestId];
+ if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
+ LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId));
+ m_dataBuffers.remove(requestId);
+ return;
}
}
+ if (m_dataBuffers.contains(requestId)) {
+ const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
+ if (!buffers.responseContent.isEmpty()) {
+ LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId));
+ emit fullResponseReceived(requestId, buffers.responseContent);
+ }
+ }
+
+ cleanupRequest(requestId);
+}
+
+void ClaudeProvider::onToolExecutionComplete(
+ const QString &requestId, const QHash &toolResults)
+{
+ if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) {
+ LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId));
+ cleanupRequest(requestId);
+ return;
+ }
+
+ LOG_MESSAGE(QString("Tool execution complete for Claude request %1").arg(requestId));
+
+ ClaudeMessage *message = m_messages[requestId];
+ QJsonObject continuationRequest = m_originalRequests[requestId];
+ QJsonArray messages = continuationRequest["messages"].toArray();
+
+ messages.append(message->toProviderFormat());
+
+ QJsonObject userMessage;
+ userMessage["role"] = "user";
+ userMessage["content"] = message->createToolResultsContent(toolResults);
+ messages.append(userMessage);
+
+ continuationRequest["messages"] = messages;
+
+ LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results")
+ .arg(requestId)
+ .arg(toolResults.size()));
+
+ sendRequest(requestId, m_requestUrls[requestId], continuationRequest);
+}
+
+void ClaudeProvider::processStreamEvent(const QString &requestId, const QJsonObject &event)
+{
+ QString eventType = event["type"].toString();
+
+ LOG_MESSAGE(QString("Processing Claude event: type=%1").arg(eventType));
+
+ if (eventType == "message_stop") {
+ return;
+ }
+
+ ClaudeMessage *message = m_messages.value(requestId);
+ if (!message) {
+ if (eventType == "message_start") {
+ message = new ClaudeMessage(this);
+ m_messages[requestId] = message;
+ LOG_MESSAGE(QString("Created NEW ClaudeMessage for request %1").arg(requestId));
+ } else {
+ return;
+ }
+ }
+
+ if (eventType == "message_start") {
+ message->startNewContinuation();
+ LOG_MESSAGE(QString("Starting NEW continuation for request %1").arg(requestId));
+
+ } else if (eventType == "content_block_start") {
+ int index = event["index"].toInt();
+ QJsonObject contentBlock = event["content_block"].toObject();
+ QString blockType = contentBlock["type"].toString();
+
+ LOG_MESSAGE(
+ QString("Adding new content block: type=%1, index=%2").arg(blockType).arg(index));
+
+ message->handleContentBlockStart(index, blockType, contentBlock);
+
+ } else if (eventType == "content_block_delta") {
+ int index = event["index"].toInt();
+ QJsonObject delta = event["delta"].toObject();
+ QString deltaType = delta["type"].toString();
+
+ message->handleContentBlockDelta(index, deltaType, delta);
+
+ if (deltaType == "text_delta") {
+ QString text = delta["text"].toString();
+ LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
+ buffers.responseContent += text;
+ emit partialResponseReceived(requestId, text);
+ }
+
+ } else if (eventType == "content_block_stop") {
+ int index = event["index"].toInt();
+ message->handleContentBlockStop(index);
+
+ } else if (eventType == "message_delta") {
+ QJsonObject delta = event["delta"].toObject();
+ if (delta.contains("stop_reason")) {
+ QString stopReason = delta["stop_reason"].toString();
+ message->handleStopReason(stopReason);
+ handleMessageComplete(requestId);
+ }
+ }
+}
+
+void ClaudeProvider::handleMessageComplete(const QString &requestId)
+{
+ if (!m_messages.contains(requestId))
+ return;
+
+ ClaudeMessage *message = m_messages[requestId];
+
+ if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
+ LOG_MESSAGE(QString("Claude message requires tool execution for %1").arg(requestId));
+
+ auto toolUseContent = message->getCurrentToolUseContent();
+
+ if (toolUseContent.isEmpty()) {
+ LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId));
+ return;
+ }
+
+ for (auto toolContent : toolUseContent) {
+ m_toolsManager->executeToolCall(
+ requestId, toolContent->id(), toolContent->name(), toolContent->input());
+ }
+
+ } else {
+ LOG_MESSAGE(QString("Claude message marked as complete for %1").arg(requestId));
+ }
+}
+
+void ClaudeProvider::cleanupRequest(const LLMCore::RequestID &requestId)
+{
+ LOG_MESSAGE(QString("Cleaning up Claude request %1").arg(requestId));
+
+ if (m_messages.contains(requestId)) {
+ ClaudeMessage *message = m_messages.take(requestId);
+ message->deleteLater();
+ }
+
m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
+ m_originalRequests.remove(requestId);
+ m_toolsManager->cleanupRequest(requestId);
}
} // namespace QodeAssist::Providers
diff --git a/providers/ClaudeProvider.hpp b/providers/ClaudeProvider.hpp
index 4a9b4ed..9ac65e5 100644
--- a/providers/ClaudeProvider.hpp
+++ b/providers/ClaudeProvider.hpp
@@ -21,11 +21,17 @@
#include
+#include "ClaudeMessage.hpp"
+#include "tools/ToolsManager.hpp"
+
namespace QodeAssist::Providers {
class ClaudeProvider : public LLMCore::Provider
{
+ Q_OBJECT
public:
+ explicit ClaudeProvider(QObject *parent = nullptr);
+
QString name() const override;
QString url() const override;
QString completionEndpoint() const override;
@@ -45,6 +51,9 @@ public:
void sendRequest(
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override;
+ bool supportsTools() const override;
+ void cancelRequest(const LLMCore::RequestID &requestId) override;
+
public slots:
void onDataReceived(
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
@@ -52,6 +61,20 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId,
bool success,
const QString &error) override;
+
+private slots:
+ void onToolExecutionComplete(
+ const QString &requestId, const QHash &toolResults);
+
+private:
+ void processStreamEvent(const QString &requestId, const QJsonObject &event);
+ void handleMessageComplete(const QString &requestId);
+ void cleanupRequest(const LLMCore::RequestID &requestId);
+
+ QHash m_messages;
+ QHash m_requestUrls;
+ QHash m_originalRequests;
+ Tools::ToolsManager *m_toolsManager;
};
} // namespace QodeAssist::Providers
diff --git a/settings/ChatAssistantSettings.cpp b/settings/ChatAssistantSettings.cpp
index 9604342..d67718e 100644
--- a/settings/ChatAssistantSettings.cpp
+++ b/settings/ChatAssistantSettings.cpp
@@ -68,6 +68,15 @@ ChatAssistantSettings::ChatAssistantSettings()
enableChatInNavigationPanel.setLabelText(Tr::tr("Enable chat in navigation panel"));
enableChatInNavigationPanel.setDefaultValue(false);
+ useTools.setSettingsKey(Constants::CA_USE_TOOLS);
+ useTools.setLabelText(Tr::tr("Enable tools"));
+ useTools.setToolTip(
+ Tr::tr(
+ "Enable tool use capabilities for the assistant(OpenAI function calling, Claude tools "
+ "and etc) "
+ "if plugin and provider support"));
+ useTools.setDefaultValue(true);
+
// General Parameters Settings
temperature.setSettingsKey(Constants::CA_TEMPERATURE);
temperature.setLabelText(Tr::tr("Temperature:"));
@@ -242,31 +251,35 @@ ChatAssistantSettings::ChatAssistantSettings()
chatViewSettingsGrid.addRow({textFormat});
chatViewSettingsGrid.addRow({chatRenderer});
- return Column{Row{Stretch{1}, resetToDefaults},
- Space{8},
- Group{title(Tr::tr("Chat Settings")),
- Column{Row{chatTokensThreshold, Stretch{1}},
- linkOpenFiles,
- autosave,
- enableChatInBottomToolBar,
- enableChatInNavigationPanel}},
- Space{8},
- Group{
- title(Tr::tr("General Parameters")),
- Row{genGrid, Stretch{1}},
- },
- Space{8},
- Group{title(Tr::tr("Advanced Parameters")),
- Column{Row{advancedGrid, Stretch{1}}}},
- Space{8},
- Group{title(Tr::tr("Context Settings")),
- Column{
- Row{useSystemPrompt, Stretch{1}},
- systemPrompt,
- }},
- Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
- Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}},
- Stretch{1}};
+ return Column{
+ Row{Stretch{1}, resetToDefaults},
+ Space{8},
+ Group{
+ title(Tr::tr("Chat Settings")),
+ Column{
+ Row{chatTokensThreshold, Stretch{1}},
+ linkOpenFiles,
+ autosave,
+ enableChatInBottomToolBar,
+ enableChatInNavigationPanel,
+ useTools}},
+ Space{8},
+ Group{
+ title(Tr::tr("General Parameters")),
+ Row{genGrid, Stretch{1}},
+ },
+ Space{8},
+ Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
+ Space{8},
+ Group{
+ title(Tr::tr("Context Settings")),
+ Column{
+ Row{useSystemPrompt, Stretch{1}},
+ systemPrompt,
+ }},
+ Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
+ Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}},
+ Stretch{1}};
});
}
@@ -311,6 +324,7 @@ void ChatAssistantSettings::resetSettingsToDefaults()
resetAspect(codeFontSize);
resetAspect(textFormat);
resetAspect(chatRenderer);
+ resetAspect(useTools);
}
}
diff --git a/settings/ChatAssistantSettings.hpp b/settings/ChatAssistantSettings.hpp
index 984cf8d..710ad9b 100644
--- a/settings/ChatAssistantSettings.hpp
+++ b/settings/ChatAssistantSettings.hpp
@@ -38,6 +38,7 @@ public:
Utils::BoolAspect autosave{this};
Utils::BoolAspect enableChatInBottomToolBar{this};
Utils::BoolAspect enableChatInNavigationPanel{this};
+ Utils::BoolAspect useTools{this};
// General Parameters Settings
Utils::DoubleAspect temperature{this};
diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp
index aaff040..be135e6 100644
--- a/settings/SettingsConstants.hpp
+++ b/settings/SettingsConstants.hpp
@@ -84,6 +84,7 @@ const char CC_CUSTOM_LANGUAGES[] = "QodeAssist.ccCustomLanguages";
const char CA_ENABLE_CHAT_IN_BOTTOM_TOOLBAR[] = "QodeAssist.caEnableChatInBottomToolbar";
const char CA_ENABLE_CHAT_IN_NAVIGATION_PANEL[] = "QodeAssist.caEnableChatInNavigationPanel";
+const char CA_USE_TOOLS[] = "QodeAssist.caUseTools";
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
diff --git a/tools/ListProjectFilesTool.cpp b/tools/ListProjectFilesTool.cpp
index 0f085fe..733d7bd 100644
--- a/tools/ListProjectFilesTool.cpp
+++ b/tools/ListProjectFilesTool.cpp
@@ -32,6 +32,8 @@ namespace QodeAssist::Tools {
ListProjectFilesTool::ListProjectFilesTool(QObject *parent)
: BaseTool(parent)
+ , m_ignoreManager(new Context::IgnoreManager(this))
+
{}
QString ListProjectFilesTool::name() const
@@ -92,14 +94,27 @@ QFuture ListProjectFilesTool::executeAsync(const QJsonObject &input)
}
QStringList fileList;
- QString projectPath = project->projectDirectory().toString();
+ QString projectPath = project->projectDirectory().toUrlishString();
for (const auto &filePath : projectFiles) {
- QString absolutePath = filePath.toString();
+ QString absolutePath = filePath.toUrlishString();
+
+ if (m_ignoreManager->shouldIgnore(absolutePath, project)) {
+ LOG_MESSAGE(
+ QString("Ignoring file due to .qodeassistignore: %1").arg(absolutePath));
+ continue;
+ }
+
QString relativePath = QDir(projectPath).relativeFilePath(absolutePath);
fileList.append(relativePath);
}
+ if (fileList.isEmpty()) {
+ result += QString("Project '%1': No files after applying .qodeassistignore\n\n")
+ .arg(project->displayName());
+ continue;
+ }
+
fileList.sort();
result += QString("Project '%1' (%2 files):\n")
diff --git a/tools/ListProjectFilesTool.hpp b/tools/ListProjectFilesTool.hpp
index 360e40c..6163dcc 100644
--- a/tools/ListProjectFilesTool.hpp
+++ b/tools/ListProjectFilesTool.hpp
@@ -19,7 +19,9 @@
#pragma once
-#include "llmcore/BaseTool.hpp"
+#include
+
+#include
namespace QodeAssist::Tools {
@@ -36,6 +38,7 @@ public:
private:
QString formatFileList(const QStringList &files) const;
+ Context::IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Tools
diff --git a/tools/ReadProjectFileByNameTool.cpp b/tools/ReadProjectFileByNameTool.cpp
index 60e23a4..171d4c1 100644
--- a/tools/ReadProjectFileByNameTool.cpp
+++ b/tools/ReadProjectFileByNameTool.cpp
@@ -36,6 +36,7 @@ namespace QodeAssist::Tools {
ReadProjectFileByNameTool::ReadProjectFileByNameTool(QObject *parent)
: BaseTool(parent)
+ , m_ignoreManager(new Context::IgnoreManager(this))
{}
QString ReadProjectFileByNameTool::name() const
@@ -100,6 +101,14 @@ QFuture ReadProjectFileByNameTool::executeAsync(const QJsonObject &inpu
throw std::runtime_error(error.toStdString());
}
+ auto project = ProjectExplorer::ProjectManager::projectForFile(
+ Utils::FilePath::fromString(filePath));
+ if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
+ QString error
+ = QString("Error: File '%1' is excluded by .qodeassistignore").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);
@@ -126,22 +135,40 @@ QString ReadProjectFileByNameTool::findFileInProject(const QString &fileName) co
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
for (const auto &projectFile : std::as_const(projectFiles)) {
- QFileInfo fileInfo(projectFile.path());
+ QString absolutePath = projectFile.path();
+
+ if (m_ignoreManager->shouldIgnore(absolutePath, project)) {
+ continue;
+ }
+
+ QFileInfo fileInfo(absolutePath);
if (fileInfo.fileName() == fileName) {
- return projectFile.path();
+ return absolutePath;
}
}
for (const auto &projectFile : std::as_const(projectFiles)) {
+ QString absolutePath = projectFile.path();
+
+ if (m_ignoreManager->shouldIgnore(absolutePath, project)) {
+ continue;
+ }
+
if (projectFile.endsWith(fileName)) {
- return projectFile.path();
+ return absolutePath;
}
}
for (const auto &projectFile : std::as_const(projectFiles)) {
- QFileInfo fileInfo(projectFile.path());
+ QString absolutePath = projectFile.path();
+
+ if (m_ignoreManager->shouldIgnore(absolutePath, project)) {
+ continue;
+ }
+
+ QFileInfo fileInfo(absolutePath);
if (fileInfo.fileName().contains(fileName, Qt::CaseInsensitive)) {
- return projectFile.path();
+ return absolutePath;
}
}
}
diff --git a/tools/ReadProjectFileByNameTool.hpp b/tools/ReadProjectFileByNameTool.hpp
index 5c591e5..c308dbe 100644
--- a/tools/ReadProjectFileByNameTool.hpp
+++ b/tools/ReadProjectFileByNameTool.hpp
@@ -19,7 +19,8 @@
#pragma once
-#include "llmcore/BaseTool.hpp"
+#include
+#include
namespace QodeAssist::Tools {
@@ -37,6 +38,7 @@ public:
private:
QString findFileInProject(const QString &fileName) const;
QString readFileContent(const QString &filePath) const;
+ Context::IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Tools
diff --git a/tools/ReadVisibleFilesTool.cpp b/tools/ReadVisibleFilesTool.cpp
index 4a6bf55..42e9bea 100644
--- a/tools/ReadVisibleFilesTool.cpp
+++ b/tools/ReadVisibleFilesTool.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -30,6 +31,7 @@ namespace QodeAssist::Tools {
ReadVisibleFilesTool::ReadVisibleFilesTool(QObject *parent)
: BaseTool(parent)
+ , m_ignoreManager(new Context::IgnoreManager(this))
{}
QString ReadVisibleFilesTool::name() const
@@ -84,6 +86,15 @@ QFuture ReadVisibleFilesTool::executeAsync(const QJsonObject &input)
}
QString filePath = editor->document()->filePath().toFSPathString();
+
+ auto project = ProjectExplorer::ProjectManager::projectForFile(
+ editor->document()->filePath());
+ if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
+ LOG_MESSAGE(
+ QString("Ignoring visible file due to .qodeassistignore: %1").arg(filePath));
+ continue;
+ }
+
QByteArray contentBytes = editor->document()->contents();
QString fileContent = QString::fromUtf8(contentBytes);
@@ -98,6 +109,11 @@ QFuture ReadVisibleFilesTool::executeAsync(const QJsonObject &input)
results.append(fileResult);
}
+ if (results.isEmpty()) {
+ QString error = "Error: All visible files are excluded by .qodeassistignore";
+ throw std::runtime_error(error.toStdString());
+ }
+
return results.join("\n\n" + QString(80, '=') + "\n\n");
});
}
diff --git a/tools/ReadVisibleFilesTool.hpp b/tools/ReadVisibleFilesTool.hpp
index f471c0b..84f6671 100644
--- a/tools/ReadVisibleFilesTool.hpp
+++ b/tools/ReadVisibleFilesTool.hpp
@@ -19,7 +19,8 @@
#pragma once
-#include "llmcore/BaseTool.hpp"
+#include
+#include
namespace QodeAssist::Tools {
@@ -33,6 +34,9 @@ public:
QString description() const override;
QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
+
+private:
+ Context::IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Tools
diff --git a/tools/ToolsManager.cpp b/tools/ToolsManager.cpp
new file mode 100644
index 0000000..b664da3
--- /dev/null
+++ b/tools/ToolsManager.cpp
@@ -0,0 +1,158 @@
+/*
+ * 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 "ToolsManager.hpp"
+#include "logger/Logger.hpp"
+
+namespace QodeAssist::Tools {
+
+ToolsManager::ToolsManager(QObject *parent)
+ : QObject(parent)
+ , m_toolsFactory(new ToolsFactory(this))
+ , m_toolHandler(new ToolHandler(this))
+{
+ connect(
+ m_toolHandler,
+ &ToolHandler::toolCompleted,
+ this,
+ [this](const QString &requestId, const QString &toolId, const QString &result) {
+ onToolFinished(requestId, toolId, result, true);
+ });
+
+ connect(
+ m_toolHandler,
+ &ToolHandler::toolFailed,
+ this,
+ [this](const QString &requestId, const QString &toolId, const QString &error) {
+ onToolFinished(requestId, toolId, error, false);
+ });
+}
+
+void ToolsManager::executeToolCall(
+ const QString &requestId,
+ const QString &toolId,
+ const QString &toolName,
+ const QJsonObject &input)
+{
+ LOG_MESSAGE(QString("ToolsManager: Executing tool %1 (ID: %2) for request %3")
+ .arg(toolName, toolId, requestId));
+
+ if (!m_pendingTools.contains(requestId)) {
+ m_pendingTools[requestId] = QHash();
+ }
+
+ if (m_pendingTools[requestId].contains(toolId)) {
+ LOG_MESSAGE(QString("Tool %1 already in progress for request %2").arg(toolId, requestId));
+ return;
+ }
+
+ auto tool = m_toolsFactory->getToolByName(toolName);
+ if (!tool) {
+ LOG_MESSAGE(QString("ToolsManager: Tool not found: %1").arg(toolName));
+ return;
+ }
+
+ PendingTool pendingTool{toolId, toolName, input, "", false};
+ m_pendingTools[requestId][toolId] = pendingTool;
+
+ m_toolHandler->executeToolAsync(requestId, toolId, tool, input);
+ LOG_MESSAGE(QString("ToolsManager: Started async execution of %1").arg(toolName));
+}
+
+QJsonArray ToolsManager::getToolsDefinitions(ToolSchemaFormat format) const
+{
+ if (!m_toolsFactory) {
+ return QJsonArray();
+ }
+
+ LLMCore::ToolSchemaFormat coreFormat = (format == ToolSchemaFormat::OpenAI)
+ ? LLMCore::ToolSchemaFormat::OpenAI
+ : LLMCore::ToolSchemaFormat::Claude;
+
+ return m_toolsFactory->getToolsDefinitions(coreFormat);
+}
+
+void ToolsManager::cleanupRequest(const QString &requestId)
+{
+ m_pendingTools.remove(requestId);
+ m_toolHandler->cleanupRequest(requestId);
+ LOG_MESSAGE(QString("ToolsManager: Cleaned up request %1").arg(requestId));
+}
+
+void ToolsManager::onToolFinished(
+ const QString &requestId, const QString &toolId, const QString &result, bool success)
+{
+ if (!m_pendingTools.contains(requestId) || !m_pendingTools[requestId].contains(toolId)) {
+ LOG_MESSAGE(QString("ToolsManager: Tool result for unknown tool %1 in request %2")
+ .arg(toolId, requestId));
+ return;
+ }
+
+ PendingTool &tool = m_pendingTools[requestId][toolId];
+ tool.result = success ? result : QString("Error: %1").arg(result);
+ tool.complete = true;
+
+ LOG_MESSAGE(QString("ToolsManager: Tool %1 %2 for request %3")
+ .arg(toolId)
+ .arg(success ? QString("completed") : QString("failed"))
+ .arg(requestId));
+
+ if (isExecutionComplete(requestId)) {
+ QHash results = getToolResults(requestId);
+ LOG_MESSAGE(QString("ToolsManager: All tools complete for request %1, emitting results")
+ .arg(requestId));
+ emit toolExecutionComplete(requestId, results);
+ } else {
+ LOG_MESSAGE(QString("ToolsManager: Tools still pending for request %1").arg(requestId));
+ }
+}
+
+bool ToolsManager::isExecutionComplete(const QString &requestId) const
+{
+ if (!m_pendingTools.contains(requestId)) {
+ return true;
+ }
+
+ const auto &tools = m_pendingTools[requestId];
+ for (auto it = tools.begin(); it != tools.end(); ++it) {
+ if (!it.value().complete) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+QHash ToolsManager::getToolResults(const QString &requestId) const
+{
+ QHash results;
+
+ if (m_pendingTools.contains(requestId)) {
+ const auto &tools = m_pendingTools[requestId];
+ for (auto it = tools.begin(); it != tools.end(); ++it) {
+ if (it.value().complete) {
+ results[it.key()] = it.value().result;
+ }
+ }
+ }
+
+ return results;
+}
+
+} // namespace QodeAssist::Tools
diff --git a/tools/ToolsManager.hpp b/tools/ToolsManager.hpp
new file mode 100644
index 0000000..3ebd2e4
--- /dev/null
+++ b/tools/ToolsManager.hpp
@@ -0,0 +1,75 @@
+/*
+ * 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 "ToolHandler.hpp"
+#include "ToolsFactory.hpp"
+
+namespace QodeAssist::Tools {
+
+enum class ToolSchemaFormat { OpenAI, Claude };
+
+struct PendingTool
+{
+ QString id;
+ QString name;
+ QJsonObject input;
+ QString result;
+ bool complete = false;
+};
+
+class ToolsManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit ToolsManager(QObject *parent = nullptr);
+
+ void executeToolCall(
+ const QString &requestId,
+ const QString &toolId,
+ const QString &toolName,
+ const QJsonObject &input);
+
+ QJsonArray getToolsDefinitions(ToolSchemaFormat format) const;
+ void cleanupRequest(const QString &requestId);
+
+signals:
+ void toolExecutionComplete(const QString &requestId, const QHash &toolResults);
+
+private slots:
+ void onToolFinished(
+ const QString &requestId, const QString &toolId, const QString &result, bool success);
+
+private:
+ ToolsFactory *m_toolsFactory;
+ ToolHandler *m_toolHandler;
+ QHash> m_pendingTools;
+
+ bool isExecutionComplete(const QString &requestId) const;
+ QHash getToolResults(const QString &requestId) const;
+};
+
+} // namespace QodeAssist::Tools