mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-12 10:10:44 -05:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c1d9ddc0e | |||
| 6ff6901421 | |||
| a2f3ae4f64 | |||
| 6937b48fbf | |||
| c6e77c59d3 | |||
| 655471aec6 | |||
| af90d3cad2 | |||
| 9b90aaa06e | |||
| e7110810f8 | |||
| 1848d44503 | |||
| db82fb08e8 | |||
| 9117572f82 | |||
| a143cc8e20 | |||
| eae2b748d5 | |||
| 64bca47290 | |||
| 531fce96b5 | |||
| e7e437590a | |||
| 00b7287e08 | |||
| 5a49a2e7eb | |||
| 3b56c1f07a | |||
| d483ca372d | |||
| dfd9979450 | |||
| 6cb0b14b18 | |||
| 43b64b9166 | |||
| 608103b92e | |||
| e1025df21e | |||
| fad2453dbe | |||
| 5e1530715c | |||
| dfac209c23 | |||
| cab8718979 | |||
| 5dc28fc1ad | |||
| 1122332423 | |||
| 7e878cdbf8 | |||
| c95b20d6d4 | |||
| 70c610997a | |||
| 8a4bf54fff | |||
| db7da29fa4 | |||
| a0a76f2665 | |||
| 56354e8d87 | |||
| b7322be00c | |||
| 254fac246d | |||
| 0365018834 | |||
| 755be518be | |||
| fe82b48bef | |||
| 8a338ecb69 | |||
| 238ca00227 | |||
| 18fb2b530f | |||
| f0d2e42680 | |||
| ff0f994ec6 | |||
| 45df27e749 | |||
| 02863003a9 | |||
| 002b8e01e5 | |||
| f54d1185aa | |||
| bcb0c6f761 | |||
| d285ab6117 | |||
| 5ae6f9e3bf | |||
| fb5903e44f | |||
| ce66c8e4f7 | |||
| 5f094887e7 | |||
| 69d9af1a97 | |||
| 86b52bf858 | |||
| cac6068ee7 | |||
| 8d495dd1bf | |||
| 906c161729 | |||
| ebd71daf3d | |||
| 84770abb20 | |||
| b4e8bdf6da | |||
| d2b28093a6 | |||
| bde58fb9aa | |||
| d4b6f8976b | |||
| cd08b5d919 | |||
| f6de03f601 | |||
| 1a08eebe92 | |||
| ea4f8b9df9 | |||
| 7f77f7175d | |||
| bed42f9098 | |||
| 10b924d78a | |||
| 8aa37c5c8c | |||
| f8b87da2ca | |||
| 7663bd34af | |||
| a52c86c6f0 | |||
| ac53296e85 | |||
| c688cba3dd | |||
| ff750c271a | |||
| d0f8c1098f | |||
| 5cde6ffac7 | |||
| 8c6f1e514b | |||
| 99cd79aac8 | |||
| d2b6c11569 | |||
| ec1b5bdf5f | |||
| 561661b476 | |||
| 76309be0a6 | |||
| 5969d530bd | |||
| 809f1c6614 | |||
| 851e681cf5 | |||
| f2f3b7cce0 | |||
| 5b7a9b681c | |||
| 29af277139 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -12,4 +12,4 @@ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cl
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
custom: ['https://www.paypal.com/paypalme/palm1r', 'https://github.com/Palm1r/QodeAssist#support-the-development-of-qodeassist']
|
||||
|
||||
37
.github/workflows/build_cmake.yml
vendored
37
.github/workflows/build_cmake.yml
vendored
@ -45,17 +45,21 @@ jobs:
|
||||
cc: "clang", cxx: "clang++"
|
||||
}
|
||||
qt_config:
|
||||
- {
|
||||
qt_version: "6.9.1",
|
||||
qt_creator_version: "17.0.0"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.8.3",
|
||||
qt_creator_version: "16.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.9.2",
|
||||
qt_creator_version: "17.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.10.0",
|
||||
qt_creator_version: "18.0.0"
|
||||
}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
||||
|
||||
- name: Checkout submodules
|
||||
id: git
|
||||
@ -73,7 +77,7 @@ jobs:
|
||||
endif()
|
||||
|
||||
- name: Download Ninja and CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541
|
||||
with:
|
||||
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||
@ -105,13 +109,18 @@ jobs:
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(qt_version "${{ matrix.qt_config.qt_version }}")
|
||||
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
|
||||
|
||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(url_os "windows_x86")
|
||||
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
|
||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
|
||||
else()
|
||||
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
|
||||
endif()
|
||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||
set(url_os "linux_x64")
|
||||
if (qt_version VERSION_LESS "6.7.0")
|
||||
@ -120,7 +129,11 @@ jobs:
|
||||
set(qt_package_arch_suffix "linux_gcc_64")
|
||||
endif()
|
||||
set(qt_dir_prefix "${qt_version}/gcc_64")
|
||||
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
|
||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
|
||||
else()
|
||||
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
|
||||
endif()
|
||||
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
||||
set(url_os "mac_x64")
|
||||
set(qt_package_arch_suffix "clang_64")
|
||||
@ -186,7 +199,7 @@ jobs:
|
||||
endif()
|
||||
|
||||
- name: Download Qt Creator
|
||||
uses: qt-creator/install-dev-package@v2.0
|
||||
uses: qt-creator/install-dev-package@1460787a21551eb3d867b0de30e8d3f1aadef5ac
|
||||
with:
|
||||
version: ${{ matrix.qt_config.qt_creator_version }}
|
||||
unzip-to: 'qtcreator'
|
||||
@ -252,7 +265,7 @@ jobs:
|
||||
endif()
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
@ -269,7 +282,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: release-with-dirs
|
||||
|
||||
@ -280,7 +293,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -73,4 +73,8 @@ CMakeLists.txt.user*
|
||||
*.dll
|
||||
*.exe
|
||||
|
||||
/build
|
||||
/build
|
||||
/.qodeassist
|
||||
/.cursor
|
||||
/.vscode
|
||||
.qtc_clangd/compile_commands.json
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "3rdparty/inja"]
|
||||
path = 3rdparty/inja
|
||||
url = https://github.com/pantor/inja
|
||||
|
||||
1
3rdparty/inja
vendored
1
3rdparty/inja
vendored
Submodule 3rdparty/inja deleted from 384a6bef3f
@ -37,6 +37,7 @@ add_definitions(
|
||||
add_subdirectory(llmcore)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(logger)
|
||||
add_subdirectory(UIControls)
|
||||
add_subdirectory(ChatView)
|
||||
add_subdirectory(context)
|
||||
if(GTest_FOUND)
|
||||
@ -49,6 +50,7 @@ add_qtc_plugin(QodeAssist
|
||||
QtCreator::LanguageClient
|
||||
QtCreator::TextEditor
|
||||
QtCreator::ProjectExplorer
|
||||
QtCreator::CppEditor
|
||||
DEPENDS
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
@ -57,6 +59,7 @@ add_qtc_plugin(QodeAssist
|
||||
Qt::Network
|
||||
QtCreator::ExtensionSystem
|
||||
QtCreator::Utils
|
||||
QtCreator::CPlusPlus
|
||||
QodeAssistChatViewplugin
|
||||
SOURCES
|
||||
.github/workflows/build_cmake.yml
|
||||
@ -111,6 +114,22 @@ 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/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
|
||||
tools/ToolHandler.hpp tools/ToolHandler.cpp
|
||||
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
|
||||
tools/ToolsManager.hpp tools/ToolsManager.cpp
|
||||
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
||||
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
||||
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
||||
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
|
||||
)
|
||||
|
||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||
|
||||
@ -6,17 +6,21 @@ qt_policy(SET QTP0004 NEW)
|
||||
qt_add_qml_module(QodeAssistChatView
|
||||
URI ChatView
|
||||
VERSION 1.0
|
||||
DEPENDENCIES QtQuick
|
||||
DEPENDENCIES
|
||||
QtQuick
|
||||
QML_FILES
|
||||
qml/RootItem.qml
|
||||
qml/ChatItem.qml
|
||||
qml/Badge.qml
|
||||
qml/dialog/CodeBlock.qml
|
||||
qml/dialog/TextBlock.qml
|
||||
qml/controls/QoAButton.qml
|
||||
qml/parts/TopBar.qml
|
||||
qml/parts/BottomBar.qml
|
||||
qml/parts/AttachedFilesPlace.qml
|
||||
qml/parts/Toast.qml
|
||||
qml/ToolStatusItem.qml
|
||||
qml/FileEditItem.qml
|
||||
qml/parts/RulesViewer.qml
|
||||
qml/parts/FileEditsActionBar.qml
|
||||
|
||||
RESOURCES
|
||||
icons/attach-file-light.svg
|
||||
@ -33,6 +37,12 @@ qt_add_qml_module(QodeAssistChatView
|
||||
icons/window-unlock.svg
|
||||
icons/chat-icon.svg
|
||||
icons/chat-pause-icon.svg
|
||||
icons/rules-icon.svg
|
||||
icons/open-in-editor.svg
|
||||
icons/apply-changes-button.svg
|
||||
icons/undo-changes-button.svg
|
||||
icons/reject-changes-button.svg
|
||||
|
||||
SOURCES
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
ChatModel.hpp ChatModel.cpp
|
||||
@ -42,6 +52,8 @@ qt_add_qml_module(QodeAssistChatView
|
||||
ChatUtils.h ChatUtils.cpp
|
||||
ChatSerializer.hpp ChatSerializer.cpp
|
||||
ChatView.hpp ChatView.cpp
|
||||
ChatData.hpp
|
||||
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
@ -55,6 +67,8 @@ target_link_libraries(QodeAssistChatView
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
QodeAssistUIControlsplugin
|
||||
QodeAssistLogger
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
|
||||
32
ChatView/ChatData.hpp
Normal file
32
ChatView/ChatData.hpp
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
Q_NAMESPACE
|
||||
QML_NAMED_ELEMENT(MessagePartType)
|
||||
|
||||
enum class MessagePartType { Code, Text };
|
||||
Q_ENUM_NS(MessagePartType)
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@ -19,10 +19,14 @@
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include <utils/aspects.h>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QtQml>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "context/ChangesManager.h"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -36,6 +40,21 @@ ChatModel::ChatModel(QObject *parent)
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatModel::tokensThresholdChanged);
|
||||
|
||||
connect(&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditApplied,
|
||||
this,
|
||||
&ChatModel::onFileEditApplied);
|
||||
|
||||
connect(&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditRejected,
|
||||
this,
|
||||
&ChatModel::onFileEditRejected);
|
||||
|
||||
connect(&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditArchived,
|
||||
this,
|
||||
&ChatModel::onFileEditArchived);
|
||||
}
|
||||
|
||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||
@ -91,7 +110,8 @@ void ChatModel::addMessage(
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
|
||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
||||
&& m_messages.last().role == role) {
|
||||
Message &lastMessage = m_messages.last();
|
||||
lastMessage.content = content;
|
||||
lastMessage.attachments = attachments;
|
||||
@ -102,6 +122,45 @@ void ChatModel::addMessage(
|
||||
newMessage.attachments = attachments;
|
||||
m_messages.append(newMessage);
|
||||
endInsertRows();
|
||||
|
||||
if (m_loadingFromHistory && role == ChatRole::FileEdit) {
|
||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||
if (content.contains(marker)) {
|
||||
int markerPos = content.indexOf(marker);
|
||||
int jsonStart = markerPos + marker.length();
|
||||
|
||||
if (jsonStart < content.length()) {
|
||||
QString jsonStr = content.mid(jsonStart);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||
|
||||
if (doc.isObject()) {
|
||||
QJsonObject editData = doc.object();
|
||||
QString editId = editData.value("edit_id").toString();
|
||||
QString filePath = editData.value("file").toString();
|
||||
QString oldContent = editData.value("old_content").toString();
|
||||
QString newContent = editData.value("new_content").toString();
|
||||
QString originalStatus = editData.value("status").toString();
|
||||
|
||||
if (!editId.isEmpty() && !filePath.isEmpty()) {
|
||||
Context::ChangesManager::instance().addFileEdit(
|
||||
editId, filePath, oldContent, newContent, false, true);
|
||||
|
||||
editData["status"] = "archived";
|
||||
editData["status_message"] = "Loaded from chat history";
|
||||
|
||||
QString updatedContent = marker
|
||||
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
|
||||
m_messages.last().content = updatedContent;
|
||||
|
||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||
|
||||
LOG_MESSAGE(QString("Registered historical file edit: %1 (original status: %2, now: archived)")
|
||||
.arg(editId, originalStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,7 +183,6 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||
int lastIndex = 0;
|
||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||
bool foundCodeBlock = blockMatches.hasNext();
|
||||
|
||||
while (blockMatches.hasNext()) {
|
||||
auto match = blockMatches.next();
|
||||
@ -132,10 +190,19 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
||||
QString textBetween
|
||||
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||
if (!textBetween.isEmpty()) {
|
||||
parts.append({MessagePart::Text, textBetween, ""});
|
||||
MessagePart part;
|
||||
part.type = MessagePartType::Text;
|
||||
part.text = textBetween;
|
||||
parts.append(part);
|
||||
}
|
||||
}
|
||||
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
|
||||
|
||||
MessagePart codePart;
|
||||
codePart.type = MessagePartType::Code;
|
||||
codePart.text = match.captured(2).trimmed();
|
||||
codePart.language = match.captured(1);
|
||||
parts.append(codePart);
|
||||
|
||||
lastIndex = match.capturedEnd();
|
||||
}
|
||||
|
||||
@ -148,13 +215,22 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
||||
if (unclosedMatch.hasMatch()) {
|
||||
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
|
||||
if (!beforeCodeBlock.isEmpty()) {
|
||||
parts.append({MessagePart::Text, beforeCodeBlock, ""});
|
||||
MessagePart part;
|
||||
part.type = MessagePartType::Text;
|
||||
part.text = beforeCodeBlock;
|
||||
parts.append(part);
|
||||
}
|
||||
|
||||
parts.append(
|
||||
{MessagePart::Code, unclosedMatch.captured(2).trimmed(), unclosedMatch.captured(1)});
|
||||
MessagePart codePart;
|
||||
codePart.type = MessagePartType::Code;
|
||||
codePart.text = unclosedMatch.captured(2).trimmed();
|
||||
codePart.language = unclosedMatch.captured(1);
|
||||
parts.append(codePart);
|
||||
} else if (!remainingText.isEmpty()) {
|
||||
parts.append({MessagePart::Text, remainingText, ""});
|
||||
MessagePart part;
|
||||
part.type = MessagePartType::Text;
|
||||
part.text = remainingText;
|
||||
parts.append(part);
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,6 +251,9 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
|
||||
case ChatRole::Assistant:
|
||||
role = "assistant";
|
||||
break;
|
||||
case ChatRole::Tool:
|
||||
case ChatRole::FileEdit:
|
||||
continue;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
@ -222,4 +301,182 @@ void ChatModel::resetModelTo(int index)
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::addToolExecutionStatus(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||
{
|
||||
QString content = toolName;
|
||||
|
||||
LOG_MESSAGE(QString("Adding tool execution status: requestId=%1, toolId=%2, toolName=%3")
|
||||
.arg(requestId, toolId, toolName));
|
||||
|
||||
if (!m_messages.isEmpty() && !toolId.isEmpty() && m_messages.last().id == toolId
|
||||
&& m_messages.last().role == ChatRole::Tool) {
|
||||
Message &lastMessage = m_messages.last();
|
||||
lastMessage.content = content;
|
||||
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
|
||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
Message newMessage{ChatRole::Tool, content, toolId};
|
||||
m_messages.append(newMessage);
|
||||
endInsertRows();
|
||||
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
|
||||
.arg(m_messages.size() - 1)
|
||||
.arg(toolId));
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::updateToolResult(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
|
||||
{
|
||||
if (m_messages.isEmpty() || toolId.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Cannot update tool result: messages empty=%1, toolId empty=%2")
|
||||
.arg(m_messages.isEmpty())
|
||||
.arg(toolId.isEmpty()));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("Updating tool result: requestId=%1, toolId=%2, toolName=%3, result length=%4")
|
||||
.arg(requestId, toolId, toolName)
|
||||
.arg(result.length()));
|
||||
|
||||
bool toolMessageFound = false;
|
||||
for (int i = m_messages.size() - 1; i >= 0; --i) {
|
||||
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
|
||||
m_messages[i].content = toolName + "\n" + result;
|
||||
emit dataChanged(index(i), index(i));
|
||||
toolMessageFound = true;
|
||||
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!toolMessageFound) {
|
||||
LOG_MESSAGE(QString("WARNING: Tool message with requestId=%1 toolId=%2 not found!")
|
||||
.arg(requestId, toolId));
|
||||
}
|
||||
|
||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||
if (result.contains(marker)) {
|
||||
LOG_MESSAGE(QString("File edit marker detected in tool result"));
|
||||
|
||||
int markerPos = result.indexOf(marker);
|
||||
int jsonStart = markerPos + marker.length();
|
||||
|
||||
if (jsonStart < result.length()) {
|
||||
QString jsonStr = result.mid(jsonStart);
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
LOG_MESSAGE(QString("ERROR: Failed to parse file edit JSON at offset %1: %2")
|
||||
.arg(parseError.offset)
|
||||
.arg(parseError.errorString()));
|
||||
} else if (!doc.isObject()) {
|
||||
LOG_MESSAGE(
|
||||
QString("ERROR: Parsed JSON is not an object, is array=%1").arg(doc.isArray()));
|
||||
} else {
|
||||
QJsonObject editData = doc.object();
|
||||
|
||||
QString editId = editData.value("edit_id").toString();
|
||||
|
||||
if (editId.isEmpty()) {
|
||||
editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Adding FileEdit message, editId=%1").arg(editId));
|
||||
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
Message fileEditMsg;
|
||||
fileEditMsg.role = ChatRole::FileEdit;
|
||||
fileEditMsg.content = result;
|
||||
fileEditMsg.id = editId;
|
||||
m_messages.append(fileEditMsg);
|
||||
endInsertRows();
|
||||
|
||||
LOG_MESSAGE(QString("Added FileEdit message with editId=%1").arg(editId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent)
|
||||
{
|
||||
for (int i = 0; i < m_messages.size(); ++i) {
|
||||
if (m_messages[i].id == messageId) {
|
||||
m_messages[i].content = newContent;
|
||||
emit dataChanged(index(i), index(i));
|
||||
LOG_MESSAGE(QString("Updated message content for id: %1").arg(messageId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::setLoadingFromHistory(bool loading)
|
||||
{
|
||||
m_loadingFromHistory = loading;
|
||||
LOG_MESSAGE(QString("ChatModel loading from history: %1").arg(loading ? "true" : "false"));
|
||||
}
|
||||
|
||||
bool ChatModel::isLoadingFromHistory() const
|
||||
{
|
||||
return m_loadingFromHistory;
|
||||
}
|
||||
|
||||
void ChatModel::onFileEditApplied(const QString &editId)
|
||||
{
|
||||
updateFileEditStatus(editId, "applied", "Successfully applied");
|
||||
}
|
||||
|
||||
void ChatModel::onFileEditRejected(const QString &editId)
|
||||
{
|
||||
updateFileEditStatus(editId, "rejected", "Rejected by user");
|
||||
}
|
||||
|
||||
void ChatModel::onFileEditArchived(const QString &editId)
|
||||
{
|
||||
updateFileEditStatus(editId, "archived", "Archived (from previous conversation turn)");
|
||||
}
|
||||
|
||||
void ChatModel::updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage)
|
||||
{
|
||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||
|
||||
for (int i = 0; i < m_messages.size(); ++i) {
|
||||
if (m_messages[i].role == ChatRole::FileEdit && m_messages[i].id == editId) {
|
||||
const QString &content = m_messages[i].content;
|
||||
|
||||
if (content.contains(marker)) {
|
||||
int markerPos = content.indexOf(marker);
|
||||
int jsonStart = markerPos + marker.length();
|
||||
|
||||
if (jsonStart < content.length()) {
|
||||
QString jsonStr = content.mid(jsonStart);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||
|
||||
if (doc.isObject()) {
|
||||
QJsonObject editData = doc.object();
|
||||
|
||||
editData["status"] = status;
|
||||
editData["status_message"] = statusMessage;
|
||||
|
||||
QString updatedContent = marker
|
||||
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
|
||||
|
||||
m_messages[i].content = updatedContent;
|
||||
|
||||
emit dataChanged(index(i), index(i));
|
||||
|
||||
LOG_MESSAGE(QString("Updated FileEdit message status: editId=%1, status=%2")
|
||||
.arg(editId, status));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -37,10 +37,11 @@ class ChatModel : public QAbstractListModel
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
enum ChatRole { System, User, Assistant };
|
||||
enum ChatRole { System, User, Assistant, Tool, FileEdit };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
|
||||
Q_ENUM(Roles)
|
||||
|
||||
struct Message
|
||||
{
|
||||
@ -75,12 +76,32 @@ public:
|
||||
|
||||
Q_INVOKABLE void resetModelTo(int index);
|
||||
|
||||
void addToolExecutionStatus(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||
void updateToolResult(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &result);
|
||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
||||
|
||||
void setLoadingFromHistory(bool loading);
|
||||
bool isLoadingFromHistory() const;
|
||||
|
||||
signals:
|
||||
void tokensThresholdChanged();
|
||||
void modelReseted();
|
||||
|
||||
private slots:
|
||||
void onFileEditApplied(const QString &editId);
|
||||
void onFileEditRejected(const QString &editId);
|
||||
void onFileEditArchived(const QString &editId);
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
||||
|
||||
QVector<Message> m_messages;
|
||||
bool m_loadingFromHistory = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -29,16 +29,20 @@
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "ToolsSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "context/ChangesManager.h"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "context/TokenUtils.hpp"
|
||||
#include "llmcore/RulesLoader.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -47,6 +51,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||
, m_isRequestInProgress(false)
|
||||
{
|
||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||
connect(
|
||||
@ -76,7 +81,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::updateInputTokensCount);
|
||||
|
||||
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
|
||||
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
||||
setRecentFilePath(QString{});
|
||||
m_currentMessageRequestId.clear();
|
||||
updateCurrentMessageEditsStats();
|
||||
});
|
||||
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||
connect(
|
||||
@ -131,8 +140,69 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::textFormatChanged);
|
||||
connect(m_clientInterface, &ClientInterface::errorOccurred, this, [this](const QString &error) {
|
||||
this->setRequestProgressStatus(false);
|
||||
m_lastErrorMessage = error;
|
||||
emit lastErrorMessageChanged();
|
||||
});
|
||||
|
||||
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
|
||||
if (!m_currentMessageRequestId.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId));
|
||||
}
|
||||
|
||||
m_currentMessageRequestId = requestId;
|
||||
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
|
||||
updateCurrentMessageEditsStats();
|
||||
});
|
||||
|
||||
connect(
|
||||
&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditAdded,
|
||||
this,
|
||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
||||
|
||||
connect(
|
||||
&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditApplied,
|
||||
this,
|
||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
||||
|
||||
connect(
|
||||
&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditRejected,
|
||||
this,
|
||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
||||
|
||||
connect(
|
||||
&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditUndone,
|
||||
this,
|
||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
||||
|
||||
connect(
|
||||
&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditArchived,
|
||||
this,
|
||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
||||
|
||||
updateInputTokensCount();
|
||||
refreshRules();
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
||||
this,
|
||||
&ChatRootView::refreshRules);
|
||||
|
||||
QSettings appSettings;
|
||||
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
|
||||
|
||||
connect(
|
||||
&Settings::toolsSettings().useTools,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::toolsSupportEnabledChanged);
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
@ -158,7 +228,7 @@ void ChatRootView::sendMessage(const QString &message)
|
||||
}
|
||||
}
|
||||
|
||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles, m_isAgentMode);
|
||||
clearAttachmentFiles();
|
||||
setRequestProgressStatus(true);
|
||||
}
|
||||
@ -235,7 +305,10 @@ void ChatRootView::loadHistory(const QString &filePath)
|
||||
} else {
|
||||
setRecentFilePath(filePath);
|
||||
}
|
||||
|
||||
m_currentMessageRequestId.clear();
|
||||
updateInputTokensCount();
|
||||
updateCurrentMessageEditsStats();
|
||||
}
|
||||
|
||||
void ChatRootView::showSaveDialog()
|
||||
@ -333,8 +406,7 @@ QString ChatRootView::getSuggestedFileName() const
|
||||
QFileInfo finalCheck(fullPath);
|
||||
|
||||
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
||||
fileName = QString("chat_%1").arg(
|
||||
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
||||
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
||||
}
|
||||
|
||||
return fileName;
|
||||
@ -485,6 +557,25 @@ void ChatRootView::openChatHistoryFolder()
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void ChatRootView::openRulesFolder()
|
||||
{
|
||||
auto project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString projectPath = project->projectDirectory().toFSPathString();
|
||||
QString rulesPath = projectPath + "/.qodeassist/rules";
|
||||
|
||||
QDir dir(rulesPath);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void ChatRootView::updateInputTokensCount()
|
||||
{
|
||||
int inputTokens = m_messageTokensCount;
|
||||
@ -621,4 +712,379 @@ void ChatRootView::setRequestProgressStatus(bool state)
|
||||
emit isRequestInProgressChanged();
|
||||
}
|
||||
|
||||
QString ChatRootView::lastErrorMessage() const
|
||||
{
|
||||
return m_lastErrorMessage;
|
||||
}
|
||||
|
||||
QVariantList ChatRootView::activeRules() const
|
||||
{
|
||||
return m_activeRules;
|
||||
}
|
||||
|
||||
int ChatRootView::activeRulesCount() const
|
||||
{
|
||||
return m_activeRules.size();
|
||||
}
|
||||
|
||||
QString ChatRootView::getRuleContent(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_activeRules.size())
|
||||
return QString();
|
||||
|
||||
return LLMCore::RulesLoader::loadRuleFileContent(
|
||||
m_activeRules[index].toMap()["filePath"].toString());
|
||||
}
|
||||
|
||||
void ChatRootView::refreshRules()
|
||||
{
|
||||
m_activeRules.clear();
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
if (!project) {
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
auto ruleFiles
|
||||
= LLMCore::RulesLoader::getRuleFilesForProject(project, LLMCore::RulesContext::Chat);
|
||||
|
||||
for (const auto &ruleFile : ruleFiles) {
|
||||
QVariantMap ruleMap;
|
||||
ruleMap["filePath"] = ruleFile.filePath;
|
||||
ruleMap["fileName"] = ruleFile.fileName;
|
||||
ruleMap["category"] = ruleFile.category;
|
||||
m_activeRules.append(ruleMap);
|
||||
}
|
||||
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
}
|
||||
|
||||
bool ChatRootView::isAgentMode() const
|
||||
{
|
||||
return m_isAgentMode;
|
||||
}
|
||||
|
||||
void ChatRootView::setIsAgentMode(bool newIsAgentMode)
|
||||
{
|
||||
if (m_isAgentMode != newIsAgentMode) {
|
||||
m_isAgentMode = newIsAgentMode;
|
||||
|
||||
QSettings settings;
|
||||
settings.setValue("QodeAssist/Chat/AgentMode", newIsAgentMode);
|
||||
|
||||
emit isAgentModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatRootView::toolsSupportEnabled() const
|
||||
{
|
||||
return Settings::toolsSettings().useTools();
|
||||
}
|
||||
|
||||
void ChatRootView::applyFileEdit(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||
m_lastInfoMessage = QString("File edit applied successfully");
|
||||
emit lastInfoMessageChanged();
|
||||
|
||||
updateFileEditStatus(editId, "applied");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
||||
? QString("Failed to apply file edit")
|
||||
: QString("Failed to apply file edit: %1").arg(edit.statusMessage);
|
||||
emit lastErrorMessageChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::rejectFileEdit(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||
m_lastInfoMessage = QString("File edit rejected");
|
||||
emit lastInfoMessageChanged();
|
||||
|
||||
updateFileEditStatus(editId, "rejected");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
||||
? QString("Failed to reject file edit")
|
||||
: QString("Failed to reject file edit: %1").arg(edit.statusMessage);
|
||||
emit lastErrorMessageChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::undoFileEdit(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||
m_lastInfoMessage = QString("File edit undone successfully");
|
||||
emit lastInfoMessageChanged();
|
||||
|
||||
updateFileEditStatus(editId, "rejected");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
||||
? QString("Failed to undo file edit")
|
||||
: QString("Failed to undo file edit: %1").arg(edit.statusMessage);
|
||||
emit lastErrorMessageChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::openFileEditInEditor(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
|
||||
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
if (edit.editId.isEmpty()) {
|
||||
m_lastErrorMessage = QString("File edit not found: %1").arg(editId);
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
|
||||
|
||||
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
|
||||
if (!editor) {
|
||||
m_lastErrorMessage = QString("Failed to open file in editor: %1").arg(edit.filePath);
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
|
||||
if (textEditor && textEditor->editorWidget()) {
|
||||
QTextDocument *doc = textEditor->editorWidget()->document();
|
||||
if (doc) {
|
||||
QString currentContent = doc->toPlainText();
|
||||
int position = -1;
|
||||
|
||||
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
||||
position = currentContent.indexOf(edit.newContent);
|
||||
}
|
||||
else if (!edit.oldContent.isEmpty()) {
|
||||
position = currentContent.indexOf(edit.oldContent);
|
||||
}
|
||||
|
||||
if (position >= 0) {
|
||||
QTextCursor cursor(doc);
|
||||
cursor.setPosition(position);
|
||||
textEditor->editorWidget()->setTextCursor(cursor);
|
||||
textEditor->editorWidget()->centerCursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||
}
|
||||
|
||||
void ChatRootView::updateFileEditStatus(const QString &editId, const QString &status)
|
||||
{
|
||||
auto messages = m_chatModel->getChatHistory();
|
||||
for (int i = 0; i < messages.size(); ++i) {
|
||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
||||
QString content = messages[i].content;
|
||||
|
||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||
int markerPos = content.indexOf(marker);
|
||||
|
||||
QString jsonStr = content;
|
||||
if (markerPos >= 0) {
|
||||
jsonStr = content.mid(markerPos + marker.length());
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
QJsonObject obj = doc.object();
|
||||
obj["status"] = status;
|
||||
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
if (!edit.statusMessage.isEmpty()) {
|
||||
obj["status_message"] = edit.statusMessage;
|
||||
}
|
||||
|
||||
QString updatedContent = marker + QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentMessageEditsStats();
|
||||
}
|
||||
|
||||
void ChatRootView::applyAllFileEditsForCurrentMessage()
|
||||
{
|
||||
if (m_currentMessageRequestId.isEmpty()) {
|
||||
m_lastErrorMessage = QString("No active message with file edits");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentMessageRequestId));
|
||||
|
||||
QString errorMsg;
|
||||
bool success = Context::ChangesManager::instance()
|
||||
.reapplyAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
|
||||
|
||||
if (success) {
|
||||
m_lastInfoMessage = QString("All file edits applied successfully");
|
||||
emit lastInfoMessageChanged();
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Applied) {
|
||||
updateFileEditStatus(edit.editId, "applied");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_lastErrorMessage = errorMsg.isEmpty()
|
||||
? QString("Failed to apply some file edits")
|
||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg);
|
||||
emit lastErrorMessageChanged();
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Applied) {
|
||||
updateFileEditStatus(edit.editId, "applied");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentMessageEditsStats();
|
||||
}
|
||||
|
||||
void ChatRootView::undoAllFileEditsForCurrentMessage()
|
||||
{
|
||||
if (m_currentMessageRequestId.isEmpty()) {
|
||||
m_lastErrorMessage = QString("No active message with file edits");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentMessageRequestId));
|
||||
|
||||
QString errorMsg;
|
||||
bool success = Context::ChangesManager::instance()
|
||||
.undoAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
|
||||
|
||||
if (success) {
|
||||
m_lastInfoMessage = QString("All file edits undone successfully");
|
||||
emit lastInfoMessageChanged();
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Rejected) {
|
||||
updateFileEditStatus(edit.editId, "rejected");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_lastErrorMessage = errorMsg.isEmpty()
|
||||
? QString("Failed to undo some file edits")
|
||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg);
|
||||
emit lastErrorMessageChanged();
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Rejected) {
|
||||
updateFileEditStatus(edit.editId, "rejected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentMessageEditsStats();
|
||||
}
|
||||
|
||||
void ChatRootView::updateCurrentMessageEditsStats()
|
||||
{
|
||||
if (m_currentMessageRequestId.isEmpty()) {
|
||||
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 ||
|
||||
m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
|
||||
m_currentMessageTotalEdits = 0;
|
||||
m_currentMessageAppliedEdits = 0;
|
||||
m_currentMessagePendingEdits = 0;
|
||||
m_currentMessageRejectedEdits = 0;
|
||||
emit currentMessageEditsStatsChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
||||
|
||||
int total = edits.size();
|
||||
int applied = 0;
|
||||
int pending = 0;
|
||||
int rejected = 0;
|
||||
|
||||
for (const auto &edit : edits) {
|
||||
switch (edit.status) {
|
||||
case Context::ChangesManager::Applied:
|
||||
applied++;
|
||||
break;
|
||||
case Context::ChangesManager::Pending:
|
||||
pending++;
|
||||
break;
|
||||
case Context::ChangesManager::Rejected:
|
||||
rejected++;
|
||||
break;
|
||||
case Context::ChangesManager::Archived:
|
||||
total--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
if (m_currentMessageTotalEdits != total) {
|
||||
m_currentMessageTotalEdits = total;
|
||||
changed = true;
|
||||
}
|
||||
if (m_currentMessageAppliedEdits != applied) {
|
||||
m_currentMessageAppliedEdits = applied;
|
||||
changed = true;
|
||||
}
|
||||
if (m_currentMessagePendingEdits != pending) {
|
||||
m_currentMessagePendingEdits = pending;
|
||||
changed = true;
|
||||
}
|
||||
if (m_currentMessageRejectedEdits != rejected) {
|
||||
m_currentMessageRejectedEdits = rejected;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
LOG_MESSAGE(QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
||||
.arg(total).arg(applied).arg(pending).arg(rejected));
|
||||
emit currentMessageEditsStatsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
int ChatRootView::currentMessageTotalEdits() const
|
||||
{
|
||||
return m_currentMessageTotalEdits;
|
||||
}
|
||||
|
||||
int ChatRootView::currentMessageAppliedEdits() const
|
||||
{
|
||||
return m_currentMessageAppliedEdits;
|
||||
}
|
||||
|
||||
int ChatRootView::currentMessagePendingEdits() const
|
||||
{
|
||||
return m_currentMessagePendingEdits;
|
||||
}
|
||||
|
||||
int ChatRootView::currentMessageRejectedEdits() const
|
||||
{
|
||||
return m_currentMessageRejectedEdits;
|
||||
}
|
||||
|
||||
QString ChatRootView::lastInfoMessage() const
|
||||
{
|
||||
return m_lastInfoMessage;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -43,8 +43,19 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(int codeFontSize READ codeFontSize NOTIFY codeFontSizeChanged FINAL)
|
||||
Q_PROPERTY(int textFontSize READ textFontSize NOTIFY textFontSizeChanged FINAL)
|
||||
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
|
||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
|
||||
Q_PROPERTY(
|
||||
bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
|
||||
|
||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
|
||||
QML_ELEMENT
|
||||
|
||||
@ -73,6 +84,7 @@ public:
|
||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||
Q_INVOKABLE void openChatHistoryFolder();
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
|
||||
Q_INVOKABLE void updateInputTokensCount();
|
||||
int inputTokensCount() const;
|
||||
@ -97,6 +109,34 @@ public:
|
||||
bool isRequestInProgress() const;
|
||||
void setRequestProgressStatus(bool state);
|
||||
|
||||
QString lastErrorMessage() const;
|
||||
|
||||
QVariantList activeRules() const;
|
||||
int activeRulesCount() const;
|
||||
Q_INVOKABLE QString getRuleContent(int index);
|
||||
Q_INVOKABLE void refreshRules();
|
||||
|
||||
bool isAgentMode() const;
|
||||
void setIsAgentMode(bool newIsAgentMode);
|
||||
bool toolsSupportEnabled() const;
|
||||
|
||||
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
||||
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
||||
Q_INVOKABLE void undoFileEdit(const QString &editId);
|
||||
Q_INVOKABLE void openFileEditInEditor(const QString &editId);
|
||||
|
||||
// Mass file edit operations for current message
|
||||
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
|
||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
||||
|
||||
int currentMessageTotalEdits() const;
|
||||
int currentMessageAppliedEdits() const;
|
||||
int currentMessagePendingEdits() const;
|
||||
int currentMessageRejectedEdits() const;
|
||||
|
||||
QString lastInfoMessage() const;
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message);
|
||||
void copyToClipboard(const QString &text);
|
||||
@ -120,7 +160,17 @@ signals:
|
||||
void chatRequestStarted();
|
||||
void isRequestInProgressChanged();
|
||||
|
||||
void lastErrorMessageChanged();
|
||||
void lastInfoMessageChanged();
|
||||
void activeRulesChanged();
|
||||
void activeRulesCountChanged();
|
||||
|
||||
void isAgentModeChanged();
|
||||
void toolsSupportEnabledChanged();
|
||||
void currentMessageEditsStatsChanged();
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
QString getChatsHistoryDir() const;
|
||||
QString getSuggestedFileName() const;
|
||||
|
||||
@ -136,6 +186,16 @@ private:
|
||||
bool m_isSyncOpenFiles;
|
||||
QList<Core::IEditor *> m_currentEditors;
|
||||
bool m_isRequestInProgress;
|
||||
QString m_lastErrorMessage;
|
||||
QVariantList m_activeRules;
|
||||
bool m_isAgentMode;
|
||||
|
||||
QString m_currentMessageRequestId;
|
||||
int m_currentMessageTotalEdits{0};
|
||||
int m_currentMessageAppliedEdits{0};
|
||||
int m_currentMessagePendingEdits{0};
|
||||
int m_currentMessageRejectedEdits{0};
|
||||
QString m_lastInfoMessage;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -120,9 +120,14 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
|
||||
}
|
||||
|
||||
model->clear();
|
||||
|
||||
model->setLoadingFromHistory(true);
|
||||
|
||||
for (const auto &message : messages) {
|
||||
model->addMessage(message.content, message.role, message.id);
|
||||
}
|
||||
|
||||
model->setLoadingFromHistory(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -28,50 +28,47 @@
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "ToolsSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "RequestConfig.hpp"
|
||||
#include <context/ChangesManager.h>
|
||||
#include <RulesLoader.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(
|
||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_requestHandler(new LLMCore::RequestHandler(this))
|
||||
, m_chatModel(chatModel)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{
|
||||
connect(
|
||||
m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
|
||||
handleLLMResponse(completion, request, isComplete);
|
||||
});
|
||||
{}
|
||||
|
||||
connect(
|
||||
m_requestHandler,
|
||||
&LLMCore::RequestHandler::requestFinished,
|
||||
this,
|
||||
[this](const QString &, bool success, const QString &errorString) {
|
||||
if (!success) {
|
||||
emit errorOccurred(errorString);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface() = default;
|
||||
|
||||
void ClientInterface::sendMessage(
|
||||
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
|
||||
ClientInterface::~ClientInterface()
|
||||
{
|
||||
cancelRequest();
|
||||
}
|
||||
|
||||
void ClientInterface::sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments,
|
||||
const QList<QString> &linkedFiles,
|
||||
bool useAgentMode)
|
||||
{
|
||||
cancelRequest();
|
||||
m_accumulatedResponses.clear();
|
||||
|
||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||
|
||||
auto attachFiles = m_contextManager->getContentFiles(attachments);
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
||||
@ -96,8 +93,21 @@ void ClientInterface::sendMessage(
|
||||
|
||||
LLMCore::ContextData context;
|
||||
|
||||
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode;
|
||||
|
||||
if (chatAssistantSettings.useSystemPrompt()) {
|
||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules
|
||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n# Project Rules\n\n" + projectRules;
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||
}
|
||||
@ -106,6 +116,9 @@ void ClientInterface::sendMessage(
|
||||
|
||||
QVector<LLMCore::Message> messages;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
||||
continue;
|
||||
}
|
||||
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content});
|
||||
}
|
||||
context.history = messages;
|
||||
@ -115,8 +128,7 @@ void ClientInterface::sendMessage(
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
||||
: QString{"generateContent?"};
|
||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3")
|
||||
.arg(
|
||||
Settings::generalSettings().caUrl(),
|
||||
@ -126,17 +138,59 @@ void ClientInterface::sendMessage(
|
||||
config.url
|
||||
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
||||
config.providerRequest
|
||||
= {{"model", Settings::generalSettings().caModel()},
|
||||
{"stream", chatAssistantSettings.stream()}};
|
||||
= {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
}
|
||||
|
||||
config.apiKey = provider->apiKey();
|
||||
|
||||
config.provider
|
||||
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
|
||||
config.provider->prepareRequest(
|
||||
config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat, isToolsEnabled);
|
||||
|
||||
QJsonObject request{{"id", QUuid::createUuid().toString()}};
|
||||
m_requestHandler->sendLLMRequest(config, request);
|
||||
QString requestId = QUuid::createUuid().toString();
|
||||
QJsonObject request{{"id", requestId}};
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
|
||||
emit requestStarted(requestId);
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::partialResponseReceived,
|
||||
this,
|
||||
&ClientInterface::handlePartialResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
this,
|
||||
&ClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
this,
|
||||
&ClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::toolExecutionStarted,
|
||||
m_chatModel,
|
||||
&ChatModel::addToolExecutionStatus,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::toolExecutionCompleted,
|
||||
m_chatModel,
|
||||
&ChatModel::updateToolResult,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::continuationStarted,
|
||||
this,
|
||||
&ClientInterface::handleCleanAccumulatedData,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
@ -147,8 +201,28 @@ void ClientInterface::clearMessages()
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
{
|
||||
auto id = m_chatModel->lastMessageId();
|
||||
m_requestHandler->cancelRequest(id);
|
||||
QSet<LLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
const RequestContext &ctx = it.value();
|
||||
if (ctx.provider) {
|
||||
ctx.provider->cancelRequest(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
m_activeRequests.clear();
|
||||
m_accumulatedResponses.clear();
|
||||
|
||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::handleLLMResponse(
|
||||
@ -214,4 +288,60 @@ Context::ContextManager *ClientInterface::contextManager() const
|
||||
return m_contextManager;
|
||||
}
|
||||
|
||||
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
m_accumulatedResponses[requestId] += partialText;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest, false);
|
||||
}
|
||||
|
||||
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
|
||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
||||
|
||||
QString applyError;
|
||||
bool applySuccess = Context::ChangesManager::instance()
|
||||
.applyPendingEditsForRequest(requestId, &applyError);
|
||||
|
||||
if (!applySuccess) {
|
||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||
.arg(requestId, applyError));
|
||||
}
|
||||
|
||||
handleLLMResponse(finalText, ctx.originalRequest, true);
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
}
|
||||
|
||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
||||
emit errorOccurred(error);
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
}
|
||||
|
||||
void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
|
||||
{
|
||||
m_accumulatedResponses[requestId].clear();
|
||||
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
#include <QVector>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "RequestHandler.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "llmcore/IPromptProvider.hpp"
|
||||
#include <context/ContextManager.hpp>
|
||||
|
||||
@ -42,7 +42,8 @@ public:
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
const QList<QString> &linkedFiles = {});
|
||||
const QList<QString> &linkedFiles = {},
|
||||
bool useAgentMode = false);
|
||||
void clearMessages();
|
||||
void cancelRequest();
|
||||
|
||||
@ -51,6 +52,13 @@ public:
|
||||
signals:
|
||||
void errorOccurred(const QString &error);
|
||||
void messageReceivedCompletely();
|
||||
void requestStarted(const QString &requestId);
|
||||
|
||||
private slots:
|
||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
void handleCleanAccumulatedData(const QString &requestId);
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||
@ -58,10 +66,18 @@ private:
|
||||
QString getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
};
|
||||
|
||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
ChatModel *m_chatModel;
|
||||
LLMCore::RequestHandler *m_requestHandler;
|
||||
Context::ContextManager *m_contextManager;
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
QHash<QString, QString> m_accumulatedResponses;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -19,33 +19,24 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <QObject>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "ChatData.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
Q_NAMESPACE
|
||||
|
||||
class MessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(PartType type MEMBER type CONSTANT FINAL)
|
||||
Q_PROPERTY(MessagePartType type MEMBER type CONSTANT FINAL)
|
||||
Q_PROPERTY(QString text MEMBER text CONSTANT FINAL)
|
||||
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
|
||||
QML_VALUE_TYPE(messagePart)
|
||||
public:
|
||||
enum PartType { Code, Text };
|
||||
Q_ENUM(PartType)
|
||||
|
||||
PartType type;
|
||||
MessagePartType type;
|
||||
QString text;
|
||||
QString language;
|
||||
};
|
||||
|
||||
class MessagePartType : public MessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
};
|
||||
|
||||
QML_NAMED_ELEMENT(MessagePart)
|
||||
QML_FOREIGN_NAMESPACE(QodeAssist::Chat::MessagePartType)
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
15
ChatView/icons/apply-changes-button.svg
Normal file
15
ChatView/icons/apply-changes-button.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_74_61)">
|
||||
<mask id="mask0_74_61" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_74_61)">
|
||||
<path d="M8 22L18 32L36 12" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_74_61">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 548 B |
17
ChatView/icons/open-in-editor.svg
Normal file
17
ChatView/icons/open-in-editor.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_74_52)">
|
||||
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_74_52)">
|
||||
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
|
||||
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_74_52">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 943 B |
16
ChatView/icons/reject-changes-button.svg
Normal file
16
ChatView/icons/reject-changes-button.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_74_76)">
|
||||
<mask id="mask0_74_76" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_74_76)">
|
||||
<path d="M12 12L32 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M32 12L12 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_74_76">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 599 B |
9
ChatView/icons/rules-icon.svg
Normal file
9
ChatView/icons/rules-icon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35.75 2.75H8.25C6.73122 2.75 5.5 3.98122 5.5 5.5V38.5C5.5 40.0188 6.73122 41.25 8.25 41.25H35.75C37.2688 41.25 38.5 40.0188 38.5 38.5V5.5C38.5 3.98122 37.2688 2.75 35.75 2.75Z" stroke="black" stroke-width="4"/>
|
||||
<path d="M13.75 14.4375C14.8891 14.4375 15.8125 13.5141 15.8125 12.375C15.8125 11.2359 14.8891 10.3125 13.75 10.3125C12.6109 10.3125 11.6875 11.2359 11.6875 12.375C11.6875 13.5141 12.6109 14.4375 13.75 14.4375Z" fill="black"/>
|
||||
<path d="M19.25 12.375H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M13.75 24.0625C14.8891 24.0625 15.8125 23.1391 15.8125 22C15.8125 20.8609 14.8891 19.9375 13.75 19.9375C12.6109 19.9375 11.6875 20.8609 11.6875 22C11.6875 23.1391 12.6109 24.0625 13.75 24.0625Z" fill="black"/>
|
||||
<path d="M19.25 22H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M13.75 33.6875C14.8891 33.6875 15.8125 32.7641 15.8125 31.625C15.8125 30.4859 14.8891 29.5625 13.75 29.5625C12.6109 29.5625 11.6875 30.4859 11.6875 31.625C11.6875 32.7641 12.6109 33.6875 13.75 33.6875Z" fill="black"/>
|
||||
<path d="M19.25 31.625H27.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
16
ChatView/icons/undo-changes-button.svg
Normal file
16
ChatView/icons/undo-changes-button.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_74_68)">
|
||||
<mask id="mask0_74_68" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_74_68)">
|
||||
<path d="M12 12L6 18L12 24" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 18H28C33 18 38 23 38 28C38 33 33 38 28 38H22" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_74_68">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 663 B |
@ -20,6 +20,8 @@
|
||||
import QtQuick
|
||||
import ChatView
|
||||
import QtQuick.Layouts
|
||||
import UIControls
|
||||
|
||||
import "./dialog"
|
||||
|
||||
Rectangle {
|
||||
@ -46,7 +48,6 @@ Rectangle {
|
||||
|
||||
property bool isUserMessage: false
|
||||
property int messageIndex: -1
|
||||
property real listViewContentY: 0
|
||||
|
||||
signal resetChatToMessage(int index)
|
||||
|
||||
@ -85,8 +86,8 @@ Rectangle {
|
||||
}
|
||||
|
||||
switch(modelData.type) {
|
||||
case MessagePart.Text: return textComponent;
|
||||
case MessagePart.Code: return codeBlockComponent;
|
||||
case MessagePartType.Text: return textComponent;
|
||||
case MessagePartType.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
@ -102,8 +103,6 @@ Rectangle {
|
||||
id: codeBlockComponent
|
||||
CodeBlockComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
blockStart: root.y + msgCreatorDelegate.y
|
||||
currentContentY: root.listViewContentY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
423
ChatView/qml/FileEditItem.qml
Normal file
423
ChatView/qml/FileEditItem.qml
Normal file
@ -0,0 +1,423 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import UIControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string editContent: ""
|
||||
|
||||
readonly property var editData: parseEditData(editContent)
|
||||
readonly property string filePath: editData.file || ""
|
||||
readonly property string fileName: getFileName(filePath)
|
||||
readonly property string editStatus: editData.status || "pending"
|
||||
readonly property string statusMessage: editData.status_message || ""
|
||||
readonly property string oldContent: editData.old_content || ""
|
||||
readonly property string newContent: editData.new_content || ""
|
||||
|
||||
signal applyEdit(string editId)
|
||||
signal rejectEdit(string editId)
|
||||
signal undoEdit(string editId)
|
||||
signal openInEditor(string editId)
|
||||
|
||||
readonly property int borderRadius: 4
|
||||
readonly property int contentMargin: 10
|
||||
readonly property int contentBottomPadding: 20
|
||||
readonly property int headerPadding: 8
|
||||
readonly property int statusIndicatorWidth: 4
|
||||
|
||||
readonly property bool isPending: editStatus === "pending"
|
||||
readonly property bool isApplied: editStatus === "applied"
|
||||
readonly property bool isRejected: editStatus === "rejected"
|
||||
readonly property bool isArchived: editStatus === "archived"
|
||||
|
||||
readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
||||
readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
||||
readonly property color rejectedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
|
||||
readonly property color archivedColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
|
||||
readonly property color pendingColor: palette.highlight
|
||||
|
||||
readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
||||
readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3)
|
||||
readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3)
|
||||
readonly property color archivedBgColor: Qt.rgba(0.5, 0.5, 0.5, 0.3)
|
||||
|
||||
readonly property string codeFontFamily: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows": return "Consolas"
|
||||
case "osx": return "Menlo"
|
||||
case "linux": return "DejaVu Sans Mono"
|
||||
default: return "monospace"
|
||||
}
|
||||
}
|
||||
readonly property int codeFontSize: Qt.application.font.pointSize
|
||||
|
||||
readonly property color statusColor: {
|
||||
if (isArchived) return archivedColor
|
||||
if (isApplied) return appliedColor
|
||||
if (isRejected) return rejectedColor
|
||||
return pendingColor
|
||||
}
|
||||
|
||||
readonly property color statusBgColor: {
|
||||
if (isArchived) return archivedBgColor
|
||||
if (isApplied) return appliedBgColor
|
||||
if (isRejected) return rejectedBgColor
|
||||
return palette.button
|
||||
}
|
||||
|
||||
readonly property string statusText: {
|
||||
if (isArchived) return qsTr("ARCHIVED")
|
||||
if (isApplied) return qsTr("APPLIED")
|
||||
if (isRejected) return qsTr("REJECTED")
|
||||
return qsTr("PENDING")
|
||||
}
|
||||
|
||||
readonly property int addedLines: countLines(newContent)
|
||||
readonly property int removedLines: countLines(oldContent)
|
||||
|
||||
function parseEditData(content) {
|
||||
try {
|
||||
const marker = "QODEASSIST_FILE_EDIT:";
|
||||
let jsonStr = content;
|
||||
if (content.indexOf(marker) >= 0) {
|
||||
jsonStr = content.substring(content.indexOf(marker) + marker.length);
|
||||
}
|
||||
return JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
return {
|
||||
edit_id: "",
|
||||
file: "",
|
||||
old_content: "",
|
||||
new_content: "",
|
||||
status: "error",
|
||||
status_message: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFileName(path) {
|
||||
if (!path) return "";
|
||||
const parts = path.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
function countLines(text) {
|
||||
if (!text) return 0;
|
||||
return text.split('\n').length;
|
||||
}
|
||||
|
||||
implicitHeight: fileEditView.implicitHeight
|
||||
|
||||
Rectangle {
|
||||
id: fileEditView
|
||||
|
||||
property bool expanded: false
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: expanded ? headerArea.height + contentColumn.implicitHeight + root.contentBottomPadding + root.contentMargin * 2
|
||||
: headerArea.height
|
||||
radius: root.borderRadius
|
||||
|
||||
color: palette.base
|
||||
|
||||
border.width: 1
|
||||
border.color: root.isPending
|
||||
? (color.hslLightness > 0.5 ? Qt.darker(color, 1.3) : Qt.lighter(color, 1.3))
|
||||
: Qt.alpha(root.statusColor, 0.6)
|
||||
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "expanded"
|
||||
when: fileEditView.expanded
|
||||
PropertyChanges { target: contentColumn; opacity: 1 }
|
||||
},
|
||||
State {
|
||||
name: "collapsed"
|
||||
when: !fileEditView.expanded
|
||||
PropertyChanges { target: contentColumn; opacity: 0 }
|
||||
}
|
||||
]
|
||||
|
||||
transitions: Transition {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
duration: 200
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: headerArea
|
||||
width: parent.width
|
||||
height: headerRow.height + 16
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: fileEditView.expanded = !fileEditView.expanded
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
right: actionButtons.left
|
||||
leftMargin: root.contentMargin
|
||||
rightMargin: root.contentMargin
|
||||
}
|
||||
spacing: root.headerPadding
|
||||
|
||||
Rectangle {
|
||||
width: root.statusIndicatorWidth
|
||||
height: headerText.height
|
||||
radius: 2
|
||||
color: root.statusColor
|
||||
}
|
||||
|
||||
Text {
|
||||
id: headerText
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
||||
if (root.oldContent.length > 0) {
|
||||
return qsTr("%1: %2 (+%3 -%4)")
|
||||
.arg(modeText)
|
||||
.arg(root.fileName)
|
||||
.arg(root.addedLines)
|
||||
.arg(root.removedLines)
|
||||
} else {
|
||||
return qsTr("%1: %2 (+%3)")
|
||||
.arg(modeText)
|
||||
.arg(root.fileName)
|
||||
.arg(root.addedLines)
|
||||
}
|
||||
}
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
Text {
|
||||
text: fileEditView.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !root.isPending
|
||||
Layout.preferredWidth: badgeText.width + 12
|
||||
Layout.preferredHeight: badgeText.height + 4
|
||||
color: root.statusBgColor
|
||||
radius: 3
|
||||
|
||||
Text {
|
||||
id: badgeText
|
||||
anchors.centerIn: parent
|
||||
text: root.statusText
|
||||
font.pixelSize: 9
|
||||
font.bold: true
|
||||
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: actionButtons
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
spacing: 6
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
hoverEnabled: true
|
||||
onClicked: root.openInEditor(editData.edit_id)
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Open file in editor and navigate to changes")
|
||||
ToolTip.delay: 500
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/apply-changes-button.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
} enabled: (root.isPending || root.isRejected) && !root.isArchived
|
||||
visible: !root.isApplied && !root.isArchived
|
||||
onClicked: root.applyEdit(editData.edit_id)
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
enabled: root.isApplied && !root.isArchived
|
||||
visible: root.isApplied && !root.isArchived
|
||||
onClicked: root.undoEdit(editData.edit_id)
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/reject-changes-button.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
enabled: root.isPending && !root.isArchived
|
||||
visible: root.isPending && !root.isArchived
|
||||
onClicked: root.rejectEdit(editData.edit_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: headerArea.bottom
|
||||
margins: root.contentMargin
|
||||
}
|
||||
spacing: 8
|
||||
visible: opacity > 0
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.filePath
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: oldContentColumn.implicitHeight + 12
|
||||
color: Qt.rgba(1, 0.2, 0.2, 0.1)
|
||||
radius: 4
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(1, 0.2, 0.2, 0.3)
|
||||
visible: root.oldContent.length > 0
|
||||
|
||||
Column {
|
||||
id: oldContentColumn
|
||||
width: parent.width
|
||||
x: 6
|
||||
y: 6
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: qsTr("- Removed:")
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
color: Qt.rgba(1, 0.2, 0.2, 0.9)
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: oldContentText
|
||||
width: parent.width - 12
|
||||
height: contentHeight
|
||||
text: root.oldContent
|
||||
font.family: root.codeFontFamily
|
||||
font.pixelSize: root.codeFontSize
|
||||
color: palette.text
|
||||
wrapMode: TextEdit.Wrap
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
textFormat: TextEdit.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: newContentColumn.implicitHeight + 12
|
||||
color: Qt.rgba(0.2, 0.8, 0.2, 0.1)
|
||||
radius: 4
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
||||
|
||||
Column {
|
||||
id: newContentColumn
|
||||
width: parent.width
|
||||
x: 6
|
||||
y: 6
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: qsTr("+ Added:")
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
color: Qt.rgba(0.2, 0.8, 0.2, 0.9)
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: newContentText
|
||||
width: parent.width - 12
|
||||
height: contentHeight
|
||||
text: root.newContent
|
||||
font.family: root.codeFontFamily
|
||||
font.pixelSize: root.codeFontSize
|
||||
color: palette.text
|
||||
wrapMode: TextEdit.Wrap
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
textFormat: TextEdit.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: root.statusMessage.length > 0
|
||||
text: root.statusMessage
|
||||
font.pixelSize: 10
|
||||
font.italic: true
|
||||
color: root.isApplied
|
||||
? Qt.rgba(0.2, 0.6, 0.2, 1)
|
||||
: Qt.rgba(0.8, 0.2, 0.2, 1)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,8 @@ import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic as QQC
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import "./controls"
|
||||
import UIControls
|
||||
import Qt.labs.platform as Platform
|
||||
import "./parts"
|
||||
|
||||
ChatRootView {
|
||||
@ -64,7 +65,7 @@ ChatRootView {
|
||||
id: topBar
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredHeight: childrenRect.height + 10
|
||||
|
||||
saveButton.onClicked: root.showSaveDialog()
|
||||
loadButton.onClicked: root.showLoadDialog()
|
||||
@ -73,14 +74,23 @@ ChatRootView {
|
||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||
}
|
||||
recentPath {
|
||||
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
}
|
||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||
rulesButton.onClicked: rulesViewer.open()
|
||||
activeRulesCount: root.activeRulesCount
|
||||
pinButton {
|
||||
visible: typeof _chatview !== 'undefined'
|
||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
||||
}
|
||||
agentModeSwitch {
|
||||
checked: root.isAgentMode
|
||||
enabled: root.toolsSupportEnabled
|
||||
onToggled: {
|
||||
root.isAgentMode = agentModeSwitch.checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
@ -95,26 +105,20 @@ ChatRootView {
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 2000
|
||||
|
||||
delegate: ChatItem {
|
||||
delegate: Loader {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width - scroll.width
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
messageAttachments: model.attachments
|
||||
isUserMessage: model.roleType === ChatModel.User
|
||||
messageIndex: index
|
||||
listViewContentY: chatListView.contentY
|
||||
textFontFamily: root.textFontFamily
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
textFontSize: root.textFontSize
|
||||
textFormat: root.textFormat
|
||||
|
||||
onResetChatToMessage: function(index) {
|
||||
messageInput.text = model.content
|
||||
messageInput.cursorPosition = model.content.length
|
||||
root.chatModel.resetModelTo(index)
|
||||
sourceComponent: {
|
||||
if (model.roleType === ChatModel.Tool) {
|
||||
return toolMessageComponent
|
||||
} else if (model.roleType === ChatModel.FileEdit) {
|
||||
return fileEditMessageComponent
|
||||
} else {
|
||||
return chatItemComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,6 +140,65 @@ ChatRootView {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: chatItemComponent
|
||||
|
||||
ChatItem {
|
||||
id: chatItemInstance
|
||||
|
||||
width: parent.width
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
messageAttachments: model.attachments
|
||||
isUserMessage: model.roleType === ChatModel.User
|
||||
messageIndex: index
|
||||
textFontFamily: root.textFontFamily
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
textFontSize: root.textFontSize
|
||||
textFormat: root.textFormat
|
||||
|
||||
onResetChatToMessage: function(idx) {
|
||||
messageInput.text = model.content
|
||||
messageInput.cursorPosition = model.content.length
|
||||
root.chatModel.resetModelTo(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: toolMessageComponent
|
||||
|
||||
ToolStatusItem {
|
||||
width: parent.width
|
||||
toolContent: model.content
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileEditMessageComponent
|
||||
|
||||
FileEditItem {
|
||||
width: parent.width
|
||||
editContent: model.content
|
||||
|
||||
onApplyEdit: function(editId) {
|
||||
root.applyFileEdit(editId)
|
||||
}
|
||||
|
||||
onRejectEdit: function(editId) {
|
||||
root.rejectFileEdit(editId)
|
||||
}
|
||||
|
||||
onUndoEdit: function(editId) {
|
||||
root.undoFileEdit(editId)
|
||||
}
|
||||
|
||||
onOpenInEditor: function(editId) {
|
||||
root.openFileEditInEditor(editId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
@ -148,7 +211,9 @@ ChatRootView {
|
||||
QQC.TextArea {
|
||||
id: messageInput
|
||||
|
||||
placeholderText: qsTr("Type your message here...")
|
||||
placeholderText: Qt.platform.os === "osx"
|
||||
? qsTr("Type your message here... (⌘+↩ to send)")
|
||||
: qsTr("Type your message here... (Ctrl+Enter to send)")
|
||||
placeholderTextColor: palette.mid
|
||||
color: palette.text
|
||||
background: Rectangle {
|
||||
@ -171,15 +236,53 @@ ChatRootView {
|
||||
|
||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
|
||||
root.sendChatMessage()
|
||||
event.accepted = true;
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: messageContextMenu.open()
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: messageContextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Cut")
|
||||
enabled: messageInput.selectedText.length > 0
|
||||
onTriggered: messageInput.cut()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: messageInput.selectedText.length > 0
|
||||
onTriggered: messageInput.copy()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Paste")
|
||||
enabled: messageInput.canPaste
|
||||
onTriggered: messageInput.paste()
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Select All")
|
||||
enabled: messageInput.text.length > 0
|
||||
onTriggered: messageInput.selectAll()
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Clear")
|
||||
enabled: messageInput.text.length > 0
|
||||
onTriggered: messageInput.clear()
|
||||
}
|
||||
}
|
||||
|
||||
AttachedFilesPlace {
|
||||
id: attachedFilesPlace
|
||||
|
||||
@ -202,6 +305,19 @@ ChatRootView {
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||
}
|
||||
|
||||
FileEditsActionBar {
|
||||
id: fileEditsActionBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
totalEdits: root.currentMessageTotalEdits
|
||||
appliedEdits: root.currentMessageAppliedEdits
|
||||
pendingEdits: root.currentMessagePendingEdits
|
||||
rejectedEdits: root.currentMessageRejectedEdits
|
||||
|
||||
onApplyAllClicked: root.applyAllFileEditsForCurrentMessage()
|
||||
onUndoAllClicked: root.undoAllFileEditsForCurrentMessage()
|
||||
}
|
||||
|
||||
BottomBar {
|
||||
id: bottomBar
|
||||
|
||||
@ -210,7 +326,10 @@ ChatRootView {
|
||||
|
||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||
: root.cancelRequest()
|
||||
isRequestInProgress: root.isRequestInProgress
|
||||
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
||||
: qsTr("Stop")
|
||||
syncOpenFiles {
|
||||
checked: root.isSyncOpenFiles
|
||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||
@ -220,6 +339,18 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
id: sendMessageShortcut
|
||||
|
||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
||||
root.sendChatMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.chatModel.clear()
|
||||
root.clearAttachmentFiles()
|
||||
@ -236,6 +367,53 @@ ChatRootView {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
Toast {
|
||||
id: errorToast
|
||||
z: 1000
|
||||
|
||||
color: Qt.rgba(0.8, 0.2, 0.2, 0.7)
|
||||
border.color: Qt.darker(infoToast.color, 1.3)
|
||||
toastTextColor: "#FFFFFF"
|
||||
}
|
||||
|
||||
Toast {
|
||||
id: infoToast
|
||||
z: 1000
|
||||
|
||||
color: Qt.rgba(0.2, 0.8, 0.2, 0.7)
|
||||
border.color: Qt.darker(infoToast.color, 1.3)
|
||||
toastTextColor: "#FFFFFF"
|
||||
}
|
||||
|
||||
RulesViewer {
|
||||
id: rulesViewer
|
||||
|
||||
width: parent.width * 0.8
|
||||
height: parent.height * 0.8
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
activeRules: root.activeRules
|
||||
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex)
|
||||
|
||||
onRefreshRules: root.refreshRules()
|
||||
onOpenRulesFolder: root.openRulesFolder()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onLastErrorMessageChanged() {
|
||||
if (root.lastErrorMessage.length > 0) {
|
||||
errorToast.show(root.lastErrorMessage)
|
||||
}
|
||||
}
|
||||
function onLastInfoMessageChanged() {
|
||||
if (root.lastInfoMessage.length > 0) {
|
||||
infoToast.show(root.lastInfoMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
messageInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
160
ChatView/qml/ToolStatusItem.qml
Normal file
160
ChatView/qml/ToolStatusItem.qml
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string toolContent: ""
|
||||
property bool expanded: false
|
||||
|
||||
readonly property int firstNewline: toolContent.indexOf('\n')
|
||||
readonly property string toolName: firstNewline > 0 ? toolContent.substring(0, firstNewline) : toolContent
|
||||
readonly property string toolResult: firstNewline > 0 ? toolContent.substring(firstNewline + 1) : ""
|
||||
|
||||
radius: 6
|
||||
color: palette.base
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: header
|
||||
|
||||
width: parent.width
|
||||
height: headerRow.height + 10
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expanded = !root.expanded
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
}
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: qsTr("Tool: %1").arg(root.toolName)
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: header.bottom
|
||||
margins: 10
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
TextEdit {
|
||||
id: resultText
|
||||
|
||||
width: parent.width
|
||||
text: root.toolResult
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: resultText.selectedText.length > 0
|
||||
onTriggered: resultText.copy()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Select All")
|
||||
enabled: resultText.text.length > 0
|
||||
onTriggered: resultText.selectAll()
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: messageMarker
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: root.height - root.radius
|
||||
color: root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
|
||||
: Qt.lighter(palette.alternateBase, 1.3)
|
||||
radius: root.radius
|
||||
}
|
||||
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: !root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height + contentColumn.height + 20
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -20,33 +20,19 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
import UIControls
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
|
||||
property real currentContentY: 0
|
||||
property real blockStart: 0
|
||||
property bool expanded: false
|
||||
|
||||
property alias codeFontFamily: codeText.font.family
|
||||
property alias codeFontSize: codeText.font.pointSize
|
||||
|
||||
readonly property real buttonTopMargin: 5
|
||||
readonly property real blockEnd: blockStart + root.height
|
||||
readonly property real maxButtonOffset: Math.max(0, root.height - copyButton.height - buttonTopMargin)
|
||||
|
||||
readonly property real buttonPosition: {
|
||||
if (currentContentY > blockEnd) {
|
||||
return buttonTopMargin;
|
||||
}
|
||||
else if (currentContentY > blockStart) {
|
||||
let offset = currentContentY - blockStart;
|
||||
return Math.min(offset, maxButtonOffset);
|
||||
}
|
||||
return buttonTopMargin;
|
||||
}
|
||||
readonly property real collapsedHeight: copyButton.height + 10
|
||||
|
||||
color: palette.alternateBase
|
||||
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
||||
@ -54,47 +40,107 @@ Rectangle {
|
||||
border.width: 2
|
||||
radius: 4
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeText.implicitHeight + 20
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
enabled: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: header
|
||||
|
||||
width: parent.width
|
||||
height: root.collapsedHeight
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expanded = !root.expanded
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
}
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: root.language ? qsTr("Code (%1)").arg(root.language) :
|
||||
qsTr("Code")
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: codeText
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: header.bottom
|
||||
margins: 10
|
||||
}
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
text: root.language
|
||||
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
||||
: Qt.lighter(root.color, 1.1)
|
||||
font.pointSize: codeText.font.pointSize - 4
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
onTriggered: {
|
||||
const textToCopy = codeText.selectedText || root.code
|
||||
utils.copyToClipboard(textToCopy)
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: copyButton
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: root.buttonPosition
|
||||
right: parent.right
|
||||
rightMargin: root.buttonTopMargin
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 5
|
||||
|
||||
y: 5
|
||||
text: qsTr("Copy")
|
||||
|
||||
onClicked: {
|
||||
utils.copyToClipboard(root.code)
|
||||
text = qsTr("Copied")
|
||||
@ -107,4 +153,21 @@ Rectangle {
|
||||
onTriggered: parent.text = qsTr("Copy")
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: !root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: root.collapsedHeight
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height + codeText.implicitHeight + 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
TextEdit {
|
||||
id: root
|
||||
@ -27,4 +28,27 @@ TextEdit {
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: root.selectedText.length > 0
|
||||
onTriggered: root.copy()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Select All")
|
||||
enabled: root.text.length > 0
|
||||
onTriggered: root.selectAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import UIControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@ -30,7 +31,6 @@ Rectangle {
|
||||
property alias attachFiles: attachFilesId
|
||||
property alias linkFiles: linkFilesId
|
||||
|
||||
property bool isRequestInProgress: false
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
@ -53,15 +53,11 @@ Rectangle {
|
||||
id: sendButtonId
|
||||
|
||||
icon {
|
||||
source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM")
|
||||
: qsTr("Stop")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
|
||||
161
ChatView/qml/parts/FileEditsActionBar.qml
Normal file
161
ChatView/qml/parts/FileEditsActionBar.qml
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import UIControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int totalEdits: 0
|
||||
property int appliedEdits: 0
|
||||
property int pendingEdits: 0
|
||||
property int rejectedEdits: 0
|
||||
property bool hasAppliedEdits: appliedEdits > 0
|
||||
property bool hasRejectedEdits: rejectedEdits > 0
|
||||
property bool hasPendingEdits: pendingEdits > 0
|
||||
|
||||
signal applyAllClicked()
|
||||
signal undoAllClicked()
|
||||
|
||||
visible: totalEdits > 0
|
||||
implicitHeight: visible ? 40 : 0
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.05) :
|
||||
Qt.lighter(palette.window, 1.05)
|
||||
|
||||
border.width: 1
|
||||
border.color: palette.mid
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
radius: 12
|
||||
color: {
|
||||
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.2)
|
||||
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.2)
|
||||
return Qt.rgba(0.8, 0.6, 0.2, 0.2)
|
||||
}
|
||||
border.width: 2
|
||||
border.color: {
|
||||
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.8)
|
||||
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
||||
return Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.totalEdits
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
}
|
||||
|
||||
// Status text
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: root.totalEdits === 1
|
||||
? qsTr("File Edit in Current Message")
|
||||
: qsTr("%1 File Edits in Current Message").arg(root.totalEdits)
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.totalEdits > 0
|
||||
text: {
|
||||
let parts = [];
|
||||
if (root.appliedEdits > 0) {
|
||||
parts.push(qsTr("%1 applied").arg(root.appliedEdits));
|
||||
}
|
||||
if (root.pendingEdits > 0) {
|
||||
parts.push(qsTr("%1 pending").arg(root.pendingEdits));
|
||||
}
|
||||
if (root.rejectedEdits > 0) {
|
||||
parts.push(qsTr("%1 rejected").arg(root.rejectedEdits));
|
||||
}
|
||||
return parts.join(", ");
|
||||
}
|
||||
font.pixelSize: 9
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: applyAllButton
|
||||
|
||||
visible: root.hasPendingEdits || root.hasRejectedEdits
|
||||
enabled: root.hasPendingEdits || root.hasRejectedEdits
|
||||
text: root.hasPendingEdits
|
||||
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
|
||||
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: root.hasPendingEdits
|
||||
? qsTr("Apply all pending and rejected edits in this message")
|
||||
: qsTr("Reapply all rejected edits in this message")
|
||||
|
||||
onClicked: root.applyAllClicked()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: undoAllButton
|
||||
|
||||
visible: root.hasAppliedEdits
|
||||
enabled: root.hasAppliedEdits
|
||||
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Undo all applied edits in this message")
|
||||
|
||||
onClicked: root.undoAllClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
259
ChatView/qml/parts/RulesViewer.qml
Normal file
259
ChatView/qml/parts/RulesViewer.qml
Normal file
@ -0,0 +1,259 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls.Basic as QQC
|
||||
|
||||
import UIControls
|
||||
import ChatView
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property var activeRules
|
||||
|
||||
property alias rulesCurrentIndex: rulesList.currentIndex
|
||||
property alias ruleContentAreaText: ruleContentArea.text
|
||||
|
||||
signal refreshRules()
|
||||
signal openRulesFolder()
|
||||
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: palette.window
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 4
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: qsTr("Active Project Rules")
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Open Folder")
|
||||
onClicked: root.openRulesFolder()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Refresh")
|
||||
onClicked: root.refreshRules()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Close")
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: palette.mid
|
||||
}
|
||||
|
||||
SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
Rectangle {
|
||||
SplitView.minimumWidth: 200
|
||||
SplitView.preferredWidth: parent.width * 0.3
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Rules Files (%1)").arg(rulesList.count)
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: rulesList
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: root.activeRules
|
||||
currentIndex: 0
|
||||
|
||||
delegate: ItemDelegate {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
highlighted: ListView.isCurrentItem
|
||||
|
||||
background: Rectangle {
|
||||
color: {
|
||||
if (parent.highlighted) {
|
||||
return palette.highlight
|
||||
} else if (parent.hovered) {
|
||||
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
||||
}
|
||||
return "transparent"
|
||||
}
|
||||
radius: 2
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: modelData.fileName
|
||||
font.pixelSize: 11
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Category: %1").arg(modelData.category)
|
||||
font.pixelSize: 9
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
rulesList.currentIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: QQC.ScrollBar {
|
||||
id: scroll
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: rulesList.count === 0
|
||||
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
SplitView.fillWidth: true
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Content")
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Copy")
|
||||
enabled: ruleContentArea.text.length > 0
|
||||
onClicked: utils.copyToClipboard(ruleContentArea.text)
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
QQC.TextArea {
|
||||
id: ruleContentArea
|
||||
|
||||
readOnly: true
|
||||
wrapMode: TextArea.Wrap
|
||||
selectByMouse: true
|
||||
color: palette.text
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.darker(palette.base, 1.02)
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
}
|
||||
|
||||
placeholderText: qsTr("Select a rule file to view its content")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
|
||||
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
|
||||
font.pixelSize: 9
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
103
ChatView/qml/parts/Toast.qml
Normal file
103
ChatView/qml/parts/Toast.qml
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias toastTextItem: textItem
|
||||
property alias toastTextColor: textItem.color
|
||||
|
||||
property string errorText: ""
|
||||
property int displayDuration: 5000
|
||||
|
||||
width: Math.min(parent.width - 40, textItem.implicitWidth + radius)
|
||||
height: visible ? (textItem.implicitHeight + 12) : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 10
|
||||
|
||||
color: "#d32f2f"
|
||||
radius: height / 2
|
||||
border.color: "#b71c1c"
|
||||
border.width: 1
|
||||
visible: false
|
||||
opacity: 0
|
||||
|
||||
TextEdit {
|
||||
id: textItem
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 6
|
||||
text: root.errorText
|
||||
color: palette.text
|
||||
font.pixelSize: 13
|
||||
wrapMode: TextEdit.Wrap
|
||||
width: Math.min(implicitWidth, root.parent.width - 60)
|
||||
horizontalAlignment: TextEdit.AlignHCenter
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
selectionColor: "#b71c1c"
|
||||
}
|
||||
|
||||
function show(message) {
|
||||
errorText = message
|
||||
visible = true
|
||||
showAnimation.start()
|
||||
hideTimer.restart()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
hideAnimation.start()
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: showAnimation
|
||||
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: hideAnimation
|
||||
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: 200
|
||||
easing.type: Easing.InQuad
|
||||
onFinished: root.visible = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
|
||||
interval: root.displayDuration
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: root.hide()
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
import UIControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@ -32,20 +33,21 @@ Rectangle {
|
||||
property alias recentPath: recentPathId
|
||||
property alias openChatHistory: openChatHistoryId
|
||||
property alias pinButton: pinButtonId
|
||||
property alias rulesButton: rulesButtonId
|
||||
property alias agentModeSwitch: agentModeSwitchId
|
||||
property alias activeRulesCount: activeRulesCountId.text
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
|
||||
RowLayout {
|
||||
Flow {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
margins: 5
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
@ -66,75 +68,144 @@ Rectangle {
|
||||
: qsTr("Pin chat window to the top")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: saveButtonId
|
||||
QoATextSlider {
|
||||
id: agentModeSwitchId
|
||||
|
||||
leftText: "chat"
|
||||
rightText: "AI Agent"
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Save chat to *.json file")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: loadButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
ToolTip.text: {
|
||||
if (!agentModeSwitchId.enabled) {
|
||||
return qsTr("Tools are disabled in General Settings")
|
||||
}
|
||||
return checked
|
||||
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
|
||||
: qsTr("Chat Mode: Simple conversation without tool access")
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Load chat from *.json file")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: clearButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Clean chat")
|
||||
}
|
||||
|
||||
Text {
|
||||
id: recentPathId
|
||||
|
||||
elide: Text.ElideMiddle
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: openChatHistoryId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Show in system")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
height: agentModeSwitchId.height
|
||||
width: recentPathId.width
|
||||
|
||||
Text {
|
||||
id: recentPathId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.min(implicitWidth, root.width)
|
||||
elide: Text.ElideMiddle
|
||||
color: palette.text
|
||||
font.pixelSize: 12
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
ToolTip.visible: containsMouse
|
||||
ToolTip.delay: 500
|
||||
ToolTip.text: recentPathId.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Badge {
|
||||
id: tokensBadgeId
|
||||
RowLayout {
|
||||
Layout.preferredWidth: root.width
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: saveButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Save chat to *.json file")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: loadButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Load chat from *.json file")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: clearButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Clean chat")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: openChatHistoryId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Show in system")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: rulesButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
text: " "
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: root.activeRulesCount > 0
|
||||
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
|
||||
: qsTr("View active project rules (no rules found)")
|
||||
|
||||
Text {
|
||||
id: activeRulesCountId
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 2
|
||||
right: parent.right
|
||||
rightMargin: 4
|
||||
}
|
||||
|
||||
color: palette.text
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Badge {
|
||||
id: tokensBadgeId
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,12 +26,11 @@
|
||||
#include "CodeHandler.hpp"
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "context/Utils.hpp"
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <llmcore/RulesLoader.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
@ -40,34 +39,21 @@ LLMClientInterface::LLMClientInterface(
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
LLMCore::IProviderRegistry &providerRegistry,
|
||||
LLMCore::IPromptProvider *promptProvider,
|
||||
LLMCore::RequestHandlerBase &requestHandler,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger)
|
||||
: m_generalSettings(generalSettings)
|
||||
, m_completeSettings(completeSettings)
|
||||
, m_providerRegistry(providerRegistry)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_requestHandler(requestHandler)
|
||||
, m_documentReader(documentReader)
|
||||
, m_performanceLogger(performanceLogger)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{
|
||||
connect(
|
||||
&m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
&LLMClientInterface::sendCompletionToClient);
|
||||
}
|
||||
|
||||
// TODO handle error
|
||||
// connect(
|
||||
// &m_requestHandler,
|
||||
// &LLMCore::RequestHandler::requestFinished,
|
||||
// this,
|
||||
// [this](const QString &, bool success, const QString &errorString) {
|
||||
// if (!success) {
|
||||
// emit error(errorString);
|
||||
// }
|
||||
// });
|
||||
LLMClientInterface::~LLMClientInterface()
|
||||
{
|
||||
handleCancelRequest();
|
||||
}
|
||||
|
||||
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
||||
@ -80,6 +66,29 @@ void LLMClientInterface::startImpl()
|
||||
emit started();
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
sendCompletionToClient(fullText, ctx.originalRequest, true);
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
||||
m_activeRequests.erase(it);
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendData(const QByteArray &data)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
@ -102,7 +111,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
handleCompletion(request);
|
||||
} else if (method == "$/cancelRequest") {
|
||||
handleCancelRequest(request);
|
||||
handleCancelRequest();
|
||||
} else if (method == "exit") {
|
||||
// TODO make exit handler
|
||||
} else {
|
||||
@ -110,14 +119,29 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
}
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
|
||||
void LLMClientInterface::handleCancelRequest()
|
||||
{
|
||||
QString id = request["params"].toObject()["id"].toString();
|
||||
if (m_requestHandler.cancelRequest(id)) {
|
||||
LOG_MESSAGE(QString("Request %1 cancelled successfully").arg(id));
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Request %1 not found").arg(id));
|
||||
QSet<LLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
const RequestContext &ctx = it.value();
|
||||
if (ctx.provider) {
|
||||
ctx.provider->cancelRequest(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
m_activeRequests.clear();
|
||||
|
||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleInitialize(const QJsonObject &request)
|
||||
@ -214,13 +238,12 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
config.promptTemplate = promptTemplate;
|
||||
// TODO refactor networking
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
||||
: QString{"generateContent?"};
|
||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
||||
} else {
|
||||
config.url = QUrl(
|
||||
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
||||
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}};
|
||||
config.providerRequest = {{"model", modelName}, {"stream", true}};
|
||||
}
|
||||
config.apiKey = provider->apiKey();
|
||||
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
|
||||
@ -236,6 +259,18 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
&& promptTemplate->type() == LLMCore::TemplateType::Chat
|
||||
? m_completeSettings.systemPromptForNonFimModels()
|
||||
: m_completeSettings.systemPrompt());
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules
|
||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
LOG_MESSAGE("Loaded project rules for completion");
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedContext.fileContext.has_value())
|
||||
systemPrompt.append(updatedContext.fileContext.value());
|
||||
|
||||
@ -273,7 +308,8 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
config.providerRequest,
|
||||
promptTemplate,
|
||||
updatedContext,
|
||||
LLMCore::RequestType::CodeCompletion);
|
||||
LLMCore::RequestType::CodeCompletion,
|
||||
false);
|
||||
|
||||
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
||||
if (!errors.isEmpty()) {
|
||||
@ -281,7 +317,26 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
LOG_MESSAGES(errors);
|
||||
return;
|
||||
}
|
||||
m_requestHandler.sendLLMRequest(config, request);
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
this,
|
||||
&LLMClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
this,
|
||||
&LLMClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||
}
|
||||
|
||||
LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
#include <llmcore/ContextData.hpp>
|
||||
#include <llmcore/IPromptProvider.hpp>
|
||||
#include <llmcore/IProviderRegistry.hpp>
|
||||
#include <llmcore/RequestHandler.hpp>
|
||||
#include <logger/IRequestPerformanceLogger.hpp>
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
@ -48,9 +47,9 @@ public:
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
LLMCore::IProviderRegistry &providerRegistry,
|
||||
LLMCore::IPromptProvider *promptProvider,
|
||||
LLMCore::RequestHandlerBase &requestHandler,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger);
|
||||
~LLMClientInterface() override;
|
||||
|
||||
Utils::FilePath serverDeviceTemplate() const override;
|
||||
|
||||
@ -67,13 +66,23 @@ public:
|
||||
protected:
|
||||
void startImpl() override;
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
void handleInitialize(const QJsonObject &request);
|
||||
void handleShutdown(const QJsonObject &request);
|
||||
void handleTextDocumentDidOpen(const QJsonObject &request);
|
||||
void handleInitialized(const QJsonObject &request);
|
||||
void handleExit(const QJsonObject &request);
|
||||
void handleCancelRequest(const QJsonObject &request);
|
||||
void handleCancelRequest();
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
};
|
||||
|
||||
LLMCore::ContextData prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||
@ -83,11 +92,11 @@ private:
|
||||
const Settings::GeneralSettings &m_generalSettings;
|
||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
LLMCore::IProviderRegistry &m_providerRegistry;
|
||||
LLMCore::RequestHandlerBase &m_requestHandler;
|
||||
Context::IDocumentReader &m_documentReader;
|
||||
IRequestPerformanceLogger &m_performanceLogger;
|
||||
QElapsedTimer m_completionTimer;
|
||||
Context::ContextManager *m_contextManager;
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.6.1",
|
||||
"Version" : "0.8.1",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
|
||||
@ -28,6 +28,8 @@
|
||||
#include <context/Utils.hpp>
|
||||
#include <llmcore/PromptTemplateManager.hpp>
|
||||
#include <llmcore/ProvidersManager.hpp>
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <llmcore/RulesLoader.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/ChatAssistantSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
@ -36,30 +38,10 @@ namespace QodeAssist {
|
||||
|
||||
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_requestHandler(new LLMCore::RequestHandler(this))
|
||||
, m_currentEditor(nullptr)
|
||||
, m_isRefactoringInProgress(false)
|
||||
, m_contextManager(this)
|
||||
{
|
||||
connect(
|
||||
m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
&QuickRefactorHandler::handleLLMResponse);
|
||||
|
||||
connect(
|
||||
m_requestHandler,
|
||||
&LLMCore::RequestHandler::requestFinished,
|
||||
this,
|
||||
[this](const QString &requestId, bool success, const QString &errorString) {
|
||||
if (!success && requestId == m_lastRequestId) {
|
||||
m_isRefactoringInProgress = false;
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = errorString;
|
||||
emit refactoringCompleted(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QuickRefactorHandler::~QuickRefactorHandler() {}
|
||||
@ -153,18 +135,21 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
||||
}
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Chat;
|
||||
config.requestType = LLMCore::RequestType::QuickRefactoring;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint());
|
||||
config.providerRequest
|
||||
= {{"model", settings.caModel()}, {"stream", Settings::chatAssistantSettings().stream()}};
|
||||
config.providerRequest = {{"model", settings.caModel()}, {"stream", true}};
|
||||
config.apiKey = provider->apiKey();
|
||||
|
||||
LLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||
|
||||
provider
|
||||
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
|
||||
provider->prepareRequest(
|
||||
config.providerRequest,
|
||||
promptTemplate,
|
||||
context,
|
||||
LLMCore::RequestType::QuickRefactoring,
|
||||
false);
|
||||
|
||||
QString requestId = QUuid::createUuid().toString();
|
||||
m_lastRequestId = requestId;
|
||||
@ -172,7 +157,23 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
||||
|
||||
m_isRefactoringInProgress = true;
|
||||
|
||||
m_requestHandler->sendLLMRequest(config, request);
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
this,
|
||||
&QuickRefactorHandler::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
this,
|
||||
&QuickRefactorHandler::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||
}
|
||||
|
||||
LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
@ -210,6 +211,18 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
}
|
||||
|
||||
QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt();
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
|
||||
project, LLMCore::RulesContext::QuickRefactor);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
LOG_MESSAGE("Loaded project rules for quick refactor");
|
||||
}
|
||||
}
|
||||
|
||||
systemPrompt += "\n\nFile information:";
|
||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||
@ -280,7 +293,17 @@ void QuickRefactorHandler::handleLLMResponse(
|
||||
void QuickRefactorHandler::cancelRequest()
|
||||
{
|
||||
if (m_isRefactoringInProgress) {
|
||||
m_requestHandler->cancelRequest(m_lastRequestId);
|
||||
auto id = m_lastRequestId;
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.key() == id) {
|
||||
const RequestContext &ctx = it.value();
|
||||
ctx.provider->cancelRequest(id);
|
||||
m_activeRequests.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_isRefactoringInProgress = false;
|
||||
|
||||
RefactorResult result;
|
||||
@ -290,4 +313,23 @@ void QuickRefactorHandler::cancelRequest()
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
QJsonObject request{{"id", requestId}};
|
||||
handleLLMResponse(fullText, request, true);
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
m_isRefactoringInProgress = false;
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
emit refactoringCompleted(result);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -27,7 +27,8 @@
|
||||
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <llmcore/RequestHandler.hpp>
|
||||
#include <llmcore/ContextData.hpp>
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
@ -54,6 +55,10 @@ public:
|
||||
signals:
|
||||
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
void prepareAndSendRequest(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
@ -66,7 +71,13 @@ private:
|
||||
const Utils::Text::Range &range,
|
||||
const QString &instructions);
|
||||
|
||||
LLMCore::RequestHandler *m_requestHandler;
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
};
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
TextEditor::TextEditorWidget *m_currentEditor;
|
||||
Utils::Text::Range m_currentRange;
|
||||
bool m_isRefactoringInProgress;
|
||||
|
||||
59
README.md
59
README.md
@ -3,7 +3,7 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/BGMkUsXUgf)
|
||||
|
||||
 QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
|
||||
@ -40,7 +40,8 @@
|
||||
- Sharing IDE opened files with model context (disabled by default, need enable in settings)
|
||||
- Quick refactor code via fast chat command and opened files
|
||||
- Chat functionality:
|
||||
- Side and Bottom panels
|
||||
- Side and Bottom panels(enabling in chat settings due stability reason with QQuickWidget problem)
|
||||
- Chat in additional popup window with pinning(recommended)
|
||||
- Chat history autosave and restore
|
||||
- Token usage monitoring and management
|
||||
- Attach files for one-time code analysis
|
||||
@ -54,9 +55,10 @@
|
||||
- LM Studio
|
||||
- Mistral AI
|
||||
- Google AI
|
||||
- OpenAI-compatible providers(eg. llama.cpp, https://openrouter.ai)
|
||||
- OpenAI-compatible providers (eg. llama.cpp, https://openrouter.ai)
|
||||
- Extensive library of model-specific templates
|
||||
- Easy configuration and model selection
|
||||
- Support tools/function calling (enabled by default)
|
||||
|
||||
Join our Discord Community: Have questions or want to discuss QodeAssist? Join our [Discord server](https://discord.gg/BGMkUsXUgf) to connect with other users and get support!
|
||||
|
||||
@ -85,11 +87,21 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
|
||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Chat in addtional window: (click to expand)</summary>
|
||||
<img width="851" height="865" alt="image" src="https://github.com/user-attachments/assets/a68894b7-886e-4501-a61b-7161ae34b427" />
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Automatic syncing with open editor files: (click to expand)</summary>
|
||||
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Example how tools works: (click to expand)</summary>
|
||||
<img width="600" alt="ToolsDemo" src="https://github.com/user-attachments/assets/cf6273ad-d5c8-47fc-81e6-23d929547f6c">
|
||||
</details>
|
||||
|
||||
## Install plugin to QtCreator
|
||||
1. Install Latest Qt Creator
|
||||
2. Download the QodeAssist plugin for your Qt Creator
|
||||
@ -180,6 +192,7 @@ ollama run qwen2.5-coder:32b
|
||||
- The URL is set to http://localhost:11434
|
||||
- Your installed model appears in the model selection
|
||||
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
|
||||
- Disable using tools if your model doesn't support tooling
|
||||
4. Click Apply if you made any changes
|
||||
|
||||
You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
@ -195,6 +208,7 @@ You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
- Set the llama.cpp URL (e.g. http://localhost:8080)
|
||||
- Fill in model name
|
||||
- Choose template for model(e.g. llama.cpp FIM for any model with FIM support)
|
||||
- Disable using tools if your model doesn't support tooling
|
||||
<details>
|
||||
<summary>Example of llama.cpp settings: (click to expand)</summary>
|
||||
<img width="829" alt="llama.cpp Settings" src="https://github.com/user-attachments/assets/8c75602c-60f3-49ed-a7a9-d3c972061ea2" />
|
||||
@ -204,6 +218,34 @@ You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
|
||||
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
|
||||
|
||||
## Project Rules Configuration
|
||||
|
||||
QodeAssist supports project-specific rules to customize AI behavior for your codebase. Create a `.qodeassist/rules/` directory in your project root.
|
||||
|
||||
### Quick Start
|
||||
```bash
|
||||
mkdir -p .qodeassist/rules/{common,completion,chat,quickrefactor}
|
||||
```
|
||||
```
|
||||
.qodeassist/
|
||||
└── rules/
|
||||
├── common/ # Applied to all contexts
|
||||
├── completion/ # Code completion only
|
||||
├── chat/ # Chat assistant only
|
||||
└── quickrefactor/ # Quick refactor only
|
||||
```
|
||||
All .md files in each directory are automatically loaded and added to the system prompt.
|
||||
|
||||
Example
|
||||
Create .qodeassist/rules/common/general.md:
|
||||
```markdown
|
||||
# Project Guidelines
|
||||
- Use snake_case for private members
|
||||
- Prefix interfaces with 'I'
|
||||
- Always document public APIs
|
||||
- Prefer Qt containers over STL
|
||||
```
|
||||
|
||||
## File Context Feature
|
||||
|
||||
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant.
|
||||
@ -271,7 +313,16 @@ Linked files provide persistent context throughout the conversation:
|
||||
- [ ] Support MCP
|
||||
|
||||
## Hotkeys
|
||||
|
||||
All hotkeys available in QtCreator Settings
|
||||
Also you can find default hotkeys here:
|
||||
- To call chat with llm in separate window, you can use:
|
||||
- on Mac: Option + Command + W
|
||||
- on Windows: Ctrl + Alt + W
|
||||
- on Linux: Ctrl + Alt + W
|
||||
- To close chat with llm in separate window, you can use:
|
||||
- on Mac: Option + Command + S
|
||||
- on Windows: Ctrl + Alt + S
|
||||
- on Linux: Ctrl + Alt + S
|
||||
- To call manual request to suggestion, you can use or change it in settings
|
||||
- on Mac: Option + Command + Q
|
||||
- on Windows: Ctrl + Alt + Q
|
||||
|
||||
24
UIControls/CMakeLists.txt
Normal file
24
UIControls/CMakeLists.txt
Normal file
@ -0,0 +1,24 @@
|
||||
qt_add_library(QodeAssistUIControls STATIC)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
|
||||
qt_add_qml_module(QodeAssistUIControls
|
||||
URI UIControls
|
||||
VERSION 1.0
|
||||
DEPENDENCIES QtQuick
|
||||
QML_FILES
|
||||
qml/Badge.qml
|
||||
qml/QoAButton.qml
|
||||
qml/QoATextSlider.qml
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistUIControls
|
||||
PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Quick
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistUIControls
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
155
UIControls/qml/QoATextSlider.qml
Normal file
155
UIControls/qml/QoATextSlider.qml
Normal file
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias leftText: leftLabel.text
|
||||
property alias rightText: rightLabel.text
|
||||
|
||||
property bool checked: false
|
||||
property bool enabled: true
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property int padding: 8
|
||||
|
||||
signal toggled()
|
||||
|
||||
readonly property real maxTextWidth: Math.max(leftLabel.implicitWidth, rightLabel.implicitWidth) + padding * 2
|
||||
readonly property real maxTextHeight: Math.max(leftLabel.implicitHeight, rightLabel.implicitHeight) + padding
|
||||
|
||||
implicitWidth: maxTextWidth * 2
|
||||
implicitHeight: maxTextHeight
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: height / 2
|
||||
color: root.enabled ? palette.button : palette.mid
|
||||
border.color: root.enabled ? palette.mid : palette.midlight
|
||||
border.width: 1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: slider
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: root.checked ? parent.width / 2 - 1 : 1
|
||||
width: parent.width / 2
|
||||
height: parent.height - 2
|
||||
opacity: 0.6
|
||||
radius: height / 2
|
||||
|
||||
color: root.enabled
|
||||
? (mouseArea.pressed ? palette.dark : palette.highlight)
|
||||
: palette.midlight
|
||||
|
||||
border.color: root.enabled ? palette.highlight : palette.mid
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Qt.alpha(palette.highlight, 0.4) }
|
||||
GradientStop { position: 1.0; color: Qt.alpha(palette.highlight, 0.2) }
|
||||
}
|
||||
opacity: root.hovered ? 0.3 : 0.01
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 250 }
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: leftContainer
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height
|
||||
|
||||
Text {
|
||||
id: leftLabel
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: root.leftText
|
||||
font.pixelSize: 14
|
||||
color: root.enabled
|
||||
? (!root.checked ? palette.highlightedText : palette.windowText)
|
||||
: Qt.alpha(palette.windowText, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightContainer
|
||||
|
||||
x: parent.width / 2
|
||||
width: parent.width / 2
|
||||
height: parent.height
|
||||
|
||||
Text {
|
||||
id: rightLabel
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: root.rightText
|
||||
font.pixelSize: 14
|
||||
color: root.enabled
|
||||
? (root.checked ? palette.highlightedText : palette.windowText)
|
||||
: Qt.alpha(palette.windowText, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onClicked: {
|
||||
if (root.enabled) {
|
||||
root.checked = !root.checked
|
||||
root.toggled()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ add_library(Context STATIC
|
||||
ProgrammingLanguage.hpp ProgrammingLanguage.cpp
|
||||
IContextManager.hpp
|
||||
IgnoreManager.hpp IgnoreManager.cpp
|
||||
ProjectUtils.hpp ProjectUtils.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(Context
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,8 +22,10 @@
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QMutex>
|
||||
#include <QQueue>
|
||||
#include <QTimer>
|
||||
#include <QUndoStack>
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
@ -39,21 +41,119 @@ public:
|
||||
QString lineContent;
|
||||
};
|
||||
|
||||
enum FileEditStatus { Pending, Applied, Rejected, Archived };
|
||||
|
||||
struct DiffHunk
|
||||
{
|
||||
int oldStartLine; // Starting line in old file (1-based)
|
||||
int oldLineCount; // Number of lines in old file
|
||||
int newStartLine; // Starting line in new file (1-based)
|
||||
int newLineCount; // Number of lines in new file
|
||||
QStringList contextBefore; // Lines of context before the change (for anchoring)
|
||||
QStringList removedLines; // Lines to remove (prefixed with -)
|
||||
QStringList addedLines; // Lines to add (prefixed with +)
|
||||
QStringList contextAfter; // Lines of context after the change (for anchoring)
|
||||
};
|
||||
|
||||
struct DiffInfo
|
||||
{
|
||||
QList<DiffHunk> hunks; // List of diff hunks
|
||||
QString originalContent; // Full original file content (for fallback)
|
||||
QString modifiedContent; // Full modified file content (for fallback)
|
||||
int contextLines = 3; // Number of context lines to keep
|
||||
bool useFallback = false; // If true, use original content-based approach
|
||||
};
|
||||
|
||||
struct FileEdit
|
||||
{
|
||||
QString editId;
|
||||
QString filePath;
|
||||
QString oldContent; // Kept for backward compatibility and fallback
|
||||
QString newContent; // Kept for backward compatibility and fallback
|
||||
DiffInfo diffInfo; // Initial diff (created once, may become stale after formatting)
|
||||
FileEditStatus status;
|
||||
QDateTime timestamp;
|
||||
bool wasAutoApplied = false; // Track if edit was already auto-applied once
|
||||
bool isFromHistory = false; // Track if edit was loaded from chat history
|
||||
QString statusMessage;
|
||||
};
|
||||
|
||||
static ChangesManager &instance();
|
||||
|
||||
void addChange(
|
||||
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
|
||||
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
|
||||
|
||||
void addFileEdit(
|
||||
const QString &editId,
|
||||
const QString &filePath,
|
||||
const QString &oldContent,
|
||||
const QString &newContent,
|
||||
bool autoApply = true,
|
||||
bool isFromHistory = false,
|
||||
const QString &requestId = QString());
|
||||
bool applyFileEdit(const QString &editId);
|
||||
bool rejectFileEdit(const QString &editId);
|
||||
bool undoFileEdit(const QString &editId);
|
||||
FileEdit getFileEdit(const QString &editId) const;
|
||||
QList<FileEdit> getPendingEdits() const;
|
||||
|
||||
bool applyPendingEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
||||
|
||||
QList<FileEdit> getEditsForRequest(const QString &requestId) const;
|
||||
|
||||
bool undoAllEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
||||
|
||||
bool reapplyAllEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
||||
|
||||
void archiveAllNonArchivedEdits();
|
||||
|
||||
signals:
|
||||
void fileEditAdded(const QString &editId);
|
||||
void fileEditApplied(const QString &editId);
|
||||
void fileEditRejected(const QString &editId);
|
||||
void fileEditUndone(const QString &editId);
|
||||
void fileEditArchived(const QString &editId);
|
||||
|
||||
private:
|
||||
ChangesManager();
|
||||
~ChangesManager();
|
||||
ChangesManager(const ChangesManager &) = delete;
|
||||
ChangesManager &operator=(const ChangesManager &) = delete;
|
||||
|
||||
void cleanupOldChanges();
|
||||
bool performFileEdit(const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg = nullptr);
|
||||
bool performFileEditWithDiff(const QString &filePath, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
|
||||
QString readFileContent(const QString &filePath) const;
|
||||
|
||||
DiffInfo createDiffInfo(const QString &originalContent, const QString &modifiedContent, const QString &filePath);
|
||||
bool applyDiffToContent(QString &content, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
|
||||
bool findHunkLocation(const QStringList &fileLines, const DiffHunk &hunk, int &actualStartLine, QString *debugInfo = nullptr) const;
|
||||
|
||||
// Helper method for fragment-based apply/undo operations
|
||||
bool performFragmentReplacement(
|
||||
const QString &filePath,
|
||||
const QString &searchContent,
|
||||
const QString &replaceContent,
|
||||
bool isAppendOperation,
|
||||
QString *errorMsg = nullptr,
|
||||
bool isUndo = false);
|
||||
|
||||
int levenshteinDistance(const QString &s1, const QString &s2) const;
|
||||
QString findBestMatch(const QString &fileContent, const QString &searchContent, double threshold = 0.82, double *outSimilarity = nullptr) const;
|
||||
QString findBestMatchLineBased(const QString &fileContent, const QString &searchContent, double threshold = 0.82, double *outSimilarity = nullptr) const;
|
||||
QString findBestMatchWithNormalization(const QString &fileContent, const QString &searchContent, double *outSimilarity = nullptr, QString *outMatchType = nullptr) const;
|
||||
|
||||
struct RequestEdits
|
||||
{
|
||||
QStringList editIds;
|
||||
bool autoApplyPending = false;
|
||||
};
|
||||
|
||||
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
||||
QHash<QString, FileEdit> m_fileEdits;
|
||||
QHash<QString, RequestEdits> m_requestEdits; // requestId → ordered edits
|
||||
QUndoStack *m_undoStack;
|
||||
mutable QMutex m_mutex;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
|
||||
73
context/ProjectUtils.cpp
Normal file
73
context/ProjectUtils.cpp
Normal file
@ -0,0 +1,73 @@
|
||||
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ProjectUtils.hpp"
|
||||
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/filepath.h>
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
bool ProjectUtils::isFileInProject(const QString &filePath)
|
||||
{
|
||||
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||
Utils::FilePath targetPath = Utils::FilePath::fromString(filePath);
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
for (const auto &projectFile : std::as_const(projectFiles)) {
|
||||
if (projectFile == targetPath) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Utils::FilePath projectDir = project->projectDirectory();
|
||||
if (targetPath.isChildOf(projectDir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ProjectUtils::findFileInProject(const QString &filename)
|
||||
{
|
||||
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
for (const auto &projectFile : std::as_const(projectFiles)) {
|
||||
if (projectFile.fileName() == filename) {
|
||||
return projectFile.toFSPathString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
57
context/ProjectUtils.hpp
Normal file
57
context/ProjectUtils.hpp
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
/**
|
||||
* @brief Utility functions for working with Qt Creator projects
|
||||
*/
|
||||
class ProjectUtils
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Check if a file is part of any open project
|
||||
*
|
||||
* Checks if the given file path is either:
|
||||
* 1. Explicitly listed in project source files
|
||||
* 2. Located within a project directory
|
||||
*
|
||||
* @param filePath Absolute or canonical file path to check
|
||||
* @return true if file is part of any open project, false otherwise
|
||||
*/
|
||||
static bool isFileInProject(const QString &filePath);
|
||||
|
||||
/**
|
||||
* @brief Find a file in open projects by filename
|
||||
*
|
||||
* Searches all open projects for a file matching the given filename.
|
||||
* If multiple files with the same name exist, returns the first match.
|
||||
*
|
||||
* @param filename File name to search for (e.g., "main.cpp")
|
||||
* @return Absolute file path if found, empty string otherwise
|
||||
*/
|
||||
static QString findFileInProject(const QString &filename);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
73
llmcore/BaseTool.cpp
Normal file
73
llmcore/BaseTool.cpp
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "BaseTool.hpp"
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
BaseTool::BaseTool(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
QJsonObject BaseTool::customizeForGoogle(const QJsonObject &baseDefinition) const
|
||||
{
|
||||
QJsonObject functionDeclaration;
|
||||
functionDeclaration["name"] = name();
|
||||
functionDeclaration["description"] = description();
|
||||
functionDeclaration["parameters"] = baseDefinition;
|
||||
|
||||
QJsonArray functionDeclarations;
|
||||
functionDeclarations.append(functionDeclaration);
|
||||
|
||||
QJsonObject tool;
|
||||
tool["function_declarations"] = functionDeclarations;
|
||||
|
||||
return tool;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
62
llmcore/BaseTool.hpp
Normal file
62
llmcore/BaseTool.hpp
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QFuture>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
enum class ToolSchemaFormat { OpenAI, Claude, Ollama, Google };
|
||||
|
||||
enum ToolPermission {
|
||||
None = 0,
|
||||
FileSystemRead = 1 << 0,
|
||||
FileSystemWrite = 1 << 1,
|
||||
NetworkAccess = 1 << 2
|
||||
};
|
||||
Q_DECLARE_FLAGS(ToolPermissions, ToolPermission)
|
||||
|
||||
class BaseTool : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BaseTool(QObject *parent = nullptr);
|
||||
~BaseTool() override = default;
|
||||
|
||||
virtual QString name() const = 0;
|
||||
virtual QString stringName() const = 0;
|
||||
virtual QString description() const = 0;
|
||||
virtual QJsonObject getDefinition(ToolSchemaFormat format) const = 0;
|
||||
virtual ToolPermissions requiredPermissions() const = 0;
|
||||
|
||||
virtual QFuture<QString> 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;
|
||||
virtual QJsonObject customizeForGoogle(const QJsonObject &baseDefinition) const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -1,6 +1,6 @@
|
||||
add_library(LLMCore STATIC
|
||||
RequestType.hpp
|
||||
Provider.hpp
|
||||
Provider.hpp Provider.cpp
|
||||
ProvidersManager.hpp ProvidersManager.cpp
|
||||
ContextData.hpp
|
||||
IPromptProvider.hpp
|
||||
@ -10,12 +10,14 @@ add_library(LLMCore STATIC
|
||||
PromptTemplate.hpp
|
||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||
RequestConfig.hpp
|
||||
RequestHandlerBase.hpp RequestHandlerBase.cpp
|
||||
RequestHandler.hpp RequestHandler.cpp
|
||||
OllamaMessage.hpp OllamaMessage.cpp
|
||||
OpenAIMessage.hpp OpenAIMessage.cpp
|
||||
ValidationUtils.hpp ValidationUtils.cpp
|
||||
ProviderID.hpp
|
||||
HttpClient.hpp HttpClient.cpp
|
||||
DataBuffers.hpp
|
||||
SSEBuffer.hpp SSEBuffer.cpp
|
||||
BaseTool.hpp BaseTool.cpp
|
||||
ContentBlocks.hpp
|
||||
RulesLoader.hpp RulesLoader.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(LLMCore
|
||||
|
||||
140
llmcore/ContentBlocks.hpp
Normal file
140
llmcore/ContentBlocks.hpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
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
|
||||
39
llmcore/DataBuffers.hpp
Normal file
39
llmcore/DataBuffers.hpp
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "SSEBuffer.hpp"
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
struct DataBuffers
|
||||
{
|
||||
SSEBuffer rawStreamBuffer;
|
||||
QString responseContent;
|
||||
|
||||
void clear()
|
||||
{
|
||||
rawStreamBuffer.clear();
|
||||
responseContent.clear();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
156
llmcore/HttpClient.cpp
Normal file
156
llmcore/HttpClient.cpp
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "HttpClient.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QMutexLocker>
|
||||
#include <QUuid>
|
||||
|
||||
#include <Logger.hpp>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
HttpClient::HttpClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{
|
||||
connect(this, &HttpClient::sendRequest, this, &HttpClient::onSendRequest);
|
||||
}
|
||||
|
||||
HttpClient::~HttpClient()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
for (auto *reply : std::as_const(m_activeRequests)) {
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_activeRequests.clear();
|
||||
}
|
||||
|
||||
void HttpClient::onSendRequest(const HttpRequest &request)
|
||||
{
|
||||
QJsonDocument doc(request.payload);
|
||||
LOG_MESSAGE(QString("HttpClient: data: %1").arg(doc.toJson(QJsonDocument::Indented)));
|
||||
|
||||
QNetworkReply *reply
|
||||
= m_manager->post(request.networkRequest, doc.toJson(QJsonDocument::Compact));
|
||||
addActiveRequest(reply, request.requestId);
|
||||
|
||||
connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead);
|
||||
connect(reply, &QNetworkReply::finished, this, &HttpClient::onFinished);
|
||||
}
|
||||
|
||||
void HttpClient::onReadyRead()
|
||||
{
|
||||
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||
if (!reply || reply->isFinished())
|
||||
return;
|
||||
|
||||
QString requestId;
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
bool found = false;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value() == reply) {
|
||||
requestId = it.key();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestId.isEmpty())
|
||||
return;
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (!data.isEmpty()) {
|
||||
emit dataReceived(requestId, data);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpClient::onFinished()
|
||||
{
|
||||
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||
if (!reply)
|
||||
return;
|
||||
|
||||
reply->disconnect();
|
||||
|
||||
QString requestId;
|
||||
bool hasError = false;
|
||||
QString errorMsg;
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
bool found = false;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value() == reply) {
|
||||
requestId = it.key();
|
||||
m_activeRequests.erase(it);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
hasError = true;
|
||||
errorMsg = reply->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requestId.isEmpty()) {
|
||||
emit requestFinished(requestId, !hasError, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
QString HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_activeRequests[requestId] = reply;
|
||||
LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId));
|
||||
return requestId;
|
||||
}
|
||||
|
||||
void HttpClient::cancelRequest(const QString &requestId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it != m_activeRequests.end()) {
|
||||
QNetworkReply *reply = it.value();
|
||||
if (reply) {
|
||||
reply->disconnect();
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_activeRequests.erase(it);
|
||||
LOG_MESSAGE(QString("HttpClient: Cancelled request: %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
68
llmcore/HttpClient.hpp
Normal file
68
llmcore/HttpClient.hpp
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
struct HttpRequest
|
||||
{
|
||||
QNetworkRequest networkRequest;
|
||||
QString requestId;
|
||||
QJsonObject payload;
|
||||
};
|
||||
|
||||
class HttpClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HttpClient(QObject *parent = nullptr);
|
||||
~HttpClient();
|
||||
|
||||
void cancelRequest(const QString &requestId);
|
||||
|
||||
signals:
|
||||
void sendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
||||
void dataReceived(const QString &requestId, const QByteArray &data);
|
||||
void requestFinished(const QString &requestId, bool success, const QString &error);
|
||||
|
||||
private slots:
|
||||
void onSendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
||||
void onReadyRead();
|
||||
void onFinished();
|
||||
|
||||
private:
|
||||
QString addActiveRequest(QNetworkReply *reply, const QString &requestId);
|
||||
|
||||
QNetworkAccessManager *m_manager;
|
||||
QHash<QString, QNetworkReply *> m_activeRequests;
|
||||
mutable QMutex m_mutex;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OllamaMessage.hpp"
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
QJsonObject OllamaMessage::parseJsonFromData(const QByteArray &data)
|
||||
{
|
||||
QByteArrayList lines = data.split('\n');
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(line, &error);
|
||||
if (!doc.isNull() && error.error == QJsonParseError::NoError) {
|
||||
return doc.object();
|
||||
}
|
||||
}
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
OllamaMessage OllamaMessage::fromJson(const QByteArray &data, Type type)
|
||||
{
|
||||
OllamaMessage msg;
|
||||
QJsonObject obj = parseJsonFromData(data);
|
||||
|
||||
if (obj.isEmpty()) {
|
||||
msg.error = "Invalid JSON response";
|
||||
return msg;
|
||||
}
|
||||
|
||||
msg.model = obj["model"].toString();
|
||||
msg.createdAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate);
|
||||
msg.done = obj["done"].toBool();
|
||||
msg.doneReason = obj["done_reason"].toString();
|
||||
msg.error = obj["error"].toString();
|
||||
|
||||
if (type == Type::Generate) {
|
||||
auto &genResponse = msg.response.emplace<GenerateResponse>();
|
||||
genResponse.response = obj["response"].toString();
|
||||
if (msg.done && obj.contains("context")) {
|
||||
const auto array = obj["context"].toArray();
|
||||
genResponse.context.reserve(array.size());
|
||||
for (const auto &val : array) {
|
||||
genResponse.context.append(val.toInt());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto &chatResponse = msg.response.emplace<ChatResponse>();
|
||||
const auto msgObj = obj["message"].toObject();
|
||||
chatResponse.role = msgObj["role"].toString();
|
||||
chatResponse.content = msgObj["content"].toString();
|
||||
}
|
||||
|
||||
if (msg.done) {
|
||||
msg.metrics
|
||||
= {obj["total_duration"].toVariant().toLongLong(),
|
||||
obj["load_duration"].toVariant().toLongLong(),
|
||||
obj["prompt_eval_count"].toVariant().toLongLong(),
|
||||
obj["prompt_eval_duration"].toVariant().toLongLong(),
|
||||
obj["eval_count"].toVariant().toLongLong(),
|
||||
obj["eval_duration"].toVariant().toLongLong()};
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
QString OllamaMessage::getContent() const
|
||||
{
|
||||
if (std::holds_alternative<GenerateResponse>(response)) {
|
||||
return std::get<GenerateResponse>(response).response;
|
||||
}
|
||||
return std::get<ChatResponse>(response).content;
|
||||
}
|
||||
|
||||
bool OllamaMessage::hasError() const
|
||||
{
|
||||
return !error.isEmpty();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class OllamaMessage
|
||||
{
|
||||
public:
|
||||
enum class Type { Generate, Chat };
|
||||
|
||||
struct Metrics
|
||||
{
|
||||
qint64 totalDuration{0};
|
||||
qint64 loadDuration{0};
|
||||
qint64 promptEvalCount{0};
|
||||
qint64 promptEvalDuration{0};
|
||||
qint64 evalCount{0};
|
||||
qint64 evalDuration{0};
|
||||
};
|
||||
|
||||
struct GenerateResponse
|
||||
{
|
||||
QString response;
|
||||
QVector<int> context;
|
||||
};
|
||||
|
||||
struct ChatResponse
|
||||
{
|
||||
QString role;
|
||||
QString content;
|
||||
};
|
||||
|
||||
QString model;
|
||||
QDateTime createdAt;
|
||||
std::variant<GenerateResponse, ChatResponse> response;
|
||||
bool done{false};
|
||||
QString doneReason;
|
||||
QString error;
|
||||
Metrics metrics;
|
||||
|
||||
static OllamaMessage fromJson(const QByteArray &data, Type type);
|
||||
QString getContent() const;
|
||||
bool hasError() const;
|
||||
|
||||
private:
|
||||
static QJsonObject parseJsonFromData(const QByteArray &data);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OpenAIMessage.hpp"
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
OpenAIMessage OpenAIMessage::fromJson(const QJsonObject &obj)
|
||||
{
|
||||
OpenAIMessage msg;
|
||||
|
||||
if (obj.contains("error")) {
|
||||
msg.error = obj["error"].toObject()["message"].toString();
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (obj.contains("choices")) {
|
||||
auto choices = obj["choices"].toArray();
|
||||
if (!choices.isEmpty()) {
|
||||
auto choiceObj = choices[0].toObject();
|
||||
|
||||
if (choiceObj.contains("delta")) {
|
||||
QJsonObject delta = choiceObj["delta"].toObject();
|
||||
msg.choice.content = delta["content"].toString();
|
||||
} else if (choiceObj.contains("message")) {
|
||||
QJsonObject message = choiceObj["message"].toObject();
|
||||
msg.choice.content = message["content"].toString();
|
||||
}
|
||||
|
||||
msg.choice.finishReason = choiceObj["finish_reason"].toString();
|
||||
if (!msg.choice.finishReason.isEmpty()) {
|
||||
msg.done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contains("usage")) {
|
||||
QJsonObject usage = obj["usage"].toObject();
|
||||
msg.usage.promptTokens = usage["prompt_tokens"].toInt();
|
||||
msg.usage.completionTokens = usage["completion_tokens"].toInt();
|
||||
msg.usage.totalTokens = usage["total_tokens"].toInt();
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
QString OpenAIMessage::getContent() const
|
||||
{
|
||||
return choice.content;
|
||||
}
|
||||
|
||||
bool OpenAIMessage::hasError() const
|
||||
{
|
||||
return !error.isEmpty();
|
||||
}
|
||||
|
||||
bool OpenAIMessage::isDone() const
|
||||
{
|
||||
return done
|
||||
|| (!choice.finishReason.isEmpty()
|
||||
&& (choice.finishReason == "stop" || choice.finishReason == "length"));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
36
llmcore/Provider.cpp
Normal file
36
llmcore/Provider.cpp
Normal file
@ -0,0 +1,36 @@
|
||||
#include "Provider.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
Provider::Provider(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_httpClient(new HttpClient(this))
|
||||
{
|
||||
connect(m_httpClient, &HttpClient::dataReceived, this, &Provider::onDataReceived);
|
||||
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;
|
||||
}
|
||||
|
||||
QJsonObject Provider::parseEventLine(const QString &line)
|
||||
{
|
||||
if (!line.startsWith("data: "))
|
||||
return QJsonObject();
|
||||
|
||||
QString jsonStr = line.mid(6);
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||
return doc.object();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -21,9 +21,12 @@
|
||||
|
||||
#include <utils/environment.h>
|
||||
#include <QNetworkRequest>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "ContextData.hpp"
|
||||
#include "DataBuffers.hpp"
|
||||
#include "HttpClient.hpp"
|
||||
#include "PromptTemplate.hpp"
|
||||
#include "RequestType.hpp"
|
||||
|
||||
@ -32,9 +35,12 @@ class QJsonObject;
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class Provider
|
||||
class Provider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Provider(QObject *parent = nullptr);
|
||||
|
||||
virtual ~Provider() = default;
|
||||
|
||||
virtual QString name() const = 0;
|
||||
@ -46,14 +52,55 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
= 0;
|
||||
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0;
|
||||
virtual QList<QString> getInstalledModels(const QString &url) = 0;
|
||||
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
|
||||
virtual QString apiKey() const = 0;
|
||||
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
|
||||
virtual ProviderID providerID() const = 0;
|
||||
|
||||
virtual void sendRequest(const RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
= 0;
|
||||
|
||||
virtual bool supportsTools() const { return false; };
|
||||
|
||||
virtual void cancelRequest(const RequestID &requestId);
|
||||
|
||||
HttpClient *httpClient() const;
|
||||
|
||||
public slots:
|
||||
virtual void onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
= 0;
|
||||
virtual void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
= 0;
|
||||
|
||||
signals:
|
||||
void partialResponseReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QString &partialText);
|
||||
void fullResponseReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QString &fullText);
|
||||
void requestFailed(const QodeAssist::LLMCore::RequestID &requestId, const QString &error);
|
||||
void toolExecutionStarted(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||
void toolExecutionCompleted(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &result);
|
||||
void continuationStarted(const QodeAssist::LLMCore::RequestID &requestId);
|
||||
|
||||
protected:
|
||||
QJsonObject parseEventLine(const QString &line);
|
||||
|
||||
QHash<RequestID, DataBuffers> m_dataBuffers;
|
||||
QHash<RequestID, QUrl> m_requestUrls;
|
||||
|
||||
private:
|
||||
HttpClient *m_httpClient;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
|
||||
@ -1,202 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "RequestHandler.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
RequestHandler::RequestHandler(QObject *parent)
|
||||
: RequestHandlerBase(parent)
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{
|
||||
connect(
|
||||
this,
|
||||
&RequestHandler::doSendRequest,
|
||||
this,
|
||||
&RequestHandler::sendLLMRequestInternal,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(
|
||||
this,
|
||||
&RequestHandler::doCancelRequest,
|
||||
this,
|
||||
&RequestHandler::cancelRequestInternal,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
RequestHandler::~RequestHandler()
|
||||
{
|
||||
for (auto reply : m_activeRequests) {
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_activeRequests.clear();
|
||||
m_accumulatedResponses.clear();
|
||||
}
|
||||
|
||||
void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request)
|
||||
{
|
||||
emit doSendRequest(config, request);
|
||||
}
|
||||
|
||||
bool RequestHandler::cancelRequest(const QString &id)
|
||||
{
|
||||
emit doCancelRequest(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RequestHandler::sendLLMRequestInternal(const LLMConfig &config, const QJsonObject &request)
|
||||
{
|
||||
LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
|
||||
.arg(
|
||||
config.url.toString(),
|
||||
QString::fromUtf8(
|
||||
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QNetworkRequest networkRequest(config.url);
|
||||
networkRequest.setTransferTimeout(300000);
|
||||
|
||||
config.provider->prepareNetworkRequest(networkRequest);
|
||||
|
||||
QNetworkReply *reply
|
||||
= m_manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson());
|
||||
if (!reply) {
|
||||
LOG_MESSAGE("Error: Failed to create network reply");
|
||||
return;
|
||||
}
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
m_activeRequests[requestId] = reply;
|
||||
|
||||
connect(reply, &QNetworkReply::readyRead, this, [this, reply, request, config]() {
|
||||
handleLLMResponse(reply, request, config);
|
||||
});
|
||||
|
||||
connect(
|
||||
reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[this, reply, requestId]() {
|
||||
m_activeRequests.remove(requestId);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMessage = reply->errorString();
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("Error details: %1\nStatus code: %2").arg(errorMessage).arg(statusCode));
|
||||
|
||||
emit requestFinished(requestId, false, errorMessage);
|
||||
} else {
|
||||
LOG_MESSAGE("Request finished successfully");
|
||||
emit requestFinished(requestId, true, QString());
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void RequestHandler::handleLLMResponse(
|
||||
QNetworkReply *reply, const QJsonObject &request, const LLMConfig &config)
|
||||
{
|
||||
QString &accumulatedResponse = m_accumulatedResponses[reply];
|
||||
|
||||
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
|
||||
|
||||
if (config.requestType == RequestType::CodeCompletion) {
|
||||
if (!config.multiLineCompletion
|
||||
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
auto cleanedCompletion
|
||||
= removeStopWords(accumulatedResponse, config.promptTemplate->stopWords());
|
||||
|
||||
emit completionReceived(cleanedCompletion, request, true);
|
||||
}
|
||||
} else if (config.requestType == RequestType::Chat) {
|
||||
emit completionReceived(accumulatedResponse, request, isComplete);
|
||||
}
|
||||
|
||||
if (isComplete)
|
||||
m_accumulatedResponses.remove(reply);
|
||||
}
|
||||
|
||||
void RequestHandler::cancelRequestInternal(const QString &id)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
if (m_activeRequests.contains(id)) {
|
||||
QNetworkReply *reply = m_activeRequests[id];
|
||||
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
|
||||
reply->abort();
|
||||
m_activeRequests.remove(id);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
locker.unlock();
|
||||
|
||||
m_manager->clearConnectionCache();
|
||||
m_manager->clearAccessCache();
|
||||
|
||||
emit requestCancelled(id);
|
||||
}
|
||||
}
|
||||
|
||||
bool RequestHandler::processSingleLineCompletion(
|
||||
QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedResponse,
|
||||
const LLMConfig &config)
|
||||
{
|
||||
QString cleanedResponse = accumulatedResponse;
|
||||
|
||||
int newlinePos = cleanedResponse.indexOf('\n');
|
||||
if (newlinePos != -1) {
|
||||
QString singleLineCompletion = cleanedResponse.left(newlinePos).trimmed();
|
||||
singleLineCompletion
|
||||
= removeStopWords(singleLineCompletion, config.promptTemplate->stopWords());
|
||||
emit completionReceived(singleLineCompletion, request, true);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
reply->abort();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString RequestHandler::removeStopWords(const QStringView &completion, const QStringList &stopWords)
|
||||
{
|
||||
QString filteredCompletion = completion.toString();
|
||||
|
||||
for (const QString &stopWord : stopWords) {
|
||||
filteredCompletion = filteredCompletion.replace(stopWord, "");
|
||||
}
|
||||
|
||||
return filteredCompletion;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
#include "RequestConfig.hpp"
|
||||
#include "RequestHandlerBase.hpp"
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class RequestHandler : public RequestHandlerBase
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RequestHandler(QObject *parent = nullptr);
|
||||
~RequestHandler() override;
|
||||
|
||||
void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) override;
|
||||
bool cancelRequest(const QString &id) override;
|
||||
|
||||
signals:
|
||||
void doSendRequest(QodeAssist::LLMCore::LLMConfig config, QJsonObject request);
|
||||
void doCancelRequest(QString id);
|
||||
|
||||
private slots:
|
||||
void sendLLMRequestInternal(
|
||||
const QodeAssist::LLMCore::LLMConfig &config, const QJsonObject &request);
|
||||
void cancelRequestInternal(const QString &id);
|
||||
void handleLLMResponse(
|
||||
QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QodeAssist::LLMCore::LLMConfig &config);
|
||||
|
||||
private:
|
||||
QMap<QString, QNetworkReply *> m_activeRequests;
|
||||
QMap<QNetworkReply *, QString> m_accumulatedResponses;
|
||||
QNetworkAccessManager *m_manager;
|
||||
QMutex m_mutex;
|
||||
|
||||
bool processSingleLineCompletion(
|
||||
QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedResponse,
|
||||
const LLMConfig &config);
|
||||
QString removeStopWords(const QStringView &completion, const QStringList &stopWords);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -17,9 +17,13 @@
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QString>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
enum RequestType { CodeCompletion, Chat, Embedding };
|
||||
enum RequestType { CodeCompletion, Chat, Embedding, QuickRefactoring };
|
||||
|
||||
using RequestID = QString;
|
||||
}
|
||||
|
||||
181
llmcore/RulesLoader.cpp
Normal file
181
llmcore/RulesLoader.cpp
Normal file
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "RulesLoader.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
QString RulesLoader::loadRules(const QString &projectPath, RulesContext context)
|
||||
{
|
||||
if (projectPath.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString combined;
|
||||
QString basePath = projectPath + "/.qodeassist/rules";
|
||||
|
||||
combined += loadAllMarkdownFiles(basePath + "/common");
|
||||
|
||||
switch (context) {
|
||||
case RulesContext::Completions:
|
||||
combined += loadAllMarkdownFiles(basePath + "/completions");
|
||||
break;
|
||||
case RulesContext::Chat:
|
||||
combined += loadAllMarkdownFiles(basePath + "/chat");
|
||||
break;
|
||||
case RulesContext::QuickRefactor:
|
||||
combined += loadAllMarkdownFiles(basePath + "/quickrefactor");
|
||||
break;
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
QString RulesLoader::loadRulesForProject(ProjectExplorer::Project *project, RulesContext context)
|
||||
{
|
||||
if (!project) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString projectPath = getProjectPath(project);
|
||||
return loadRules(projectPath, context);
|
||||
}
|
||||
|
||||
ProjectExplorer::Project *RulesLoader::getActiveProject()
|
||||
{
|
||||
auto currentEditor = Core::EditorManager::currentEditor();
|
||||
if (currentEditor && currentEditor->document()) {
|
||||
Utils::FilePath filePath = currentEditor->document()->filePath();
|
||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
||||
if (project) {
|
||||
return project;
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectExplorer::ProjectManager::startupProject();
|
||||
}
|
||||
|
||||
QString RulesLoader::loadAllMarkdownFiles(const QString &dirPath)
|
||||
{
|
||||
QString combined;
|
||||
QDir dir(dirPath);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
|
||||
|
||||
for (const QString &fileName : mdFiles) {
|
||||
QFile file(dir.filePath(fileName));
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
combined += file.readAll();
|
||||
combined += "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
QString RulesLoader::getProjectPath(ProjectExplorer::Project *project)
|
||||
{
|
||||
if (!project) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return project->projectDirectory().toUrlishString();
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> RulesLoader::getRuleFiles(const QString &projectPath, RulesContext context)
|
||||
{
|
||||
if (projectPath.isEmpty()) {
|
||||
return QVector<RuleFileInfo>();
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> result;
|
||||
QString basePath = projectPath + "/.qodeassist/rules";
|
||||
|
||||
// Always include common rules
|
||||
result.append(collectMarkdownFiles(basePath + "/common", "common"));
|
||||
|
||||
// Add context-specific rules
|
||||
switch (context) {
|
||||
case RulesContext::Completions:
|
||||
result.append(collectMarkdownFiles(basePath + "/completions", "completions"));
|
||||
break;
|
||||
case RulesContext::Chat:
|
||||
result.append(collectMarkdownFiles(basePath + "/chat", "chat"));
|
||||
break;
|
||||
case RulesContext::QuickRefactor:
|
||||
result.append(collectMarkdownFiles(basePath + "/quickrefactor", "quickrefactor"));
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> RulesLoader::getRuleFilesForProject(
|
||||
ProjectExplorer::Project *project, RulesContext context)
|
||||
{
|
||||
if (!project) {
|
||||
return QVector<RuleFileInfo>();
|
||||
}
|
||||
|
||||
QString projectPath = getProjectPath(project);
|
||||
return getRuleFiles(projectPath, context);
|
||||
}
|
||||
|
||||
QString RulesLoader::loadRuleFileContent(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return file.readAll();
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> RulesLoader::collectMarkdownFiles(
|
||||
const QString &dirPath, const QString &category)
|
||||
{
|
||||
QVector<RuleFileInfo> result;
|
||||
QDir dir(dirPath);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
|
||||
|
||||
for (const QString &fileName : mdFiles) {
|
||||
QString fullPath = dir.filePath(fileName);
|
||||
result.append({fullPath, fileName, category});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
57
llmcore/RulesLoader.hpp
Normal file
57
llmcore/RulesLoader.hpp
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace ProjectExplorer {
|
||||
class Project;
|
||||
}
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
enum class RulesContext { Completions, Chat, QuickRefactor };
|
||||
|
||||
struct RuleFileInfo
|
||||
{
|
||||
QString filePath;
|
||||
QString fileName;
|
||||
QString category; // "common", "chat", "completions", "quickrefactor"
|
||||
};
|
||||
|
||||
class RulesLoader
|
||||
{
|
||||
public:
|
||||
static QString loadRules(const QString &projectPath, RulesContext context);
|
||||
static QString loadRulesForProject(ProjectExplorer::Project *project, RulesContext context);
|
||||
static ProjectExplorer::Project *getActiveProject();
|
||||
|
||||
// New methods for getting rule files info
|
||||
static QVector<RuleFileInfo> getRuleFiles(const QString &projectPath, RulesContext context);
|
||||
static QVector<RuleFileInfo> getRuleFilesForProject(ProjectExplorer::Project *project, RulesContext context);
|
||||
static QString loadRuleFileContent(const QString &filePath);
|
||||
|
||||
private:
|
||||
static QString loadAllMarkdownFiles(const QString &dirPath);
|
||||
static QVector<RuleFileInfo> collectMarkdownFiles(const QString &dirPath, const QString &category);
|
||||
static QString getProjectPath(ProjectExplorer::Project *project);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
51
llmcore/SSEBuffer.cpp
Normal file
51
llmcore/SSEBuffer.cpp
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SSEBuffer.hpp"
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
QStringList SSEBuffer::processData(const QByteArray &data)
|
||||
{
|
||||
m_buffer += QString::fromUtf8(data);
|
||||
|
||||
QStringList lines = m_buffer.split('\n');
|
||||
m_buffer = lines.takeLast();
|
||||
|
||||
lines.removeAll(QString());
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
void SSEBuffer::clear()
|
||||
{
|
||||
m_buffer.clear();
|
||||
}
|
||||
|
||||
QString SSEBuffer::currentBuffer() const
|
||||
{
|
||||
return m_buffer;
|
||||
}
|
||||
|
||||
bool SSEBuffer::hasIncompleteData() const
|
||||
{
|
||||
return !m_buffer.isEmpty();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -19,38 +19,24 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class OpenAIMessage
|
||||
class SSEBuffer
|
||||
{
|
||||
public:
|
||||
struct Choice
|
||||
{
|
||||
QString content;
|
||||
QString finishReason;
|
||||
};
|
||||
SSEBuffer() = default;
|
||||
|
||||
struct Usage
|
||||
{
|
||||
int promptTokens{0};
|
||||
int completionTokens{0};
|
||||
int totalTokens{0};
|
||||
};
|
||||
QStringList processData(const QByteArray &data);
|
||||
|
||||
Choice choice;
|
||||
QString error;
|
||||
bool done{false};
|
||||
Usage usage;
|
||||
void clear();
|
||||
QString currentBuffer() const;
|
||||
bool hasIncompleteData() const;
|
||||
|
||||
QString getContent() const;
|
||||
bool hasError() const;
|
||||
bool isDone() const;
|
||||
|
||||
static OpenAIMessage fromJson(const QJsonObject &obj);
|
||||
private:
|
||||
QString m_buffer;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
162
providers/ClaudeMessage.cpp
Normal file
162
providers/ClaudeMessage.cpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ClaudeMessage.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
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<LLMCore::TextContent>();
|
||||
|
||||
} else if (blockType == "tool_use") {
|
||||
QString toolId = data["id"].toString();
|
||||
QString toolName = data["name"].toString();
|
||||
QJsonObject toolInput = data["input"].toObject();
|
||||
|
||||
addCurrentContent<LLMCore::ToolUseContent>(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<LLMCore::TextContent *>(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<LLMCore::ToolUseContent *>(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<QString, QString> &toolResults) const
|
||||
{
|
||||
QJsonArray results;
|
||||
|
||||
for (auto toolContent : getCurrentToolUseContent()) {
|
||||
if (toolResults.contains(toolContent->id())) {
|
||||
auto toolResult = std::make_unique<LLMCore::ToolResultContent>(
|
||||
toolContent->id(), toolResults[toolContent->id()]);
|
||||
results.append(toolResult->toJson(LLMCore::ProviderFormat::Claude));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
QList<LLMCore::ToolUseContent *> ClaudeMessage::getCurrentToolUseContent() const
|
||||
{
|
||||
QList<LLMCore::ToolUseContent *> toolBlocks;
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (auto toolContent = qobject_cast<LLMCore::ToolUseContent *>(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
|
||||
63
providers/ClaudeMessage.hpp
Normal file
63
providers/ClaudeMessage.hpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <llmcore/ContentBlocks.hpp>
|
||||
|
||||
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<QString, QString> &toolResults) const;
|
||||
|
||||
LLMCore::MessageState state() const { return m_state; }
|
||||
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||
|
||||
void startNewContinuation();
|
||||
|
||||
private:
|
||||
QString m_stopReason;
|
||||
LLMCore::MessageState m_state = LLMCore::MessageState::Building;
|
||||
QList<LLMCore::ContentBlock *> m_currentBlocks;
|
||||
QHash<int, QString> m_pendingToolInputs;
|
||||
|
||||
void updateStateFromStopReason();
|
||||
|
||||
template<typename T, typename... Args>
|
||||
T *addCurrentContent(Args &&...args)
|
||||
{
|
||||
T *content = new T(std::forward<Args>(args)...);
|
||||
content->setParent(this);
|
||||
m_currentBlocks.append(content);
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -30,10 +30,22 @@
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
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";
|
||||
@ -63,7 +75,8 @@ void ClaudeProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -86,53 +99,15 @@ void ClaudeProvider::prepareRequest(
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
}
|
||||
|
||||
bool ClaudeProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
bool isComplete = false;
|
||||
QString tempResponse;
|
||||
|
||||
while (reply->canReadLine()) {
|
||||
QByteArray line = reply->readLine().trimmed();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.startsWith("data:")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
line = line.mid(6);
|
||||
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
|
||||
if (jsonResponse.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject responseObj = jsonResponse.object();
|
||||
QString eventType = responseObj["type"].toString();
|
||||
|
||||
if (eventType == "message_delta") {
|
||||
if (responseObj.contains("delta")) {
|
||||
QJsonObject delta = responseObj["delta"].toObject();
|
||||
if (delta.contains("stop_reason")) {
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
} else if (eventType == "content_block_delta") {
|
||||
QJsonObject delta = responseObj["delta"].toObject();
|
||||
if (delta["type"].toString() == "text_delta") {
|
||||
tempResponse += delta["text"].toString();
|
||||
}
|
||||
if (isToolsEnabled) {
|
||||
auto toolsDefinitions = m_toolsManager->getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat::Claude);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
request["tools"] = toolsDefinitions;
|
||||
LOG_MESSAGE(QString("Added %1 tools to Claude request").arg(toolsDefinitions.size()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!tempResponse.isEmpty()) {
|
||||
accumulatedResponse += tempResponse;
|
||||
}
|
||||
|
||||
return isComplete;
|
||||
}
|
||||
|
||||
QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
|
||||
@ -193,7 +168,8 @@ QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCo
|
||||
{"top_p", {}},
|
||||
{"top_k", {}},
|
||||
{"stop", QJsonArray{}},
|
||||
{"stream", {}}};
|
||||
{"stream", {}},
|
||||
{"tools", {}}};
|
||||
|
||||
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||
}
|
||||
@ -206,10 +182,10 @@ QString ClaudeProvider::apiKey() const
|
||||
void ClaudeProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||
{
|
||||
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
networkRequest.setRawHeader("anthropic-version", "2023-06-01");
|
||||
|
||||
if (!apiKey().isEmpty()) {
|
||||
networkRequest.setRawHeader("x-api-key", apiKey().toUtf8());
|
||||
networkRequest.setRawHeader("anthropic-version", "2023-06-01");
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,4 +194,232 @@ LLMCore::ProviderID ClaudeProvider::providerID() const
|
||||
return LLMCore::ProviderID::Claude;
|
||||
}
|
||||
|
||||
void ClaudeProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(QString("ClaudeProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
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);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
QJsonObject responseObj = parseEventLine(line);
|
||||
if (responseObj.isEmpty())
|
||||
continue;
|
||||
|
||||
processStreamEvent(requestId, responseObj);
|
||||
}
|
||||
}
|
||||
|
||||
void ClaudeProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
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<QString, QString> &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));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
ClaudeMessage *message = m_messages[requestId];
|
||||
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, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
emit continuationStarted(requestId);
|
||||
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) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
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
|
||||
|
||||
@ -19,13 +19,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
#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;
|
||||
@ -35,13 +41,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamEvent(const QString &requestId, const QJsonObject &event);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<QodeAssist::LLMCore::RequestID, ClaudeMessage *> m_messages;
|
||||
QHash<QodeAssist::LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<QodeAssist::LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
@ -30,10 +30,22 @@
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
GoogleAIProvider::GoogleAIProvider(QObject *parent)
|
||||
: LLMCore::Provider(parent)
|
||||
, m_toolsManager(new Tools::ToolsManager(this))
|
||||
{
|
||||
connect(
|
||||
m_toolsManager,
|
||||
&Tools::ToolsManager::toolExecutionComplete,
|
||||
this,
|
||||
&GoogleAIProvider::onToolExecutionComplete);
|
||||
}
|
||||
|
||||
QString GoogleAIProvider::name() const
|
||||
{
|
||||
return "Google AI";
|
||||
@ -63,7 +75,8 @@ void GoogleAIProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -89,33 +102,14 @@ void GoogleAIProvider::prepareRequest(
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
}
|
||||
|
||||
bool GoogleAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
if (reply->isFinished()) {
|
||||
if (reply->bytesAvailable() > 0) {
|
||||
QByteArray data = reply->readAll();
|
||||
|
||||
if (data.startsWith("data: ")) {
|
||||
return handleStreamResponse(data, accumulatedResponse);
|
||||
} else {
|
||||
return handleRegularResponse(data, accumulatedResponse);
|
||||
}
|
||||
if (isToolsEnabled) {
|
||||
auto toolsDefinitions = m_toolsManager->getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat::Google);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
request["tools"] = toolsDefinitions;
|
||||
LOG_MESSAGE(QString("Added %1 tools to Google AI request").arg(toolsDefinitions.size()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.startsWith("data: ")) {
|
||||
return handleStreamResponse(data, accumulatedResponse);
|
||||
} else {
|
||||
return handleRegularResponse(data, accumulatedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +165,8 @@ QList<QString> GoogleAIProvider::validateRequest(
|
||||
{"system_instruction", QJsonArray{}},
|
||||
{"generationConfig",
|
||||
QJsonObject{{"temperature", {}}, {"maxOutputTokens", {}}, {"topP", {}}, {"topK", {}}}},
|
||||
{"safetySettings", QJsonArray{}}};
|
||||
{"safetySettings", QJsonArray{}},
|
||||
{"tools", QJsonArray{}}};
|
||||
|
||||
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||
}
|
||||
@ -197,106 +192,260 @@ LLMCore::ProviderID GoogleAIProvider::providerID() const
|
||||
return LLMCore::ProviderID::GoogleAI;
|
||||
}
|
||||
|
||||
bool GoogleAIProvider::handleStreamResponse(const QByteArray &data, QString &accumulatedResponse)
|
||||
void GoogleAIProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
QByteArrayList lines = data.split('\n');
|
||||
bool isDone = false;
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
QByteArray trimmedLine = line.trimmed();
|
||||
if (trimmedLine.isEmpty()) {
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("GoogleAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
}
|
||||
|
||||
bool GoogleAIProvider::supportsTools() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void GoogleAIProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("GoogleAIProvider: Cancelling request %1").arg(requestId));
|
||||
LLMCore::Provider::cancelRequest(requestId);
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void GoogleAIProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||
if (!doc.isNull() && doc.isObject()) {
|
||||
QJsonObject obj = doc.object();
|
||||
if (obj.contains("error")) {
|
||||
QJsonObject error = obj["error"].toObject();
|
||||
QString errorMessage = error["message"].toString();
|
||||
int errorCode = error["code"].toInt();
|
||||
QString fullError
|
||||
= QString("Google AI API Error %1: %2").arg(errorCode).arg(errorMessage);
|
||||
|
||||
LOG_MESSAGE(fullError);
|
||||
emit requestFailed(requestId, fullError);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmedLine == "data: [DONE]") {
|
||||
isDone = true;
|
||||
QJsonObject chunk = parseEventLine(line);
|
||||
if (chunk.isEmpty())
|
||||
continue;
|
||||
|
||||
processStreamChunk(requestId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void GoogleAIProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
GoogleMessage *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 (trimmedLine.startsWith("data: ")) {
|
||||
QByteArray jsonData = trimmedLine.mid(6); // Remove "data: " prefix
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
continue;
|
||||
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 GoogleAIProvider::onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &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 Google AI request %1").arg(requestId));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
GoogleMessage *message = m_messages[requestId];
|
||||
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, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject responseObj = doc.object();
|
||||
GoogleMessage *message = m_messages[requestId];
|
||||
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||
QJsonArray contents = continuationRequest["contents"].toArray();
|
||||
|
||||
if (responseObj.contains("error")) {
|
||||
QJsonObject error = responseObj["error"].toObject();
|
||||
LOG_MESSAGE("Error in Google AI stream response: " + error["message"].toString());
|
||||
continue;
|
||||
}
|
||||
contents.append(message->toProviderFormat());
|
||||
|
||||
if (responseObj.contains("candidates")) {
|
||||
QJsonArray candidates = responseObj["candidates"].toArray();
|
||||
if (!candidates.isEmpty()) {
|
||||
QJsonObject candidate = candidates.first().toObject();
|
||||
QJsonObject userMessage;
|
||||
userMessage["role"] = "user";
|
||||
userMessage["parts"] = message->createToolResultParts(toolResults);
|
||||
contents.append(userMessage);
|
||||
|
||||
if (candidate.contains("finishReason")
|
||||
&& !candidate["finishReason"].toString().isEmpty()) {
|
||||
isDone = true;
|
||||
}
|
||||
continuationRequest["contents"] = contents;
|
||||
|
||||
if (candidate.contains("content")) {
|
||||
QJsonObject content = candidate["content"].toObject();
|
||||
if (content.contains("parts")) {
|
||||
QJsonArray parts = content["parts"].toArray();
|
||||
for (const auto &part : parts) {
|
||||
QJsonObject partObj = part.toObject();
|
||||
if (partObj.contains("text")) {
|
||||
accumulatedResponse += partObj["text"].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results")
|
||||
.arg(requestId)
|
||||
.arg(toolResults.size()));
|
||||
|
||||
sendRequest(requestId, m_requestUrls[requestId], continuationRequest);
|
||||
}
|
||||
|
||||
void GoogleAIProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk)
|
||||
{
|
||||
if (!chunk.contains("candidates")) {
|
||||
return;
|
||||
}
|
||||
|
||||
GoogleMessage *message = m_messages.value(requestId);
|
||||
if (!message) {
|
||||
message = new GoogleMessage(this);
|
||||
m_messages[requestId] = message;
|
||||
LOG_MESSAGE(QString("Created NEW GoogleMessage for request %1").arg(requestId));
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId));
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
QJsonArray candidates = chunk["candidates"].toArray();
|
||||
for (const QJsonValue &candidate : candidates) {
|
||||
QJsonObject candidateObj = candidate.toObject();
|
||||
|
||||
if (candidateObj.contains("content")) {
|
||||
QJsonObject content = candidateObj["content"].toObject();
|
||||
if (content.contains("parts")) {
|
||||
QJsonArray parts = content["parts"].toArray();
|
||||
for (const QJsonValue &part : parts) {
|
||||
QJsonObject partObj = part.toObject();
|
||||
|
||||
if (partObj.contains("text")) {
|
||||
QString text = partObj["text"].toString();
|
||||
message->handleContentDelta(text);
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += text;
|
||||
emit partialResponseReceived(requestId, text);
|
||||
} else if (partObj.contains("functionCall")) {
|
||||
QJsonObject functionCall = partObj["functionCall"].toObject();
|
||||
QString name = functionCall["name"].toString();
|
||||
QJsonObject args = functionCall["args"].toObject();
|
||||
|
||||
message->handleFunctionCallStart(name);
|
||||
message->handleFunctionCallArgsDelta(
|
||||
QString::fromUtf8(QJsonDocument(args).toJson(QJsonDocument::Compact)));
|
||||
message->handleFunctionCallComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
bool GoogleAIProvider::handleRegularResponse(const QByteArray &data, QString &accumulatedResponse)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
LOG_MESSAGE("Invalid JSON response from Google AI API");
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject response = doc.object();
|
||||
|
||||
if (response.contains("error")) {
|
||||
QJsonObject error = response["error"].toObject();
|
||||
LOG_MESSAGE("Error in Google AI response: " + error["message"].toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response.contains("candidates") || response["candidates"].toArray().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject candidate = response["candidates"].toArray().first().toObject();
|
||||
if (!candidate.contains("content")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject content = candidate["content"].toObject();
|
||||
if (!content.contains("parts")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray parts = content["parts"].toArray();
|
||||
for (const auto &part : parts) {
|
||||
QJsonObject partObj = part.toObject();
|
||||
if (partObj.contains("text")) {
|
||||
accumulatedResponse += partObj["text"].toString();
|
||||
if (candidateObj.contains("finishReason")) {
|
||||
QString finishReason = candidateObj["finishReason"].toString();
|
||||
message->handleFinishReason(finishReason);
|
||||
handleMessageComplete(requestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
void GoogleAIProvider::handleMessageComplete(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId))
|
||||
return;
|
||||
|
||||
GoogleMessage *message = m_messages[requestId];
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
LOG_MESSAGE(QString("Google AI 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) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Google AI message marked as complete for %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
void GoogleAIProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Cleaning up Google AI request %1").arg(requestId));
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
GoogleMessage *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
|
||||
|
||||
@ -19,13 +19,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GoogleMessage.hpp"
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include "tools/ToolsManager.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class GoogleAIProvider : public LLMCore::Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GoogleAIProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
QString completionEndpoint() const override;
|
||||
@ -35,17 +40,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
bool handleStreamResponse(const QByteArray &data, QString &accumulatedResponse);
|
||||
bool handleRegularResponse(const QByteArray &data, QString &accumulatedResponse);
|
||||
void processStreamChunk(const QString &requestId, const QJsonObject &chunk);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, GoogleMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
173
providers/GoogleMessage.cpp
Normal file
173
providers/GoogleMessage.cpp
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "GoogleMessage.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
GoogleMessage::GoogleMessage(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void GoogleMessage::handleContentDelta(const QString &text)
|
||||
{
|
||||
if (m_currentBlocks.isEmpty() || !qobject_cast<LLMCore::TextContent *>(m_currentBlocks.last())) {
|
||||
auto textContent = new LLMCore::TextContent();
|
||||
textContent->setParent(this);
|
||||
m_currentBlocks.append(textContent);
|
||||
}
|
||||
|
||||
if (auto textContent = qobject_cast<LLMCore::TextContent *>(m_currentBlocks.last())) {
|
||||
textContent->appendText(text);
|
||||
}
|
||||
}
|
||||
|
||||
void GoogleMessage::handleFunctionCallStart(const QString &name)
|
||||
{
|
||||
m_currentFunctionName = name;
|
||||
m_pendingFunctionArgs.clear();
|
||||
|
||||
LOG_MESSAGE(QString("Google: Starting function call: %1").arg(name));
|
||||
}
|
||||
|
||||
void GoogleMessage::handleFunctionCallArgsDelta(const QString &argsJson)
|
||||
{
|
||||
m_pendingFunctionArgs += argsJson;
|
||||
}
|
||||
|
||||
void GoogleMessage::handleFunctionCallComplete()
|
||||
{
|
||||
if (m_currentFunctionName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject args;
|
||||
if (!m_pendingFunctionArgs.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(m_pendingFunctionArgs.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
args = doc.object();
|
||||
}
|
||||
}
|
||||
|
||||
QString id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
auto toolContent = new LLMCore::ToolUseContent(id, m_currentFunctionName, args);
|
||||
toolContent->setParent(this);
|
||||
m_currentBlocks.append(toolContent);
|
||||
|
||||
LOG_MESSAGE(QString("Google: Completed function call: name=%1, args=%2")
|
||||
.arg(m_currentFunctionName)
|
||||
.arg(QString::fromUtf8(QJsonDocument(args).toJson(QJsonDocument::Compact))));
|
||||
|
||||
m_currentFunctionName.clear();
|
||||
m_pendingFunctionArgs.clear();
|
||||
}
|
||||
|
||||
void GoogleMessage::handleFinishReason(const QString &reason)
|
||||
{
|
||||
m_finishReason = reason;
|
||||
updateStateFromFinishReason();
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("Google: Finish reason: %1, state: %2").arg(reason).arg(static_cast<int>(m_state)));
|
||||
}
|
||||
|
||||
QJsonObject GoogleMessage::toProviderFormat() const
|
||||
{
|
||||
QJsonObject content;
|
||||
content["role"] = "model";
|
||||
|
||||
QJsonArray parts;
|
||||
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (!block)
|
||||
continue;
|
||||
|
||||
if (auto text = qobject_cast<LLMCore::TextContent *>(block)) {
|
||||
parts.append(QJsonObject{{"text", text->text()}});
|
||||
} else if (auto tool = qobject_cast<LLMCore::ToolUseContent *>(block)) {
|
||||
QJsonObject functionCall;
|
||||
functionCall["name"] = tool->name();
|
||||
functionCall["args"] = tool->input();
|
||||
parts.append(QJsonObject{{"functionCall", functionCall}});
|
||||
}
|
||||
}
|
||||
|
||||
content["parts"] = parts;
|
||||
return content;
|
||||
}
|
||||
|
||||
QJsonArray GoogleMessage::createToolResultParts(const QHash<QString, QString> &toolResults) const
|
||||
{
|
||||
QJsonArray parts;
|
||||
|
||||
for (auto toolContent : getCurrentToolUseContent()) {
|
||||
if (toolResults.contains(toolContent->id())) {
|
||||
QJsonObject functionResponse;
|
||||
functionResponse["name"] = toolContent->name();
|
||||
|
||||
QJsonObject response;
|
||||
response["result"] = toolResults[toolContent->id()];
|
||||
functionResponse["response"] = response;
|
||||
|
||||
parts.append(QJsonObject{{"functionResponse", functionResponse}});
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
QList<LLMCore::ToolUseContent *> GoogleMessage::getCurrentToolUseContent() const
|
||||
{
|
||||
QList<LLMCore::ToolUseContent *> toolBlocks;
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (auto toolContent = qobject_cast<LLMCore::ToolUseContent *>(block)) {
|
||||
toolBlocks.append(toolContent);
|
||||
}
|
||||
}
|
||||
return toolBlocks;
|
||||
}
|
||||
|
||||
void GoogleMessage::startNewContinuation()
|
||||
{
|
||||
LOG_MESSAGE(QString("GoogleMessage: Starting new continuation"));
|
||||
|
||||
m_currentBlocks.clear();
|
||||
m_pendingFunctionArgs.clear();
|
||||
m_currentFunctionName.clear();
|
||||
m_finishReason.clear();
|
||||
m_state = LLMCore::MessageState::Building;
|
||||
}
|
||||
|
||||
void GoogleMessage::updateStateFromFinishReason()
|
||||
{
|
||||
if (m_finishReason == "STOP" || m_finishReason == "MAX_TOKENS") {
|
||||
m_state = getCurrentToolUseContent().isEmpty()
|
||||
? LLMCore::MessageState::Complete
|
||||
: LLMCore::MessageState::RequiresToolExecution;
|
||||
} else {
|
||||
m_state = LLMCore::MessageState::Complete;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
62
providers/GoogleMessage.hpp
Normal file
62
providers/GoogleMessage.hpp
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
#include <llmcore/ContentBlocks.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class GoogleMessage : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GoogleMessage(QObject *parent = nullptr);
|
||||
|
||||
void handleContentDelta(const QString &text);
|
||||
void handleFunctionCallStart(const QString &name);
|
||||
void handleFunctionCallArgsDelta(const QString &argsJson);
|
||||
void handleFunctionCallComplete();
|
||||
void handleFinishReason(const QString &reason);
|
||||
|
||||
QJsonObject toProviderFormat() const;
|
||||
QJsonArray createToolResultParts(const QHash<QString, QString> &toolResults) const;
|
||||
|
||||
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||
QList<LLMCore::ContentBlock *> currentBlocks() const { return m_currentBlocks; }
|
||||
|
||||
LLMCore::MessageState state() const { return m_state; }
|
||||
void startNewContinuation();
|
||||
|
||||
private:
|
||||
void updateStateFromFinishReason();
|
||||
|
||||
QList<LLMCore::ContentBlock *> m_currentBlocks;
|
||||
QString m_pendingFunctionArgs;
|
||||
QString m_currentFunctionName;
|
||||
QString m_finishReason;
|
||||
LLMCore::MessageState m_state = LLMCore::MessageState::Building;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
@ -19,20 +19,32 @@
|
||||
|
||||
#include "LMStudioProvider.hpp"
|
||||
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
LMStudioProvider::LMStudioProvider(QObject *parent)
|
||||
: LLMCore::Provider(parent)
|
||||
, m_toolsManager(new Tools::ToolsManager(this))
|
||||
{
|
||||
connect(
|
||||
m_toolsManager,
|
||||
&Tools::ToolsManager::toolExecutionComplete,
|
||||
this,
|
||||
&LMStudioProvider::onToolExecutionComplete);
|
||||
}
|
||||
|
||||
QString LMStudioProvider::name() const
|
||||
{
|
||||
return "LM Studio";
|
||||
@ -58,57 +70,6 @@ bool LMStudioProvider::supportsModelListing() const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDone = false;
|
||||
QByteArrayList lines = data.split('\n');
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "data: [DONE]") {
|
||||
isDone = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
QByteArray jsonData = line;
|
||||
if (line.startsWith("data: ")) {
|
||||
jsonData = line.mid(6);
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||
|
||||
if (doc.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||
if (message.hasError()) {
|
||||
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (message.isDone()) {
|
||||
isDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
||||
{
|
||||
QList<QString> models;
|
||||
@ -125,15 +86,15 @@ QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
QJsonObject jsonObject = jsonResponse.object();
|
||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
QJsonObject modelObject = value.toObject();
|
||||
QString modelId = modelObject["id"].toString();
|
||||
models.append(modelId);
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
QJsonObject modelObject = value.toObject();
|
||||
QString modelId = modelObject["id"].toString();
|
||||
models.append(modelId);
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching models: %1").arg(reply->errorString()));
|
||||
LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
@ -153,7 +114,8 @@ QList<QString> LMStudioProvider::validateRequest(
|
||||
{"frequency_penalty", {}},
|
||||
{"presence_penalty", {}},
|
||||
{"stop", QJsonArray{}},
|
||||
{"stream", {}}};
|
||||
{"stream", {}},
|
||||
{"tools", {}}};
|
||||
|
||||
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||
}
|
||||
@ -173,11 +135,95 @@ LLMCore::ProviderID LMStudioProvider::providerID() const
|
||||
return LLMCore::ProviderID::LMStudio;
|
||||
}
|
||||
|
||||
void QodeAssist::Providers::LMStudioProvider::prepareRequest(
|
||||
void LMStudioProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("LMStudioProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
}
|
||||
|
||||
bool LMStudioProvider::supportsTools() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void LMStudioProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("LMStudioProvider: Cancelling request %1").arg(requestId));
|
||||
LLMCore::Provider::cancelRequest(requestId);
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void LMStudioProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().isEmpty() || line == "data: [DONE]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject chunk = parseEventLine(line);
|
||||
if (chunk.isEmpty())
|
||||
continue;
|
||||
|
||||
processStreamChunk(requestId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void LMStudioProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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 LMStudioProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -204,6 +250,174 @@ void QodeAssist::Providers::LMStudioProvider::prepareRequest(
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
|
||||
if (isToolsEnabled) {
|
||||
auto toolsDefinitions = m_toolsManager->getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat::OpenAI);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
request["tools"] = toolsDefinitions;
|
||||
LOG_MESSAGE(QString("Added %1 tools to LMStudio request").arg(toolsDefinitions.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LMStudioProvider::onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &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 LMStudio request %1").arg(requestId));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
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, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||
QJsonArray messages = continuationRequest["messages"].toArray();
|
||||
|
||||
messages.append(message->toProviderFormat());
|
||||
|
||||
QJsonArray toolResultMessages = message->createToolResultMessages(toolResults);
|
||||
for (const auto &toolMsg : toolResultMessages) {
|
||||
messages.append(toolMsg);
|
||||
}
|
||||
|
||||
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 LMStudioProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk)
|
||||
{
|
||||
QJsonArray choices = chunk["choices"].toArray();
|
||||
if (choices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject choice = choices[0].toObject();
|
||||
QJsonObject delta = choice["delta"].toObject();
|
||||
QString finishReason = choice["finish_reason"].toString();
|
||||
|
||||
OpenAIMessage *message = m_messages.value(requestId);
|
||||
if (!message) {
|
||||
message = new OpenAIMessage(this);
|
||||
m_messages[requestId] = message;
|
||||
LOG_MESSAGE(QString("Created NEW OpenAIMessage for request %1").arg(requestId));
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId));
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
if (delta.contains("content") && !delta["content"].isNull()) {
|
||||
QString content = delta["content"].toString();
|
||||
message->handleContentDelta(content);
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += content;
|
||||
emit partialResponseReceived(requestId, content);
|
||||
}
|
||||
|
||||
if (delta.contains("tool_calls")) {
|
||||
QJsonArray toolCalls = delta["tool_calls"].toArray();
|
||||
for (const auto &toolCallValue : toolCalls) {
|
||||
QJsonObject toolCall = toolCallValue.toObject();
|
||||
int index = toolCall["index"].toInt();
|
||||
|
||||
if (toolCall.contains("id")) {
|
||||
QString id = toolCall["id"].toString();
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
QString name = function["name"].toString();
|
||||
message->handleToolCallStart(index, id, name);
|
||||
}
|
||||
|
||||
if (toolCall.contains("function")) {
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
if (function.contains("arguments")) {
|
||||
QString args = function["arguments"].toString();
|
||||
message->handleToolCallDelta(index, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!finishReason.isEmpty() && finishReason != "null") {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
message->handleToolCallComplete(i);
|
||||
}
|
||||
|
||||
message->handleFinishReason(finishReason);
|
||||
handleMessageComplete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
void LMStudioProvider::handleMessageComplete(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId))
|
||||
return;
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
LOG_MESSAGE(QString("LMStudio 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) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_MESSAGE(QString("LMStudio message marked as complete for %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
void LMStudioProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Cleaning up LMStudio request %1").arg(requestId));
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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
|
||||
|
||||
@ -19,13 +19,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include "OpenAIMessage.hpp"
|
||||
#include "tools/ToolsManager.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class LMStudioProvider : public LLMCore::Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LMStudioProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
QString completionEndpoint() const override;
|
||||
@ -35,13 +40,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamChunk(const QString &requestId, const QJsonObject &chunk);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, OpenAIMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
@ -19,20 +19,31 @@
|
||||
|
||||
#include "LlamaCppProvider.hpp"
|
||||
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
LlamaCppProvider::LlamaCppProvider(QObject *parent)
|
||||
: LLMCore::Provider(parent)
|
||||
, m_toolsManager(new Tools::ToolsManager(this))
|
||||
{
|
||||
connect(
|
||||
m_toolsManager,
|
||||
&Tools::ToolsManager::toolExecutionComplete,
|
||||
this,
|
||||
&LlamaCppProvider::onToolExecutionComplete);
|
||||
}
|
||||
|
||||
QString LlamaCppProvider::name() const
|
||||
{
|
||||
return "llama.cpp";
|
||||
@ -62,7 +73,8 @@ void LlamaCppProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -89,69 +101,15 @@ void LlamaCppProvider::prepareRequest(
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
}
|
||||
|
||||
bool LlamaCppProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDone = data.contains("\"stop\":true") || data.contains("data: [DONE]");
|
||||
|
||||
QByteArrayList lines = data.split('\n');
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "data: [DONE]") {
|
||||
isDone = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
QByteArray jsonData = line;
|
||||
if (line.startsWith("data: ")) {
|
||||
jsonData = line.mid(6);
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||
if (doc.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
|
||||
if (obj.contains("content")) {
|
||||
QString content = obj["content"].toString();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
} else if (obj.contains("choices")) {
|
||||
auto message = LLMCore::OpenAIMessage::fromJson(obj);
|
||||
if (message.hasError()) {
|
||||
LOG_MESSAGE("Error in llama.cpp response: " + message.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (message.isDone()) {
|
||||
isDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj["stop"].toBool()) {
|
||||
isDone = true;
|
||||
if (isToolsEnabled) {
|
||||
auto toolsDefinitions = m_toolsManager->getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat::OpenAI);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
request["tools"] = toolsDefinitions;
|
||||
LOG_MESSAGE(QString("Added %1 tools to llama.cpp request").arg(toolsDefinitions.size()));
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> LlamaCppProvider::getInstalledModels(const QString &url)
|
||||
@ -190,7 +148,8 @@ QList<QString> LlamaCppProvider::validateRequest(
|
||||
{"frequency_penalty", {}},
|
||||
{"presence_penalty", {}},
|
||||
{"stop", QJsonArray{}},
|
||||
{"stream", {}}};
|
||||
{"stream", {}},
|
||||
{"tools", {}}};
|
||||
|
||||
return LLMCore::ValidationUtils::validateRequestFields(request, chatReq);
|
||||
}
|
||||
@ -211,4 +170,258 @@ LLMCore::ProviderID LlamaCppProvider::providerID() const
|
||||
return LLMCore::ProviderID::LlamaCpp;
|
||||
}
|
||||
|
||||
void LlamaCppProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("LlamaCppProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
}
|
||||
|
||||
bool LlamaCppProvider::supportsTools() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void LlamaCppProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("LlamaCppProvider: Cancelling request %1").arg(requestId));
|
||||
LLMCore::Provider::cancelRequest(requestId);
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void LlamaCppProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().isEmpty() || line == "data: [DONE]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject chunk = parseEventLine(line);
|
||||
if (chunk.isEmpty())
|
||||
continue;
|
||||
|
||||
if (chunk.contains("content")) {
|
||||
QString content = chunk["content"].toString();
|
||||
if (!content.isEmpty()) {
|
||||
buffers.responseContent += content;
|
||||
emit partialResponseReceived(requestId, content);
|
||||
}
|
||||
if (chunk["stop"].toBool()) {
|
||||
emit fullResponseReceived(requestId, buffers.responseContent);
|
||||
m_dataBuffers.remove(requestId);
|
||||
}
|
||||
} else if (chunk.contains("choices")) {
|
||||
processStreamChunk(requestId, chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LlamaCppProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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 LlamaCppProvider::onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &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 llama.cpp request %1").arg(requestId));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
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, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||
QJsonArray messages = continuationRequest["messages"].toArray();
|
||||
|
||||
messages.append(message->toProviderFormat());
|
||||
|
||||
QJsonArray toolResultMessages = message->createToolResultMessages(toolResults);
|
||||
for (const auto &toolMsg : toolResultMessages) {
|
||||
messages.append(toolMsg);
|
||||
}
|
||||
|
||||
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 LlamaCppProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk)
|
||||
{
|
||||
QJsonArray choices = chunk["choices"].toArray();
|
||||
if (choices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject choice = choices[0].toObject();
|
||||
QJsonObject delta = choice["delta"].toObject();
|
||||
QString finishReason = choice["finish_reason"].toString();
|
||||
|
||||
OpenAIMessage *message = m_messages.value(requestId);
|
||||
if (!message) {
|
||||
message = new OpenAIMessage(this);
|
||||
m_messages[requestId] = message;
|
||||
LOG_MESSAGE(QString("Created NEW OpenAIMessage for llama.cpp request %1").arg(requestId));
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId));
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
if (delta.contains("content") && !delta["content"].isNull()) {
|
||||
QString content = delta["content"].toString();
|
||||
message->handleContentDelta(content);
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += content;
|
||||
emit partialResponseReceived(requestId, content);
|
||||
}
|
||||
|
||||
if (delta.contains("tool_calls")) {
|
||||
QJsonArray toolCalls = delta["tool_calls"].toArray();
|
||||
for (const auto &toolCallValue : toolCalls) {
|
||||
QJsonObject toolCall = toolCallValue.toObject();
|
||||
int index = toolCall["index"].toInt();
|
||||
|
||||
if (toolCall.contains("id")) {
|
||||
QString id = toolCall["id"].toString();
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
QString name = function["name"].toString();
|
||||
message->handleToolCallStart(index, id, name);
|
||||
}
|
||||
|
||||
if (toolCall.contains("function")) {
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
if (function.contains("arguments")) {
|
||||
QString args = function["arguments"].toString();
|
||||
message->handleToolCallDelta(index, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!finishReason.isEmpty() && finishReason != "null") {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
message->handleToolCallComplete(i);
|
||||
}
|
||||
|
||||
message->handleFinishReason(finishReason);
|
||||
handleMessageComplete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
void LlamaCppProvider::handleMessageComplete(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId))
|
||||
return;
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
LOG_MESSAGE(QString("llama.cpp 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) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_MESSAGE(QString("llama.cpp message marked as complete for %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
void LlamaCppProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Cleaning up llama.cpp request %1").arg(requestId));
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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
|
||||
|
||||
@ -19,13 +19,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include "OpenAIMessage.hpp"
|
||||
#include "tools/ToolsManager.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class LlamaCppProvider : public LLMCore::Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LlamaCppProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
QString completionEndpoint() const override;
|
||||
@ -35,13 +40,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamChunk(const QString &requestId, const QJsonObject &chunk);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, OpenAIMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
@ -1,21 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2024-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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MistralAIProvider.hpp"
|
||||
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QtCore/qeventloop.h>
|
||||
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
MistralAIProvider::MistralAIProvider(QObject *parent)
|
||||
: LLMCore::Provider(parent)
|
||||
, m_toolsManager(new Tools::ToolsManager(this))
|
||||
{
|
||||
connect(
|
||||
m_toolsManager,
|
||||
&Tools::ToolsManager::toolExecutionComplete,
|
||||
this,
|
||||
&MistralAIProvider::onToolExecutionComplete);
|
||||
}
|
||||
|
||||
QString MistralAIProvider::name() const
|
||||
{
|
||||
return "Mistral AI";
|
||||
@ -41,57 +70,6 @@ bool MistralAIProvider::supportsModelListing() const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MistralAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDone = false;
|
||||
QByteArrayList lines = data.split('\n');
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "data: [DONE]") {
|
||||
isDone = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
QByteArray jsonData = line;
|
||||
if (line.startsWith("data: ")) {
|
||||
jsonData = line.mid(6);
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||
|
||||
if (doc.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||
if (message.hasError()) {
|
||||
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (message.isDone()) {
|
||||
isDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> MistralAIProvider::getInstalledModels(const QString &url)
|
||||
{
|
||||
QList<QString> models;
|
||||
@ -148,10 +126,12 @@ QList<QString> MistralAIProvider::validateRequest(
|
||||
{"temperature", {}},
|
||||
{"max_tokens", {}},
|
||||
{"top_p", {}},
|
||||
{"top_k", {}},
|
||||
{"frequency_penalty", {}},
|
||||
{"presence_penalty", {}},
|
||||
{"stop", QJsonArray{}},
|
||||
{"stream", {}}};
|
||||
{"stream", {}},
|
||||
{"tools", {}}};
|
||||
|
||||
return LLMCore::ValidationUtils::validateRequestFields(
|
||||
request, type == LLMCore::TemplateType::FIM ? fimReq : templateReq);
|
||||
@ -176,11 +156,95 @@ LLMCore::ProviderID MistralAIProvider::providerID() const
|
||||
return LLMCore::ProviderID::MistralAI;
|
||||
}
|
||||
|
||||
void MistralAIProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("MistralAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
}
|
||||
|
||||
bool MistralAIProvider::supportsTools() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void MistralAIProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("MistralAIProvider: Cancelling request %1").arg(requestId));
|
||||
LLMCore::Provider::cancelRequest(requestId);
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void MistralAIProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().isEmpty() || line == "data: [DONE]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject chunk = parseEventLine(line);
|
||||
if (chunk.isEmpty())
|
||||
continue;
|
||||
|
||||
processStreamChunk(requestId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void MistralAIProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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 MistralAIProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -188,33 +252,193 @@ void MistralAIProvider::prepareRequest(
|
||||
|
||||
prompt->prepareRequest(request, context);
|
||||
|
||||
if (type == LLMCore::RequestType::Chat) {
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
auto applyModelParams = [&request](const auto &settings) {
|
||||
request["max_tokens"] = settings.maxTokens();
|
||||
request["temperature"] = settings.temperature();
|
||||
|
||||
if (settings.useTopP())
|
||||
request["top_p"] = settings.topP();
|
||||
|
||||
// request["random_seed"] = "";
|
||||
|
||||
if (settings.useTopK())
|
||||
request["top_k"] = settings.topK();
|
||||
if (settings.useFrequencyPenalty())
|
||||
request["frequency_penalty"] = settings.frequencyPenalty();
|
||||
if (settings.usePresencePenalty())
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else {
|
||||
auto &settings = Settings::codeCompletionSettings();
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
|
||||
request["max_tokens"] = settings.maxTokens();
|
||||
request["temperature"] = settings.temperature();
|
||||
|
||||
if (settings.useTopP())
|
||||
request["top_p"] = settings.topP();
|
||||
|
||||
// request["random_seed"] = "";
|
||||
if (isToolsEnabled) {
|
||||
auto toolsDefinitions = m_toolsManager->getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat::OpenAI);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
request["tools"] = toolsDefinitions;
|
||||
LOG_MESSAGE(QString("Added %1 tools to Mistral request").arg(toolsDefinitions.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MistralAIProvider::onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &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 Mistral request %1").arg(requestId));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
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, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||
QJsonArray messages = continuationRequest["messages"].toArray();
|
||||
|
||||
messages.append(message->toProviderFormat());
|
||||
|
||||
QJsonArray toolResultMessages = message->createToolResultMessages(toolResults);
|
||||
for (const auto &toolMsg : toolResultMessages) {
|
||||
messages.append(toolMsg);
|
||||
}
|
||||
|
||||
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 MistralAIProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk)
|
||||
{
|
||||
QJsonArray choices = chunk["choices"].toArray();
|
||||
if (choices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject choice = choices[0].toObject();
|
||||
QJsonObject delta = choice["delta"].toObject();
|
||||
QString finishReason = choice["finish_reason"].toString();
|
||||
|
||||
OpenAIMessage *message = m_messages.value(requestId);
|
||||
if (!message) {
|
||||
message = new OpenAIMessage(this);
|
||||
m_messages[requestId] = message;
|
||||
LOG_MESSAGE(QString("Created NEW OpenAIMessage for Mistral request %1").arg(requestId));
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId));
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
if (delta.contains("content") && !delta["content"].isNull()) {
|
||||
QString content = delta["content"].toString();
|
||||
message->handleContentDelta(content);
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += content;
|
||||
emit partialResponseReceived(requestId, content);
|
||||
}
|
||||
|
||||
if (delta.contains("tool_calls")) {
|
||||
QJsonArray toolCalls = delta["tool_calls"].toArray();
|
||||
for (const auto &toolCallValue : toolCalls) {
|
||||
QJsonObject toolCall = toolCallValue.toObject();
|
||||
int index = toolCall["index"].toInt();
|
||||
|
||||
if (toolCall.contains("id")) {
|
||||
QString id = toolCall["id"].toString();
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
QString name = function["name"].toString();
|
||||
message->handleToolCallStart(index, id, name);
|
||||
}
|
||||
|
||||
if (toolCall.contains("function")) {
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
if (function.contains("arguments")) {
|
||||
QString args = function["arguments"].toString();
|
||||
message->handleToolCallDelta(index, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!finishReason.isEmpty() && finishReason != "null") {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
message->handleToolCallComplete(i);
|
||||
}
|
||||
|
||||
message->handleFinishReason(finishReason);
|
||||
handleMessageComplete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
void MistralAIProvider::handleMessageComplete(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId))
|
||||
return;
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
LOG_MESSAGE(QString("Mistral 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) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Mistral message marked as complete for %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
void MistralAIProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Cleaning up Mistral request %1").arg(requestId));
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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
|
||||
|
||||
@ -19,13 +19,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include "OpenAIMessage.hpp"
|
||||
#include "tools/ToolsManager.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class MistralAIProvider : public LLMCore::Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MistralAIProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
QString completionEndpoint() const override;
|
||||
@ -35,13 +40,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamChunk(const QString &requestId, const QJsonObject &chunk);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, OpenAIMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
312
providers/OllamaMessage.cpp
Normal file
312
providers/OllamaMessage.cpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OllamaMessage.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
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<LLMCore::ToolUseContent>(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<LLMCore::TextContent *>(*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<LLMCore::TextContent *>(block)) {
|
||||
LOG_MESSAGE(QString("OllamaMessage: Removing TextContent block (tool call detected)"));
|
||||
}
|
||||
}
|
||||
m_currentBlocks.clear();
|
||||
|
||||
addCurrentContent<LLMCore::ToolUseContent>(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<LLMCore::TextContent *>(block)) {
|
||||
textContent += text->text();
|
||||
} else if (auto tool = qobject_cast<LLMCore::ToolUseContent *>(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<QString, QString> &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<LLMCore::ToolUseContent *> OllamaMessage::getCurrentToolUseContent() const
|
||||
{
|
||||
QList<LLMCore::ToolUseContent *> toolBlocks;
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (auto toolContent = qobject_cast<LLMCore::ToolUseContent *>(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<LLMCore::TextContent *>(block)) {
|
||||
return textContent;
|
||||
}
|
||||
}
|
||||
|
||||
return addCurrentContent<LLMCore::TextContent>();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
67
providers/OllamaMessage.hpp
Normal file
67
providers/OllamaMessage.hpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <llmcore/ContentBlocks.hpp>
|
||||
|
||||
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<QString, QString> &toolResults) const;
|
||||
|
||||
LLMCore::MessageState state() const { return m_state; }
|
||||
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||
QList<LLMCore::ContentBlock *> currentBlocks() const { return m_currentBlocks; }
|
||||
|
||||
void startNewContinuation();
|
||||
|
||||
private:
|
||||
bool m_done = false;
|
||||
LLMCore::MessageState m_state = LLMCore::MessageState::Building;
|
||||
QList<LLMCore::ContentBlock *> m_currentBlocks;
|
||||
QString m_accumulatedContent;
|
||||
bool m_contentAddedToTextBlock = false;
|
||||
|
||||
void updateStateFromDone();
|
||||
bool tryParseToolCall();
|
||||
bool isLikelyToolCallJson(const QString &content) const;
|
||||
LLMCore::TextContent *getOrCreateTextContent();
|
||||
|
||||
template<typename T, typename... Args>
|
||||
T *addCurrentContent(Args &&...args)
|
||||
{
|
||||
T *content = new T(std::forward<Args>(args)...);
|
||||
content->setParent(this);
|
||||
m_currentBlocks.append(content);
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
@ -25,15 +25,26 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QtCore/qeventloop.h>
|
||||
|
||||
#include "llmcore/OllamaMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
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";
|
||||
@ -63,7 +74,8 @@ void OllamaProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -95,44 +107,16 @@ void OllamaProvider::prepareRequest(
|
||||
} else {
|
||||
applySettings(Settings::chatAssistantSettings());
|
||||
}
|
||||
}
|
||||
|
||||
bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArrayList lines = data.split('\n');
|
||||
bool isDone = false;
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString endpoint = reply->url().path();
|
||||
auto messageType = endpoint == completionEndpoint() ? LLMCore::OllamaMessage::Type::Generate
|
||||
: LLMCore::OllamaMessage::Type::Chat;
|
||||
|
||||
auto message = LLMCore::OllamaMessage::fromJson(line, messageType);
|
||||
if (message.hasError()) {
|
||||
LOG_MESSAGE("Error in Ollama response: " + message.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (message.done) {
|
||||
isDone = true;
|
||||
if (isToolsEnabled) {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> OllamaProvider::getInstalledModels(const QString &url)
|
||||
@ -190,6 +174,7 @@ QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCo
|
||||
{"model", {}},
|
||||
{"stream", {}},
|
||||
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
|
||||
{"tools", QJsonArray{}},
|
||||
{"options",
|
||||
QJsonObject{
|
||||
{"temperature", {}},
|
||||
@ -223,4 +208,300 @@ LLMCore::ProviderID OllamaProvider::providerID() const
|
||||
return LLMCore::ProviderID::Ollama;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(QString("OllamaProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
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)
|
||||
{
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &error);
|
||||
if (doc.isNull()) {
|
||||
LOG_MESSAGE(QString("Failed to parse JSON: %1").arg(error.errorString()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
|
||||
if (obj.contains("error") && !obj["error"].toString().isEmpty()) {
|
||||
LOG_MESSAGE("Error in Ollama response: " + obj["error"].toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
processStreamData(requestId, obj);
|
||||
}
|
||||
}
|
||||
|
||||
void OllamaProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
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<LLMCore::TextContent *>(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<QString, QString> &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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
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 (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId));
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation 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<LLMCore::TextContent *>(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<LLMCore::TextContent *>(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
|
||||
|
||||
@ -19,13 +19,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
#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;
|
||||
@ -35,13 +41,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamData(const QString &requestId, const QJsonObject &data);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<QodeAssist::LLMCore::RequestID, OllamaMessage *> m_messages;
|
||||
QHash<QodeAssist::LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<QodeAssist::LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
@ -19,8 +19,11 @@
|
||||
|
||||
#include "OpenAICompatProvider.hpp"
|
||||
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
@ -28,12 +31,19 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
OpenAICompatProvider::OpenAICompatProvider(QObject *parent)
|
||||
: LLMCore::Provider(parent)
|
||||
, m_toolsManager(new Tools::ToolsManager(this))
|
||||
{
|
||||
connect(
|
||||
m_toolsManager,
|
||||
&Tools::ToolsManager::toolExecutionComplete,
|
||||
this,
|
||||
&OpenAICompatProvider::onToolExecutionComplete);
|
||||
}
|
||||
|
||||
QString OpenAICompatProvider::name() const
|
||||
{
|
||||
return "OpenAI Compatible";
|
||||
@ -63,7 +73,8 @@ void OpenAICompatProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -90,57 +101,16 @@ void OpenAICompatProvider::prepareRequest(
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDone = false;
|
||||
QByteArrayList lines = data.split('\n');
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "data: [DONE]") {
|
||||
isDone = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
QByteArray jsonData = line;
|
||||
if (line.startsWith("data: ")) {
|
||||
jsonData = line.mid(6);
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||
|
||||
if (doc.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||
if (message.hasError()) {
|
||||
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (message.isDone()) {
|
||||
isDone = true;
|
||||
if (isToolsEnabled) {
|
||||
auto toolsDefinitions = m_toolsManager->getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat::OpenAI);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
request["tools"] = toolsDefinitions;
|
||||
LOG_MESSAGE(
|
||||
QString("Added %1 tools to OpenAICompat request").arg(toolsDefinitions.size()));
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
|
||||
@ -161,7 +131,8 @@ QList<QString> OpenAICompatProvider::validateRequest(
|
||||
{"frequency_penalty", {}},
|
||||
{"presence_penalty", {}},
|
||||
{"stop", QJsonArray{}},
|
||||
{"stream", {}}};
|
||||
{"stream", {}},
|
||||
{"tools", {}}};
|
||||
|
||||
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||
}
|
||||
@ -185,4 +156,246 @@ LLMCore::ProviderID OpenAICompatProvider::providerID() const
|
||||
return LLMCore::ProviderID::OpenAICompatible;
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("OpenAICompatProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
}
|
||||
|
||||
bool OpenAICompatProvider::supportsTools() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("OpenAICompatProvider: Cancelling request %1").arg(requestId));
|
||||
LLMCore::Provider::cancelRequest(requestId);
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().isEmpty() || line == "data: [DONE]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject chunk = parseEventLine(line);
|
||||
if (chunk.isEmpty())
|
||||
continue;
|
||||
|
||||
processStreamChunk(requestId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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 OpenAICompatProvider::onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &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 OpenAICompat request %1").arg(requestId));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
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, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||
QJsonArray messages = continuationRequest["messages"].toArray();
|
||||
|
||||
messages.append(message->toProviderFormat());
|
||||
|
||||
QJsonArray toolResultMessages = message->createToolResultMessages(toolResults);
|
||||
for (const auto &toolMsg : toolResultMessages) {
|
||||
messages.append(toolMsg);
|
||||
}
|
||||
|
||||
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 OpenAICompatProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk)
|
||||
{
|
||||
QJsonArray choices = chunk["choices"].toArray();
|
||||
if (choices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject choice = choices[0].toObject();
|
||||
QJsonObject delta = choice["delta"].toObject();
|
||||
QString finishReason = choice["finish_reason"].toString();
|
||||
|
||||
OpenAIMessage *message = m_messages.value(requestId);
|
||||
if (!message) {
|
||||
message = new OpenAIMessage(this);
|
||||
m_messages[requestId] = message;
|
||||
LOG_MESSAGE(QString("Created NEW OpenAIMessage for request %1").arg(requestId));
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId));
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
if (delta.contains("content") && !delta["content"].isNull()) {
|
||||
QString content = delta["content"].toString();
|
||||
message->handleContentDelta(content);
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += content;
|
||||
emit partialResponseReceived(requestId, content);
|
||||
}
|
||||
|
||||
if (delta.contains("tool_calls")) {
|
||||
QJsonArray toolCalls = delta["tool_calls"].toArray();
|
||||
for (const auto &toolCallValue : toolCalls) {
|
||||
QJsonObject toolCall = toolCallValue.toObject();
|
||||
int index = toolCall["index"].toInt();
|
||||
|
||||
if (toolCall.contains("id")) {
|
||||
QString id = toolCall["id"].toString();
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
QString name = function["name"].toString();
|
||||
message->handleToolCallStart(index, id, name);
|
||||
}
|
||||
|
||||
if (toolCall.contains("function")) {
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
if (function.contains("arguments")) {
|
||||
QString args = function["arguments"].toString();
|
||||
message->handleToolCallDelta(index, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!finishReason.isEmpty() && finishReason != "null") {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
message->handleToolCallComplete(i);
|
||||
}
|
||||
|
||||
message->handleFinishReason(finishReason);
|
||||
handleMessageComplete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::handleMessageComplete(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId))
|
||||
return;
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
LOG_MESSAGE(QString("OpenAICompat 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) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_MESSAGE(QString("OpenAICompat message marked as complete for %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Cleaning up OpenAICompat request %1").arg(requestId));
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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
|
||||
|
||||
@ -19,13 +19,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include "OpenAIMessage.hpp"
|
||||
#include "tools/ToolsManager.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class OpenAICompatProvider : public LLMCore::Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OpenAICompatProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
QString completionEndpoint() const override;
|
||||
@ -35,13 +40,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamChunk(const QString &requestId, const QJsonObject &chunk);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, OpenAIMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
181
providers/OpenAIMessage.cpp
Normal file
181
providers/OpenAIMessage.cpp
Normal file
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OpenAIMessage.hpp"
|
||||
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
OpenAIMessage::OpenAIMessage(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void OpenAIMessage::handleContentDelta(const QString &content)
|
||||
{
|
||||
auto textContent = getOrCreateTextContent();
|
||||
textContent->appendText(content);
|
||||
}
|
||||
|
||||
void OpenAIMessage::handleToolCallStart(int index, const QString &id, const QString &name)
|
||||
{
|
||||
LOG_MESSAGE(QString("OpenAIMessage: handleToolCallStart index=%1, id=%2, name=%3")
|
||||
.arg(index)
|
||||
.arg(id, name));
|
||||
|
||||
while (m_currentBlocks.size() <= index) {
|
||||
m_currentBlocks.append(nullptr);
|
||||
}
|
||||
|
||||
auto toolContent = new LLMCore::ToolUseContent(id, name);
|
||||
toolContent->setParent(this);
|
||||
m_currentBlocks[index] = toolContent;
|
||||
m_pendingToolArguments[index] = "";
|
||||
}
|
||||
|
||||
void OpenAIMessage::handleToolCallDelta(int index, const QString &argumentsDelta)
|
||||
{
|
||||
if (m_pendingToolArguments.contains(index)) {
|
||||
m_pendingToolArguments[index] += argumentsDelta;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIMessage::handleToolCallComplete(int index)
|
||||
{
|
||||
if (m_pendingToolArguments.contains(index)) {
|
||||
QString jsonArgs = m_pendingToolArguments[index];
|
||||
QJsonObject argsObject;
|
||||
|
||||
if (!jsonArgs.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonArgs.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
argsObject = doc.object();
|
||||
}
|
||||
}
|
||||
|
||||
if (index < m_currentBlocks.size()) {
|
||||
if (auto toolContent = qobject_cast<LLMCore::ToolUseContent *>(m_currentBlocks[index])) {
|
||||
toolContent->setInput(argsObject);
|
||||
}
|
||||
}
|
||||
|
||||
m_pendingToolArguments.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIMessage::handleFinishReason(const QString &finishReason)
|
||||
{
|
||||
m_finishReason = finishReason;
|
||||
updateStateFromFinishReason();
|
||||
}
|
||||
|
||||
QJsonObject OpenAIMessage::toProviderFormat() const
|
||||
{
|
||||
QJsonObject message;
|
||||
message["role"] = "assistant";
|
||||
|
||||
QString textContent;
|
||||
QJsonArray toolCalls;
|
||||
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (!block)
|
||||
continue;
|
||||
|
||||
if (auto text = qobject_cast<LLMCore::TextContent *>(block)) {
|
||||
textContent += text->text();
|
||||
} else if (auto tool = qobject_cast<LLMCore::ToolUseContent *>(block)) {
|
||||
toolCalls.append(tool->toJson(LLMCore::ProviderFormat::OpenAI));
|
||||
}
|
||||
}
|
||||
|
||||
if (!textContent.isEmpty()) {
|
||||
message["content"] = textContent;
|
||||
} else {
|
||||
message["content"] = QJsonValue();
|
||||
}
|
||||
|
||||
if (!toolCalls.isEmpty()) {
|
||||
message["tool_calls"] = toolCalls;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
QJsonArray OpenAIMessage::createToolResultMessages(const QHash<QString, QString> &toolResults) const
|
||||
{
|
||||
QJsonArray messages;
|
||||
|
||||
for (auto toolContent : getCurrentToolUseContent()) {
|
||||
if (toolResults.contains(toolContent->id())) {
|
||||
auto toolResult = std::make_unique<LLMCore::ToolResultContent>(
|
||||
toolContent->id(), toolResults[toolContent->id()]);
|
||||
messages.append(toolResult->toJson(LLMCore::ProviderFormat::OpenAI));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
QList<LLMCore::ToolUseContent *> OpenAIMessage::getCurrentToolUseContent() const
|
||||
{
|
||||
QList<LLMCore::ToolUseContent *> toolBlocks;
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (auto toolContent = qobject_cast<LLMCore::ToolUseContent *>(block)) {
|
||||
toolBlocks.append(toolContent);
|
||||
}
|
||||
}
|
||||
return toolBlocks;
|
||||
}
|
||||
|
||||
void OpenAIMessage::startNewContinuation()
|
||||
{
|
||||
LOG_MESSAGE(QString("OpenAIAPIMessage: Starting new continuation"));
|
||||
|
||||
m_currentBlocks.clear();
|
||||
m_pendingToolArguments.clear();
|
||||
m_finishReason.clear();
|
||||
m_state = LLMCore::MessageState::Building;
|
||||
}
|
||||
|
||||
void OpenAIMessage::updateStateFromFinishReason()
|
||||
{
|
||||
if (m_finishReason == "tool_calls" && !getCurrentToolUseContent().empty()) {
|
||||
m_state = LLMCore::MessageState::RequiresToolExecution;
|
||||
} else if (m_finishReason == "stop") {
|
||||
m_state = LLMCore::MessageState::Final;
|
||||
} else {
|
||||
m_state = LLMCore::MessageState::Complete;
|
||||
}
|
||||
}
|
||||
|
||||
LLMCore::TextContent *OpenAIMessage::getOrCreateTextContent()
|
||||
{
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (auto textContent = qobject_cast<LLMCore::TextContent *>(block)) {
|
||||
return textContent;
|
||||
}
|
||||
}
|
||||
|
||||
return addCurrentContent<LLMCore::TextContent>();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
65
providers/OpenAIMessage.hpp
Normal file
65
providers/OpenAIMessage.hpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <llmcore/ContentBlocks.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class OpenAIMessage : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OpenAIMessage(QObject *parent = nullptr);
|
||||
|
||||
void handleContentDelta(const QString &content);
|
||||
void handleToolCallStart(int index, const QString &id, const QString &name);
|
||||
void handleToolCallDelta(int index, const QString &argumentsDelta);
|
||||
void handleToolCallComplete(int index);
|
||||
void handleFinishReason(const QString &finishReason);
|
||||
|
||||
QJsonObject toProviderFormat() const;
|
||||
QJsonArray createToolResultMessages(const QHash<QString, QString> &toolResults) const;
|
||||
|
||||
LLMCore::MessageState state() const { return m_state; }
|
||||
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||
|
||||
void startNewContinuation();
|
||||
|
||||
private:
|
||||
QString m_finishReason;
|
||||
LLMCore::MessageState m_state = LLMCore::MessageState::Building;
|
||||
QList<LLMCore::ContentBlock *> m_currentBlocks;
|
||||
QHash<int, QString> m_pendingToolArguments;
|
||||
|
||||
void updateStateFromFinishReason();
|
||||
LLMCore::TextContent *getOrCreateTextContent();
|
||||
|
||||
template<typename T, typename... Args>
|
||||
T *addCurrentContent(Args &&...args)
|
||||
{
|
||||
T *content = new T(std::forward<Args>(args)...);
|
||||
content->setParent(this);
|
||||
m_currentBlocks.append(content);
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
@ -19,8 +19,11 @@
|
||||
|
||||
#include "OpenAIProvider.hpp"
|
||||
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
@ -29,12 +32,19 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
OpenAIProvider::OpenAIProvider(QObject *parent)
|
||||
: LLMCore::Provider(parent)
|
||||
, m_toolsManager(new Tools::ToolsManager(this))
|
||||
{
|
||||
connect(
|
||||
m_toolsManager,
|
||||
&Tools::ToolsManager::toolExecutionComplete,
|
||||
this,
|
||||
&OpenAIProvider::onToolExecutionComplete);
|
||||
}
|
||||
|
||||
QString OpenAIProvider::name() const
|
||||
{
|
||||
return "OpenAI";
|
||||
@ -64,7 +74,8 @@ void OpenAIProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type)
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
@ -91,57 +102,15 @@ void OpenAIProvider::prepareRequest(
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDone = false;
|
||||
QByteArrayList lines = data.split('\n');
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "data: [DONE]") {
|
||||
isDone = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
QByteArray jsonData = line;
|
||||
if (line.startsWith("data: ")) {
|
||||
jsonData = line.mid(6);
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||
|
||||
if (doc.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||
if (message.hasError()) {
|
||||
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (message.isDone()) {
|
||||
isDone = true;
|
||||
if (isToolsEnabled) {
|
||||
auto toolsDefinitions = m_toolsManager->getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat::OpenAI);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
request["tools"] = toolsDefinitions;
|
||||
LOG_MESSAGE(QString("Added %1 tools to OpenAI request").arg(toolsDefinitions.size()));
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
|
||||
@ -180,7 +149,7 @@ QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching ChatGPT models: %1").arg(reply->errorString()));
|
||||
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
@ -199,7 +168,8 @@ QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCo
|
||||
{"frequency_penalty", {}},
|
||||
{"presence_penalty", {}},
|
||||
{"stop", QJsonArray{}},
|
||||
{"stream", {}}};
|
||||
{"stream", {}},
|
||||
{"tools", {}}};
|
||||
|
||||
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||
}
|
||||
@ -223,4 +193,245 @@ LLMCore::ProviderID OpenAIProvider::providerID() const
|
||||
return LLMCore::ProviderID::OpenAI;
|
||||
}
|
||||
|
||||
void OpenAIProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(QString("OpenAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
}
|
||||
|
||||
bool OpenAIProvider::supportsTools() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenAIProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("OpenAIProvider: Cancelling request %1").arg(requestId));
|
||||
LLMCore::Provider::cancelRequest(requestId);
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void OpenAIProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().isEmpty() || line == "data: [DONE]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject chunk = parseEventLine(line);
|
||||
if (chunk.isEmpty())
|
||||
continue;
|
||||
|
||||
processStreamChunk(requestId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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 OpenAIProvider::onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &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 OpenAI request %1").arg(requestId));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
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, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||
QJsonArray messages = continuationRequest["messages"].toArray();
|
||||
|
||||
messages.append(message->toProviderFormat());
|
||||
|
||||
QJsonArray toolResultMessages = message->createToolResultMessages(toolResults);
|
||||
for (const auto &toolMsg : toolResultMessages) {
|
||||
messages.append(toolMsg);
|
||||
}
|
||||
|
||||
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 OpenAIProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk)
|
||||
{
|
||||
QJsonArray choices = chunk["choices"].toArray();
|
||||
if (choices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject choice = choices[0].toObject();
|
||||
QJsonObject delta = choice["delta"].toObject();
|
||||
QString finishReason = choice["finish_reason"].toString();
|
||||
|
||||
OpenAIMessage *message = m_messages.value(requestId);
|
||||
if (!message) {
|
||||
message = new OpenAIMessage(this);
|
||||
m_messages[requestId] = message;
|
||||
LOG_MESSAGE(QString("Created NEW OpenAIAPIMessage for request %1").arg(requestId));
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId));
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
emit continuationStarted(requestId);
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
if (delta.contains("content") && !delta["content"].isNull()) {
|
||||
QString content = delta["content"].toString();
|
||||
message->handleContentDelta(content);
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += content;
|
||||
emit partialResponseReceived(requestId, content);
|
||||
}
|
||||
|
||||
if (delta.contains("tool_calls")) {
|
||||
QJsonArray toolCalls = delta["tool_calls"].toArray();
|
||||
for (const auto &toolCallValue : toolCalls) {
|
||||
QJsonObject toolCall = toolCallValue.toObject();
|
||||
int index = toolCall["index"].toInt();
|
||||
|
||||
if (toolCall.contains("id")) {
|
||||
QString id = toolCall["id"].toString();
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
QString name = function["name"].toString();
|
||||
message->handleToolCallStart(index, id, name);
|
||||
}
|
||||
|
||||
if (toolCall.contains("function")) {
|
||||
QJsonObject function = toolCall["function"].toObject();
|
||||
if (function.contains("arguments")) {
|
||||
QString args = function["arguments"].toString();
|
||||
message->handleToolCallDelta(index, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!finishReason.isEmpty() && finishReason != "null") {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
message->handleToolCallComplete(i);
|
||||
}
|
||||
|
||||
message->handleFinishReason(finishReason);
|
||||
handleMessageComplete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIProvider::handleMessageComplete(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId))
|
||||
return;
|
||||
|
||||
OpenAIMessage *message = m_messages[requestId];
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
LOG_MESSAGE(QString("OpenAI 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) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_MESSAGE(QString("OpenAI message marked as complete for %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Cleaning up OpenAI request %1").arg(requestId));
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIMessage *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
|
||||
|
||||
@ -19,13 +19,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include "OpenAIMessage.hpp"
|
||||
#include "tools/ToolsManager.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class OpenAIProvider : public LLMCore::Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OpenAIProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
QString completionEndpoint() const override;
|
||||
@ -35,13 +40,41 @@ public:
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type) override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
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;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamChunk(const QString &requestId, const QJsonObject &chunk);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, OpenAIMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
@ -26,9 +26,6 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
QString OpenRouterProvider::name() const
|
||||
@ -41,57 +38,6 @@ QString OpenRouterProvider::url() const
|
||||
return "https://openrouter.ai/api";
|
||||
}
|
||||
|
||||
bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDone = false;
|
||||
QByteArrayList lines = data.split('\n');
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty() || line.contains("OPENROUTER PROCESSING")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "data: [DONE]") {
|
||||
isDone = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
QByteArray jsonData = line;
|
||||
if (line.startsWith("data: ")) {
|
||||
jsonData = line.mid(6);
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||
|
||||
if (doc.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||
if (message.hasError()) {
|
||||
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (message.isDone()) {
|
||||
isDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QString OpenRouterProvider::apiKey() const
|
||||
{
|
||||
return Settings::providerSettings().openRouterApiKey();
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "llmcore/Provider.hpp"
|
||||
#include "providers/OpenAICompatProvider.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
@ -29,7 +28,6 @@ class OpenRouterProvider : public OpenAICompatProvider
|
||||
public:
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||
QString apiKey() const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
};
|
||||
|
||||
@ -82,7 +82,6 @@ public:
|
||||
QodeAssistPlugin()
|
||||
: m_updater(new PluginUpdater(this))
|
||||
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
||||
, m_requestHandler(this)
|
||||
{}
|
||||
|
||||
~QodeAssistPlugin() final
|
||||
@ -121,6 +120,9 @@ public:
|
||||
Constants::QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY,
|
||||
":/resources/images/qoderassist-icon.png");
|
||||
#endif
|
||||
QQuickWindow::setSceneGraphBackend(
|
||||
Settings::chatAssistantSettings().chatRenderer.stringValue());
|
||||
|
||||
loadTranslations();
|
||||
|
||||
Providers::registerProviders();
|
||||
@ -203,6 +205,10 @@ public:
|
||||
showChatViewAction.setText(Tr::tr("Show QodeAssist Chat"));
|
||||
showChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
|
||||
showChatViewAction.addOnTriggered(this, [this] {
|
||||
if (!m_chatView) {
|
||||
m_chatView.reset(new Chat::ChatView());
|
||||
}
|
||||
|
||||
if (!m_chatView->isVisible()) {
|
||||
m_chatView->show();
|
||||
}
|
||||
@ -238,7 +244,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void extensionsInitialized() final { m_chatView.reset(new Chat::ChatView()); }
|
||||
void extensionsInitialized() final {}
|
||||
|
||||
void restartClient()
|
||||
{
|
||||
@ -248,7 +254,6 @@ public:
|
||||
Settings::codeCompletionSettings(),
|
||||
LLMCore::ProvidersManager::instance(),
|
||||
&m_promptProvider,
|
||||
m_requestHandler,
|
||||
m_documentReader,
|
||||
m_performanceLogger));
|
||||
}
|
||||
@ -290,7 +295,6 @@ private:
|
||||
|
||||
QPointer<QodeAssistClient> m_qodeAssistClient;
|
||||
LLMCore::PromptProviderFim m_promptProvider;
|
||||
LLMCore::RequestHandler m_requestHandler{this};
|
||||
Context::DocumentReaderQtCreator m_documentReader;
|
||||
RequestPerformanceLogger m_performanceLogger;
|
||||
QPointer<Chat::ChatOutputPane> m_chatOutputPane;
|
||||
|
||||
@ -7,6 +7,7 @@ add_library(QodeAssistSettings STATIC
|
||||
SettingsTr.hpp
|
||||
CodeCompletionSettings.hpp CodeCompletionSettings.cpp
|
||||
ChatAssistantSettings.hpp ChatAssistantSettings.cpp
|
||||
ToolsSettings.hpp ToolsSettings.cpp
|
||||
SettingsDialog.hpp SettingsDialog.cpp
|
||||
ProjectSettings.hpp ProjectSettings.cpp
|
||||
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
|
||||
|
||||
@ -56,10 +56,6 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default"));
|
||||
linkOpenFiles.setDefaultValue(false);
|
||||
|
||||
stream.setSettingsKey(Constants::CA_STREAM);
|
||||
stream.setDefaultValue(true);
|
||||
stream.setLabelText(Tr::tr("Enable stream option"));
|
||||
|
||||
autosave.setSettingsKey(Constants::CA_AUTOSAVE);
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
@ -72,6 +68,7 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
enableChatInNavigationPanel.setLabelText(Tr::tr("Enable chat in navigation panel"));
|
||||
enableChatInNavigationPanel.setDefaultValue(false);
|
||||
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::CA_TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
@ -206,6 +203,17 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
textFormat.addOption("HTML");
|
||||
textFormat.addOption("Plain Text");
|
||||
|
||||
chatRenderer.setSettingsKey(Constants::CA_CHAT_RENDERER);
|
||||
chatRenderer.setLabelText(Tr::tr("Chat Renderer:"));
|
||||
chatRenderer.addOption("rhi");
|
||||
chatRenderer.addOption("software");
|
||||
chatRenderer.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
#ifdef Q_OS_WIN
|
||||
chatRenderer.setDefaultValue("software");
|
||||
#else
|
||||
chatRenderer.setDefaultValue("rhi");
|
||||
#endif
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
|
||||
readSettings();
|
||||
@ -233,33 +241,36 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
chatViewSettingsGrid.addRow({textFontFamily, textFontSize});
|
||||
chatViewSettingsGrid.addRow({codeFontFamily, codeFontSize});
|
||||
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,
|
||||
stream,
|
||||
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}},
|
||||
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}};
|
||||
});
|
||||
}
|
||||
|
||||
@ -282,7 +293,6 @@ void ChatAssistantSettings::resetSettingsToDefaults()
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(stream);
|
||||
resetAspect(chatTokensThreshold);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
@ -304,6 +314,7 @@ void ChatAssistantSettings::resetSettingsToDefaults()
|
||||
resetAspect(textFontSize);
|
||||
resetAspect(codeFontSize);
|
||||
resetAspect(textFormat);
|
||||
resetAspect(chatRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,6 @@ public:
|
||||
// Chat settings
|
||||
Utils::IntegerAspect chatTokensThreshold{this};
|
||||
Utils::BoolAspect linkOpenFiles{this};
|
||||
Utils::BoolAspect stream{this};
|
||||
Utils::BoolAspect autosave{this};
|
||||
Utils::BoolAspect enableChatInBottomToolBar{this};
|
||||
Utils::BoolAspect enableChatInNavigationPanel{this};
|
||||
@ -72,6 +71,8 @@ public:
|
||||
Utils::IntegerAspect codeFontSize{this};
|
||||
Utils::SelectionAspect textFormat{this};
|
||||
|
||||
Utils::SelectionAspect chatRenderer{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
|
||||
@ -51,10 +51,6 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
multiLineCompletion.setDefaultValue(true);
|
||||
multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion"));
|
||||
|
||||
stream.setSettingsKey(Constants::CC_STREAM);
|
||||
stream.setDefaultValue(true);
|
||||
stream.setLabelText(Tr::tr("Enable stream option"));
|
||||
|
||||
modelOutputHandler.setLabelText(Tr::tr("Text output proccessing mode:"));
|
||||
modelOutputHandler.setSettingsKey(Constants::CC_MODEL_OUTPUT_HANDLER);
|
||||
modelOutputHandler.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
@ -303,7 +299,6 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
Column{autoCompletion,
|
||||
Space{8},
|
||||
multiLineCompletion,
|
||||
stream,
|
||||
Row{modelOutputHandler, Stretch{1}},
|
||||
Row{autoCompletionCharThreshold,
|
||||
autoCompletionTypingInterval,
|
||||
@ -365,7 +360,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(autoCompletion);
|
||||
resetAspect(multiLineCompletion);
|
||||
resetAspect(stream);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(useTopP);
|
||||
|
||||
@ -35,7 +35,6 @@ public:
|
||||
// Auto Completion Settings
|
||||
Utils::BoolAspect autoCompletion{this};
|
||||
Utils::BoolAspect multiLineCompletion{this};
|
||||
Utils::BoolAspect stream{this};
|
||||
Utils::SelectionAspect modelOutputHandler{this};
|
||||
|
||||
Utils::IntegerAspect startSuggestionTimer{this};
|
||||
|
||||
@ -247,8 +247,12 @@ GeneralSettings::GeneralSettings()
|
||||
ccTemplateDescription,
|
||||
Row{specifyPreset1, preset1Language, Stretch{1}},
|
||||
ccPreset1Grid}};
|
||||
auto caGroup
|
||||
= Group{title(TrConstants::CHAT_ASSISTANT), Column{caGrid, caTemplateDescription}};
|
||||
|
||||
auto caGroup = Group{
|
||||
title(TrConstants::CHAT_ASSISTANT),
|
||||
Column{
|
||||
caGrid,
|
||||
caTemplateDescription}};
|
||||
|
||||
auto rootLayout = Column{
|
||||
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
|
||||
@ -354,7 +358,12 @@ void GeneralSettings::showModelsNotSupportedDialog(Utils::StringAspect &aspect)
|
||||
.append(
|
||||
(&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY
|
||||
: Constants::CA_MODEL_HISTORY);
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0)
|
||||
QStringList historyList
|
||||
= Utils::QtcSettings().value(Utils::Key(key.toLocal8Bit())).toStringList();
|
||||
#else
|
||||
QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList();
|
||||
#endif
|
||||
|
||||
auto modelList = dialog.addComboBox(historyList, aspect.value());
|
||||
dialog.addSpacing();
|
||||
@ -391,7 +400,12 @@ void GeneralSettings::showUrlSelectionDialog(
|
||||
(&aspect == &ccUrl) ? Constants::CC_URL_HISTORY
|
||||
: (&aspect == &ccPreset1Url) ? Constants::CC_PRESET1_URL_HISTORY
|
||||
: Constants::CA_URL_HISTORY);
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0)
|
||||
QStringList historyList
|
||||
= Utils::QtcSettings().value(Utils::Key(key.toLocal8Bit())).toStringList();
|
||||
#else
|
||||
QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList();
|
||||
#endif
|
||||
allUrls.append(historyList);
|
||||
allUrls.removeDuplicates();
|
||||
|
||||
|
||||
@ -75,17 +75,22 @@ const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCha
|
||||
const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
|
||||
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";
|
||||
const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion";
|
||||
const char CC_STREAM[] = "QodeAssist.ccStream";
|
||||
const char CC_MODEL_OUTPUT_HANDLER[] = "QodeAssist.ccModelOutputHandler";
|
||||
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
|
||||
const char CA_AUTO_APPLY_FILE_EDITS[] = "QodeAssist.caAutoApplyFileEdits";
|
||||
const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold";
|
||||
const char CA_LINK_OPEN_FILES[] = "QodeAssist.caLinkOpenFiles";
|
||||
const char CA_STREAM[] = "QodeAssist.caStream";
|
||||
const char CA_AUTOSAVE[] = "QodeAssist.caAutosave";
|
||||
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 CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead";
|
||||
const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite";
|
||||
const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
||||
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
||||
const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
@ -93,13 +98,14 @@ const char QODE_ASSIST_CODE_COMPLETION_SETTINGS_PAGE_ID[]
|
||||
= "QodeAssist.2CodeCompletionSettingsPageId";
|
||||
const char QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID[]
|
||||
= "QodeAssist.3ChatAssistantSettingsPageId";
|
||||
const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.4CustomPromptSettingsPageId";
|
||||
const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.4ToolsSettingsPageId";
|
||||
const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.5CustomPromptSettingsPageId";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";
|
||||
|
||||
// Provider Settings Page ID
|
||||
const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.5ProviderSettingsPageId";
|
||||
const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.6ProviderSettingsPageId";
|
||||
|
||||
// Provider API Keys
|
||||
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
|
||||
@ -167,5 +173,6 @@ const char CA_TEXT_FONT_SIZE[] = "QodeAssist.caTextFontSize";
|
||||
const char CA_CODE_FONT_FAMILY[] = "QodeAssist.caCodeFontFamily";
|
||||
const char CA_CODE_FONT_SIZE[] = "QodeAssist.caCodeFontSize";
|
||||
const char CA_TEXT_FORMAT[] = "QodeAssist.caTextFormat";
|
||||
const char CA_CHAT_RENDERER[] = "QodeAssist.caChatRenderer";
|
||||
|
||||
} // namespace QodeAssist::Constants
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user