Compare commits

..

8 Commits

Author SHA1 Message Date
Petr Mironychev
142afa725f feat: Add using vector and chunks in Context Manager 2025-02-21 18:19:16 +01:00
Petr Mironychev
f36db033e6 refactor: Improvment rag storage 2025-02-09 10:53:35 +01:00
Petr Mironychev
5dfcf74128 feat: add chunking 2025-02-09 10:10:06 +01:00
Petr Mironychev
02101665ca feat: Add version to vector db 2025-02-09 01:00:30 +01:00
Petr Mironychev
77a03d42ed feat: add enhancedSearch 2025-02-09 00:04:45 +01:00
Petr Mironychev
09c38c8b0e fix: Rebase on main 2025-02-09 00:04:45 +01:00
Petr Mironychev
7b73d7af7b feat: Add similarity search 2025-02-09 00:04:44 +01:00
Petr Mironychev
5a426b4d9f feat: RAG init 2025-02-09 00:04:44 +01:00
206 changed files with 4502 additions and 7601 deletions

View File

@@ -6,67 +6,22 @@
"llm", "llm",
"ai" "ai"
], ],
"compatibility": "Qt 6.8.3", "compatibility": "Qt 6.8.1",
"platforms": [ "platforms": [
"Windows", "Windows",
"macOS", "macOS",
"Linux" "Linux"
], ],
"license": "GPLv3", "license": "GPLv3",
"version": "0.5.10", "version": "0.4.0",
"status": "draft", "status": "draft",
"is_pack": false, "is_pack": false,
"released_at": null, "released_at": null,
"version_history": [ "version_history": [
{ {
"version": "0.4.0", "version": "0.4.0",
"is_latest": false, "is_latest": true,
"released_at": "2024-01-24T15:00:00Z" "released_at": "2024-01-24T15:00:00Z"
},
{
"version": "0.5.2",
"is_latest": false,
"released_at": "2025-03-13T17:00:00Z"
},
{
"version": "0.5.3",
"is_latest": false,
"released_at": "2025-03-14T11:00:00Z"
},
{
"version": "0.5.4",
"is_latest": false,
"released_at": "2025-03-17T03:00:00Z"
},
{
"version": "0.5.5",
"is_latest": false,
"released_at": "2025-03-20T19:00:00Z"
},
{
"version": "0.5.6",
"is_latest": false,
"released_at": "2025-04-04T19:00:00Z"
},
{
"version": "0.5.7",
"is_latest": false,
"released_at": "2025-04-14T01:00:00Z"
},
{
"version": "0.5.8",
"is_latest": true,
"released_at": "2025-04-17T10:00:00Z"
},
{
"version": "0.5.9",
"is_latest": true,
"released_at": "2025-04-21T10:00:00Z"
},
{
"version": "0.5.10",
"is_latest": true,
"released_at": "2025-04-24T10:00:00Z"
} }
], ],
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d", "icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",

View File

@@ -12,9 +12,9 @@ on:
env: env:
PLUGIN_NAME: QodeAssist PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.3 QT_VERSION: 6.8.1
QT_CREATOR_VERSION: 16.0.1 QT_CREATOR_VERSION: 15.0.1
QT_CREATOR_VERSION_INTERNAL: 16.0.1 QT_CREATOR_VERSION_INTERNAL: 15.0.1
MACOS_DEPLOYMENT_TARGET: "11.0" MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6" CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1" NINJA_VERSION: "1.12.1"
@@ -36,8 +36,8 @@ jobs:
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat", environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
} }
- { - {
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64", name: "Ubuntu Latest GCC", artifact: "Linux-x64",
os: ubuntu-22.04, os: ubuntu-latest,
platform: linux_x64, platform: linux_x64,
cc: "gcc", cxx: "g++" cc: "gcc", cxx: "g++"
} }
@@ -70,7 +70,7 @@ jobs:
cmakeVersion: ${{ env.CMAKE_VERSION }} cmakeVersion: ${{ env.CMAKE_VERSION }}
ninjaVersion: ${{ env.NINJA_VERSION }} ninjaVersion: ${{ env.NINJA_VERSION }}
- name: Install dependencies - name: Install system libs
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
if ("${{ runner.os }}" STREQUAL "Linux") if ("${{ runner.os }}" STREQUAL "Linux")
@@ -78,13 +78,7 @@ jobs:
COMMAND sudo apt update COMMAND sudo apt update
) )
execute_process( execute_process(
COMMAND sudo apt install COMMAND sudo apt install libgl1-mesa-dev
# build dependencies
libgl1-mesa-dev libgtest-dev libgmock-dev
# runtime dependencies for tests (Qt is downloaded outside package manager,
# thus minimal dependencies must be installed explicitly)
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb-xkb1 libxkbcommon-x11-0 xvfb
RESULT_VARIABLE result RESULT_VARIABLE result
) )
if (NOT result EQUAL 0) if (NOT result EQUAL 0)
@@ -223,7 +217,7 @@ jobs:
COMMAND python COMMAND python
-u -u
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}" "${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}" --name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
--src . --src .
--build build --build build
--qt-path "${{ steps.qt.outputs.qt_dir }}" --qt-path "${{ steps.qt.outputs.qt_dir }}"
@@ -241,31 +235,24 @@ jobs:
- name: Upload - name: Upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
# The json is the same for all platforms, but we need to save one # The json is the same for all platforms, but we need to save one
- name: Upload plugin json - name: Upload plugin json
if: startsWith(matrix.config.os, 'ubuntu') if: matrix.config.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.PLUGIN_NAME }}-origin-json name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./build/build/${{ env.PLUGIN_NAME }}.json path: ./build/build/${{ env.PLUGIN_NAME }}.json
- name: Run unit tests
if: startsWith(matrix.config.os, 'ubuntu')
run: |
xvfb-run ./build/build/test/QodeAssistTest
update_json: update_json:
if: contains(github.ref, 'tags/v') if: contains(github.ref, 'tags/v')
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
needs: build needs: build
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'

View File

@@ -1,24 +0,0 @@
name: Check formatting
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang-format-19
- name: Check formatting
run: |
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
git diff --exit-code || exit 1

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "3rdparty/inja"]
path = 3rdparty/inja
url = https://github.com/pantor/inja

1
3rdparty/inja vendored

Submodule 3rdparty/inja deleted from 384a6bef3f

View File

@@ -8,38 +8,15 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
find_package(QtCreator REQUIRED COMPONENTS Core) find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
find_package(GTest)
# IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
set(QODEASSIST_QT_CREATOR_VERSION_MINOR ${CMAKE_MATCH_2})
set(QODEASSIST_QT_CREATOR_VERSION_PATCH ${CMAKE_MATCH_3})
if(NOT version_match)
message(FATAL_ERROR "Failed to parse Qt Creator version string: ${IDE_VERSION}")
endif()
message(STATUS "Qt Creator Version: ${QODEASSIST_QT_CREATOR_VERSION_MAJOR}.${QODEASSIST_QT_CREATOR_VERSION_MINOR}.${QODEASSIST_QT_CREATOR_VERSION_PATCH}")
add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
)
add_subdirectory(llmcore) add_subdirectory(llmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context) add_subdirectory(context)
if(GTest_FOUND)
add_subdirectory(test)
endif()
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
@@ -66,33 +43,26 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp templates/Templates.hpp
templates/CodeLlamaFim.hpp templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp templates/CustomFimTemplate.hpp
templates/Qwen.hpp templates/Qwen.hpp
templates/OpenAICompatible.hpp templates/Ollama.hpp
templates/BasicChat.hpp
templates/Llama3.hpp templates/Llama3.hpp
templates/ChatML.hpp templates/ChatML.hpp
templates/Alpaca.hpp templates/Alpaca.hpp
templates/Llama2.hpp templates/Llama2.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/CodeLlamaQMLFim.hpp templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
providers/Providers.hpp providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
@@ -102,27 +72,4 @@ add_qtc_plugin(QodeAssist
ConfigurationManager.hpp ConfigurationManager.cpp ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
) )
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable
NAMES
qtcreator "Qt Creator"
PATHS
"${QtCreatorCorePath}/../../../bin"
"${QtCreatorCorePath}/../../../MacOS"
NO_DEFAULT_PATH
)
if (QtCreatorExecutable)
add_custom_target(RunQtCreator
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
DEPENDS QodeAssist
)
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
endif()

View File

