From 906c161729c479a56f2deaff7e910aaae6bef34f Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:42:31 +0200 Subject: [PATCH] feat: Add ollama support tooling (#236) --- CMakeLists.txt | 1 + providers/OllamaMessage.cpp | 312 +++++++++++++++++++++++++++++++++++ providers/OllamaMessage.hpp | 67 ++++++++ providers/OllamaProvider.cpp | 285 ++++++++++++++++++++++++++++---- providers/OllamaProvider.hpp | 25 ++- 5 files changed, 653 insertions(+), 37 deletions(-) create mode 100644 providers/OllamaMessage.cpp create mode 100644 providers/OllamaMessage.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ee6e399..7e21e5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,7 @@ add_qtc_plugin(QodeAssist tools/ToolsManager.hpp tools/ToolsManager.cpp providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp + providers/OllamaMessage.hpp providers/OllamaMessage.cpp ) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) diff --git a/providers/OllamaMessage.cpp b/providers/OllamaMessage.cpp new file mode 100644 index 0000000..cb2125d --- /dev/null +++ b/providers/OllamaMessage.cpp @@ -0,0 +1,312 @@ +/* + * 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 "OllamaMessage.hpp" +#include "logger/Logger.hpp" + +#include +#include + +namespace QodeAssist::Providers { + +OllamaMessage::OllamaMessage(QObject *parent) + : QObject(parent) +{} + +void OllamaMessage::handleContentDelta(const QString &content) +{ + m_accumulatedContent += content; + QString trimmed = m_accumulatedContent.trimmed(); + + if (trimmed.startsWith('{')) { + return; + } + + if (!m_contentAddedToTextBlock) { + LLMCore::TextContent *textContent = getOrCreateTextContent(); + textContent->setText(m_accumulatedContent); + m_contentAddedToTextBlock = true; + LOG_MESSAGE(QString("OllamaMessage: Added accumulated content to TextContent, length=%1") + .arg(m_accumulatedContent.length())); + } else { + LLMCore::TextContent *textContent = getOrCreateTextContent(); + textContent->appendText(content); + } +} + +void OllamaMessage::handleToolCall(const QJsonObject &toolCall) +{ + QJsonObject function = toolCall["function"].toObject(); + QString name = function["name"].toString(); + QJsonObject arguments = function["arguments"].toObject(); + + QString toolId = QString("call_%1_%2").arg(name).arg(QDateTime::currentMSecsSinceEpoch()); + + if (!m_contentAddedToTextBlock && !m_accumulatedContent.trimmed().isEmpty()) { + LOG_MESSAGE( + QString("OllamaMessage: Clearing accumulated content (tool call detected), length=%1") + .arg(m_accumulatedContent.length())); + m_accumulatedContent.clear(); + } + + addCurrentContent(toolId, name, arguments); + + LOG_MESSAGE( + QString("OllamaMessage: Structured tool call detected - name=%1, id=%2").arg(name, toolId)); +} +void OllamaMessage::handleDone(bool done) +{ + m_done = done; + if (done) { + bool isToolCall = tryParseToolCall(); + + if (!isToolCall && !m_contentAddedToTextBlock && !m_accumulatedContent.trimmed().isEmpty()) { + QString trimmed = m_accumulatedContent.trimmed(); + + if (trimmed.startsWith('{') + && (trimmed.contains("\"name\"") || trimmed.contains("\"arguments\""))) { + LOG_MESSAGE( + QString("OllamaMessage: Skipping invalid/incomplete tool call JSON (length=%1)") + .arg(trimmed.length())); + + for (auto it = m_currentBlocks.begin(); it != m_currentBlocks.end();) { + if (qobject_cast(*it)) { + LOG_MESSAGE(QString( + "OllamaMessage: Removing TextContent block (incomplete tool call)")); + (*it)->deleteLater(); + it = m_currentBlocks.erase(it); + } else { + ++it; + } + } + + m_accumulatedContent.clear(); + } else { + LLMCore::TextContent *textContent = getOrCreateTextContent(); + textContent->setText(m_accumulatedContent); + m_contentAddedToTextBlock = true; + LOG_MESSAGE( + QString( + "OllamaMessage: Added final accumulated content to TextContent, length=%1") + .arg(m_accumulatedContent.length())); + } + } + + updateStateFromDone(); + } +} +bool OllamaMessage::tryParseToolCall() +{ + QString trimmed = m_accumulatedContent.trimmed(); + + if (trimmed.isEmpty()) { + return false; + } + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(trimmed.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError) { + LOG_MESSAGE(QString("OllamaMessage: Content is not valid JSON (not a tool call): %1") + .arg(parseError.errorString())); + return false; + } + + if (!doc.isObject()) { + LOG_MESSAGE(QString("OllamaMessage: Content is not a JSON object (not a tool call)")); + return false; + } + + QJsonObject obj = doc.object(); + + if (!obj.contains("name") || !obj.contains("arguments")) { + LOG_MESSAGE( + QString("OllamaMessage: JSON missing 'name' or 'arguments' fields (not a tool call)")); + return false; + } + + QString name = obj["name"].toString(); + QJsonValue argsValue = obj["arguments"]; + QJsonObject arguments; + + if (argsValue.isObject()) { + arguments = argsValue.toObject(); + } else if (argsValue.isString()) { + QJsonDocument argsDoc = QJsonDocument::fromJson(argsValue.toString().toUtf8()); + if (argsDoc.isObject()) { + arguments = argsDoc.object(); + } else { + LOG_MESSAGE(QString("OllamaMessage: Failed to parse arguments as JSON object")); + return false; + } + } else { + LOG_MESSAGE(QString("OllamaMessage: Arguments field is neither object nor string")); + return false; + } + + if (name.isEmpty()) { + LOG_MESSAGE(QString("OllamaMessage: Tool name is empty")); + return false; + } + + QString toolId = QString("call_%1_%2").arg(name).arg(QDateTime::currentMSecsSinceEpoch()); + + for (auto block : m_currentBlocks) { + if (qobject_cast(block)) { + LOG_MESSAGE(QString("OllamaMessage: Removing TextContent block (tool call detected)")); + } + } + m_currentBlocks.clear(); + + addCurrentContent(toolId, name, arguments); + + LOG_MESSAGE( + QString( + "OllamaMessage: Successfully parsed tool call from legacy format - name=%1, id=%2, " + "args=%3") + .arg( + name, + toolId, + QString::fromUtf8(QJsonDocument(arguments).toJson(QJsonDocument::Compact)))); + + return true; +} + +bool OllamaMessage::isLikelyToolCallJson(const QString &content) const +{ + QString trimmed = content.trimmed(); + + if (trimmed.startsWith('{')) { + if (trimmed.contains("\"name\"") && trimmed.contains("\"arguments\"")) { + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(trimmed.toUtf8(), &parseError); + + if (parseError.error == QJsonParseError::NoError && doc.isObject()) { + QJsonObject obj = doc.object(); + if (obj.contains("name") && obj.contains("arguments")) { + return true; + } + } + } + } + + return false; +} + +QJsonObject OllamaMessage::toProviderFormat() const +{ + QJsonObject message; + message["role"] = "assistant"; + + QString textContent; + QJsonArray toolCalls; + + for (auto block : m_currentBlocks) { + if (!block) + continue; + + if (auto text = qobject_cast(block)) { + textContent += text->text(); + } else if (auto tool = qobject_cast(block)) { + QJsonObject toolCall; + toolCall["type"] = "function"; + toolCall["function"] = QJsonObject{{"name", tool->name()}, {"arguments", tool->input()}}; + toolCalls.append(toolCall); + } + } + + if (!textContent.isEmpty()) { + message["content"] = textContent; + } + + if (!toolCalls.isEmpty()) { + message["tool_calls"] = toolCalls; + } + + return message; +} + +QJsonArray OllamaMessage::createToolResultMessages(const QHash &toolResults) const +{ + QJsonArray messages; + + for (auto toolContent : getCurrentToolUseContent()) { + if (toolResults.contains(toolContent->id())) { + QJsonObject toolMessage; + toolMessage["role"] = "tool"; + toolMessage["content"] = toolResults[toolContent->id()]; + messages.append(toolMessage); + + LOG_MESSAGE(QString( + "OllamaMessage: Created tool result message for tool %1 (id=%2), " + "content length=%3") + .arg(toolContent->name(), toolContent->id()) + .arg(toolResults[toolContent->id()].length())); + } + } + + return messages; +} + +QList OllamaMessage::getCurrentToolUseContent() const +{ + QList toolBlocks; + for (auto block : m_currentBlocks) { + if (auto toolContent = qobject_cast(block)) { + toolBlocks.append(toolContent); + } + } + return toolBlocks; +} + +void OllamaMessage::startNewContinuation() +{ + LOG_MESSAGE(QString("OllamaMessage: Starting new continuation")); + + m_currentBlocks.clear(); + m_accumulatedContent.clear(); + m_done = false; + m_state = LLMCore::MessageState::Building; + m_contentAddedToTextBlock = false; +} + +void OllamaMessage::updateStateFromDone() +{ + if (!getCurrentToolUseContent().empty()) { + m_state = LLMCore::MessageState::RequiresToolExecution; + LOG_MESSAGE(QString("OllamaMessage: State set to RequiresToolExecution, tools count=%1") + .arg(getCurrentToolUseContent().size())); + } else { + m_state = LLMCore::MessageState::Final; + LOG_MESSAGE(QString("OllamaMessage: State set to Final")); + } +} + +LLMCore::TextContent *OllamaMessage::getOrCreateTextContent() +{ + for (auto block : m_currentBlocks) { + if (auto textContent = qobject_cast(block)) { + return textContent; + } + } + + return addCurrentContent(); +} + +} // namespace QodeAssist::Providers diff --git a/providers/OllamaMessage.hpp b/providers/OllamaMessage.hpp new file mode 100644 index 0000000..442d170 --- /dev/null +++ b/providers/OllamaMessage.hpp @@ -0,0 +1,67 @@ +/* + * 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::Providers { + +class OllamaMessage : public QObject +{ + Q_OBJECT +public: + explicit OllamaMessage(QObject *parent = nullptr); + + void handleContentDelta(const QString &content); + void handleToolCall(const QJsonObject &toolCall); + void handleDone(bool done); + + QJsonObject toProviderFormat() const; + QJsonArray createToolResultMessages(const QHash &toolResults) const; + + LLMCore::MessageState state() const { return m_state; } + QList getCurrentToolUseContent() const; + QList currentBlocks() const { return m_currentBlocks; } + + void startNewContinuation(); + +private: + bool m_done = false; + LLMCore::MessageState m_state = LLMCore::MessageState::Building; + QList m_currentBlocks; + QString m_accumulatedContent; + bool m_contentAddedToTextBlock = false; + + void updateStateFromDone(); + bool tryParseToolCall(); + bool isLikelyToolCallJson(const QString &content) const; + LLMCore::TextContent *getOrCreateTextContent(); + + template + T *addCurrentContent(Args &&...args) + { + T *content = new T(std::forward(args)...); + content->setParent(this); + m_currentBlocks.append(content); + return content; + } +}; + +} // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index 4794fb1..61f8a0f 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -33,6 +33,17 @@ namespace QodeAssist::Providers { +OllamaProvider::OllamaProvider(QObject *parent) + : LLMCore::Provider(parent) + , m_toolsManager(new Tools::ToolsManager(this)) +{ + connect( + m_toolsManager, + &Tools::ToolsManager::toolExecutionComplete, + this, + &OllamaProvider::onToolExecutionComplete); +} + QString OllamaProvider::name() const { return "Ollama"; @@ -94,6 +105,17 @@ void OllamaProvider::prepareRequest( } else { applySettings(Settings::chatAssistantSettings()); } + + if (supportsTools() && type == LLMCore::RequestType::Chat + && Settings::chatAssistantSettings().useTools()) { + auto toolsDefinitions = m_toolsManager->toolsFactory()->getToolsDefinitions( + LLMCore::ToolSchemaFormat::Ollama); + if (!toolsDefinitions.isEmpty()) { + request["tools"] = toolsDefinitions; + LOG_MESSAGE( + QString("OllamaProvider: Added %1 tools to request").arg(toolsDefinitions.size())); + } + } } QList OllamaProvider::getInstalledModels(const QString &url) @@ -151,6 +173,7 @@ QList OllamaProvider::validateRequest(const QJsonObject &request, LLMCo {"model", {}}, {"stream", {}}, {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, + {"tools", QJsonArray{}}, {"options", QJsonObject{ {"temperature", {}}, @@ -188,7 +211,9 @@ void OllamaProvider::sendRequest( const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) { m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + m_originalRequests[requestId] = payload; QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -201,6 +226,18 @@ void OllamaProvider::sendRequest( emit httpClient()->sendRequest(request); } +bool OllamaProvider::supportsTools() const +{ + return true; +} + +void OllamaProvider::cancelRequest(const LLMCore::RequestID &requestId) +{ + LOG_MESSAGE(QString("OllamaProvider: Cancelling request %1").arg(requestId)); + LLMCore::Provider::cancelRequest(requestId); + cleanupRequest(requestId); +} + void OllamaProvider::onDataReceived( const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) { @@ -211,9 +248,6 @@ void OllamaProvider::onDataReceived( return; } - bool isDone = false; - QString tempResponse; - for (const QString &line : lines) { if (line.trimmed().isEmpty()) { continue; @@ -222,6 +256,7 @@ void OllamaProvider::onDataReceived( QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &error); if (doc.isNull()) { + LOG_MESSAGE(QString("Failed to parse JSON: %1").arg(error.errorString())); continue; } @@ -232,32 +267,7 @@ void OllamaProvider::onDataReceived( continue; } - QString content; - - if (obj.contains("response")) { - content = obj["response"].toString(); - } else if (obj.contains("message")) { - QJsonObject messageObj = obj["message"].toObject(); - content = messageObj["content"].toString(); - } - - if (!content.isEmpty()) { - tempResponse += content; - } - - if (obj["done"].toBool()) { - isDone = true; - } - } - - if (!tempResponse.isEmpty()) { - buffers.responseContent += tempResponse; - emit partialResponseReceived(requestId, tempResponse); - } - - if (isDone) { - emit fullResponseReceived(requestId, buffers.responseContent); - m_dataBuffers.remove(requestId); + processStreamData(requestId, obj); } } @@ -267,17 +277,220 @@ void OllamaProvider::onRequestFinished( if (!success) { LOG_MESSAGE(QString("OllamaProvider 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)) { + OllamaMessage *message = m_messages[requestId]; + if (message->state() == LLMCore::MessageState::RequiresToolExecution) { + LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); + return; + } + } + + QString finalText; + if (m_messages.contains(requestId)) { + OllamaMessage *message = m_messages[requestId]; + + for (auto block : message->currentBlocks()) { + if (auto textContent = qobject_cast(block)) { + finalText += textContent->text(); + } + } + + if (!finalText.isEmpty()) { + LOG_MESSAGE(QString("Emitting full response for %1, length=%2") + .arg(requestId) + .arg(finalText.length())); + emit fullResponseReceived(requestId, finalText); + } + } + + cleanupRequest(requestId); +} + +void OllamaProvider::onToolExecutionComplete( + const QString &requestId, const QHash &toolResults) +{ + if (!m_messages.contains(requestId)) { + LOG_MESSAGE(QString("ERROR: No message found for request %1").arg(requestId)); + cleanupRequest(requestId); + return; + } + + if (!m_requestUrls.contains(requestId) || !m_originalRequests.contains(requestId)) { + LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); + cleanupRequest(requestId); + return; + } + + LOG_MESSAGE(QString("Tool execution complete for Ollama request %1").arg(requestId)); + + OllamaMessage *message = m_messages[requestId]; + + for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { + auto toolContent = message->getCurrentToolUseContent(); + for (auto tool : toolContent) { + if (tool->id() == it.key()) { + auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); + emit toolExecutionCompleted(requestId, tool->id(), toolStringName, it.value()); + break; } } } - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); + QJsonObject continuationRequest = m_originalRequests[requestId]; + QJsonArray messages = continuationRequest["messages"].toArray(); + + QJsonObject assistantMessage = message->toProviderFormat(); + messages.append(assistantMessage); + + LOG_MESSAGE(QString("Assistant message with tool_calls:\n%1") + .arg( + QString::fromUtf8( + QJsonDocument(assistantMessage).toJson(QJsonDocument::Indented)))); + + QJsonArray toolResultMessages = message->createToolResultMessages(toolResults); + for (const auto &toolMsg : toolResultMessages) { + messages.append(toolMsg); + LOG_MESSAGE(QString("Tool result message:\n%1") + .arg( + QString::fromUtf8( + QJsonDocument(toolMsg.toObject()).toJson(QJsonDocument::Indented)))); + } + + continuationRequest["messages"] = messages; + + LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") + .arg(requestId) + .arg(toolResults.size())); + + message->startNewContinuation(); + sendRequest(requestId, m_requestUrls[requestId], continuationRequest); } +void OllamaProvider::processStreamData(const QString &requestId, const QJsonObject &data) +{ + OllamaMessage *message = m_messages.value(requestId); + if (!message) { + message = new OllamaMessage(this); + m_messages[requestId] = message; + LOG_MESSAGE(QString("Created NEW OllamaMessage for request %1").arg(requestId)); + } + + if (data.contains("message")) { + QJsonObject messageObj = data["message"].toObject(); + + if (messageObj.contains("content")) { + QString content = messageObj["content"].toString(); + if (!content.isEmpty()) { + message->handleContentDelta(content); + + bool hasTextContent = false; + for (auto block : message->currentBlocks()) { + if (qobject_cast(block)) { + hasTextContent = true; + break; + } + } + + if (hasTextContent) { + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + buffers.responseContent += content; + emit partialResponseReceived(requestId, content); + } + } + } + + if (messageObj.contains("tool_calls")) { + QJsonArray toolCalls = messageObj["tool_calls"].toArray(); + LOG_MESSAGE( + QString("OllamaProvider: Found %1 structured tool calls").arg(toolCalls.size())); + for (const auto &toolCallValue : toolCalls) { + message->handleToolCall(toolCallValue.toObject()); + } + } + } + else if (data.contains("response")) { + QString content = data["response"].toString(); + if (!content.isEmpty()) { + message->handleContentDelta(content); + + bool hasTextContent = false; + for (auto block : message->currentBlocks()) { + if (qobject_cast(block)) { + hasTextContent = true; + break; + } + } + + if (hasTextContent) { + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + buffers.responseContent += content; + emit partialResponseReceived(requestId, content); + } + } + } + + if (data["done"].toBool()) { + message->handleDone(true); + handleMessageComplete(requestId); + } +} + +void OllamaProvider::handleMessageComplete(const QString &requestId) +{ + if (!m_messages.contains(requestId)) + return; + + OllamaMessage *message = m_messages[requestId]; + + if (message->state() == LLMCore::MessageState::RequiresToolExecution) { + LOG_MESSAGE(QString("Ollama message requires tool execution for %1").arg(requestId)); + + auto toolUseContent = message->getCurrentToolUseContent(); + + if (toolUseContent.isEmpty()) { + LOG_MESSAGE( + QString("WARNING: No tools to execute for %1 despite RequiresToolExecution state") + .arg(requestId)); + return; + } + + for (auto toolContent : toolUseContent) { + auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); + emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); + + LOG_MESSAGE( + QString("Executing tool: name=%1, id=%2, input=%3") + .arg(toolContent->name()) + .arg(toolContent->id()) + .arg( + QString::fromUtf8( + QJsonDocument(toolContent->input()).toJson(QJsonDocument::Compact)))); + + m_toolsManager->executeToolCall( + requestId, toolContent->id(), toolContent->name(), toolContent->input()); + } + + } else { + LOG_MESSAGE(QString("Ollama message marked as complete for %1").arg(requestId)); + } +} + +void OllamaProvider::cleanupRequest(const LLMCore::RequestID &requestId) +{ + LOG_MESSAGE(QString("Cleaning up Ollama request %1").arg(requestId)); + + if (m_messages.contains(requestId)) { + auto msg = m_messages.take(requestId); + msg->deleteLater(); + } + + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); + m_originalRequests.remove(requestId); + m_toolsManager->cleanupRequest(requestId); +} } // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.hpp b/providers/OllamaProvider.hpp index 497a902..1a975c4 100644 --- a/providers/OllamaProvider.hpp +++ b/providers/OllamaProvider.hpp @@ -19,13 +19,19 @@ #pragma once -#include "llmcore/Provider.hpp" +#include + +#include "OllamaMessage.hpp" +#include "tools/ToolsManager.hpp" namespace QodeAssist::Providers { class OllamaProvider : public LLMCore::Provider { + Q_OBJECT public: + explicit OllamaProvider(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 processStreamData(const QString &requestId, const QJsonObject &data); + 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