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