@@ -17,7 +17,6 @@ qt_add_qml_module(QodeAssistChatView
qml/parts/TopBar.qml qml/parts/TopBar.qml
qml/parts/BottomBar.qml qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml qml/parts/AttachedFilesPlace.qml
qml/parts/ChatPreviewBar.qml
RESOURCES RESOURCES
icons/attach-file-light.svg icons/attach-file-light.svg
icons/attach-file-dark.svg icons/attach-file-dark.svg

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -18,9 +18,9 @@
*/ */
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include <utils/aspects.h>
#include <QtCore/qjsonobject.h> #include <QtCore/qjsonobject.h>
#include <QtQml> #include <QtQml>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
@@ -31,8 +31,7 @@ ChatModel::ChatModel(QObject *parent)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
connect( connect(&settings.chatTokensThreshold,
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
this, this,
&ChatModel::tokensThresholdChanged); &ChatModel::tokensThresholdChanged);
@@ -124,13 +123,11 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```"); QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
int lastIndex = 0; int lastIndex = 0;
auto blockMatches = codeBlockRegex.globalMatch(content); auto blockMatches = codeBlockRegex.globalMatch(content);
bool foundCodeBlock = blockMatches.hasNext();
while (blockMatches.hasNext()) { while (blockMatches.hasNext()) {
auto match = blockMatches.next(); auto match = blockMatches.next();
if (match.capturedStart() > lastIndex) { if (match.capturedStart() > lastIndex) {
QString textBetween QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) { if (!textBetween.isEmpty()) {
parts.append({MessagePart::Text, textBetween, ""}); parts.append({MessagePart::Text, textBetween, ""});
} }
@@ -141,19 +138,7 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
if (lastIndex < content.length()) { if (lastIndex < content.length()) {
QString remainingText = content.mid(lastIndex).trimmed(); QString remainingText = content.mid(lastIndex).trimmed();
if (!remainingText.isEmpty()) {
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
if (unclosedMatch.hasMatch()) {
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
if (!beforeCodeBlock.isEmpty()) {
parts.append({MessagePart::Text, beforeCodeBlock, ""});
}
parts.append(
{MessagePart::Code, unclosedMatch.captured(2).trimmed(), unclosedMatch.captured(1)});
} else if (!remainingText.isEmpty()) {
parts.append({MessagePart::Text, remainingText, ""}); parts.append({MessagePart::Text, remainingText, ""});
} }
} }
@@ -210,16 +195,4 @@ QString ChatModel::lastMessageId() const
return !m_messages.isEmpty() ? m_messages.last().id : ""; return !m_messages.isEmpty() ? m_messages.last().id : "";
} }
void ChatModel::resetModelTo(int index)
{
if (index < 0 || index >= m_messages.size())
return;
if (index < m_messages.size()) {
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
m_messages.remove(index, m_messages.size() - index);
endRemoveRows();
}
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -73,8 +73,6 @@ public:
QString currentModel() const; QString currentModel() const;
QString lastMessageId() const; QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index);
signals: signals:
void tokensThresholdChanged(); void tokensThresholdChanged();
void modelReseted(); void modelReseted();

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -29,6 +29,7 @@
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h> #include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include <projectexplorer/projecttree.h>
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
@@ -38,6 +39,8 @@
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/FileChunker.hpp"
#include "context/RAGManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -45,20 +48,21 @@ namespace QodeAssist::Chat {
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(LLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, this))
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
{ {
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles(); m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect( connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
&Settings::chatAssistantSettings().linkOpenFiles,
&Utils::BaseAspect::changed,
this, this,
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); }); [this](){
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
});
auto &settings = Settings::generalSettings(); auto &settings = Settings::generalSettings();
connect( connect(&settings.caModel,
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged); &Utils::BaseAspect::changed,
this,
&ChatRootView::currentTemplateChanged);
connect( connect(
m_clientInterface, m_clientInterface,
@@ -75,16 +79,10 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); }); connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount); connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount); connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
connect( connect(&Settings::chatAssistantSettings().useSystemPrompt, &Utils::BaseAspect::changed,
&Settings::chatAssistantSettings().useSystemPrompt, this, &ChatRootView::updateInputTokensCount);
&Utils::BaseAspect::changed, connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
this, this, &ChatRootView::updateInputTokensCount);
&ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::updateInputTokensCount);
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@@ -165,10 +163,9 @@ QString ChatRootView::getChatsHistoryDir() const
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString(); path = projectSettings.chatHistoryPath().toString();
} else { } else {
path = QString("%1/qodeassist/chat_history") path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
.arg(Core::ICore::userResourcePath().toFSPathString());
} }
QDir dir(path); QDir dir(path);
@@ -352,7 +349,7 @@ void ChatRootView::showAttachFilesDialog()
dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString()); dialog.setDirectory(project->projectDirectory().toString());
} }
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
@@ -386,7 +383,7 @@ void ChatRootView::showLinkFilesDialog()
dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString()); dialog.setDirectory(project->projectDirectory().toString());
} }
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
@@ -439,10 +436,9 @@ void ChatRootView::openChatHistoryFolder()
QString path; QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString(); path = projectSettings.chatHistoryPath().toString();
} else { } else {
path = QString("%1/qodeassist/chat_history") path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
.arg(Core::ICore::userResourcePath().toFSPathString());
} }
QDir dir(path); QDir dir(path);
@@ -454,27 +450,90 @@ void ChatRootView::openChatHistoryFolder()
QDesktopServices::openUrl(url); QDesktopServices::openUrl(url);
} }
// ChatRootView.cpp
void ChatRootView::testRAG(const QString &message)
{
auto project = ProjectExplorer::ProjectTree::currentProject();
if (!project) {
qDebug() << "No active project found";
return;
}
const QString TEST_QUERY = message;
qDebug() << "Starting RAG test with query:";
qDebug() << TEST_QUERY;
qDebug() << "\nFirst, processing project files...";
auto files = Context::ContextManager::instance().getProjectSourceFiles(project);
// Было: auto future = Context::RAGManager::instance().processFiles(project, files);
// Стало:
auto future = Context::RAGManager::instance().processProjectFiles(project, files);
connect(
&Context::RAGManager::instance(),
&Context::RAGManager::vectorizationProgress,
this,
[](int processed, int total) {
qDebug() << QString("Vectorization progress: %1 of %2 files").arg(processed).arg(total);
});
connect(
&Context::RAGManager::instance(),
&Context::RAGManager::vectorizationFinished,
this,
[this, project, TEST_QUERY]() {
qDebug() << "\nVectorization completed. Starting similarity search...\n";
// Было: Context::RAGManager::instance().searchSimilarDocuments(TEST_QUERY, project, 5);
// Стало:
auto future = Context::RAGManager::instance().findRelevantChunks(TEST_QUERY, project, 5);
future.then([](const QList<Context::RAGManager::ChunkSearchResult> &results) {
qDebug() << "Found" << results.size() << "relevant chunks:";
for (const auto &result : results) {
qDebug() << "File:" << result.filePath;
qDebug() << "Lines:" << result.startLine << "-" << result.endLine;
qDebug() << "Score:" << result.combinedScore;
qDebug() << "Content:" << result.content;
qDebug() << "---";
}
});
});
}
void ChatRootView::testChunking()
{
auto project = ProjectExplorer::ProjectTree::currentProject();
if (!project) {
qDebug() << "No active project found";
return;
}
Context::FileChunker::ChunkingConfig config;
Context::ContextManager::instance().testProjectChunks(project, config);
}
void ChatRootView::updateInputTokensCount() void ChatRootView::updateInputTokensCount()
{ {
int inputTokens = m_messageTokensCount; int inputTokens = m_messageTokensCount;
auto &settings = Settings::chatAssistantSettings(); auto& settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) { if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt()); inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
} }
if (!m_attachmentFiles.isEmpty()) { if (!m_attachmentFiles.isEmpty()) {
auto attachFiles = m_clientInterface->contextManager()->getContentFiles(m_attachmentFiles); auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles); inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
} }
if (!m_linkedFiles.isEmpty()) { if (!m_linkedFiles.isEmpty()) {
auto linkFiles = m_clientInterface->contextManager()->getContentFiles(m_linkedFiles); auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles); inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
} }
const auto &history = m_chatModel->getChatHistory(); const auto& history = m_chatModel->getChatHistory();
for (const auto &message : history) { for (const auto& message : history) {
inputTokens += Context::TokenUtils::estimateTokens(message.content); inputTokens += Context::TokenUtils::estimateTokens(message.content);
inputTokens += 4; // + role inputTokens += 4; // + role
} }
@@ -496,7 +555,7 @@ bool ChatRootView::isSyncOpenFiles() const
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor) void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
{ {
if (auto document = editor->document(); document && isSyncOpenFiles()) { if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString(); QString filePath = document->filePath().toString();
m_linkedFiles.removeOne(filePath); m_linkedFiles.removeOne(filePath);
emit linkedFilesChanged(); emit linkedFilesChanged();
} }
@@ -509,8 +568,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor) void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
{ {
if (auto document = editor->document(); document && isSyncOpenFiles()) { if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString(); QString filePath = document->filePath().toString();
if (!m_linkedFiles.contains(filePath) && !shouldIgnoreFileForAttach(document->filePath())) { if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath); m_linkedFiles.append(filePath);
emit linkedFilesChanged(); emit linkedFilesChanged();
} }
@@ -537,19 +596,4 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
} }
} }
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString()));
return true;
}
return false;
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,7 +23,6 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "llmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -66,6 +65,8 @@ public:
Q_INVOKABLE void calculateMessageTokensCount(const QString &message); Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE void setIsSyncOpenFiles(bool state); Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder(); Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void testRAG(const QString &message);
Q_INVOKABLE void testChunking();
Q_INVOKABLE void updateInputTokensCount(); Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const; int inputTokensCount() const;
@@ -78,7 +79,6 @@ public:
QString chatFileName() const; QString chatFileName() const;
void setRecentFilePath(const QString &filePath); void setRecentFilePath(const QString &filePath);
bool shouldIgnoreFileForAttach(const Utils::FilePath &filePath);
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message);
@@ -101,7 +101,6 @@ private:
QString getSuggestedFileName() const; QString getSuggestedFileName() const;
ChatModel *m_chatModel; ChatModel *m_chatModel;
LLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
QString m_currentTemplate; QString m_currentTemplate;
QString m_recentFilePath; QString m_recentFilePath;

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -40,4 +40,4 @@ void ChatWidget::scrollToBottom()
{ {
QMetaObject::invokeMethod(rootObject(), "scrollToBottom"); QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
} }
} // namespace QodeAssist::Chat }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -38,4 +38,4 @@ signals:
void clearPressed(); void clearPressed();
}; };
} // namespace QodeAssist::Chat }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,11 +19,11 @@
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <texteditor/textdocument.h>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <texteditor/textdocument.h>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h> #include <coreplugin/editormanager/ieditor.h>
@@ -33,30 +33,27 @@
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ContextManager.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp" #include "ProvidersManager.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent) : QObject(parent)
, m_requestHandler(new LLMCore::RequestHandler(this)) , m_requestHandler(new LLMCore::RequestHandler(this))
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this))
{ {
connect( connect(m_requestHandler,
m_requestHandler,
&LLMCore::RequestHandler::completionReceived, &LLMCore::RequestHandler::completionReceived,
this, this,
[this](const QString &completion, const QJsonObject &request, bool isComplete) { [this](const QString &completion, const QJsonObject &request, bool isComplete) {
handleLLMResponse(completion, request, isComplete); handleLLMResponse(completion, request, isComplete);
}); });
connect( connect(m_requestHandler,
m_requestHandler,
&LLMCore::RequestHandler::requestFinished, &LLMCore::RequestHandler::requestFinished,
this, this,
[this](const QString &, bool success, const QString &errorString) { [this](const QString &, bool success, const QString &errorString) {
@@ -73,7 +70,7 @@ void ClientInterface::sendMessage(
{ {
cancelRequest(); cancelRequest();
auto attachFiles = m_contextManager->getContentFiles(attachments); auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles); m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
auto &chatAssistantSettings = Settings::chatAssistantSettings(); auto &chatAssistantSettings = Settings::chatAssistantSettings();
@@ -87,7 +84,8 @@ void ClientInterface::sendMessage(
} }
auto templateName = Settings::generalSettings().caTemplate(); auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) { if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@@ -95,47 +93,51 @@ void ClientInterface::sendMessage(
} }
LLMCore::ContextData context; LLMCore::ContextData context;
context.prefix = message;
context.suffix = "";
QString systemPrompt;
if (chatAssistantSettings.useSystemPrompt())
systemPrompt = chatAssistantSettings.systemPrompt();
if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt();
if (!linkedFiles.isEmpty()) { if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles); systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
} }
context.systemPrompt = systemPrompt;
}
QVector<LLMCore::Message> messages; QJsonObject providerRequest;
for (const auto &msg : m_chatModel->getChatHistory()) { providerRequest["model"] = Settings::generalSettings().caModel();
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content}); providerRequest["stream"] = chatAssistantSettings.stream();
} providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
context.history = messages;
if (promptTemplate)
promptTemplate->prepareRequest(providerRequest, context);
else
qWarning("No prompt template found");
if (provider)
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
else
qWarning("No provider found");
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat; config.requestType = LLMCore::RequestType::Chat;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"} config.providerRequest = providerRequest;
: QString{"generateContent?"}; config.multiLineCompletion = false;
config.url = QUrl(QString("%1/models/%2:%3")
.arg(
Settings::generalSettings().caUrl(),
Settings::generalSettings().caModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().caModel()},
{"stream", chatAssistantSettings.stream()}};
}
config.apiKey = provider->apiKey(); config.apiKey = provider->apiKey();
config.provider QJsonObject request;
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat); request["id"] = QUuid::createUuid().toString();
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for chat request:");
LOG_MESSAGES(errors);
return;
}
QJsonObject request{{"id", QUuid::createUuid().toString()}};
m_requestHandler->sendLLMRequest(config, request); m_requestHandler->sendLLMRequest(config, request);
} }
@@ -151,8 +153,9 @@ void ClientInterface::cancelRequest()
m_requestHandler->cancelRequest(id); m_requestHandler->cancelRequest(id);
} }
void ClientInterface::handleLLMResponse( void ClientInterface::handleLLMResponse(const QString &response,
const QString &response, const QJsonObject &request, bool isComplete) const QJsonObject &request,
bool isComplete)
{ {
const auto message = response.trimmed(); const auto message = response.trimmed();
@@ -183,35 +186,30 @@ QString ClientInterface::getCurrentFileContext() const
} }
QString fileInfo = QString("Language: %1\nFile: %2\n\n") QString fileInfo = QString("Language: %1\nFile: %2\n\n")
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString()); .arg(textDocument->mimeType(), textDocument->filePath().toString());
QString content = textDocument->document()->toPlainText(); QString content = textDocument->document()->toPlainText();
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString())); LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content); return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
} }
QString ClientInterface::getSystemPromptWithLinkedFiles( QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
const QString &basePrompt, const QList<QString> &linkedFiles) const
{ {
QString updatedPrompt = basePrompt; QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) { if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n"; updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles); auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
for (const auto &file : contentFiles) { for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content); updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
.arg(file.filename, file.content);
} }
} }
return updatedPrompt; return updatedPrompt;
} }
Context::ContextManager *ClientInterface::contextManager() const
{
return m_contextManager;
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -25,8 +25,6 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "RequestHandler.hpp" #include "RequestHandler.hpp"
#include "llmcore/IPromptProvider.hpp"
#include <context/ContextManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -35,8 +33,7 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void sendMessage( void sendMessage(
@@ -46,8 +43,6 @@ public:
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
Context::ContextManager *contextManager() const;
signals: signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely(); void messageReceivedCompletely();
@@ -56,12 +51,11 @@ private:
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles( QString getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const; const QString &basePrompt,
const QList<QString> &linkedFiles) const;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel;
LLMCore::RequestHandler *m_requestHandler; LLMCore::RequestHandler *m_requestHandler;
Context::ContextManager *m_contextManager; ChatModel *m_chatModel;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -27,25 +27,14 @@ Rectangle {
property alias msgModel: msgCreator.model property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model property alias messageAttachments: attachmentsModel.model
property bool isUserMessage: false
property int messageIndex: -1
signal resetChatToMessage(int index)
height: msgColumn.implicitHeight + 10 height: msgColumn.implicitHeight + 10
radius: 8 radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
HoverHandler {
id: mouse
}
ColumnLayout { ColumnLayout {
id: msgColumn id: msgColumn
x: 5 width: parent.width
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 5 spacing: 5
@@ -124,32 +113,6 @@ Rectangle {
} }
} }
Rectangle {
id: userMessageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: "#92BD6C"
radius: root.radius
visible: root.isUserMessage
}
QoAButton {
id: stopButtonId
anchors {
right: parent.right
top: parent.top
}
text: qsTr("ResetTo")
visible: root.isUserMessage && mouse.hovered
onClicked: function() {
root.resetChatToMessage(root.messageIndex)
}
}
component TextComponent : TextBlock { component TextComponent : TextBlock {
required property var itemData required property var itemData
height: implicitHeight + 10 height: implicitHeight + 10

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -76,10 +76,6 @@ ChatRootView {
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved") text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
} }
openChatHistory.onClicked: root.openChatHistoryFolder() openChatHistory.onClicked: root.openChatHistoryFolder()
expandScrollbar {
text: scroll.isPreviewMode ? "»" : "«"
onClicked: scroll.isPreviewMode = !scroll.isPreviewMode
}
} }
ListView { ListView {
@@ -96,19 +92,12 @@ ChatRootView {
delegate: ChatItem { delegate: ChatItem {
required property var model required property var model
required property int index
width: ListView.view.width - scroll.width width: ListView.view.width - scroll.width
msgModel: root.chatModel.processMessageContent(model.content) msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments messageAttachments: model.attachments
isUserMessage: model.roleType === ChatModel.User color: model.roleType === ChatModel.User ? palette.alternateBase
messageIndex: index : palette.base
onResetChatToMessage: function(index) {
messageInput.text = model.content
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(index)
}
} }
header: Item { header: Item {
@@ -118,50 +107,6 @@ ChatRootView {
ScrollBar.vertical: QQC.ScrollBar { ScrollBar.vertical: QQC.ScrollBar {
id: scroll id: scroll
property bool isPreviewMode: false
readonly property int previewWidth: 30
implicitWidth: isPreviewMode ? scroll.previewWidth : 16
contentItem: Rectangle {
implicitWidth: scroll.isPreviewMode ? scroll.previewWidth : 6
implicitHeight: 100
radius: 3
color: scroll.pressed ? palette.dark :
scroll.hovered ? palette.mid :
palette.button
Behavior on implicitWidth {
NumberAnimation { duration: 150 }
}
}
background: Rectangle {
color: scroll.isPreviewMode ? "transparent" :
palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
radius: 3
}
ChatPreviewBar {
anchors.fill: parent
targetView: chatListView
visible: parent.isPreviewMode
opacity: parent.isPreviewMode ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
} }
onCountChanged: { onCountChanged: {
@@ -253,6 +198,8 @@ ChatRootView {
} }
attachFiles.onClicked: root.showAttachFilesDialog() attachFiles.onClicked: root.showAttachFilesDialog()
linkFiles.onClicked: root.showLinkFilesDialog() linkFiles.onClicked: root.showLinkFilesDialog()
testRag.onClicked: root.testRAG(messageInput.text)
testChunks.onClicked: root.testChunking()
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -30,6 +30,8 @@ Rectangle {
property alias syncOpenFiles: syncOpenFilesId property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId property alias attachFiles: attachFilesId
property alias linkFiles: linkFilesId property alias linkFiles: linkFilesId
property alias testRag: testRagId
property alias testChunks: testChunksId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@@ -91,6 +93,18 @@ Rectangle {
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context") ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
} }
QoAButton {
id: testRagId
text: qsTr("Test RAG")
}
QoAButton {
id: testChunksId
text: qsTr("Test Chunks")
}
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@@ -1,135 +0,0 @@
/*
* 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
Rectangle {
id: root
property ListView targetView: null
property int previewWidth: 50
property color userMessageColor: "#92BD6C"
property color assistantMessageColor: palette.button
width: previewWidth
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
Behavior on opacity {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
Column {
id: previewContainer
anchors.fill: parent
anchors.margins: 2
spacing: 2
Repeater {
model: targetView ? targetView.model : null
Rectangle {
required property int index
required property var model
width: parent.width
height: {
if (!targetView || !targetView.count) return 0
const availableHeight = root.height - ((targetView.count - 1) * previewContainer.spacing)
return availableHeight / targetView.count
}
radius: 4
color: model.roleType === ChatModel.User ?
userMessageColor :
assistantMessageColor
opacity: root.opacity
transform: Translate {
x: root.opacity * 50 - 50
}
Behavior on transform {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (targetView) {
targetView.positionViewAtIndex(index, ListView.Center)
}
}
HoverHandler {
id: hover
}
}
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: hover.hovered ? 0.2 : 0
radius: parent.radius
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: {
if (!targetView) return 0
const viewY = targetView.contentY
const viewHeight = targetView.height
const totalHeight = targetView.contentHeight
const itemPosition = index / targetView.count * totalHeight
const itemHeight = totalHeight / targetView.count
return (itemPosition + itemHeight > viewY &&
itemPosition < viewY + viewHeight) ? 0.2 : 0
}
radius: parent.radius
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
ToolTip.visible: hover.hovered
ToolTip.text: {
const maxPreviewLength = 100
return model.content.length > maxPreviewLength ?
model.content.substring(0, maxPreviewLength) + "..." :
model.content
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -30,7 +30,6 @@ Rectangle {
property alias tokensBadge: tokensBadgeId property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
property alias expandScrollbar: expandScrollbarId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@@ -85,12 +84,5 @@ Rectangle {
Badge { Badge {
id: tokensBadgeId id: tokensBadgeId
} }
QoAButton {
id: expandScrollbarId
width: 16
height: 16
}
} }
} }

View File

@@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,131 +18,29 @@
*/ */
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>
#include <QFileInfo>
#include <QHash> #include <QHash>
namespace QodeAssist { namespace QodeAssist {
struct LanguageProperties QString CodeHandler::processText(QString text)
{
QString name;
QString commentStyle;
QVector<QString> namesFromModel;
QVector<QString> fileExtensions;
};
const QVector<LanguageProperties> customLanguagesFromSettings()
{
QVector<LanguageProperties> customLanguages;
const QStringList customLanguagesList = Settings::codeCompletionSettings().customLanguages();
for (const QString &entry : customLanguagesList) {
if (entry.trimmed().isEmpty()) {
continue;
}
QStringList parts = entry.split(',');
if (parts.size() < 4) {
continue;
}
QString name = parts[0].trimmed();
QString commentStyle = parts[1].trimmed();
QStringList modelNamesList = parts[2].trimmed().split(' ', Qt::SkipEmptyParts);
QStringList extensionsList = parts[3].trimmed().split(' ', Qt::SkipEmptyParts);
if (!name.isEmpty() && !commentStyle.isEmpty() && !modelNamesList.isEmpty()
&& !extensionsList.isEmpty()) {
QVector<QString> modelNames;
for (const auto &modelName : modelNamesList) {
modelNames.append(modelName);
}
QVector<QString> extensions;
for (const auto &ext : extensionsList) {
extensions.append(ext);
}
customLanguages.append({name, commentStyle, modelNames, extensions});
}
}
return customLanguages;
}
const QVector<LanguageProperties> &getKnownLanguages()
{
static QVector<LanguageProperties> knownLanguages = {
{"python", "#", {"python", "py"}, {"py"}},
{"lua", "--", {"lua"}, {"lua"}},
{"js", "//", {"js", "javascript"}, {"js", "jsx"}},
{"ts", "//", {"ts", "typescript"}, {"ts", "tsx"}},
{"c-like", "//", {"c", "c++", "cpp"}, {"c", "h", "cpp", "hpp"}},
{"java", "//", {"java"}, {"java"}},
{"c#", "//", {"cs", "csharp"}, {"cs"}},
{"php", "//", {"php"}, {"php"}},
{"ruby", "#", {"rb", "ruby"}, {"rb"}},
{"go", "//", {"go"}, {"go"}},
{"swift", "//", {"swift"}, {"swift"}},
{"kotlin", "//", {"kt", "kotlin"}, {"kt", "kotlin"}},
{"scala", "//", {"scala"}, {"scala"}},
{"r", "#", {"r"}, {"r"}},
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
{"perl", "#", {"pl", "perl"}, {"pl"}},
{"hs", "--", {"hs", "haskell"}, {"hs"}},
{"qml", "//", {"qml"}, {"qml"}},
};
knownLanguages.append(customLanguagesFromSettings());
return knownLanguages;
}
static QHash<QString, QString> buildLanguageToCommentPrefixMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
result[languageProps.name] = languageProps.commentStyle;
}
return result;
}
static QHash<QString, QString> buildExtensionToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &extension : languageProps.fileExtensions) {
result[extension] = languageProps.name;
}
}
return result;
}
static QHash<QString, QString> buildModelLanguageNameToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &nameFromModel : languageProps.namesFromModel) {
result[nameFromModel] = languageProps.name;
}
}
return result;
}
QString CodeHandler::processText(QString text, QString currentFilePath)
{ {
QString result; QString result;
QStringList lines = text.split('\n'); QStringList lines = text.split('\n');
bool inCodeBlock = false; bool inCodeBlock = false;
QString pendingComments; QString pendingComments;
QString currentLanguage;
auto currentFileExtension = QFileInfo(currentFilePath).suffix(); for (const QString &line : lines) {
auto currentLanguage = detectLanguageFromExtension(currentFileExtension); if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
auto addPendingCommentsIfAny = [&]() { currentLanguage = detectLanguage(line);
if (pendingComments.isEmpty()) {
return;
} }
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
if (!pendingComments.isEmpty()) {
QStringList commentLines = pendingComments.split('\n'); QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage); QString commentPrefix = getCommentPrefix(currentLanguage);
@@ -155,28 +52,7 @@ QString CodeHandler::processText(QString text, QString currentFilePath)
} }
} }
pendingComments.clear(); pendingComments.clear();
};
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
auto lineLanguage = detectLanguageFromLine(line);
if (!lineLanguage.isEmpty()) {
currentLanguage = lineLanguage;
} }
addPendingCommentsIfAny();
if (lineLanguage.isEmpty()) {
// language not detected, so add direct output from model, if any
result += line.trimmed().mid(3) + "\n"; // add the remainder of line after ```
}
}
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
result += line + "\n"; result += line + "\n";
} else { } else {
QString trimmed = line.trimmed(); QString trimmed = line.trimmed();
@@ -188,27 +64,45 @@ QString CodeHandler::processText(QString text, QString currentFilePath)
} }
} }
addPendingCommentsIfAny(); if (!pendingComments.isEmpty()) {
QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage);
for (const QString &commentLine : commentLines) {
if (!commentLine.trimmed().isEmpty()) {
result += commentPrefix + " " + commentLine.trimmed() + "\n";
} else {
result += "\n";
}
}
}
return result; return result;
} }
QString CodeHandler::getCommentPrefix(const QString &language) QString CodeHandler::getCommentPrefix(const QString &language)
{ {
static const auto commentPrefixes = buildLanguageToCommentPrefixMap(); static const QHash<QString, QString> commentPrefixes
return commentPrefixes.value(language, "//"); = {{"python", "#"}, {"py", "#"}, {"lua", "--"}, {"javascript", "//"},
{"js", "//"}, {"typescript", "//"}, {"ts", "//"}, {"cpp", "//"},
{"c++", "//"}, {"c", "//"}, {"java", "//"}, {"csharp", "//"},
{"cs", "//"}, {"php", "//"}, {"ruby", "#"}, {"rb", "#"},
{"rust", "//"}, {"rs", "//"}, {"go", "//"}, {"swift", "//"},
{"kotlin", "//"}, {"kt", "//"}, {"scala", "//"}, {"r", "#"},
{"shell", "#"}, {"bash", "#"}, {"sh", "#"}, {"perl", "#"},
{"pl", "#"}, {"haskell", "--"}, {"hs", "--"}};
return commentPrefixes.value(language.toLower(), "//");
} }
QString CodeHandler::detectLanguageFromLine(const QString &line) QString CodeHandler::detectLanguage(const QString &line)
{ {
static const auto modelNameToLanguage = buildModelLanguageNameToLanguageMap(); QString trimmed = line.trimmed();
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), ""); if (trimmed.length() <= 3) { // Если только ```
} return QString();
}
QString CodeHandler::detectLanguageFromExtension(const QString &extension) return trimmed.mid(3).trimmed();
{
static const auto extensionToLanguage = buildExtensionToLanguageMap();
return extensionToLanguage.value(extension.toLower(), "");
} }
const QRegularExpression &CodeHandler::getFullCodeBlockRegex() const QRegularExpression &CodeHandler::getFullCodeBlockRegex()

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -28,20 +28,11 @@ namespace QodeAssist {
class CodeHandler class CodeHandler
{ {
public: public:
static QString processText(QString text, QString currentFileName); static QString processText(QString text);
/**
* Detects language from line, or returns empty string if this was not possible
*/
static QString detectLanguageFromLine(const QString &line);
/**
* Detects language file name, or returns empty string if this was not possible
*/
static QString detectLanguageFromExtension(const QString &extension);
private: private:
static QString getCommentPrefix(const QString &language); static QString getCommentPrefix(const QString &language);
static QString detectLanguage(const QString &line);
static const QRegularExpression &getFullCodeBlockRegex(); static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex(); static const QRegularExpression &getPartialStartBlockRegex();

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,8 +19,8 @@
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
#include <settings/ButtonAspect.hpp>
#include <QTimer> #include <QTimer>
#include <settings/ButtonAspect.hpp>
#include "QodeAssisttr.h" #include "QodeAssisttr.h"
@@ -35,49 +35,6 @@ ConfigurationManager &ConfigurationManager::instance()
void ConfigurationManager::init() void ConfigurationManager::init()
{ {
setupConnections(); setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
}
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
} }
ConfigurationManager::ConfigurationManager(QObject *parent) ConfigurationManager::ConfigurationManager(QObject *parent)
@@ -107,14 +64,6 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel); connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect( connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate); &m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
} }
void ConfigurationManager::selectProvider() void ConfigurationManager::selectProvider()
@@ -132,8 +81,10 @@ void ConfigurationManager::selectProvider()
: m_generalSettings.caProvider; : m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] { QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(providersList,
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:")); targetSettings,
Tr::tr("Select LLM Provider"),
Tr::tr("Providers:"));
}); });
} }
@@ -186,22 +137,19 @@ void ConfigurationManager::selectTemplate()
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate); const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1 const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames()
? m_templateManger.getFimTemplatesForProvider(providerID) : m_templateManger.chatTemplatesNames();
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template : isPreset1 ? m_generalSettings.ccPreset1Template
: m_generalSettings.caTemplate; : m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(templateList,
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:")); targetSettings,
Tr::tr("Select Template"),
Tr::tr("Templates:"));
}); });
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -36,11 +36,6 @@ public:
void init(); void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots: public slots:
void selectProvider(); void selectProvider();
void selectModel(); void selectModel();

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,37 +23,24 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <llmcore/RequestConfig.hpp>
#include <texteditor/textdocument.h>
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp" #include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp" #include "llmcore/MessageBuilder.hpp"
#include "llmcore/PromptTemplateManager.hpp" #include "llmcore/PromptTemplateManager.hpp"
#include "llmcore/ProvidersManager.hpp" #include "llmcore/ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <llmcore/RequestConfig.hpp>
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface()
const Settings::GeneralSettings &generalSettings, : m_requestHandler(this)
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( connect(&m_requestHandler,
&m_requestHandler,
&LLMCore::RequestHandler::completionReceived, &LLMCore::RequestHandler::completionReceived,
this, this,
&LLMClientInterface::sendCompletionToClient); &LLMClientInterface::sendCompletionToClient);
@@ -61,7 +48,7 @@ LLMClientInterface::LLMClientInterface(
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{ {
return "QodeAssist"; return "Qode Assist";
} }
void LLMClientInterface::startImpl() void LLMClientInterface::startImpl()
@@ -88,7 +75,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
handleTextDocumentDidOpen(request); handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") { } else if (method == "getCompletionsCycling") {
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId); startTimeMeasurement(requestId);
handleCompletion(request); handleCompletion(request);
} else if (method == "$/cancelRequest") { } else if (method == "$/cancelRequest") {
handleCancelRequest(request); handleCancelRequest(request);
@@ -159,37 +146,43 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
emit finished(); emit finished();
} }
bool QodeAssist::LLMClientInterface::isSpecifyCompletion(const QJsonObject &request)
{
auto &generalSettings = Settings::generalSettings();
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(request);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
}
void LLMClientInterface::handleCompletion(const QJsonObject &request) void LLMClientInterface::handleCompletion(const QJsonObject &request)
{ {
auto filePath = Context::extractFilePathFromRequest(request); const auto updatedContext = prepareContext(request);
auto documentInfo = m_documentReader.readDocument(filePath); auto &completeSettings = Settings::codeCompletionSettings();
if (!documentInfo.document) { auto &generalSettings = Settings::generalSettings();
LOG_MESSAGE("Error: Document is not available for" + filePath);
return;
}
auto updatedContext = prepareContext(request, documentInfo); bool isPreset1Active = isSpecifyCompletion(request);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo); const auto providerName = !isPreset1Active ? generalSettings.ccProvider()
: generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? generalSettings.ccModel()
: generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? generalSettings.ccUrl() : generalSettings.ccPreset1Url();
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider() const auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) { if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
return; return;
} }
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() auto templateName = !isPreset1Active ? generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template(); : generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
if (!promptTemplate) { if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@@ -201,70 +194,44 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
config.requestType = LLMCore::RequestType::CodeCompletion; config.requestType = LLMCore::RequestType::CodeCompletion;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
// TODO refactor networking
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
: QString{"generateContent?"};
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
} else {
config.url = QUrl(QString("%1%2").arg( config.url = QUrl(QString("%1%2").arg(
url, url,
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint() promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint()
: provider->chatEndpoint())); : provider->chatEndpoint()));
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}};
}
config.apiKey = provider->apiKey(); config.apiKey = provider->apiKey();
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
config.providerRequest = {{"model", modelName}, {"stream", completeSettings.stream()}};
config.multiLineCompletion = completeSettings.multiLineCompletion();
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords()); const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty()) if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords; config.providerRequest["stop"] = stopWords;
QString systemPrompt; QString systemPrompt;
if (m_completeSettings.useSystemPrompt()) if (completeSettings.useSystemPrompt())
systemPrompt.append( systemPrompt.append(completeSettings.systemPrompt());
m_completeSettings.useUserMessageTemplateForCC() if (!updatedContext.fileContext.isEmpty())
&& promptTemplate->type() == LLMCore::TemplateType::Chat systemPrompt.append(updatedContext.fileContext);
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
QString userMessage; QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) { if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) {
userMessage = m_completeSettings.processMessageToFIM( userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix);
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else { } else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or(""); userMessage = updatedContext.prefix;
} }
// TODO refactor add message auto message = LLMCore::MessageBuilder()
QVector<LLMCore::Message> messages; .addSystemMessage(systemPrompt)
messages.append({"user", userMessage}); .addUserMessage(userMessage)
updatedContext.history = messages; .addSuffix(updatedContext.suffix)
} .addTokenizer(promptTemplate);
config.provider->prepareRequest( message.saveTo(
config.providerRequest, config.providerRequest,
promptTemplate, providerName == "Ollama" ? LLMCore::ProvidersApi::Ollama : LLMCore::ProvidersApi::OpenAI);
updatedContext,
LLMCore::RequestType::CodeCompletion); config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::CodeCompletion);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type()); auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
@@ -275,36 +242,59 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
m_requestHandler.sendLLMRequest(config, request); m_requestHandler.sendLLMRequest(config, request);
} }
LLMCore::ContextData LLMClientInterface::prepareContext( LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
const QJsonObject &request, const Context::DocumentInfo &documentInfo) const QStringView &accumulatedCompletion)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject(); QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].toObject(); QJsonObject position = doc["position"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
return LLMCore::ContextData{};
}
int cursorPosition = position["character"].toInt(); int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt(); int lineNumber = position["line"].toInt();
Context::DocumentContextReader Context::DocumentContextReader reader(textDocument);
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath); return reader.prepareContext(lineNumber, cursorPosition);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
} }
Context::ContextManager *LLMClientInterface::contextManager() const Context::ProgrammingLanguage LLMClientInterface::getDocumentLanguage(const QJsonObject &request) const
{ {
return m_contextManager; QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
return Context::ProgrammingLanguage::Unknown;
}
return Context::ProgrammingLanguageUtils::fromMimeType(textDocument->mimeType());
} }
void LLMClientInterface::sendCompletionToClient( void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QString &completion, const QJsonObject &request, bool isComplete) const QJsonObject &request,
bool isComplete)
{ {
auto filePath = Context::extractFilePathFromRequest(request); bool isPreset1Active = isSpecifyCompletion(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() auto templateName = !isPreset1Active ? Settings::generalSettings().ccTemplate()
: m_generalSettings.ccPreset1Template(); : Settings::generalSettings().ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
@@ -316,12 +306,10 @@ void LLMClientInterface::sendCompletionToClient(
QJsonArray completions; QJsonArray completions;
QJsonObject completionItem; QJsonObject completionItem;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
QString processedCompletion QString processedCompletion
= promptTemplate->type() == LLMCore::TemplateType::Chat = promptTemplate->type() == LLMCore::TemplateType::Chat
&& m_completeSettings.smartProcessInstuctText() && Settings::codeCompletionSettings().smartProcessInstuctText()
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request)) ? CodeHandler::processText(completion)
: completion; : completion;
completionItem[LanguageServerProtocol::textKey] = processedCompletion; completionItem[LanguageServerProtocol::textKey] = processedCompletion;
@@ -341,13 +329,37 @@ void LLMClientInterface::sendCompletionToClient(
QString("Completions: \n%1") QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE( LOG_MESSAGE(QString("Full response: \n%1")
QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId); endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
} }
void LLMClientInterface::startTimeMeasurement(const QString &requestId)
{
m_requestStartTimes[requestId] = QDateTime::currentMSecsSinceEpoch();
}
void LLMClientInterface::endTimeMeasurement(const QString &requestId)
{
if (m_requestStartTimes.contains(requestId)) {
qint64 startTime = m_requestStartTimes[requestId];
qint64 endTime = QDateTime::currentMSecsSinceEpoch();
qint64 totalTime = endTime - startTime;
logPerformance(requestId, "TotalCompletionTime", totalTime);
m_requestStartTimes.remove(requestId);
}
}
void LLMClientInterface::logPerformance(const QString &requestId,
const QString &operation,
qint64 elapsedMs)
{
LOG_MESSAGE(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
}
void LLMClientInterface::parseCurrentMessage() {}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -22,16 +22,9 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp> #include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp> #include <llmcore/ContextData.hpp>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include <llmcore/RequestHandler.hpp> #include <llmcore/RequestHandler.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
class QNetworkReply; class QNetworkReply;
class QNetworkAccessManager; class QNetworkAccessManager;
@@ -43,29 +36,20 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
Q_OBJECT Q_OBJECT
public: public:
LLMClientInterface( LLMClientInterface();
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
LLMCore::RequestHandlerBase &requestHandler,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
Utils::FilePath serverDeviceTemplate() const override; Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient( void sendCompletionToClient(const QString &completion,
const QString &completion, const QJsonObject &request, bool isComplete); const QJsonObject &request,
bool isComplete);
void handleCompletion(const QJsonObject &request); void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected: protected:
void startImpl() override; void startImpl() override;
void sendData(const QByteArray &data) override;
void parseCurrentMessage() override;
private: private:
void handleInitialize(const QJsonObject &request); void handleInitialize(const QJsonObject &request);
@@ -76,17 +60,17 @@ private:
void handleCancelRequest(const QJsonObject &request); void handleCancelRequest(const QJsonObject &request);
LLMCore::ContextData prepareContext( LLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QJsonObject &request, const QStringView &accumulatedCompletion = QString{});
Context::ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
bool isSpecifyCompletion(const QJsonObject &request);
const Settings::CodeCompletionSettings &m_completeSettings; LLMCore::RequestHandler m_requestHandler;
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; QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager; QMap<QString, qint64> m_requestStartTimes;
void startTimeMeasurement(const QString &requestId);
void endTimeMeasurement(const QString &requestId);
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -29,36 +29,6 @@
namespace QodeAssist { namespace QodeAssist {
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
{
if (suggestion.isEmpty() || rightText.isEmpty()) {
return suggestion;
}
int j = 0;
QString processed = rightText;
QSet<int> matchedPositions;
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
if (suggestion[i] == processed[j]) {
matchedPositions.insert(j);
++j;
}
}
if (matchedPositions.isEmpty()) {
return suggestion + rightText;
}
QList<int> positions = matchedPositions.values();
std::sort(positions.begin(), positions.end(), std::greater<int>());
for (int pos : positions) {
processed.remove(pos, 1);
}
return suggestion;
}
LLMSuggestion::LLMSuggestion( LLMSuggestion::LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion) const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion) : TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
@@ -68,28 +38,21 @@ LLMSuggestion::LLMSuggestion(
int startPos = data.range.begin.toPositionInDocument(sourceDocument); int startPos = data.range.begin.toPositionInDocument(sourceDocument);
int endPos = data.range.end.toPositionInDocument(sourceDocument); int endPos = data.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount()); startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
endPos = qBound(startPos, endPos, sourceDocument->characterCount()); endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
QTextCursor cursor(sourceDocument); QTextCursor cursor(sourceDocument);
cursor.setPosition(startPos); cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
QTextBlock block = cursor.block(); QTextBlock block = cursor.block();
QString blockText = block.text(); QString blockText = block.text();
int cursorPositionInBlock = cursor.positionInBlock(); int startPosInBlock = startPos - block.position();
int endPosInBlock = endPos - block.position();
QString rightText = blockText.mid(cursorPositionInBlock); blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
replacementDocument()->setPlainText(blockText);
if (!data.text.contains('\n')) {
QString processedRightText = mergeWithRightText(data.text, rightText);
processedRightText = processedRightText.mid(data.text.length());
QString displayText = blockText.left(cursorPositionInBlock) + data.text
+ processedRightText;
replacementDocument()->setPlainText(displayText);
} else {
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
replacementDocument()->setPlainText(displayText);
}
} }
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
@@ -114,82 +77,31 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos); int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
if (next == -1) { if (next == -1)
if (part == Line) {
next = text.length();
} else {
return apply(); return apply();
}
}
if (part == Line) if (part == Line)
++next; ++next;
QString subText = text.mid(startPos, next - startPos); QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty())
if (subText.isEmpty()) {
return false; return false;
}
QTextBlock currentBlock = currentCursor.block();
QString textAfterCursor = currentBlock.text().mid(currentCursor.positionInBlock());
if (!subText.contains('\n')) {
QTextCursor deleteCursor = currentCursor;
deleteCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
deleteCursor.removeSelectedText();
QString mergedText = mergeWithRightText(subText, textAfterCursor);
currentCursor.insertText(mergedText);
} else {
currentCursor.insertText(subText); currentCursor.insertText(subText);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) { if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1); const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) { if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0}; const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())}; const Utils::Text::Position
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd}; const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}}; const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion( widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0)); std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
} }
} }
}
return false; return false;
} }
bool LLMSuggestion::apply()
{
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
const QString text = suggestions()[currentSuggestion()].text;
QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QTextCursor editCursor = cursor;
int firstLineEnd = text.indexOf('\n');
if (firstLineEnd != -1) {
QString firstLine = text.left(firstLineEnd);
QString restOfText = text.mid(firstLineEnd);
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
QString mergedFirstLine = mergeWithRightText(firstLine, textAfterCursor);
editCursor.insertText(mergedFirstLine + restOfText);
} else {
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
QString mergedText = mergeWithRightText(text, textAfterCursor);
editCursor.insertText(mergedText);
}
return true;
}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -40,6 +40,5 @@ public:
bool applyWord(TextEditor::TextEditorWidget *widget) override; bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override; bool applyLine(TextEditor::TextEditorWidget *widget) override;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget); bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
bool apply() override;
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -68,8 +68,7 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
public: public:
static constexpr LanguageServerProtocol::Key docKey{"doc"}; static constexpr LanguageServerProtocol::Key docKey{"doc"};
GetCompletionParams( GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
const LanguageServerProtocol::TextDocumentIdentifier &document,
int version, int version,
const LanguageServerProtocol::Position &position) const LanguageServerProtocol::Position &position)
{ {

View File

@@ -1,13 +1,13 @@
{ {
"Id" : "qodeassist", "Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.5.10", "Version" : "0.4.13",
"Vendor" : "Petr Mironychev", "Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev", "VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GPLv3", "License" : "GPLv3",
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).", "Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
"Url" : "https://github.com/Palm1r/QodeAssist", "Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist", "DocumentationUrl" : "",
${IDE_PLUGIN_DEPENDENCIES} ${IDE_PLUGIN_DEPENDENCIES}
} }

View File

@@ -2,11 +2,5 @@
<qresource prefix="/"> <qresource prefix="/">
<file>resources/images/qoderassist-icon@2x.png</file> <file>resources/images/qoderassist-icon@2x.png</file>
<file>resources/images/qoderassist-icon.png</file> <file>resources/images/qoderassist-icon.png</file>
<file>resources/images/repeat-last-instruct-icon@2x.png</file>
<file>resources/images/repeat-last-instruct-icon.png</file>
<file>resources/images/improve-current-code-icon@2x.png</file>
<file>resources/images/improve-current-code-icon.png</file>
<file>resources/images/suggest-new-icon.png</file>
<file>resources/images/suggest-new-icon@2x.png</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@@ -1,8 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of Qode Assist.
* *
* The Qt Company portions: * The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@@ -24,10 +24,8 @@
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
#include <QInputDialog>
#include <QTimer> #include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h> #include <languageclient/languageclientsettings.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
@@ -37,7 +35,6 @@
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp" #include "settings/ProjectSettings.hpp"
#include <context/ChangesManager.h> #include <context/ChangesManager.h>
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol; using namespace LanguageServerProtocol;
using namespace TextEditor; using namespace TextEditor;
@@ -47,12 +44,11 @@ using namespace Core;
namespace QodeAssist { namespace QodeAssist {
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(clientInterface) : LanguageClient::Client(new LLMClientInterface())
, m_llmClient(clientInterface)
, m_recentCharCount(0) , m_recentCharCount(0)
{ {
setName("QodeAssist"); setName("Qode Assist");
LanguageClient::LanguageFilter filter; LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*"; filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter); setSupportedLanguage(filter);
@@ -132,13 +128,6 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
scheduleRequest(widget); scheduleRequest(widget);
} }
}); });
// auto editors = BaseTextEditor::textEditorsForDocument(document);
// connect(
// editors.first()->editorWidget(),
// &TextEditorWidget::selectionChanged,
// this,
// [this, editors]() { m_chatButtonHandler.showButton(editors.first()->editorWidget()); });
} }
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project) bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@@ -153,26 +142,14 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
MultiTextCursor cursor = editor->multiTextCursor(); MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible()) if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return; return;
const FilePath filePath = editor->textDocument()->filePath(); const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{ GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath), documentVersion(filePath),
Position(cursor.mainCursor())}}; Position(cursor.mainCursor())}};
if (Settings::codeCompletionSettings().showProgressWidget()) {
m_progressHandler.showProgress(editor);
}
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)]( request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) { const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return); QTC_ASSERT(editor, return);
@@ -182,35 +159,6 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
sendMessage(request); sendMessage(request);
} }
void QodeAssistClient::requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this);
connect(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,
this,
&QodeAssistClient::handleRefactoringResult);
}
m_progressHandler.showProgress(editor);
m_refactorHandler->sendRefactorRequest(editor, instructions);
}
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor) void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
{ {
cancelRunningRequest(editor); cancelRunningRequest(editor);
@@ -240,8 +188,8 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position()); it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer()); it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
} }
void QodeAssistClient::handleCompletions( void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor) TextEditor::TextEditorWidget *editor)
{ {
if (response.error()) if (response.error())
log(*response.error()); log(*response.error());
@@ -288,7 +236,6 @@ void QodeAssistClient::handleCompletions(
Text::Position pos{toTextPos(c.position())}; Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()}; return TextSuggestion::Data{range, pos, c.text()};
}); });
m_progressHandler.hideProgress();
if (completions.isEmpty()) if (completions.isEmpty())
return; return;
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document())); editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
@@ -300,7 +247,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
const auto it = m_runningRequests.constFind(editor); const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd()) if (it == m_runningRequests.constEnd())
return; return;
m_progressHandler.hideProgress();
cancelRequest(it->id()); cancelRequest(it->id());
m_runningRequests.erase(it); m_runningRequests.erase(it);
} }
@@ -321,11 +267,16 @@ void QodeAssistClient::setupConnections()
openDocument(textDocument); openDocument(textDocument);
}; };
m_documentOpenedConnection m_documentOpenedConnection = connect(EditorManager::instance(),
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc); &EditorManager::documentOpened,
m_documentClosedConnection = connect( this,
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) { openDoc);
if (auto textDocument = qobject_cast<TextDocument *>(document)) m_documentClosedConnection = connect(EditorManager::instance(),
&EditorManager::documentClosed,
this,
[this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(
document))
closeDocument(textDocument); closeDocument(textDocument);
}); });
@@ -342,32 +293,4 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear(); m_scheduledRequests.clear();
} }
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
if (!result.success) {
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
return;
}
auto editor = BaseTextEditor::currentTextEditor();
if (!editor) {
LOG_MESSAGE("Refactoring failed: No active editor found");
return;
}
auto editorWidget = editor->editorWidget();
QTextCursor cursor = editorWidget->textCursor();
cursor.beginEditBlock();
int startPos = result.insertRange.begin.toPositionInDocument(editorWidget->document());
int endPos = result.insertRange.end.toPositionInDocument(editorWidget->document());
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
cursor.insertText(result.newText);
cursor.endEditBlock();
m_progressHandler.hideProgress();
}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,8 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of Qode Assist.
* *
* The Qt Company portions: * The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@@ -24,40 +24,32 @@
#pragma once #pragma once
#include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
#include "QuickRefactorHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp> #include "LSPCompletion.hpp"
namespace QodeAssist { namespace QodeAssist {
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
public: public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient();
~QodeAssistClient() override; ~QodeAssistClient() override;
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override; bool canOpenProject(ProjectExplorer::Project *project) override;
void requestCompletions(TextEditor::TextEditorWidget *editor); void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
private: private:
void scheduleRequest(TextEditor::TextEditorWidget *editor); void scheduleRequest(TextEditor::TextEditorWidget *editor);
void handleCompletions( void handleCompletions(const GetCompletionRequest::Response &response,
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor); TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor); void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
bool isEnabled(ProjectExplorer::Project *project) const; bool isEnabled(ProjectExplorer::Project *project) const;
void setupConnections(); void setupConnections();
void cleanupConnections(); void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests; QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests; QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@@ -66,10 +58,6 @@ private:
QElapsedTimer m_typingTimer; QElapsedTimer m_typingTimer;
int m_recentCharCount; int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
LLMClientInterface *m_llmClient;
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,293 +0,0 @@
/*
* 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 "QuickRefactorHandler.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <context/DocumentContextReader.hpp>
#include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp>
#include <llmcore/PromptTemplateManager.hpp>
#include <llmcore/ProvidersManager.hpp>
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp>
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() {}
void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
if (m_isRefactoringInProgress) {
cancelRequest();
}
m_currentEditor = editor;
Utils::Text::Range range;
if (editor->textCursor().hasSelection()) {
QTextCursor cursor = editor->textCursor();
int startPos = cursor.selectionStart();
int endPos = cursor.selectionEnd();
QTextBlock startBlock = editor->document()->findBlock(startPos);
int startLine = startBlock.blockNumber() + 1;
int startColumn = startPos - startBlock.position();
QTextBlock endBlock = editor->document()->findBlock(endPos);
int endLine = endBlock.blockNumber() + 1;
int endColumn = endPos - endBlock.position();
Utils::Text::Position startPosition;
startPosition.line = startLine;
startPosition.column = startColumn;
Utils::Text::Position endPosition;
endPosition.line = endLine;
endPosition.column = endColumn;
range = Utils::Text::Range();
range.begin = startPosition;
range.end = endPosition;
} else {
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
QTextBlock block = editor->document()->findBlock(cursorPos);
int line = block.blockNumber() + 1;
int column = cursorPos - block.position();
Utils::Text::Position cursorPosition;
cursorPosition.line = line;
cursorPosition.column = column;
range = Utils::Text::Range();
range.begin = cursorPosition;
range.end = cursorPosition;
}
m_currentRange = range;
prepareAndSendRequest(editor, instructions, range);
}
void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range)
{
auto &settings = Settings::generalSettings();
auto &providerRegistry = LLMCore::ProvidersManager::instance();
auto &promptManager = LLMCore::PromptTemplateManager::instance();
const auto providerName = settings.caProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No provider found with name: %1").arg(providerName);
emit refactoringCompleted(result);
return;
}
const auto templateName = settings.caTemplate();
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No template found with name: %1").arg(templateName);
emit refactoringCompleted(result);
return;
}
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat;
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.apiKey = provider->apiKey();
LLMCore::ContextData context = prepareContext(editor, range, instructions);
provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QString requestId = QUuid::createUuid().toString();
m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}};
m_isRefactoringInProgress = true;
m_requestHandler->sendLLMRequest(config, request);
}
LLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
{
LLMCore::ContextData context;
auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader;
auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString());
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available");
return context;
}
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
// TODO add selecting content before and after cursor/selection
QString fullContent = documentInfo.document->toPlainText();
QString taggedContent = fullContent;
if (cursor.hasSelection()) {
int selEnd = cursor.selectionEnd();
int selStart = cursor.selectionStart();
taggedContent
.insert(selEnd, selEnd == cursorPos ? "<selection_end><cursor>" : "<selection_end>");
taggedContent.insert(
selStart, selStart == cursorPos ? "<cursor><selection_start>" : "<selection_start>");
} else {
taggedContent.insert(cursorPos, "<cursor>");
}
QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt();
systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath;
systemPrompt += "\n\nCode context with position markers:";
systemPrompt += taggedContent;
systemPrompt += "\n\nOutput format:";
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
"between<selection_start><selection_end> or be "
"inserted at cursor position<cursor>";
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
"code block markers";
systemPrompt += "\n- The output should be ready to insert directly into the editor";
systemPrompt += "\n- Follow the existing code style and indentation patterns";
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
}
context.systemPrompt = systemPrompt;
QVector<LLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
QString cleanedResponse = response.trimmed();
if (cleanedResponse.startsWith("```")) {
int firstNewLine = cleanedResponse.indexOf('\n');
int lastFence = cleanedResponse.lastIndexOf("```");
if (firstNewLine != -1 && lastFence > firstNewLine) {
cleanedResponse
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
} else if (lastFence != -1) {
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
}
}
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::cancelRequest()
{
if (m_isRefactoringInProgress) {
m_requestHandler->cancelRequest(m_lastRequestId);
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
}
}
} // namespace QodeAssist

View File

@@ -1,77 +0,0 @@
/*
* 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 <QJsonObject>
#include <QObject>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <llmcore/RequestHandler.hpp>
namespace QodeAssist {
struct RefactorResult
{
QString newText;
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
};
class QuickRefactorHandler : public QObject
{
Q_OBJECT
public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result);
private:
void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
LLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
LLMCore::RequestHandler *m_requestHandler;
TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress;
QString m_lastRequestId;
Context::ContextManager m_contextManager;
};
} // namespace QodeAssist

169
README.md
View File

@@ -2,7 +2,7 @@
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml) [![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist) ![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.1-brightgreen) ![Static Badge](https://img.shields.io/badge/QtCreator-15.0.1-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf) [![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) 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. ![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) 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.
@@ -13,30 +13,34 @@
> - The QodeAssist developer bears no responsibility for any charges incurred > - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use > - Please carefully review the provider's pricing and your account settings before use
⚠️ **Commercial Support and Custom Development**
> The QodeAssist developer offers commercial services for:
> - Adapting the plugin for specific Qt Creator versions
> - Custom development for particular operating systems
> - Integration with specific language models
> - Implementing custom features and modifications
>
> For commercial inquiries, please contact: qodeassist.dev@pm.me
## Table of Contents ## Table of Contents
1. [Overview](#overview) 1. [Overview](#overview)
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator) 2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude) 3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
4. [Configure for OpenAI](#configure-for-openai) 4. [Configure for OpenAI](#configure-for-openai)
5. [Configure for Mistral AI](#configure-for-mistral-ai) 5. [Configure for using Ollama](#configure-for-using-ollama)
6. [Configure for Google AI](#configure-for-google-ai) 6. [System Prompt Configuration](#system-prompt-configuration)
7. [Configure for Ollama](#configure-for-ollama) 7. [File Context Features](#file-context-features)
8. [Configure for llama.cpp](#configure-for-llamacpp) 8. [Template-Model Compatibility](#template-model-compatibility)
9. [System Prompt Configuration](#system-prompt-configuration) 9. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
10. [File Context Features](#file-context-features) 10. [Development Progress](#development-progress)
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility) 11. [Hotkeys](#hotkeys)
12. [Development Progress](#development-progress) 12. [Troubleshooting](#troubleshooting)
13. [Hotkeys](#hotkeys) 13. [Support the Development](#support-the-development-of-qodeassist)
14. [Ignoring Files](#ignoring-files) 14. [How to Build](#how-to-build)
14. [Troubleshooting](#troubleshooting)
15. [Support the Development](#support-the-development-of-qodeassist)
16. [How to Build](#how-to-build)
## Overview ## Overview
- AI-powered code completion - AI-powered code completion
- 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: - Chat functionality:
- Side and Bottom panels - Side and Bottom panels
- Chat history autosave and restore - Chat history autosave and restore
@@ -46,28 +50,19 @@
- Automatic syncing with open editor files (optional) - Automatic syncing with open editor files (optional)
- Support for multiple LLM providers: - Support for multiple LLM providers:
- Ollama - Ollama
- llama.cpp
- OpenAI - OpenAI
- Anthropic Claude - Anthropic Claude
- LM Studio - LM Studio
- Mistral AI - OpenAI-compatible providers(eg. https://openrouter.ai)
- Google AI
- OpenAI-compatible providers(eg. llama.cpp, https://openrouter.ai)
- Extensive library of model-specific templates - Extensive library of model-specific templates
- Custom template support
- Easy configuration and model selection - Easy configuration and model selection
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!
<details> <details>
<summary>Code completion: (click to expand)</summary> <summary>Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
</details> </details>
<details>
<summary>Quick refactor in code: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details>
<details> <details>
<summary>Multiline Code completion: (click to expand)</summary> <summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@@ -92,8 +87,6 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
1. Install Latest Qt Creator 1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator 2. Download the QodeAssist plugin for your Qt Creator
- Remove old version plugin if already was installed - Remove old version plugin if already was installed
- on macOS for QtCreator 16: ~/Library/Application Support/QtProject/Qt Creator/plugins/16.0.0/petrmironychev.qodeassist
- on windows for QtCreator 16: C:\Users\<user>\AppData\Local\QtProject\qtcreator\plugins\16.0.0\petrmironychev.qodeassist\lib\qtcreator\plugins
3. Launch Qt Creator and install the plugin: 3. Launch Qt Creator and install the plugin:
- Go to: - Go to:
- MacOS: Qt Creator -> About Plugins... - MacOS: Qt Creator -> About Plugins...
@@ -127,33 +120,7 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" /> <img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
</details> </details>
## Configure for Mistral AI ## Configure for using Ollama
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Mistral AI api key
3. Return to General tab and configure:
- Set "Mistral AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.mistral.ai)
- Select your preferred model (e.g., mistral-large-latest)
- Choose the Mistral AI template for code completion or/and chat
<details>
<summary>Example of Mistral AI settings: (click to expand)</summary>
<img width="829" alt="Mistral AI Settings" src="https://github.com/user-attachments/assets/1c5ed13b-a29b-43f7-b33f-2e05fdea540c" />
</details>
## Configure for Google AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Google AI api key
3. Return to General tab and configure:
- Set "Google AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://generativelanguage.googleapis.com/v1beta)
- Select your preferred model (e.g., gemini-2.0-flash)
- Choose the Google AI template
<details>
<summary>Example of Google AI settings: (click to expand)</summary>
<img width="829" alt="Google AI Settings" src="https://github.com/user-attachments/assets/046ede65-a94d-496c-bc6c-41f3750be12a" />
</details>
## Configure for Ollama
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation. 1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Install a language models in Ollama via terminal. For example, you can run: 2. Install a language models in Ollama via terminal. For example, you can run:
@@ -172,7 +139,7 @@ ollama run qwen2.5-coder:32b
``` ```
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS) 1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "QodeAssist" tab 2. Navigate to the "Qode Assist" tab
3. On the "General" page, verify: 3. On the "General" page, verify:
- Ollama is selected as your LLM provider - Ollama is selected as your LLM provider
- The URL is set to http://localhost:11434 - The URL is set to http://localhost:11434
@@ -186,18 +153,6 @@ You're all set! QodeAssist is now ready to use in Qt Creator.
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" /> <img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
</details> </details>
## Configure for llama.cpp
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to General tab and configure:
- Set "llama.cpp" as the provider for code completion or/and chat assistant
- 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)
<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" />
</details>
## System Prompt Configuration ## System Prompt Configuration
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. 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.
@@ -235,11 +190,25 @@ Linked files provide persistent context throughout the conversation:
- Supports automatic syncing with open editor files (can be enabled in settings) - Supports automatic syncing with open editor files (can be enabled in settings)
- Files can be added/removed at any time during the conversation - Files can be added/removed at any time during the conversation
## Template-Model Compatibility
| Template | Compatible Models | Purpose |
|----------|------------------|----------|
| CodeLlama FIM | `codellama:code` | Code completion |
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
| Ollama Auto FIM | `Any Ollama base/fim models` | Code completion |
| Qwen FIM | `Qwen 2.5 models(exclude instruct)` | Code completion |
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
| Alpaca | `starcoder2:instruct` | Chat assistance |
| Basic Chat| `Messages without tokens` | Chat assistance |
| ChatML | `Qwen 2.5 models(exclude base models)` | Chat assistance |
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
| Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat/instruct models` | Chat assistance |
## QtCreator Version Compatibility ## QtCreator Version Compatibility
- QtCreator 16.0.1 - 0.5.7 - 0.x.x - QtCreator 15.0.1 - 0.4.8 - 0.4.x
- QtCreator 16.0.0 - 0.5.2 - 0.5.6
- QtCreator 15.0.1 - 0.4.8 - 0.5.1
- QtCreator 15.0.0 - 0.4.0 - 0.4.7 - QtCreator 15.0.0 - 0.4.0 - 0.4.7
- QtCreator 14.0.2 - 0.2.3 - 0.3.x - QtCreator 14.0.2 - 0.2.3 - 0.3.x
- QtCreator 14.0.1 - 0.2.2 plugin version and below - QtCreator 14.0.1 - 0.2.2 plugin version and below
@@ -258,49 +227,8 @@ Linked files provide persistent context throughout the conversation:
- To call manual request to suggestion, you can use or change it in settings - To call manual request to suggestion, you can use or change it in settings
- on Mac: Option + Command + Q - on Mac: Option + Command + Q
- on Windows: Ctrl + Alt + Q - on Windows: Ctrl + Alt + Q
- on Linux with KDE Plasma: Ctrl + Alt + Q
- To insert the full suggestion, you can use the TAB key - To insert the full suggestion, you can use the TAB key
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac - To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
- To call Quick Refactor dialog, select some code or place cursor and press
- on Mac: Option + Command + R
- on Windows: Ctrl + Alt + R
- on Linux with KDE Plasma: Ctrl + Alt + R
## Ignoring Files
QodeAssist supports the ability to ignore files in context using a .qodeassistignore file. This allows you to exclude specific files from the context during code completion and in the chat assistant, which is especially useful for large projects.
### How to Use .qodeassistignore
- Create a .qodeassistignore file in the root directory of your project near CMakeLists.txt or pro.
- Add patterns for files and directories that should be excluded from the context.
- QodeAssist will automatically detect this file and apply the exclusion rules.
### .qodeassistignore File Format
The file format is similar to .gitignore:
- Each pattern is written on a separate line
- Empty lines are ignored
- Lines starting with # are considered comments
- Standard wildcards work the same as in .gitignore
- To negate a pattern, use ! at the beginning of the line
```
# Ignore all files in the build directory
build/
# Ignore all temporary files
*.tmp
*.temp
# Ignore all files with .log extension
*.log
# Ignore a specific file
src/generated/autogen.cpp
# Ignore nested directories
**/node_modules/
# Negation - DO NOT ignore this file
!src/important.cpp
```
## Troubleshooting ## Troubleshooting
@@ -311,17 +239,20 @@ If QodeAssist is having problems connecting to the LLM provider, please check th
- For Ollama, the default is usually http://localhost:11434 - For Ollama, the default is usually http://localhost:11434
- For LM Studio, the default is usually http://localhost:1234 - For LM Studio, the default is usually http://localhost:1234
2. Confirm that the selected model and template are compatible: 2. Check the endpoint:
Ensure you've chosen the correct model in the "Select Models" option Make sure the endpoint in the settings matches the one required by your provider
Verify that the selected prompt template matches the model you're using - For Ollama, it should be /api/generate
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
3. On Linux the prebuilt binaries support only ubuntu 22.04+ or simililliar os. 3. Confirm that the selected model and template are compatible:
If you need compatiblity with another os, you have to build manualy. our experiments and resolution you can check here: https://github.com/Palm1r/QodeAssist/issues/48
Ensure you've chosen the correct model in the "Select Models" option
Verify that the selected prompt template matches the model you're using
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values: If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
1. Open Qt Creator settings 1. Open Qt Creator settings
2. Navigate to the "QodeAssist" tab 2. Navigate to the "Qode Assist" tab
3. Pick settings page for reset 3. Pick settings page for reset
4. Click on the "Reset Page to Defaults" button 4. Click on the "Reset Page to Defaults" button
- The API key will not reset - The API key will not reset

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -52,10 +52,10 @@ void UpdateStatusWidget::setDefaultAction(QAction *action)
void UpdateStatusWidget::showUpdateAvailable(const QString &version) void UpdateStatusWidget::showUpdateAvailable(const QString &version)
{ {
m_versionLabel->setText(tr("New version: v%1").arg(version)); m_versionLabel->setText(tr("new version: v%1").arg(version));
m_versionLabel->setVisible(true); m_versionLabel->setVisible(true);
m_updateButton->setVisible(true); m_updateButton->setVisible(true);
m_updateButton->setToolTip(tr("Check update information")); m_updateButton->setToolTip(tr("Update QodeAssist to version %1").arg(version));
} }
void UpdateStatusWidget::hideUpdateInfo() void UpdateStatusWidget::hideUpdateInfo()

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,28 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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 <QtGlobal>
#define QODEASSIST_QT_CREATOR_VERSION \
QT_VERSION_CHECK( \
QODEASSIST_QT_CREATOR_VERSION_MAJOR, \
QODEASSIST_QT_CREATOR_VERSION_MINOR, \
QODEASSIST_QT_CREATOR_VERSION_PATCH)

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,15 +23,16 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
NavigationPanel::NavigationPanel() NavigationPanel::NavigationPanel() {
{
setDisplayName(tr("QodeAssist Chat")); setDisplayName(tr("QodeAssist Chat"));
setPriority(500); setPriority(500);
setId("QodeAssistChat"); setId("QodeAssistChat");
setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C)); setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C));
} }
NavigationPanel::~NavigationPanel() {} NavigationPanel::~NavigationPanel()
{
}
Core::NavigationView NavigationPanel::createWidget() Core::NavigationView NavigationPanel::createWidget()
{ {
@@ -41,4 +42,4 @@ Core::NavigationView NavigationPanel::createWidget()
return view; return view;
} }
} // namespace QodeAssist::Chat }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,8 +19,8 @@
#pragma once #pragma once
#include <coreplugin/inavigationwidgetfactory.h>
#include <QObject> #include <QObject>
#include <coreplugin/inavigationwidgetfactory.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -34,4 +34,4 @@ public:
Core::NavigationView createWidget() override; Core::NavigationView createWidget() override;
}; };
} // namespace QodeAssist::Chat }

View File

@@ -3,17 +3,22 @@ add_library(Context STATIC
ChangesManager.h ChangesManager.cpp ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp ContextManager.hpp ContextManager.cpp
ContentFile.hpp ContentFile.hpp
DocumentReaderQtCreator.hpp
IDocumentReader.hpp
TokenUtils.hpp TokenUtils.cpp TokenUtils.hpp TokenUtils.cpp
ProgrammingLanguage.hpp ProgrammingLanguage.cpp ProgrammingLanguage.hpp ProgrammingLanguage.cpp
IContextManager.hpp RAGManager.hpp RAGManager.cpp
IgnoreManager.hpp IgnoreManager.cpp RAGStorage.hpp RAGStorage.cpp
RAGData.hpp
RAGVectorizer.hpp RAGVectorizer.cpp
RAGSimilaritySearch.hpp RAGSimilaritySearch.cpp
RAGPreprocessor.hpp RAGPreprocessor.cpp
EnhancedRAGSimilaritySearch.hpp EnhancedRAGSimilaritySearch.cpp
FileChunker.hpp FileChunker.cpp
) )
target_link_libraries(Context target_link_libraries(Context
PUBLIC PUBLIC
Qt::Core Qt::Core
Qt::Sql
QtCreator::Core QtCreator::Core
QtCreator::TextEditor QtCreator::TextEditor
QtCreator::Utils QtCreator::Utils

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -30,12 +30,17 @@ ChangesManager &ChangesManager::instance()
ChangesManager::ChangesManager() ChangesManager::ChangesManager()
: QObject(nullptr) : QObject(nullptr)
{} {
}
ChangesManager::~ChangesManager() {} ChangesManager::~ChangesManager()
{
}
void ChangesManager::addChange( void ChangesManager::addChange(TextEditor::TextDocument *document,
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded) int position,
int charsRemoved,
int charsAdded)
{ {
auto &documentQueue = m_documentChanges[document]; auto &documentQueue = m_documentChanges[document];
@@ -46,10 +51,9 @@ void ChangesManager::addChange(
ChangeInfo change{fileName, lineNumber, lineContent}; ChangeInfo change{fileName, lineNumber, lineContent};
auto it auto it = std::find_if(documentQueue.begin(),
= std::find_if(documentQueue.begin(), documentQueue.end(), [lineNumber](const ChangeInfo &c) { documentQueue.end(),
return c.lineNumber == lineNumber; [lineNumber](const ChangeInfo &c) { return c.lineNumber == lineNumber; });
});
if (it != documentQueue.end()) { if (it != documentQueue.end()) {
it->lineContent = lineContent; it->lineContent = lineContent;

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,11 +19,11 @@
#pragma once #pragma once
#include <texteditor/textdocument.h>
#include <QDateTime> #include <QDateTime>
#include <QHash> #include <QHash>
#include <QQueue> #include <QQueue>
#include <QTimer> #include <QTimer>
#include <texteditor/textdocument.h>
namespace QodeAssist::Context { namespace QodeAssist::Context {
@@ -41,8 +41,10 @@ public:
static ChangesManager &instance(); static ChangesManager &instance();
void addChange( void addChange(TextEditor::TextDocument *document,
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded); int position,
int charsRemoved,
int charsAdded);
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const; QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
private: private:

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -21,23 +21,23 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonObject>
#include <QTextStream> #include <QTextStream>
#include "settings/GeneralSettings.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h> #include <projectexplorer/projectnodes.h>
#include <texteditor/textdocument.h>
#include "Logger.hpp" #include "FileChunker.hpp"
namespace QodeAssist::Context { namespace QodeAssist::Context {
ContextManager &ContextManager::instance()
{
static ContextManager manager;
return manager;
}
ContextManager::ContextManager(QObject *parent) ContextManager::ContextManager(QObject *parent)
: QObject(parent) : QObject(parent)
, m_ignoreManager(new IgnoreManager(this))
{} {}
QString ContextManager::readFile(const QString &filePath) const QString ContextManager::readFile(const QString &filePath) const
@@ -54,19 +54,51 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
{ {
QList<ContentFile> files; QList<ContentFile> files;
for (const QString &path : filePaths) { for (const QString &path : filePaths) {
auto project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(path));
if (project && m_ignoreManager->shouldIgnore(path, project)) {
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
continue;
}
ContentFile contentFile = createContentFile(path); ContentFile contentFile = createContentFile(path);
files.append(contentFile); files.append(contentFile);
} }
return files; return files;
} }
ContentFile ContextManager::createContentFile(const QString &filePath) const
{
ContentFile contentFile;
QFileInfo fileInfo(filePath);
contentFile.filename = fileInfo.fileName();
contentFile.content = readFile(filePath);
return contentFile;
}
bool ContextManager::isInBuildDirectory(const QString &filePath) const
{
static const QStringList buildDirPatterns
= {QString(QDir::separator()) + QLatin1String("build") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("Build") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("BUILD") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("debug") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("Debug") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("DEBUG") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("release") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("Release") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("RELEASE") + QDir::separator(),
QString(QDir::separator()) + QLatin1String("builds") + QDir::separator()};
// Нормализуем путь
QString normalizedPath = QDir::fromNativeSeparators(filePath);
// Проверяем, содержит ли путь паттерны build-директории
for (const QString &pattern : buildDirPatterns) {
// Сравниваем с нормализованным паттерном
QString normalizedPattern = QDir::fromNativeSeparators(pattern);
if (normalizedPath.contains(normalizedPattern)) {
qDebug() << "Skipping build file:" << filePath;
return true;
}
}
return false;
}
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
{ {
QStringList sourceFiles; QStringList sourceFiles;
@@ -79,8 +111,11 @@ QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *proj
projectNode->forEachNode( projectNode->forEachNode(
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) { [&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) { if (fileNode) {
sourceFiles.append(fileNode->filePath().toUrlishString()); QString filePath = fileNode->filePath().toString();
if (shouldProcessFile(filePath) && !isInBuildDirectory(filePath)) {
sourceFiles.append(filePath);
}
} }
}, },
nullptr); nullptr);
@@ -88,98 +123,54 @@ QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *proj
return sourceFiles; return sourceFiles;
} }
ContentFile ContextManager::createContentFile(const QString &filePath) const bool ContextManager::shouldProcessFile(const QString &filePath) const
{ {
ContentFile contentFile; static const QStringList supportedExtensions
= {"cpp", "hpp", "c", "h", "cc", "hh", "cxx", "hxx", "qml", "js", "py"};
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
contentFile.filename = fileInfo.fileName(); return supportedExtensions.contains(fileInfo.suffix().toLower());
contentFile.content = readFile(filePath);
return contentFile;
} }
ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &documentInfo) const void ContextManager::testProjectChunks(
ProjectExplorer::Project *project, const FileChunker::ChunkingConfig &config)
{ {
if (!documentInfo.document) { if (!project) {
LOG_MESSAGE("Error: Document is not available for" + documentInfo.filePath); qDebug() << "No project provided";
return Context::ProgrammingLanguage::Unknown; return;
} }
return Context::ProgrammingLanguageUtils::fromMimeType(documentInfo.mimeType); qDebug() << "\nStarting test chunking for project:" << project->displayName();
}
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const // Get source files
{ QStringList sourceFiles = getProjectSourceFiles(project);
const auto &generalSettings = Settings::generalSettings(); qDebug() << "Found" << sourceFiles.size() << "source files";
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo); // Create chunker
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString( auto chunker = new FileChunker(config, this);
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language; // Connect progress and error signals
} connect(chunker, &FileChunker::progressUpdated, this, [](int processed, int total) {
qDebug() << "Progress:" << processed << "/" << total << "files";
});
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const connect(chunker, &FileChunker::error, this, [](const QString &error) {
{ qDebug() << "Error:" << error;
auto documents = Core::DocumentModel::openedDocuments(); });
QList<QPair<QString, QString>> files; // Start chunking and handle results
auto future = chunker->chunkFiles(sourceFiles);
for (const auto *document : std::as_const(documents)) { // Используем QFutureWatcher для обработки результатов
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document); auto watcher = new QFutureWatcher<QList<FileChunk>>(this);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString(); connect(watcher, &QFutureWatcher<QList<FileChunk>>::finished, this, [watcher, chunker]() {
// Очистка
watcher->deleteLater();
chunker->deleteLater();
});
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath()); watcher->setFuture(future);
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
if (!excludeFiles.contains(filePath)) {
files.append({filePath, textDocument->plainText()});
}
}
return files;
}
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
{
QString context = "User files context:\n";
auto documents = Core::DocumentModel::openedDocuments();
for (const auto *document : documents) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
if (excludeFiles.contains(filePath))
continue;
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
context += QString("File: %1\n").arg(filePath);
context += textDocument->plainText();
context += "\n";
}
return context;
}
IgnoreManager *ContextManager::ignoreManager() const
{
return m_ignoreManager;
} }
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,13 +19,12 @@
#pragma once #pragma once
#include "ContentFile.hpp"
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include "ContentFile.hpp" #include "FileChunker.hpp"
#include "IContextManager.hpp"
#include "IgnoreManager.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer { namespace ProjectExplorer {
class Project; class Project;
@@ -33,28 +32,29 @@ class Project;
namespace QodeAssist::Context { namespace QodeAssist::Context {
class ContextManager : public QObject, public IContextManager class ContextManager : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ContextManager(QObject *parent = nullptr); static ContextManager &instance();
~ContextManager() override = default;
QString readFile(const QString &filePath) const override; QString readFile(const QString &filePath) const;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override; QList<ContentFile> getContentFiles(const QStringList &filePaths) const;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override; QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const;
ContentFile createContentFile(const QString &filePath) const override;
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override; void testProjectChunks(
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override; ProjectExplorer::Project *project, const FileChunker::ChunkingConfig &config);
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
IgnoreManager *ignoreManager() const;
private: private:
IgnoreManager *m_ignoreManager; explicit ContextManager(QObject *parent = nullptr);
~ContextManager() = default;
ContextManager(const ContextManager &) = delete;
ContextManager &operator=(const ContextManager &) = delete;
ContentFile createContentFile(const QString &filePath) const;
bool shouldProcessFile(const QString &filePath) const;
bool isInBuildDirectory(const QString &filePath) const;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,9 +19,9 @@
#include "DocumentContextReader.hpp" #include "DocumentContextReader.hpp"
#include <languageserverprotocol/lsptypes.h>
#include <QFileInfo> #include <QFileInfo>
#include <QTextBlock> #include <QTextBlock>
#include <languageserverprotocol/lsptypes.h>
#include "CodeCompletionSettings.hpp" #include "CodeCompletionSettings.hpp"
@@ -41,18 +41,17 @@ const QRegularExpression &getNameRegex()
const QRegularExpression &getCommentRegex() const QRegularExpression &getCommentRegex()
{ {
static const QRegularExpression commentRegex( static const QRegularExpression
R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))", QRegularExpression::MultilineOption); commentRegex(R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))",
QRegularExpression::MultilineOption);
return commentRegex; return commentRegex;
} }
namespace QodeAssist::Context { namespace QodeAssist::Context {
DocumentContextReader::DocumentContextReader( DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
QTextDocument *document, const QString &mimeType, const QString &filePath) : m_textDocument(textDocument)
: m_document(document) , m_document(textDocument->document())
, m_mimeType(mimeType)
, m_filePath(filePath)
{ {
m_copyrightInfo = findCopyright(); m_copyrightInfo = findCopyright();
} }
@@ -81,26 +80,26 @@ QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) c
return QString(); return QString();
} }
QString DocumentContextReader::getContextBefore( QString DocumentContextReader::getContextBefore(int lineNumber,
int lineNumber, int cursorPosition, int linesCount) const int cursorPosition,
int linesCount) const
{ {
int startLine = lineNumber - linesCount + 1; int effectiveStartLine;
if (m_copyrightInfo.found) { if (m_copyrightInfo.found) {
startLine = qMax(m_copyrightInfo.endLine + 1, startLine); effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - linesCount);
} else {
effectiveStartLine = qMax(0, lineNumber - linesCount);
} }
return getContextBetween(startLine, -1, lineNumber, cursorPosition); return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
} }
QString DocumentContextReader::getContextAfter( QString DocumentContextReader::getContextAfter(int lineNumber,
int lineNumber, int cursorPosition, int linesCount) const int cursorPosition,
int linesCount) const
{ {
int endLine = lineNumber + linesCount - 1; int endLine = qMin(m_document->blockCount() - 1, lineNumber + linesCount);
if (m_copyrightInfo.found && m_copyrightInfo.endLine >= lineNumber) { return getContextBetween(lineNumber + 1, endLine, cursorPosition);
lineNumber = m_copyrightInfo.endLine + 1;
cursorPosition = -1;
}
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
} }
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
@@ -110,26 +109,31 @@ QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPos
startLine = m_copyrightInfo.endLine + 1; startLine = m_copyrightInfo.endLine + 1;
} }
return getContextBetween(startLine, -1, lineNumber, cursorPosition); startLine = qMin(startLine, lineNumber);
QString result = getContextBetween(startLine, lineNumber, cursorPosition);
return result;
} }
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
{ {
int endLine = m_document->blockCount() - 1; return getContextBetween(lineNumber, m_document->blockCount() - 1, cursorPosition);
if (m_copyrightInfo.found && m_copyrightInfo.endLine >= lineNumber) {
lineNumber = m_copyrightInfo.endLine + 1;
cursorPosition = -1;
}
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
} }
QString DocumentContextReader::getLanguageAndFileInfo() const QString DocumentContextReader::getLanguageAndFileInfo() const
{ {
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_mimeType); if (!m_textDocument)
QString fileExtension = QFileInfo(m_filePath).suffix(); return QString();
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(
m_textDocument->mimeType());
QString mimeType = m_textDocument->mimeType();
QString filePath = m_textDocument->filePath().toString();
QString fileExtension = QFileInfo(filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n") return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, m_mimeType, m_filePath, fileExtension); .arg(language, mimeType, filePath, fileExtension);
} }
CopyrightInfo DocumentContextReader::findCopyright() CopyrightInfo DocumentContextReader::findCopyright()
@@ -145,24 +149,9 @@ CopyrightInfo DocumentContextReader::findCopyright()
QRegularExpressionMatch match = matchIterator.next(); QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower(); QString matchedText = match.captured().toLower();
bool hasCopyrightIndicator = matchedText.contains("copyright") if (matchedText.contains("copyright") || matchedText.contains("(C)")
|| matchedText.contains("(c)") || matchedText.contains("©") || matchedText.contains("(c)") || matchedText.contains("©")
|| matchedText.contains("copr.") || getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
|| matchedText.contains("all rights reserved")
|| matchedText.contains("proprietary")
|| matchedText.contains("licensed under")
|| matchedText.contains("license:")
|| matchedText.contains("gpl") || matchedText.contains("lgpl")
|| matchedText.contains("mit license")
|| matchedText.contains("apache license")
|| matchedText.contains("bsd license")
|| matchedText.contains("mozilla public license")
|| matchedText.contains("copyleft");
bool hasYear = getYearRegex().match(matchedText).hasMatch();
bool hasName = getNameRegex().match(matchedText).hasMatch();
if ((hasCopyrightIndicator && (hasYear || hasName)) || (hasYear && hasName)) {
int startPos = match.capturedStart(); int startPos = match.capturedStart();
int endPos = match.capturedEnd(); int endPos = match.capturedEnd();
@@ -190,75 +179,21 @@ CopyrightInfo DocumentContextReader::findCopyright()
return result; return result;
} }
QString DocumentContextReader::getContextBetween( QString DocumentContextReader::getContextBetween(int startLine,
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const int endLine,
int cursorPosition) const
{ {
QString context; QString context;
for (int i = startLine; i <= endLine; ++i) {
startLine = qMax(startLine, 0); QTextBlock block = m_document->findBlockByNumber(i);
endLine = qMin(endLine, m_document->blockCount() - 1);
if (startLine > endLine) {
return context;
}
if (startLine == endLine) {
auto block = m_document->findBlockByNumber(startLine);
if (!block.isValid()) { if (!block.isValid()) {
return context; break;
} }
if (i == endLine) {
auto text = block.text(); context += block.text().left(cursorPosition);
if (startCursorPosition < 0) {
startCursorPosition = 0;
}
if (endCursorPosition < 0) {
endCursorPosition = text.size();
}
if (startCursorPosition >= endCursorPosition) {
return context;
}
return text.mid(startCursorPosition, endCursorPosition - startCursorPosition);
}
// first line
{
auto block = m_document->findBlockByNumber(startLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (startCursorPosition < 0) {
context += text + "\n";
} else { } else {
context += text.right(text.size() - startCursorPosition) + "\n";
}
}
// intermediate lines, if any
for (int i = startLine + 1; i <= endLine - 1; ++i) {
auto block = m_document->findBlockByNumber(i);
if (!block.isValid()) {
return context;
}
context += block.text() + "\n"; context += block.text() + "\n";
} }
// last line
{
auto block = m_document->findBlockByNumber(endLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (endCursorPosition < 0) {
context += text;
} else {
context += text.left(endCursorPosition);
}
} }
return context; return context;
@@ -269,31 +204,47 @@ CopyrightInfo DocumentContextReader::copyrightInfo() const
return m_copyrightInfo; return m_copyrightInfo;
} }
LLMCore::ContextData DocumentContextReader::prepareContext( LLMCore::ContextData DocumentContextReader::prepareContext(int lineNumber, int cursorPosition) const
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
{ {
QString contextBefore; QString contextBefore = getContextBefore(lineNumber, cursorPosition);
QString contextAfter; QString contextAfter = getContextAfter(lineNumber, cursorPosition);
if (settings.readFullFile()) {
contextBefore = readWholeFileBefore(lineNumber, cursorPosition);
contextAfter = readWholeFileAfter(lineNumber, cursorPosition);
} else {
// Note that readStrings{After,Before}Cursor include current line, but linesCount argument of
// getContext{After,Before} do not
contextBefore
= getContextBefore(lineNumber, cursorPosition, settings.readStringsBeforeCursor() + 1);
contextAfter
= getContextAfter(lineNumber, cursorPosition, settings.readStringsAfterCursor() + 1);
}
QString fileContext; QString fileContext;
if (Settings::codeCompletionSettings().useFilePathInContext())
fileContext.append("\n ").append(getLanguageAndFileInfo()); fileContext.append("\n ").append(getLanguageAndFileInfo());
if (settings.useProjectChangesCache()) if (Settings::codeCompletionSettings().useProjectChangesCache())
fileContext.append("Recent Project Changes Context:\n ") fileContext.append("\n ").append(
.append(ChangesManager::instance().getRecentChangesContext(m_textDocument)); ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext}; return {contextBefore, contextAfter, fileContext};
}
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileBefore(lineNumber, cursorPosition);
} else {
int effectiveStartLine;
int beforeCursor = Settings::codeCompletionSettings().readStringsBeforeCursor();
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - beforeCursor);
} else {
effectiveStartLine = qMax(0, lineNumber - beforeCursor);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
}
QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileAfter(lineNumber, cursorPosition);
} else {
int endLine = qMin(m_document->blockCount() - 1,
lineNumber + Settings::codeCompletionSettings().readStringsAfterCursor());
return getContextBetween(lineNumber + 1, endLine, -1);
}
} }
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,7 +23,6 @@
#include <QTextDocument> #include <QTextDocument>
#include <llmcore/ContextData.hpp> #include <llmcore/ContextData.hpp>
#include <settings/CodeCompletionSettings.hpp>
namespace QodeAssist::Context { namespace QodeAssist::Context {
@@ -37,55 +36,28 @@ struct CopyrightInfo
class DocumentContextReader class DocumentContextReader
{ {
public: public:
DocumentContextReader( DocumentContextReader(TextEditor::TextDocument *textDocument);
QTextDocument *m_document, const QString &mimeType, const QString &filePath);
QString getLineText(int lineNumber, int cursorPosition = -1) const; QString getLineText(int lineNumber, int cursorPosition = -1) const;
/**
* @brief Retrieves @c linesCount lines of context ending at @c lineNumber at
* @c cursorPosition in that line. The line at @c lineNumber is inclusive regardless of
* @c cursorPosition.
*/
QString getContextBefore(int lineNumber, int cursorPosition, int linesCount) const; QString getContextBefore(int lineNumber, int cursorPosition, int linesCount) const;
/**
* @brief Retrieves @c linesCount lines of context starting at @c lineNumber at
* @c cursorPosition in that line. The line at @c lineNumber is inclusive regardless of
* @c cursorPosition.
*/
QString getContextAfter(int lineNumber, int cursorPosition, int linesCount) const; QString getContextAfter(int lineNumber, int cursorPosition, int linesCount) const;
/**
* @brief Retrieves whole file ending at @c lineNumber at @c cursorPosition in that line.
*/
QString readWholeFileBefore(int lineNumber, int cursorPosition) const; QString readWholeFileBefore(int lineNumber, int cursorPosition) const;
/**
* @brief Retrieves whole file starting at @c lineNumber at @c cursorPosition in that line.
*/
QString readWholeFileAfter(int lineNumber, int cursorPosition) const; QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
QString getLanguageAndFileInfo() const; QString getLanguageAndFileInfo() const;
CopyrightInfo findCopyright(); CopyrightInfo findCopyright();
QString getContextBetween( QString getContextBetween(int startLine, int endLine, int cursorPosition) const;
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const;
CopyrightInfo copyrightInfo() const; CopyrightInfo copyrightInfo() const;
LLMCore::ContextData prepareContext( LLMCore::ContextData prepareContext(int lineNumber, int cursorPosition) const;
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
private:
QString getContextBefore(int lineNumber, int cursorPosition) const;
QString getContextAfter(int lineNumber, int cursorPosition) const;
private: private:
TextEditor::TextDocument *m_textDocument; TextEditor::TextDocument *m_textDocument;
QTextDocument *m_document; QTextDocument *m_document;
QString m_mimeType;
QString m_filePath;
// Used to omit copyright headers from context. If context would otherwise include copyright
// header it is excluded by deleting it from the returned context. This means, that the
// returned context may contain less information than requested. If the cursor is within copyright
// header, then the context may be empty if the context window is small.
CopyrightInfo m_copyrightInfo; CopyrightInfo m_copyrightInfo;
}; };

View File

@@ -0,0 +1,265 @@
#include "EnhancedRAGSimilaritySearch.hpp"
#include <QSet>
namespace QodeAssist::Context {
// Static regex getters
const QRegularExpression &EnhancedRAGSimilaritySearch::getNamespaceRegex()
{
static const QRegularExpression regex(R"(namespace\s+(?:\w+\s*::\s*)*\w+\s*\{)");
return regex;
}
const QRegularExpression &EnhancedRAGSimilaritySearch::getClassRegex()
{
static const QRegularExpression regex(
R"((?:template\s*<[^>]*>\s*)?(?:class|struct)\s+(\w+)\s*(?:final\s*)?(?::\s*(?:public|protected|private)\s+\w+(?:\s*,\s*(?:public|protected|private)\s+\w+)*\s*)?{)");
return regex;
}
const QRegularExpression &EnhancedRAGSimilaritySearch::getFunctionRegex()
{
static const QRegularExpression regex(
R"((?:virtual\s+)?(?:static\s+)?(?:inline\s+)?(?:explicit\s+)?(?:constexpr\s+)?(?:[\w:]+\s+)?(?:\w+\s*::\s*)*\w+\s*\([^)]*\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:final\s*)?(?:=\s*0\s*)?(?:=\s*default\s*)?(?:=\s*delete\s*)?(?:\s*->.*?)?\s*{)");
return regex;
}
const QRegularExpression &EnhancedRAGSimilaritySearch::getTemplateRegex()
{
static const QRegularExpression regex(R"(template\s*<[^>]*>\s*(?:class|struct|typename)\s+\w+)");
return regex;
}
// Cache getters
QCache<QString, EnhancedRAGSimilaritySearch::SimilarityScore> &
EnhancedRAGSimilaritySearch::getScoreCache()
{
static QCache<QString, SimilarityScore> cache(1000); // Cache size of 1000 entries
return cache;
}
QCache<QString, QStringList> &EnhancedRAGSimilaritySearch::getStructureCache()
{
static QCache<QString, QStringList> cache(500); // Cache size of 500 entries
return cache;
}
// Main public interface
EnhancedRAGSimilaritySearch::SimilarityScore EnhancedRAGSimilaritySearch::calculateSimilarity(
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2)
{
// Generate cache key based on content hashes
QString cacheKey = QString("%1_%2").arg(qHash(code1)).arg(qHash(code2));
// Check cache first
auto &scoreCache = getScoreCache();
if (auto *cached = scoreCache.object(cacheKey)) {
return *cached;
}
// Calculate new similarity score
SimilarityScore score = calculateSimilarityInternal(v1, v2, code1, code2);
// Cache the result
scoreCache.insert(cacheKey, new SimilarityScore(score));
return score;
}
// Internal implementation
EnhancedRAGSimilaritySearch::SimilarityScore EnhancedRAGSimilaritySearch::calculateSimilarityInternal(
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2)
{
if (v1.empty() || v2.empty()) {
LOG_MESSAGE("Warning: Empty vectors in similarity calculation");
return SimilarityScore(0.0f, 0.0f, 0.0f);
}
if (v1.size() != v2.size()) {
LOG_MESSAGE(QString("Vector size mismatch: %1 vs %2").arg(v1.size()).arg(v2.size()));
return SimilarityScore(0.0f, 0.0f, 0.0f);
}
// Calculate semantic similarity using vector embeddings
float semantic_similarity = 0.0f;
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
if (v1.size() >= 4) { // Use SSE for vectors of 4 or more elements
semantic_similarity = calculateCosineSimilaritySSE(v1, v2);
} else {
semantic_similarity = calculateCosineSimilarity(v1, v2);
}
#else
semantic_similarity = calculateCosineSimilarity(v1, v2);
#endif
// If semantic similarity is very low, skip structural analysis
if (semantic_similarity < 0.0001f) {
return SimilarityScore(0.0f, 0.0f, 0.0f);
}
// Calculate structural similarity
float structural_similarity = calculateStructuralSimilarity(code1, code2);
// Calculate combined score with dynamic weights
float semantic_weight = 0.7f;
const int large_file_threshold = 10000;
if (code1.size() > large_file_threshold || code2.size() > large_file_threshold) {
semantic_weight = 0.8f; // Increase semantic weight for large files
}
float combined_score = semantic_weight * semantic_similarity
+ (1.0f - semantic_weight) * structural_similarity;
return SimilarityScore(semantic_similarity, structural_similarity, combined_score);
}
float EnhancedRAGSimilaritySearch::calculateCosineSimilarity(const RAGVector &v1, const RAGVector &v2)
{
float dotProduct = 0.0f;
float norm1 = 0.0f;
float norm2 = 0.0f;
for (size_t i = 0; i < v1.size(); ++i) {
dotProduct += v1[i] * v2[i];
norm1 += v1[i] * v1[i];
norm2 += v2[i] * v2[i];
}
norm1 = std::sqrt(norm1);
norm2 = std::sqrt(norm2);
if (norm1 == 0.0f || norm2 == 0.0f) {
return 0.0f;
}
return dotProduct / (norm1 * norm2);
}
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
float EnhancedRAGSimilaritySearch::calculateCosineSimilaritySSE(
const RAGVector &v1, const RAGVector &v2)
{
const float *p1 = v1.data();
const float *p2 = v2.data();
const size_t size = v1.size();
const size_t alignedSize = size & ~3ULL; // Round down to multiple of 4
__m128 sum = _mm_setzero_ps();
__m128 norm1 = _mm_setzero_ps();
__m128 norm2 = _mm_setzero_ps();
// Process 4 elements at a time using SSE
for (size_t i = 0; i < alignedSize; i += 4) {
__m128 v1_vec = _mm_loadu_ps(p1 + i); // Use unaligned load for safety
__m128 v2_vec = _mm_loadu_ps(p2 + i);
sum = _mm_add_ps(sum, _mm_mul_ps(v1_vec, v2_vec));
norm1 = _mm_add_ps(norm1, _mm_mul_ps(v1_vec, v1_vec));
norm2 = _mm_add_ps(norm2, _mm_mul_ps(v2_vec, v2_vec));
}
float dotProduct = horizontalSum(sum);
float n1 = std::sqrt(horizontalSum(norm1));
float n2 = std::sqrt(horizontalSum(norm2));
// Process remaining elements
for (size_t i = alignedSize; i < size; ++i) {
dotProduct += v1[i] * v2[i];
n1 += v1[i] * v1[i];
n2 += v2[i] * v2[i];
}
if (n1 == 0.0f || n2 == 0.0f) {
return 0.0f;
}
return dotProduct / (std::sqrt(n1) * std::sqrt(n2));
}
float EnhancedRAGSimilaritySearch::horizontalSum(__m128 x)
{
__m128 shuf = _mm_shuffle_ps(x, x, _MM_SHUFFLE(2, 3, 0, 1));
__m128 sums = _mm_add_ps(x, shuf);
shuf = _mm_movehl_ps(shuf, sums);
sums = _mm_add_ss(sums, shuf);
return _mm_cvtss_f32(sums);
}
#endif
float EnhancedRAGSimilaritySearch::calculateStructuralSimilarity(
const QString &code1, const QString &code2)
{
QStringList structures1 = extractStructures(code1);
QStringList structures2 = extractStructures(code2);
return calculateJaccardSimilarity(structures1, structures2);
}
QStringList EnhancedRAGSimilaritySearch::extractStructures(const QString &code)
{
// Check cache first
auto &structureCache = getStructureCache();
QString cacheKey = QString::number(qHash(code));
if (auto *cached = structureCache.object(cacheKey)) {
return *cached;
}
QStringList structures;
structures.reserve(100); // Reserve space for typical file
// Extract namespaces
auto namespaceMatches = getNamespaceRegex().globalMatch(code);
while (namespaceMatches.hasNext()) {
structures.append(namespaceMatches.next().captured().trimmed());
}
// Extract classes
auto classMatches = getClassRegex().globalMatch(code);
while (classMatches.hasNext()) {
structures.append(classMatches.next().captured().trimmed());
}
// Extract functions
auto functionMatches = getFunctionRegex().globalMatch(code);
while (functionMatches.hasNext()) {
structures.append(functionMatches.next().captured().trimmed());
}
// Extract templates
auto templateMatches = getTemplateRegex().globalMatch(code);
while (templateMatches.hasNext()) {
structures.append(templateMatches.next().captured().trimmed());
}
// Cache the result
structureCache.insert(cacheKey, new QStringList(structures));
return structures;
}
float EnhancedRAGSimilaritySearch::calculateJaccardSimilarity(
const QStringList &set1, const QStringList &set2)
{
if (set1.isEmpty() && set2.isEmpty()) {
return 1.0f; // Пустые множества считаем идентичными
}
if (set1.isEmpty() || set2.isEmpty()) {
return 0.0f;
}
QSet<QString> set1Unique = QSet<QString>(set1.begin(), set1.end());
QSet<QString> set2Unique = QSet<QString>(set2.begin(), set2.end());
QSet<QString> intersection = set1Unique;
intersection.intersect(set2Unique);
QSet<QString> union_set = set1Unique;
union_set.unite(set2Unique);
return static_cast<float>(intersection.size()) / union_set.size();
}
} // namespace QodeAssist::Context

View File

@@ -0,0 +1,74 @@
#pragma once
#include <QCache>
#include <QHash>
#include <QRegularExpression>
#include <QString>
#include <QStringList>
#include <QtGlobal>
#include <algorithm>
#include <cmath>
#include <optional>
#include <vector>
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
#include <emmintrin.h>
#include <xmmintrin.h>
#endif
#include "RAGData.hpp"
#include "logger/Logger.hpp"
namespace QodeAssist::Context {
class EnhancedRAGSimilaritySearch
{
public:
struct SimilarityScore
{
float semantic_similarity{0.0f};
float structural_similarity{0.0f};
float combined_score{0.0f};
SimilarityScore() = default;
SimilarityScore(float sem, float str, float comb)
: semantic_similarity(sem)
, structural_similarity(str)
, combined_score(comb)
{}
};
static SimilarityScore calculateSimilarity(
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2);
private:
static SimilarityScore calculateSimilarityInternal(
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2);
static float calculateCosineSimilarity(const RAGVector &v1, const RAGVector &v2);
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
static float calculateCosineSimilaritySSE(const RAGVector &v1, const RAGVector &v2);
static float horizontalSum(__m128 x);
#endif
static float calculateStructuralSimilarity(const QString &code1, const QString &code2);
static QStringList extractStructures(const QString &code);
static float calculateJaccardSimilarity(const QStringList &set1, const QStringList &set2);
static const QRegularExpression &getNamespaceRegex();
static const QRegularExpression &getClassRegex();
static const QRegularExpression &getFunctionRegex();
static const QRegularExpression &getTemplateRegex();
// Cache for similarity scores
static QCache<QString, SimilarityScore> &getScoreCache();
// Cache for extracted structures
static QCache<QString, QStringList> &getStructureCache();
EnhancedRAGSimilaritySearch() = delete; // Prevent instantiation
};
} // namespace QodeAssist::Context

198
context/FileChunker.cpp Normal file
View File

@@ -0,0 +1,198 @@
// FileChunker.cpp
#include "FileChunker.hpp"
#include <coreplugin/idocument.h>
#include <texteditor/syntaxhighlighter.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditorconstants.h>
#include <QFutureWatcher>
#include <QTimer>
namespace QodeAssist::Context {
FileChunker::FileChunker(QObject *parent)
: QObject(parent)
{}
FileChunker::FileChunker(const ChunkingConfig &config, QObject *parent)
: QObject(parent)
, m_config(config)
{}
QFuture<QList<FileChunk>> FileChunker::chunkFiles(const QStringList &filePaths)
{
qDebug() << "\nStarting chunking process for" << filePaths.size() << "files";
qDebug() << "Configuration:"
<< "\n Max lines per chunk:" << m_config.maxLinesPerChunk
<< "\n Overlap lines:" << m_config.overlapLines
<< "\n Skip empty lines:" << m_config.skipEmptyLines
<< "\n Preserve functions:" << m_config.preserveFunctions
<< "\n Preserve classes:" << m_config.preserveClasses
<< "\n Batch size:" << m_config.batchSize;
auto promise = std::make_shared<QPromise<QList<FileChunk>>>();
promise->start();
if (filePaths.isEmpty()) {
qDebug() << "No files to process";
promise->addResult({});
promise->finish();
return promise->future();
}
processNextBatch(promise, filePaths, 0);
return promise->future();
}
void FileChunker::processNextBatch(
std::shared_ptr<QPromise<QList<FileChunk>>> promise, const QStringList &files, int startIndex)
{
if (startIndex >= files.size()) {
emit chunkingComplete();
promise->finish();
return;
}
int endIndex = qMin(startIndex + m_config.batchSize, files.size());
QList<FileChunk> batchChunks;
for (int i = startIndex; i < endIndex; ++i) {
try {
auto chunks = processFile(files[i]);
batchChunks.append(chunks);
} catch (const std::exception &e) {
emit error(QString("Error processing file %1: %2").arg(files[i], e.what()));
}
emit progressUpdated(i + 1, files.size());
}
promise->addResult(batchChunks);
// Планируем обработку следующего батча
QTimer::singleShot(0, this, [this, promise, files, endIndex]() {
processNextBatch(promise, files, endIndex);
});
}
QList<FileChunk> FileChunker::processFile(const QString &filePath)
{
qDebug() << "\nProcessing file:" << filePath;
auto document = new TextEditor::TextDocument;
auto filePathObj = Utils::FilePath::fromString(filePath);
auto result = document->open(&m_error, filePathObj, filePathObj);
if (result != Core::IDocument::OpenResult::Success) {
qDebug() << "Failed to open document:" << filePath << "-" << m_error;
emit error(QString("Failed to open document: %1 - %2").arg(filePath, m_error));
delete document;
return {};
}
qDebug() << "Document opened successfully. Line count:" << document->document()->blockCount();
auto chunks = createChunksForDocument(document);
qDebug() << "Created" << chunks.size() << "chunks for file";
delete document;
return chunks;
}
QList<FileChunk> FileChunker::createChunksForDocument(TextEditor::TextDocument *document)
{
QList<FileChunk> chunks;
QString filePath = document->filePath().toString();
qDebug() << "\nCreating chunks for document:" << filePath << "\nConfiguration:"
<< "\n Max lines per chunk:" << m_config.maxLinesPerChunk
<< "\n Min lines per chunk:" << m_config.minLinesPerChunk
<< "\n Overlap lines:" << m_config.overlapLines;
// Если файл меньше минимального размера чанка, создаем один чанк
if (document->document()->blockCount() <= m_config.minLinesPerChunk) {
FileChunk chunk;
chunk.filePath = filePath;
chunk.startLine = 0;
chunk.endLine = document->document()->blockCount() - 1;
chunk.createdAt = QDateTime::currentDateTime();
chunk.updatedAt = chunk.createdAt;
QString content;
QTextBlock block = document->document()->firstBlock();
while (block.isValid()) {
content += block.text() + "\n";
block = block.next();
}
chunk.content = content;
qDebug() << "File is smaller than minimum chunk size. Creating single chunk:"
<< "\n Lines:" << chunk.lineCount() << "\n Content size:" << chunk.content.size()
<< "bytes";
chunks.append(chunk);
return chunks;
}
// Для больших файлов создаем чанки фиксированного размера с перекрытием
int currentStartLine = 0;
int lineCount = 0;
QString content;
QTextBlock block = document->document()->firstBlock();
while (block.isValid()) {
content += block.text() + "\n";
lineCount++;
// Если достигли размера чанка или это последний блок
if (lineCount >= m_config.maxLinesPerChunk || !block.next().isValid()) {
FileChunk chunk;
chunk.filePath = filePath;
chunk.startLine = currentStartLine;
chunk.endLine = currentStartLine + lineCount - 1;
chunk.content = content;
chunk.createdAt = QDateTime::currentDateTime();
chunk.updatedAt = chunk.createdAt;
qDebug() << "Creating chunk:"
<< "\n Start line:" << chunk.startLine << "\n End line:" << chunk.endLine
<< "\n Lines:" << chunk.lineCount()
<< "\n Content size:" << chunk.content.size() << "bytes";
chunks.append(chunk);
// Начинаем новый чанк с учетом перекрытия
if (block.next().isValid()) {
// Отступаем назад на размер перекрытия
int overlapLines = qMin(m_config.overlapLines, lineCount);
currentStartLine = chunk.endLine - overlapLines + 1;
// Сбрасываем контент, но добавляем перекрывающиеся строки
content.clear();
QTextBlock overlapBlock = document->document()->findBlockByLineNumber(
currentStartLine);
while (overlapBlock.isValid() && overlapBlock.blockNumber() <= chunk.endLine) {
content += overlapBlock.text() + "\n";
overlapBlock = overlapBlock.next();
}
lineCount = overlapLines;
}
}
block = block.next();
}
qDebug() << "Finished creating chunks for file:" << filePath
<< "\nTotal chunks:" << chunks.size();
return chunks;
}
void FileChunker::setConfig(const ChunkingConfig &config)
{
m_config = config;
}
FileChunker::ChunkingConfig FileChunker::config() const
{
return m_config;
}
} // namespace QodeAssist::Context

68
context/FileChunker.hpp Normal file
View File

@@ -0,0 +1,68 @@
// FileChunker.hpp
#pragma once
#include <texteditor/textdocument.h>
#include <QDateTime>
#include <QFuture>
#include <QString>
namespace QodeAssist::Context {
struct FileChunk
{
QString filePath; // Path to the source file
int startLine; // Starting line of the chunk
int endLine; // Ending line of the chunk
QDateTime createdAt; // When the chunk was created
QDateTime updatedAt; // When the chunk was last updated
QString content; // Content of the chunk
// Helper methods
int lineCount() const { return endLine - startLine + 1; }
bool isValid() const { return !filePath.isEmpty() && startLine >= 0 && endLine >= startLine; }
};
class FileChunker : public QObject
{
Q_OBJECT
public:
struct ChunkingConfig
{
int maxLinesPerChunk = 80;
int minLinesPerChunk = 40;
int overlapLines = 20;
bool skipEmptyLines = true;
bool preserveFunctions = true;
bool preserveClasses = true;
int batchSize = 10;
};
explicit FileChunker(QObject *parent = nullptr);
explicit FileChunker(const ChunkingConfig &config, QObject *parent = nullptr);
// Main chunking method
QFuture<QList<FileChunk>> chunkFiles(const QStringList &filePaths);
// Configuration
void setConfig(const ChunkingConfig &config);
ChunkingConfig config() const;
signals:
void progressUpdated(int processedFiles, int totalFiles);
void chunkingComplete();
void error(const QString &errorMessage);
private:
QList<FileChunk> processFile(const QString &filePath);
QList<FileChunk> createChunksForDocument(TextEditor::TextDocument *document);
void processNextBatch(
std::shared_ptr<QPromise<QList<FileChunk>>> promise,
const QStringList &files,
int startIndex);
ChunkingConfig m_config;
QString m_error;
};
} // namespace QodeAssist::Context

View File

@@ -1,49 +0,0 @@
/*
* 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 <QString>
#include "ContentFile.hpp"
#include "IDocumentReader.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class IContextManager
{
public:
virtual ~IContextManager() = default;
virtual QString readFile(const QString &filePath) const = 0;
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
virtual QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const = 0;
virtual ContentFile createContentFile(const QString &filePath) const = 0;
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;
virtual bool isSpecifyCompletion(const DocumentInfo &documentInfo) const = 0;
};
} // namespace QodeAssist::Context

View File

@@ -1,274 +0,0 @@
/*
* 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 "IgnoreManager.hpp"
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QTextStream>
#include "logger/Logger.hpp"
namespace QodeAssist::Context {
IgnoreManager::IgnoreManager(QObject *parent)
: QObject(parent)
{
auto projectManager = ProjectExplorer::ProjectManager::instance();
if (projectManager) {
connect(
projectManager,
&ProjectExplorer::ProjectManager::projectRemoved,
this,
&IgnoreManager::removeIgnorePatterns);
}
connect(
QCoreApplication::instance(),
&QCoreApplication::aboutToQuit,
this,
&IgnoreManager::cleanupConnections);
}
IgnoreManager::~IgnoreManager()
{
cleanupConnections();
}
void IgnoreManager::cleanupConnections()
{
QList<ProjectExplorer::Project *> projects = m_projectConnections.keys();
for (ProjectExplorer::Project *project : projects) {
if (project) {
disconnect(m_projectConnections.take(project));
}
}
m_projectConnections.clear();
m_projectIgnorePatterns.clear();
m_ignoreCache.clear();
}
bool IgnoreManager::shouldIgnore(const QString &filePath, ProjectExplorer::Project *project) const
{
if (!project)
return false;
if (!m_projectIgnorePatterns.contains(project)) {
const_cast<IgnoreManager *>(this)->reloadIgnorePatterns(project);
}
const QStringList &patterns = m_projectIgnorePatterns[project];
if (patterns.isEmpty())
return false;
QDir projectDir(project->projectDirectory().toUrlishString());
QString relativePath = projectDir.relativeFilePath(filePath);
return matchesIgnorePatterns(relativePath, patterns);
}
bool IgnoreManager::matchesIgnorePatterns(const QString &path, const QStringList &patterns) const
{
QString cacheKey = path + ":" + patterns.join("|");
if (m_ignoreCache.contains(cacheKey))
return m_ignoreCache[cacheKey];
bool result = isPathExcluded(path, patterns);
m_ignoreCache.insert(cacheKey, result);
return result;
}
bool IgnoreManager::isPathExcluded(const QString &path, const QStringList &patterns) const
{
bool excluded = false;
for (const QString &pattern : patterns) {
if (pattern.isEmpty() || pattern.startsWith('#'))
continue;
bool isNegative = pattern.startsWith('!');
QString actualPattern = isNegative ? pattern.mid(1) : pattern;
bool matches = matchPathWithPattern(path, actualPattern);
if (matches) {
excluded = !isNegative;
}
}
return excluded;
}
bool IgnoreManager::matchPathWithPattern(const QString &path, const QString &pattern) const
{
QString adjustedPattern = pattern.trimmed();
bool matchFromRoot = adjustedPattern.startsWith('/');
if (matchFromRoot)
adjustedPattern = adjustedPattern.mid(1);
bool matchDirOnly = adjustedPattern.endsWith('/');
if (matchDirOnly)
adjustedPattern.chop(1);
QString regexPattern = QRegularExpression::escape(adjustedPattern);
regexPattern.replace("\\*\\*", ".*");
regexPattern.replace("\\*", "[^/]*");
regexPattern.replace("\\?", ".");
if (matchFromRoot)
regexPattern = QString("^%1").arg(regexPattern);
else
regexPattern = QString("(^|/)%1").arg(regexPattern);
if (matchDirOnly)
regexPattern = QString("%1$").arg(regexPattern);
else
regexPattern = QString("%1($|/)").arg(regexPattern);
QRegularExpression regex(regexPattern);
QRegularExpressionMatch match = regex.match(path);
return match.hasMatch();
}
QStringList IgnoreManager::loadIgnorePatterns(ProjectExplorer::Project *project)
{
QStringList patterns;
if (!project)
return patterns;
QString ignoreFile = ignoreFilePath(project);
if (ignoreFile.isEmpty() || !QFile::exists(ignoreFile)) {
// LOG_MESSAGE(
// QString("No .qodeassistignore file found for project: %1").arg(project->displayName()));
return patterns;
}
QFile file(ignoreFile);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
LOG_MESSAGE(QString("Could not open .qodeassistignore file: %1").arg(ignoreFile));
return patterns;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (!line.isEmpty() && !line.startsWith('#'))
patterns << line;
}
LOG_MESSAGE(QString("Successfully loaded .qodeassistignore file: %1 with %2 patterns")
.arg(ignoreFile)
.arg(patterns.size()));
return patterns;
}
void IgnoreManager::reloadIgnorePatterns(ProjectExplorer::Project *project)
{
if (!project)
return;
QStringList patterns = loadIgnorePatterns(project);
m_projectIgnorePatterns[project] = patterns;
QStringList keysToRemove;
QString projectPath = project->projectDirectory().toUrlishString();
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(projectPath))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
if (!m_projectConnections.contains(project)) {
QPointer<ProjectExplorer::Project> projectPtr(project);
auto connection = connect(project, &QObject::destroyed, this, [this, projectPtr]() {
if (projectPtr) {
m_projectIgnorePatterns.remove(projectPtr);
m_projectConnections.remove(projectPtr);
QStringList keysToRemove;
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(projectPtr->projectDirectory().toUrlishString()))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
}
});
m_projectConnections[project] = connection;
}
}
void IgnoreManager::removeIgnorePatterns(ProjectExplorer::Project *project)
{
m_projectIgnorePatterns.remove(project);
QStringList keysToRemove;
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(project->projectDirectory().toUrlishString()))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
if (m_projectConnections.contains(project)) {
disconnect(m_projectConnections[project]);
m_projectConnections.remove(project);
}
LOG_MESSAGE(QString("Removed ignore patterns for project: %1").arg(project->displayName()));
}
void IgnoreManager::reloadAllPatterns()
{
QList<ProjectExplorer::Project *> projects = m_projectIgnorePatterns.keys();
for (ProjectExplorer::Project *project : projects) {
if (project) {
reloadIgnorePatterns(project);
}
}
m_ignoreCache.clear();
}
QString IgnoreManager::ignoreFilePath(ProjectExplorer::Project *project) const
{
if (!project) {
return QString();
}
return project->projectDirectory().toUrlishString() + "/.qodeassistignore";
}
} // namespace QodeAssist::Context

View File

@@ -1,62 +0,0 @@
/*
* 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 <QObject>
#include <QPointer>
#include <QStringList>
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class IgnoreManager : public QObject
{
Q_OBJECT
public:
explicit IgnoreManager(QObject *parent = nullptr);
~IgnoreManager() override;
bool shouldIgnore(const QString &filePath, ProjectExplorer::Project *project = nullptr) const;
void reloadIgnorePatterns(ProjectExplorer::Project *project);
void removeIgnorePatterns(ProjectExplorer::Project *project);
void reloadAllPatterns();
private slots:
void cleanupConnections();
private:
bool matchesIgnorePatterns(const QString &path, const QStringList &patterns) const;
bool isPathExcluded(const QString &path, const QStringList &patterns) const;
bool matchPathWithPattern(const QString &path, const QString &pattern) const;
QStringList loadIgnorePatterns(ProjectExplorer::Project *project);
QString ignoreFilePath(ProjectExplorer::Project *project) const;
QHash<ProjectExplorer::Project *, QStringList> m_projectIgnorePatterns;
mutable QHash<QString, bool> m_ignoreCache;
QHash<ProjectExplorer::Project *, QMetaObject::Connection> m_projectConnections;
};
} // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -24,8 +24,8 @@
namespace QodeAssist::Context { namespace QodeAssist::Context {
enum class ProgrammingLanguage { enum class ProgrammingLanguage {
QML, // QML/JavaScript QML,
Cpp, // C/C++ Cpp,
Python, Python,
Unknown, Unknown,
}; };

7
context/RAGData.hpp Normal file
View File

@@ -0,0 +1,7 @@
#pragma once
#include <vector>
namespace QodeAssist::Context {
using RAGVector = std::vector<float>;
}

443
context/RAGManager.cpp Normal file
View File

@@ -0,0 +1,443 @@
/*
* Copyright (C) 2024 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 "RAGManager.hpp"
#include "EnhancedRAGSimilaritySearch.hpp"
#include "RAGPreprocessor.hpp"
#include "RAGSimilaritySearch.hpp"
#include "logger/Logger.hpp"
#include <coreplugin/icore.h>
#include <projectexplorer/project.h>
#include <QFile>
#include <QtConcurrent>
namespace QodeAssist::Context {
RAGManager &RAGManager::instance()
{
static RAGManager manager;
return manager;
}
RAGManager::RAGManager(QObject *parent)
: QObject(parent)
, m_vectorizer(std::make_unique<RAGVectorizer>())
{}
RAGManager::~RAGManager() {}
QString RAGManager::getStoragePath(ProjectExplorer::Project *project) const
{
return QString("%1/qodeassist/%2/rag/vectors.db")
.arg(Core::ICore::userResourcePath().toString(), project->displayName());
}
std::optional<QString> RAGManager::loadFileContent(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "ERROR: Failed to open file for reading:" << filePath
<< "Error:" << file.errorString();
return std::nullopt;
}
QFileInfo fileInfo(filePath);
qDebug() << "Loading content from file:" << fileInfo.fileName() << "Size:" << fileInfo.size()
<< "bytes";
QString content = QString::fromUtf8(file.readAll());
if (content.isEmpty()) {
qDebug() << "WARNING: Empty content read from file:" << filePath;
}
return content;
}
void RAGManager::ensureStorageForProject(ProjectExplorer::Project *project) const
{
qDebug() << "Ensuring storage for project:" << project->displayName();
if (m_currentProject == project && m_currentStorage) {
qDebug() << "Using existing storage";
return;
}
qDebug() << "Creating new storage";
m_currentStorage.reset();
m_currentProject = project;
if (project) {
QString storagePath = getStoragePath(project);
qDebug() << "Storage path:" << storagePath;
StorageOptions options;
m_currentStorage = std::make_unique<RAGStorage>(storagePath, options);
qDebug() << "Initializing storage...";
if (!m_currentStorage->init()) {
qDebug() << "Failed to initialize storage";
m_currentStorage.reset();
return;
}
qDebug() << "Storage initialized successfully";
}
}
QFuture<void> RAGManager::processProjectFiles(
ProjectExplorer::Project *project,
const QStringList &filePaths,
const FileChunker::ChunkingConfig &config)
{
qDebug() << "\nStarting batch processing of" << filePaths.size()
<< "files for project:" << project->displayName();
auto promise = std::make_shared<QPromise<void>>();
promise->start();
qDebug() << "Initializing storage...";
ensureStorageForProject(project);
if (!m_currentStorage) {
qDebug() << "Failed to initialize storage for project:" << project->displayName();
promise->finish();
return promise->future();
}
qDebug() << "Storage initialized successfully";
qDebug() << "Checking files for processing...";
QSet<QString> uniqueFiles;
for (const QString &filePath : filePaths) {
qDebug() << "Checking file:" << filePath;
if (isFileStorageOutdated(project, filePath)) {
qDebug() << "File needs processing:" << filePath;
uniqueFiles.insert(filePath);
}
}
QStringList filesToProcess = uniqueFiles.values();
if (filesToProcess.isEmpty()) {
qDebug() << "No files need processing";
emit vectorizationFinished();
promise->finish();
return promise->future();
}
qDebug() << "Starting to process" << filesToProcess.size() << "files";
const int batchSize = 10;
processNextFileBatch(promise, project, filesToProcess, config, 0, batchSize);
return promise->future();
}
void RAGManager::processNextFileBatch(
std::shared_ptr<QPromise<void>> promise,
ProjectExplorer::Project *project,
const QStringList &files,
const FileChunker::ChunkingConfig &config,
int startIndex,
int batchSize)
{
if (startIndex >= files.size()) {
qDebug() << "All batches processed successfully";
emit vectorizationFinished();
promise->finish();
return;
}
int endIndex = qMin(startIndex + batchSize, files.size());
auto currentBatch = files.mid(startIndex, endIndex - startIndex);
qDebug() << "\nProcessing batch" << (startIndex / batchSize + 1) << "(" << currentBatch.size()
<< "files)"
<< "\nProgress:" << startIndex << "to" << endIndex << "of" << files.size();
for (const QString &filePath : currentBatch) {
qDebug() << "Starting processing file:" << filePath;
auto future = processFileWithChunks(project, filePath, config);
auto watcher = new QFutureWatcher<bool>;
watcher->setFuture(future);
connect(
watcher,
&QFutureWatcher<bool>::finished,
this,
[this,
watcher,
promise,
project,
files,
startIndex,
endIndex,
batchSize,
config,
filePath]() {
bool success = watcher->result();
qDebug() << "File processed:" << filePath << "success:" << success;
bool isLastFileInBatch = (filePath == files[endIndex - 1]);
if (isLastFileInBatch) {
qDebug() << "Batch completed, moving to next batch";
emit vectorizationProgress(endIndex, files.size());
processNextFileBatch(promise, project, files, config, endIndex, batchSize);
}
watcher->deleteLater();
});
}
}
QFuture<bool> RAGManager::processFileWithChunks(
ProjectExplorer::Project *project,
const QString &filePath,
const FileChunker::ChunkingConfig &config)
{
auto promise = std::make_shared<QPromise<bool>>();
promise->start();
ensureStorageForProject(project);
if (!m_currentStorage) {
qDebug() << "Storage not initialized for file:" << filePath;
promise->addResult(false);
promise->finish();
return promise->future();
}
auto fileContent = loadFileContent(filePath);
if (!fileContent) {
qDebug() << "Failed to load content for file:" << filePath;
promise->addResult(false);
promise->finish();
return promise->future();
}
qDebug() << "Creating chunks for file:" << filePath;
auto chunksFuture = m_chunker.chunkFiles({filePath});
auto chunks = chunksFuture.result();
if (chunks.isEmpty()) {
qDebug() << "No chunks created for file:" << filePath;
promise->addResult(false);
promise->finish();
return promise->future();
}
qDebug() << "Created" << chunks.size() << "chunks for file:" << filePath;
// Преобразуем FileChunk в FileChunkData
QList<FileChunkData> chunkData;
for (const auto &chunk : chunks) {
FileChunkData data;
data.filePath = chunk.filePath;
data.startLine = chunk.startLine;
data.endLine = chunk.endLine;
data.content = chunk.content;
chunkData.append(data);
}
qDebug() << "Deleting old chunks for file:" << filePath;
m_currentStorage->deleteChunksForFile(filePath);
auto vectorizeFuture = vectorizeAndStoreChunks(filePath, chunkData);
auto watcher = new QFutureWatcher<void>;
watcher->setFuture(vectorizeFuture);
connect(watcher, &QFutureWatcher<void>::finished, this, [promise, watcher, filePath]() {
qDebug() << "Completed processing file:" << filePath;
promise->addResult(true);
promise->finish();
watcher->deleteLater();
});
return promise->future();
}
QFuture<void> RAGManager::vectorizeAndStoreChunks(
const QString &filePath, const QList<FileChunkData> &chunks)
{
qDebug() << "Vectorizing and storing" << chunks.size() << "chunks for file:" << filePath;
auto promise = std::make_shared<QPromise<void>>();
promise->start();
// Обрабатываем чанки последовательно
processNextChunk(promise, chunks, 0);
return promise->future();
}
void RAGManager::processNextChunk(
std::shared_ptr<QPromise<void>> promise, const QList<FileChunkData> &chunks, int currentIndex)
{
if (currentIndex >= chunks.size()) {
promise->finish();
return;
}
const auto &chunk = chunks[currentIndex];
QString processedContent = RAGPreprocessor::preprocessCode(chunk.content);
qDebug() << "Processing chunk" << currentIndex + 1 << "of" << chunks.size();
auto vectorFuture = m_vectorizer->vectorizeText(processedContent);
auto watcher = new QFutureWatcher<RAGVector>;
watcher->setFuture(vectorFuture);
connect(
watcher,
&QFutureWatcher<RAGVector>::finished,
this,
[this, watcher, promise, chunks, currentIndex, chunk]() {
auto vector = watcher->result();
if (!vector.empty()) {
qDebug() << "Storing vector and chunk for file:" << chunk.filePath;
bool vectorStored = m_currentStorage->storeVector(chunk.filePath, vector);
bool chunkStored = m_currentStorage->storeChunk(chunk);
qDebug() << "Storage results - Vector:" << vectorStored << "Chunk:" << chunkStored;
} else {
qDebug() << "Failed to vectorize chunk content";
}
processNextChunk(promise, chunks, currentIndex + 1);
watcher->deleteLater();
});
}
QFuture<QList<RAGManager::ChunkSearchResult>> RAGManager::findRelevantChunks(
const QString &query, ProjectExplorer::Project *project, int topK)
{
auto promise = std::make_shared<QPromise<QList<ChunkSearchResult>>>();
promise->start();
ensureStorageForProject(project);
if (!m_currentStorage) {
qDebug() << "Storage not initialized for project:" << project->displayName();
promise->addResult({});
promise->finish();
return promise->future();
}
QString processedQuery = RAGPreprocessor::preprocessCode(query);
auto vectorFuture = m_vectorizer->vectorizeText(processedQuery);
vectorFuture.then([this, promise, project, processedQuery, topK](const RAGVector &queryVector) {
if (queryVector.empty()) {
qDebug() << "Failed to vectorize query";
promise->addResult({});
promise->finish();
return;
}
auto files = m_currentStorage->getFilesWithChunks();
QList<FileChunkData> allChunks;
for (const auto &filePath : files) {
auto fileChunks = m_currentStorage->getChunksForFile(filePath);
allChunks.append(fileChunks);
}
auto results = rankChunks(queryVector, processedQuery, allChunks);
if (results.size() > topK) {
results = results.mid(0, topK);
}
qDebug() << "Found" << results.size() << "relevant chunks";
promise->addResult(results);
promise->finish();
closeStorage();
});
return promise->future();
}
QList<RAGManager::ChunkSearchResult> RAGManager::rankChunks(
const RAGVector &queryVector, const QString &queryText, const QList<FileChunkData> &chunks)
{
QList<ChunkSearchResult> results;
results.reserve(chunks.size());
for (const auto &chunk : chunks) {
auto chunkVector = m_currentStorage->getVector(chunk.filePath);
if (!chunkVector.has_value()) {
continue;
}
QString processedChunk = RAGPreprocessor::preprocessCode(chunk.content);
auto similarity = EnhancedRAGSimilaritySearch::calculateSimilarity(
queryVector, chunkVector.value(), queryText, processedChunk);
results.append(ChunkSearchResult{
chunk.filePath,
chunk.startLine,
chunk.endLine,
chunk.content,
similarity.semantic_similarity,
similarity.structural_similarity,
similarity.combined_score});
}
std::sort(results.begin(), results.end());
return results;
}
QStringList RAGManager::getStoredFiles(ProjectExplorer::Project *project) const
{
ensureStorageForProject(project);
if (!m_currentStorage) {
return {};
}
return m_currentStorage->getAllFiles();
}
bool RAGManager::isFileStorageOutdated(
ProjectExplorer::Project *project, const QString &filePath) const
{
ensureStorageForProject(project);
if (!m_currentStorage) {
return true;
}
return m_currentStorage->needsUpdate(filePath);
}
std::optional<RAGVector> RAGManager::loadVectorFromStorage(
ProjectExplorer::Project *project, const QString &filePath)
{
ensureStorageForProject(project);
if (!m_currentStorage) {
return std::nullopt;
}
return m_currentStorage->getVector(filePath);
}
void RAGManager::closeStorage()
{
qDebug() << "Closing storage...";
if (m_currentStorage) {
m_currentStorage.reset();
m_currentProject = nullptr;
qDebug() << "Storage closed";
}
}
} // namespace QodeAssist::Context

119
context/RAGManager.hpp Normal file
View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2024 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 <memory>
#include <optional>
#include <QFuture>
#include <QObject>
#include "FileChunker.hpp"
#include "RAGData.hpp"
#include "RAGStorage.hpp"
#include "RAGVectorizer.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class RAGManager : public QObject
{
Q_OBJECT
public:
struct ChunkSearchResult
{
QString filePath;
int startLine;
int endLine;
QString content;
float semanticScore;
float structuralScore;
float combinedScore;
bool operator<(const ChunkSearchResult &other) const
{
return combinedScore > other.combinedScore;
}
};
static RAGManager &instance();
QFuture<void> processProjectFiles(
ProjectExplorer::Project *project,
const QStringList &filePaths,
const FileChunker::ChunkingConfig &config = FileChunker::ChunkingConfig());
QFuture<QList<ChunkSearchResult>> findRelevantChunks(
const QString &query, ProjectExplorer::Project *project, int topK = 5);
QStringList getStoredFiles(ProjectExplorer::Project *project) const;
bool isFileStorageOutdated(ProjectExplorer::Project *project, const QString &filePath) const;
void processNextChunk(
std::shared_ptr<QPromise<void>> promise,
const QList<FileChunkData> &chunks,
int currentIndex);
void closeStorage();
signals:
void vectorizationProgress(int processed, int total);
void vectorizationFinished();
private:
explicit RAGManager(QObject *parent = nullptr);
~RAGManager();
RAGManager(const RAGManager &) = delete;
RAGManager &operator=(const RAGManager &) = delete;
QString getStoragePath(ProjectExplorer::Project *project) const;
void ensureStorageForProject(ProjectExplorer::Project *project) const;
std::optional<QString> loadFileContent(const QString &filePath);
std::optional<RAGVector> loadVectorFromStorage(
ProjectExplorer::Project *project, const QString &filePath);
void processNextFileBatch(
std::shared_ptr<QPromise<void>> promise,
ProjectExplorer::Project *project,
const QStringList &files,
const FileChunker::ChunkingConfig &config,
int startIndex,
int batchSize);
QFuture<bool> processFileWithChunks(
ProjectExplorer::Project *project,
const QString &filePath,
const FileChunker::ChunkingConfig &config);
QFuture<void> vectorizeAndStoreChunks(
const QString &filePath, const QList<FileChunkData> &chunks);
QList<ChunkSearchResult> rankChunks(
const RAGVector &queryVector, const QString &queryText, const QList<FileChunkData> &chunks);
private:
mutable std::unique_ptr<RAGVectorizer> m_vectorizer;
mutable std::unique_ptr<RAGStorage> m_currentStorage;
mutable ProjectExplorer::Project *m_currentProject{nullptr};
FileChunker m_chunker;
};
} // namespace QodeAssist::Context

View File

@@ -0,0 +1,2 @@
#include "RAGPreprocessor.hpp"

View File

@@ -0,0 +1,64 @@
#include <QRegularExpression>
#include <QString>
#include "Logger.hpp"
namespace QodeAssist::Context {
class RAGPreprocessor
{
public:
static const QRegularExpression &getLicenseRegex()
{
static const QRegularExpression regex(
R"((/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)|//[^\n]*(?:\n|$))",
QRegularExpression::MultilineOption);
return regex;
}
static const QRegularExpression &getClassRegex()
{
static const QRegularExpression regex(
R"((?:template\s*<[^>]*>\s*)?(?:class|struct)\s+(\w+)\s*(?:final\s*)?(?::\s*(?:public|protected|private)\s+\w+(?:\s*,\s*(?:public|protected|private)\s+\w+)*\s*)?{)");
return regex;
}
static QString preprocessCode(const QString &code)
{
if (code.isEmpty()) {
return QString();
}
try {
QStringList lines = code.split('\n', Qt::SkipEmptyParts);
return processLines(lines);
} catch (const std::exception &e) {
LOG_MESSAGE(QString("Error preprocessing code: %1").arg(e.what()));
return code;
}
}
private:
static QString processLines(const QStringList &lines)
{
const int estimatedAvgLength = 80;
QString result;
result.reserve(lines.size() * estimatedAvgLength);
for (const QString &line : lines) {
const QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
result += trimmed;
result += QLatin1Char('\n');
}
}
if (result.endsWith('\n')) {
result.chop(1);
}
return result;
}
};
} // namespace QodeAssist::Context

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2024 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 "RAGSimilaritySearch.hpp"
#include "logger/Logger.hpp"
#include <cmath>
namespace QodeAssist::Context {
float RAGSimilaritySearch::l2Distance(const RAGVector &v1, const RAGVector &v2)
{
if (v1.size() != v2.size()) {
LOG_MESSAGE(QString("Vector size mismatch: %1 vs %2").arg(v1.size()).arg(v2.size()));
return std::numeric_limits<float>::max();
}
float sum = 0.0f;
for (size_t i = 0; i < v1.size(); ++i) {
float diff = v1[i] - v2[i];
sum += diff * diff;
}
return std::sqrt(sum);
}
float RAGSimilaritySearch::cosineSimilarity(const RAGVector &v1, const RAGVector &v2)
{
if (v1.size() != v2.size()) {
LOG_MESSAGE(QString("Vector size mismatch: %1 vs %2").arg(v1.size()).arg(v2.size()));
return 0.0f;
}
float dotProduct = 0.0f;
float norm1 = 0.0f;
float norm2 = 0.0f;
for (size_t i = 0; i < v1.size(); ++i) {
dotProduct += v1[i] * v2[i];
norm1 += v1[i] * v1[i];
norm2 += v2[i] * v2[i];
}
norm1 = std::sqrt(norm1);
norm2 = std::sqrt(norm2);
if (norm1 == 0.0f || norm2 == 0.0f)
return 0.0f;
return dotProduct / (norm1 * norm2);
}
} // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2025 Povilas Kanapickas * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,23 +19,19 @@
#pragma once #pragma once
#include <QTextDocument> #include "RAGData.hpp"
namespace QodeAssist::Context { namespace QodeAssist::Context {
struct DocumentInfo class RAGSimilaritySearch
{
QTextDocument *document = nullptr; // not owned
QString mimeType;
QString filePath;
};
class IDocumentReader
{ {
public: public:
virtual ~IDocumentReader() = default; static float l2Distance(const RAGVector &v1, const RAGVector &v2);
virtual DocumentInfo readDocument(const QString &path) const = 0; static float cosineSimilarity(const RAGVector &v1, const RAGVector &v2);
private:
RAGSimilaritySearch() = delete;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

1047
context/RAGStorage.cpp Normal file

File diff suppressed because it is too large Load Diff

174
context/RAGStorage.hpp Normal file
View File

@@ -0,0 +1,174 @@
/*
* Copyright (C) 2024 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/>.
*/
// RAGStorage.hpp
#pragma once
#include <optional>
#include <QDateTime>
#include <QMutex>
#include <QObject>
#include <QSqlDatabase>
#include <QString>
#include <qsqlquery.h>
#include <RAGData.hpp>
namespace QodeAssist::Context {
struct FileChunkData
{
QString filePath;
int startLine;
int endLine;
QString content;
QDateTime createdAt;
QDateTime updatedAt;
bool isValid() const
{
return !filePath.isEmpty() && startLine >= 0 && endLine >= startLine && !content.isEmpty();
}
};
struct StorageOptions
{
int maxChunkSize = 1024 * 1024;
int maxVectorSize = 1024;
bool useCompression = false;
bool enableLogging = false;
};
struct StorageStatistics
{
int totalChunks;
int totalVectors;
int totalFiles;
qint64 totalSize;
QDateTime lastUpdate;
};
class RAGStorage : public QObject
{
Q_OBJECT
public:
static constexpr int CURRENT_VERSION = 1;
enum class Status { Ok, DatabaseError, ValidationError, VersionError, ConnectionError };
struct ValidationResult
{
bool isValid;
QString errorMessage;
Status errorStatus;
};
struct Error
{
QString message;
QString sqlError;
QString query;
Status status;
};
explicit RAGStorage(
const QString &dbPath,
const StorageOptions &options = StorageOptions(),
QObject *parent = nullptr);
~RAGStorage();
bool init();
Status status() const;
Error lastError() const;
bool isReady() const;
QString dbPath() const;
bool beginTransaction();
bool commitTransaction();
bool rollbackTransaction();
bool storeVector(const QString &filePath, const RAGVector &vector);
bool updateVector(const QString &filePath, const RAGVector &vector);
std::optional<RAGVector> getVector(const QString &filePath);
bool needsUpdate(const QString &filePath);
QStringList getAllFiles();
bool storeChunk(const FileChunkData &chunk);
bool storeChunks(const QList<FileChunkData> &chunks);
bool updateChunk(const FileChunkData &chunk);
bool updateChunks(const QList<FileChunkData> &chunks);
bool deleteChunksForFile(const QString &filePath);
std::optional<FileChunkData> getChunk(const QString &filePath, int startLine, int endLine);
QList<FileChunkData> getChunksForFile(const QString &filePath);
bool chunkExists(const QString &filePath, int startLine, int endLine);
int getChunkCount(const QString &filePath);
bool deleteOldChunks(const QString &filePath, const QDateTime &olderThan);
bool deleteAllChunks();
QStringList getFilesWithChunks();
bool vacuum();
bool backup(const QString &backupPath);
bool restore(const QString &backupPath);
StorageStatistics getStatistics() const;
int getStorageVersion() const;
bool isVersionCompatible() const;
bool applyMigration(int version);
signals:
void errorOccurred(const Error &error);
void operationCompleted(const QString &operation);
private:
bool createTables();
bool createIndices();
bool createVersionTable();
bool createChunksTable();
bool createVectorsTable();
bool openDatabase();
bool initializeNewStorage();
bool upgradeStorage(int fromVersion);
bool validateSchema() const;
QDateTime getFileLastModified(const QString &filePath);
RAGVector blobToVector(const QByteArray &blob);
QByteArray vectorToBlob(const RAGVector &vector);
void setError(const QString &message, Status status = Status::DatabaseError);
void clearError();
bool prepareStatements();
ValidationResult validateChunk(const FileChunkData &chunk) const;
ValidationResult validateVector(const RAGVector &vector) const;
private:
QSqlDatabase m_db;
QString m_dbPath;
StorageOptions m_options;
mutable QMutex m_mutex;
Error m_lastError;
Status m_status;
QSqlQuery m_insertChunkQuery;
QSqlQuery m_updateChunkQuery;
QSqlQuery m_insertVectorQuery;
QSqlQuery m_updateVectorQuery;
};
} // namespace QodeAssist::Context

116
context/RAGVectorizer.cpp Normal file
View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2024 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 "RAGVectorizer.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
namespace QodeAssist::Context {
RAGVectorizer::RAGVectorizer(const QString &providerUrl,
const QString &modelName,
QObject *parent)
: QObject(parent)
, m_network(new QNetworkAccessManager(this))
, m_embedProviderUrl(providerUrl)
, m_model(modelName)
{}
RAGVectorizer::~RAGVectorizer() {}
QJsonObject RAGVectorizer::prepareEmbeddingRequest(const QString &text) const
{
return QJsonObject{{"model", m_model}, {"prompt", text}};
}
RAGVector RAGVectorizer::parseEmbeddingResponse(const QByteArray &response) const
{
QJsonDocument doc = QJsonDocument::fromJson(response);
if (doc.isNull()) {
qDebug() << "Failed to parse JSON response";
return RAGVector();
}
QJsonObject obj = doc.object();
if (!obj.contains("embedding")) {
qDebug() << "Response does not contain 'embedding' field";
// qDebug() << "Response content:" << response;
return RAGVector();
}
QJsonArray array = obj["embedding"].toArray();
if (array.isEmpty()) {
qDebug() << "Embedding array is empty";
return RAGVector();
}
RAGVector result;
result.reserve(array.size());
for (const auto &value : array) {
result.push_back(value.toDouble());
}
qDebug() << "Successfully parsed vector with size:" << result.size();
return result;
}
QFuture<RAGVector> RAGVectorizer::vectorizeText(const QString &text)
{
qDebug() << "Vectorizing text, length:" << text.length();
qDebug() << "Using embedding provider:" << m_embedProviderUrl;
auto promise = std::make_shared<QPromise<RAGVector>>();
promise->start();
QNetworkRequest request(QUrl(m_embedProviderUrl + "/api/embeddings"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject requestData = prepareEmbeddingRequest(text);
QByteArray jsonData = QJsonDocument(requestData).toJson();
qDebug() << "Sending request to embeddings API:" << jsonData;
auto reply = m_network->post(request, jsonData);
connect(reply, &QNetworkReply::finished, this, [promise, reply, this]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray response = reply->readAll();
// qDebug() << "Received response from embeddings API:" << response;
auto vector = parseEmbeddingResponse(response);
qDebug() << "Parsed vector size:" << vector.size();
promise->addResult(vector);
} else {
qDebug() << "Network error:" << reply->errorString();
qDebug() << "HTTP status code:"
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << "Response:" << reply->readAll();
promise->addResult(RAGVector());
}
promise->finish();
reply->deleteLater();
});
return promise->future();
}
} // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2025 Povilas Kanapickas * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,27 +19,33 @@
#pragma once #pragma once
#include "IDocumentReader.hpp" #include <QFuture>
#include <QNetworkAccessManager>
#include <QObject>
#include <texteditor/textdocument.h> #include <RAGData.hpp>
namespace QodeAssist::Context { namespace QodeAssist::Context {
class DocumentReaderQtCreator : public IDocumentReader class RAGVectorizer : public QObject
{ {
Q_OBJECT
public: public:
DocumentInfo readDocument(const QString &path) const override explicit RAGVectorizer(
{ const QString &providerUrl = "http://localhost:11434",
auto *textDocument = TextEditor::TextDocument::textDocumentForFilePath( const QString &modelName = "all-minilm:33m-l12-v2-fp16",
Utils::FilePath::fromString(path)); QObject *parent = nullptr);
if (!textDocument) { ~RAGVectorizer();
return {};
} QFuture<RAGVector> vectorizeText(const QString &text);
return {
.document = textDocument->document(), private:
.mimeType = textDocument->mimeType(), QJsonObject prepareEmbeddingRequest(const QString &text) const;
.filePath = path}; RAGVector parseEmbeddingResponse(const QByteArray &response) const;
}
QNetworkAccessManager *m_network;
QString m_embedProviderUrl;
QString m_model;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -21,7 +21,7 @@
namespace QodeAssist::Context { namespace QodeAssist::Context {
int TokenUtils::estimateTokens(const QString &text) int TokenUtils::estimateTokens(const QString& text)
{ {
if (text.isEmpty()) { if (text.isEmpty()) {
return 0; return 0;
@@ -31,7 +31,7 @@ int TokenUtils::estimateTokens(const QString &text)
return text.length() / 4; return text.length() / 4;
} }
int TokenUtils::estimateFileTokens(const Context::ContentFile &file) int TokenUtils::estimateFileTokens(const Context::ContentFile& file)
{ {
int total = 0; int total = 0;
@@ -42,13 +42,13 @@ int TokenUtils::estimateFileTokens(const Context::ContentFile &file)
return total; return total;
} }
int TokenUtils::estimateFilesTokens(const QList<Context::ContentFile> &files) int TokenUtils::estimateFilesTokens(const QList<Context::ContentFile>& files)
{ {
int total = 0; int total = 0;
for (const auto &file : files) { for (const auto& file : files) {
total += estimateFileTokens(file); total += estimateFileTokens(file);
} }
return total; return total;
} }
} // namespace QodeAssist::Context }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,18 +19,18 @@
#pragma once #pragma once
#include <QString>
#include "ContentFile.hpp" #include "ContentFile.hpp"
#include <QList> #include <QList>
#include <QString>
namespace QodeAssist::Context { namespace QodeAssist::Context {
class TokenUtils class TokenUtils
{ {
public: public:
static int estimateTokens(const QString &text); static int estimateTokens(const QString& text);
static int estimateFileTokens(const Context::ContentFile &file); static int estimateFileTokens(const Context::ContentFile& file);
static int estimateFilesTokens(const QList<Context::ContentFile> &files); static int estimateFilesTokens(const QList<Context::ContentFile>& files);
}; };
} // namespace QodeAssist::Context }

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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>
namespace QodeAssist::Context {
inline QString extractFilePathFromRequest(const QJsonObject &request)
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QString uri = doc["uri"].toString();
return QUrl(uri).toLocalFile();
}
} // namespace QodeAssist::Context

View File

@@ -3,19 +3,14 @@ add_library(LLMCore STATIC
Provider.hpp Provider.hpp
ProvidersManager.hpp ProvidersManager.cpp ProvidersManager.hpp ProvidersManager.cpp
ContextData.hpp ContextData.hpp
IPromptProvider.hpp
IProviderRegistry.hpp
PromptProviderChat.hpp
PromptProviderFim.hpp
PromptTemplate.hpp PromptTemplate.hpp
PromptTemplateManager.hpp PromptTemplateManager.cpp PromptTemplateManager.hpp PromptTemplateManager.cpp
RequestConfig.hpp RequestConfig.hpp
RequestHandlerBase.hpp RequestHandlerBase.cpp
RequestHandler.hpp RequestHandler.cpp RequestHandler.hpp RequestHandler.cpp
OllamaMessage.hpp OllamaMessage.cpp OllamaMessage.hpp OllamaMessage.cpp
OpenAIMessage.hpp OpenAIMessage.cpp OpenAIMessage.hpp OpenAIMessage.cpp
ValidationUtils.hpp ValidationUtils.cpp ValidationUtils.hpp ValidationUtils.cpp
ProviderID.hpp MessageBuilder.hpp MessageBuilder.cpp
) )
target_link_libraries(LLMCore target_link_libraries(LLMCore

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -20,37 +20,14 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QVector>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
struct Message
{
QString role;
QString content;
// clang-format off
bool operator==(const Message&) const = default;
// clang-format on
};
struct FileMetadata
{
QString filePath;
QString content;
bool operator==(const FileMetadata &) const = default;
};
struct ContextData struct ContextData
{ {
std::optional<QString> systemPrompt; QString prefix;
std::optional<QString> prefix; QString suffix;
std::optional<QString> suffix; QString fileContext;
std::optional<QString> fileContext;
std::optional<QVector<Message>> history;
std::optional<QList<FileMetadata>> filesMetadata;
bool operator==(const ContextData &) const = default;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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 "PromptTemplate.hpp"
#include <QString>
namespace QodeAssist::LLMCore {
class IPromptProvider
{
public:
virtual ~IPromptProvider() = default;
virtual PromptTemplate *getTemplateByName(const QString &templateName) const = 0;
virtual QStringList templatesNames() const = 0;
virtual QStringList getTemplatesForProvider(ProviderID id) const = 0;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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 "Provider.hpp"
namespace QodeAssist::LLMCore {
class IProviderRegistry
{
public:
virtual ~IProviderRegistry() = default;
virtual Provider *getProviderByName(const QString &providerName) = 0;
virtual QStringList providersNames() const = 0;
};
} // namespace QodeAssist::LLMCore

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2024 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 "MessageBuilder.hpp"
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addSystemMessage(
const QString &content)
{
m_systemMessage = content;
return *this;
}
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addUserMessage(
const QString &content)
{
m_messages.append({MessageRole::User, content});
return *this;
}
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addSuffix(
const QString &content)
{
m_suffix = content;
return *this;
}
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addTokenizer(
PromptTemplate *promptTemplate)
{
m_promptTemplate = promptTemplate;
return *this;
}
QString QodeAssist::LLMCore::MessageBuilder::roleToString(MessageRole role) const
{
switch (role) {
case MessageRole::System:
return ROLE_SYSTEM;
case MessageRole::User:
return ROLE_USER;
case MessageRole::Assistant:
return ROLE_ASSISTANT;
default:
return ROLE_USER;
}
}
void QodeAssist::LLMCore::MessageBuilder::saveTo(QJsonObject &request, ProvidersApi api)
{
if (!m_promptTemplate) {
return;
}
ContextData context{
m_messages.isEmpty() ? QString() : m_messages.last().content, m_suffix, m_systemMessage};
if (api == ProvidersApi::Ollama) {
if (m_promptTemplate->type() == TemplateType::Fim) {
request["system"] = m_systemMessage;
m_promptTemplate->prepareRequest(request, context);
} else {
QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", m_systemMessage}});
messages.append(QJsonObject{{"role", "user"}, {"content", m_messages.last().content}});
request["messages"] = messages;
m_promptTemplate->prepareRequest(request, context);
}
} else if (api == ProvidersApi::OpenAI) {
QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", m_systemMessage}});
messages.append(QJsonObject{{"role", "user"}, {"content", m_messages.last().content}});
request["messages"] = messages;
m_promptTemplate->prepareRequest(request, context);
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2024 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 <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
#include "PromptTemplate.hpp"
namespace QodeAssist::LLMCore {
enum class MessageRole { System, User, Assistant };
enum class OllamaFormat { Messages, Completions };
enum class ProvidersApi { Ollama, OpenAI, Claude };
static const QString ROLE_SYSTEM = "system";
static const QString ROLE_USER = "user";
static const QString ROLE_ASSISTANT = "assistant";
struct Message
{
MessageRole role;
QString content;
};
class MessageBuilder
{
public:
MessageBuilder &addSystemMessage(const QString &content);
MessageBuilder &addUserMessage(const QString &content);
MessageBuilder &addSuffix(const QString &content);
MessageBuilder &addTokenizer(PromptTemplate *promptTemplate);
QString roleToString(MessageRole role) const;
void saveTo(QJsonObject &request, ProvidersApi api);
private:
QString m_systemMessage;
QString m_suffix;
QVector<Message> m_messages;
PromptTemplate *m_promptTemplate;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,53 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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 "IPromptProvider.hpp"
#include "PromptTemplate.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderChat : public IPromptProvider
{
public:
explicit PromptProviderChat(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderChat() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getChatTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.chatTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getChatTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,52 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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 "IPromptProvider.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderFim : public IPromptProvider
{
public:
explicit PromptProviderFim(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderFim() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getFimTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.fimTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getFimTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -24,11 +24,10 @@
#include <QString> #include <QString>
#include "ContextData.hpp" #include "ContextData.hpp"
#include "ProviderID.hpp"
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
enum class TemplateType { Chat, FIM }; enum class TemplateType { Chat, Fim };
class PromptTemplate class PromptTemplate
{ {
@@ -36,9 +35,9 @@ public:
virtual ~PromptTemplate() = default; virtual ~PromptTemplate() = default;
virtual TemplateType type() const = 0; virtual TemplateType type() const = 0;
virtual QString name() const = 0; virtual QString name() const = 0;
virtual QString promptTemplate() const = 0;
virtual QStringList stopWords() const = 0; virtual QStringList stopWords() const = 0;
virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0; virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0;
virtual QString description() const = 0; virtual QString description() const = 0;
virtual bool isSupportProvider(ProviderID id) const = 0;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -37,32 +37,6 @@ QStringList PromptTemplateManager::chatTemplatesNames() const
return m_chatTemplates.keys(); return m_chatTemplates.keys();
} }
QStringList PromptTemplateManager::getFimTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_fimTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
QStringList PromptTemplateManager::getChatTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_chatTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
PromptTemplateManager::~PromptTemplateManager() PromptTemplateManager::~PromptTemplateManager()
{ {
qDeleteAll(m_fimTemplates); qDeleteAll(m_fimTemplates);
@@ -70,15 +44,11 @@ PromptTemplateManager::~PromptTemplateManager()
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName) PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
{ {
if (!m_fimTemplates.contains(templateName))
return m_fimTemplates.first();
return m_fimTemplates[templateName]; return m_fimTemplates[templateName];
} }
PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName) PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName)
{ {
if (!m_chatTemplates.contains(templateName))
return m_chatTemplates.first();
return m_chatTemplates[templateName]; return m_chatTemplates[templateName];
} }

Some files were not shown because too many files have changed in this diff Show More