Compare commits

..

1 Commits

Author SHA1 Message Date
Petr Mironychev
17b0eb8186 🐛 fix: Set content type for requests 2024-12-12 15:55:27 +01:00
125 changed files with 904 additions and 8641 deletions

View File

@@ -2,7 +2,7 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug labels: ''
assignees: '' assignees: ''
--- ---
@@ -23,6 +23,16 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Log** **Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -1,54 +0,0 @@
{
"name": "QodeAssist",
"vendor": "Petr Mironychev",
"tags": [
"code assistant",
"llm",
"ai"
],
"compatibility": "Qt 6.8.1",
"platforms": [
"Windows",
"macOS",
"Linux"
],
"license": "GPLv3",
"version": "0.4.0",
"status": "draft",
"is_pack": false,
"released_at": null,
"version_history": [
{
"version": "0.4.0",
"is_latest": true,
"released_at": "2024-01-24T15:00:00Z"
}
],
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
"small_icon": "https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41",
"description_paragraphs": [
{
"header": "Description",
"text": [
"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."
]
}
],
"description_links": [
{
"url": "https://github.com/Palm1r/QodeAssist",
"link_text": "Site"
}
],
"description_images": [
{
"url": "https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a",
"image_label": "Code Completion"
}
],
"copyright": "(C) Petr Mironychev",
"download_history": {
"download_count": 0
},
"plugin_sets": []
}

View File

@@ -1,147 +0,0 @@
const fs = require('fs');
const path = require('path');
const updatePluginData = (plugin, env, pluginQtcData) => {
const dictionary_platform = {
'Windows': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Windows-x64.7z`,
'Linux': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Linux-x64.7z`,
'macOS': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-macOS-universal.7z`
};
plugin.core_compat_version = env.QT_CREATOR_VERSION_INTERNAL;
plugin.core_version = env.QT_CREATOR_VERSION_INTERNAL;
plugin.status = "draft";
plugin.plugins.forEach(pluginsEntry => {
pluginsEntry.url = dictionary_platform[plugin.host_os];
pluginsEntry.meta_data = pluginQtcData;
});
return plugin;
};
const createNewPluginData = (env, platform, pluginQtcData) => {
const pluginJson = {
"status": "draft",
"core_compat_version": "<placeholder>",
"core_version": "<placeholder>",
"host_os": platform,
"host_os_version": "0", // TODO: pass the real data
"host_os_architecture": "x86_64", // TODO: pass the real data
"plugins": [
{
"url": "",
"size": 5000, // TODO: check if it is needed, pass the real data
"meta_data": {},
"dependencies": []
}
]
};
updatePluginData(pluginJson, env, pluginQtcData);
return pluginJson;
}
const updateServerPluginJson = (endJsonData, pluginQtcData, env) => {
// Update the global data in mainData
endJsonData.name = pluginQtcData.Name;
endJsonData.vendor = pluginQtcData.Vendor;
endJsonData.version = pluginQtcData.Version;
endJsonData.copyright = pluginQtcData.Copyright;
endJsonData.status = "draft";
endJsonData.version_history[0].version = pluginQtcData.Version;
endJsonData.description_paragraphs = [
{
header: "Description",
text: [
pluginQtcData.Description
]
}
];
let found = false;
// Update or Add the plugin data for the current Qt Creator version
for (const plugin of endJsonData.plugin_sets) {
if (plugin.core_compat_version === env.QT_CREATOR_VERSION_INTERNAL) {
updatePluginData(plugin, env, pluginQtcData);
found = true;
}
}
if (!found) {
for (const platform of ['Windows', 'Linux', 'macOS']) {
endJsonData.plugin_sets.push(createNewPluginData(env, platform, pluginQtcData));
}
}
// Save the updated JSON file
const serverPluginJsonPath = path.join(__dirname, `${env.PLUGIN_NAME}.json`);
fs.writeFileSync(serverPluginJsonPath, JSON.stringify(endJsonData, null, 2), 'utf8');
};
const request = async (type, url, token, data) => {
const response = await fetch(url, {
method: type,
headers: {
'Authorization': `Bearer ${token}`,
'accept': 'application/json',
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
});
if (!response.ok) {
const errorResponse = await response.json();
console.error(`${type} Request Error Response:`, errorResponse); // Log the error response
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
const put = (url, token, data) => request('PUT', url, token, data)
const post = (url, token, data) => request('POST', url, token, data)
const get = (url, token) => request('GET', url, token)
const purgeCache = async (env) => {
try {
await post(`${env.API_URL}api/v1/cache/purgeall`, env.TOKEN, {});
console.log('Cache purged successfully');
} catch (error) {
console.error('Error:', error);
}
};
async function main() {
const env = {
PLUGIN_DOWNLOAD_URL: process.env.PLUGIN_DOWNLOAD_URL || process.argv[2],
PLUGIN_NAME: process.env.PLUGIN_NAME || process.argv[3],
QT_CREATOR_VERSION: process.env.QT_CREATOR_VERSION || process.argv[4],
QT_CREATOR_VERSION_INTERNAL: process.env.QT_CREATOR_VERSION_INTERNAL || process.argv[5],
TOKEN: process.env.TOKEN || process.argv[6],
API_URL: process.env.API_URL || process.argv[7] || ''
};
const pluginQtcData = require(`../../${env.PLUGIN_NAME}-origin/${env.PLUGIN_NAME}.json`);
const templateFileData = require('./plugin.json');
if (env.API_URL === '') {
updateServerPluginJson(templateFileData, pluginQtcData, env);
process.exit(0);
}
const response = await get(`${env.API_URL}api/v1/admin/extensions?search=${env.PLUGIN_NAME}`, env.TOKEN);
if (response.items.length > 0 && response.items[0].extension_id !== '') {
const pluginId = response.items[0].extension_id;
console.log('Plugin found. Updating the plugin');
updateServerPluginJson(response.items[0], pluginQtcData, env);
await put(`${env.API_URL}api/v1/admin/extensions/${pluginId}`, env.TOKEN, response.items[0]);
} else {
console.log('No plugin found. Creating a new plugin');
updateServerPluginJson(templateFileData, pluginQtcData, env);
await post(`${env.API_URL}api/v1/admin/extensions`, env.TOKEN, templateFileData);
}
// await purgeCache(env);
}
main().then(() => console.log('JSON file updated successfully'));

View File

@@ -9,12 +9,11 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
env: env:
PLUGIN_NAME: QodeAssist PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.1 QT_VERSION: 6.7.3
QT_CREATOR_VERSION: 15.0.1 QT_CREATOR_VERSION: 14.0.2
QT_CREATOR_VERSION_INTERNAL: 15.0.1 QT_CREATOR_SNAPSHOT: NO
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"
@@ -31,44 +30,74 @@ jobs:
- { - {
name: "Windows Latest MSVC", artifact: "Windows-x64", name: "Windows Latest MSVC", artifact: "Windows-x64",
os: windows-latest, os: windows-latest,
platform: windows_x64,
cc: "cl", cxx: "cl", cc: "cl", cxx: "cl",
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 Latest GCC", artifact: "Linux-x64", name: "Ubuntu Latest GCC", artifact: "Linux-x64",
os: ubuntu-latest, os: ubuntu-latest,
platform: linux_x64,
cc: "gcc", cxx: "g++" cc: "gcc", cxx: "g++"
} }
- { - {
name: "macOS Latest Clang", artifact: "macOS-universal", name: "macOS Latest Clang", artifact: "macOS-universal",
os: macos-latest, os: macos-latest,
platform: mac_x64,
cc: "clang", cxx: "clang++" cc: "clang", cxx: "clang++"
} }
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Checkout submodules - name: Checkout submodules
id: git id: git
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
if (${{github.ref}} MATCHES "tags/v(.*)") if (${{github.ref}} MATCHES "tags/v(.*)")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}") file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}\n")
else() else()
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}") file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\n")
endif() endif()
- name: Download Ninja and CMake - name: Download Ninja and CMake
uses: lukka/get-cmake@latest shell: cmake -P {0}
with: run: |
cmakeVersion: ${{ env.CMAKE_VERSION }} set(cmake_version "$ENV{CMAKE_VERSION}")
ninjaVersion: ${{ env.NINJA_VERSION }} set(ninja_version "$ENV{NINJA_VERSION}")
if ("${{ runner.os }}" STREQUAL "Windows")
set(ninja_suffix "win.zip")
set(cmake_suffix "windows-x86_64.zip")
set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin")
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(ninja_suffix "linux.zip")
set(cmake_suffix "linux-x86_64.tar.gz")
set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin")
elseif ("${{ runner.os }}" STREQUAL "macOS")
set(ninja_suffix "mac.zip")
set(cmake_suffix "macos-universal.tar.gz")
set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin")
endif()
set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)
set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)
# Add to PATH environment variable
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
set(path_separator ":")
if ("${{ runner.os }}" STREQUAL "Windows")
set(path_separator ";")
endif()
file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}")
if (NOT "${{ runner.os }}" STREQUAL "Windows")
execute_process(
COMMAND chmod +x ninja
COMMAND chmod +x ${cmake_dir}/cmake
)
endif()
- name: Install system libs - name: Install system libs
shell: cmake -P {0} shell: cmake -P {0}
@@ -78,7 +107,7 @@ jobs:
COMMAND sudo apt update COMMAND sudo apt update
) )
execute_process( execute_process(
COMMAND sudo apt install libgl1-mesa-dev COMMAND sudo apt install libgl1-mesa-dev libcups2-dev
RESULT_VARIABLE result RESULT_VARIABLE result
) )
if (NOT result EQUAL 0) if (NOT result EQUAL 0)
@@ -95,9 +124,9 @@ jobs:
string(REPLACE "." "" qt_version_dotless "${qt_version}") string(REPLACE "." "" qt_version_dotless "${qt_version}")
if ("${{ runner.os }}" STREQUAL "Windows") if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86") set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_msvc2022_64") set(qt_package_arch_suffix "win64_msvc2019_64")
set(qt_dir_prefix "${qt_version}/msvc2022_64") set(qt_dir_prefix "${qt_version}/msvc2019_64")
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64") set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
elseif ("${{ runner.os }}" STREQUAL "Linux") elseif ("${{ runner.os }}" STREQUAL "Linux")
set(url_os "linux_x64") set(url_os "linux_x64")
if (qt_version VERSION_LESS "6.7.0") if (qt_version VERSION_LESS "6.7.0")
@@ -106,15 +135,15 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64") set(qt_package_arch_suffix "linux_gcc_64")
endif() endif()
set(qt_dir_prefix "${qt_version}/gcc_64") set(qt_dir_prefix "${qt_version}/gcc_64")
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64") set(qt_package_suffix "-Linux-RHEL_8_8-GCC-Linux-RHEL_8_8-X86_64")
elseif ("${{ runner.os }}" STREQUAL "macOS") elseif ("${{ runner.os }}" STREQUAL "macOS")
set(url_os "mac_x64") set(url_os "mac_x64")
set(qt_package_arch_suffix "clang_64") set(qt_package_arch_suffix "clang_64")
set(qt_dir_prefix "${qt_version}/macos") set(qt_dir_prefix "${qt_version}/macos")
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64") set(qt_package_suffix "-MacOS-MacOS_13-Clang-MacOS-MacOS_13-X86_64-ARM64")
endif() endif()
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}") set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}")
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS) file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
file(READ ./Updates.xml updates_xml) file(READ ./Updates.xml updates_xml)
@@ -124,7 +153,7 @@ jobs:
file(MAKE_DIRECTORY qt6) file(MAKE_DIRECTORY qt6)
# Save the path for other steps # Save the path for other steps
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6" qt_dir) file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6/${qt_dir_prefix}" qt_dir)
file(APPEND "$ENV{GITHUB_OUTPUT}" "qt_dir=${qt_dir}") file(APPEND "$ENV{GITHUB_OUTPUT}" "qt_dir=${qt_dir}")
message("Downloading Qt to ${qt_dir}") message("Downloading Qt to ${qt_dir}")
@@ -143,17 +172,11 @@ jobs:
foreach(package qt5compat qtshadertools) foreach(package qt5compat qtshadertools)
downloadAndExtract( downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z" "${qt_base_url}/qt.qt6.${qt_version_dotless}.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z ${package}.7z
) )
endforeach() endforeach()
function(downloadAndExtractLibicu url archive)
message("Downloading ${url}")
file(DOWNLOAD "${url}" ./${archive} SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../../${archive} WORKING_DIRECTORY qt6/lib)
endfunction()
# uic depends on libicu*.so # uic depends on libicu*.so
if ("${{ runner.os }}" STREQUAL "Linux") if ("${{ runner.os }}" STREQUAL "Linux")
if (qt_version VERSION_LESS "6.7.0") if (qt_version VERSION_LESS "6.7.0")
@@ -161,25 +184,47 @@ jobs:
else() else()
set(uic_suffix "Rhel8.6-x86_64") set(uic_suffix "Rhel8.6-x86_64")
endif() endif()
downloadAndExtractLibicu( downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z" "${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z"
icu.7z icu.7z
) )
endif() endif()
- name: Download Qt Creator - name: Download Qt Creator
uses: qt-creator/install-dev-package@v1.2
with:
version: ${{ env.QT_CREATOR_VERSION }}
unzip-to: 'qtcreator'
- name: Extract Qt Creator
id: qt_creator id: qt_creator
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
string(REGEX MATCH "([0-9]+.[0-9]+).[0-9]+" outvar "$ENV{QT_CREATOR_VERSION}")
set(qtc_base_url "https://download.qt.io/official_releases/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source")
set(qtc_snapshot "$ENV{QT_CREATOR_SNAPSHOT}")
if (qtc_snapshot)
set(qtc_base_url "https://download.qt.io/snapshots/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source/${qtc_snapshot}")
endif()
if ("${{ runner.os }}" STREQUAL "Windows")
set(qtc_platform "windows_x64")
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(qtc_platform "linux_x64")
elseif ("${{ runner.os }}" STREQUAL "macOS")
set(qtc_platform "mac_x64")
endif()
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qtcreator" qtc_dir) file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qtcreator" qtc_dir)
# Save the path for other steps
file(APPEND "$ENV{GITHUB_OUTPUT}" "qtc_dir=${qtc_dir}") file(APPEND "$ENV{GITHUB_OUTPUT}" "qtc_dir=${qtc_dir}")
file(MAKE_DIRECTORY qtcreator)
message("Downloading Qt Creator from ${qtc_base_url}/${qtc_platform}")
foreach(package qtcreator qtcreator_dev)
file(DOWNLOAD
"${qtc_base_url}/${qtc_platform}/${package}.7z" ./${package}.7z SHOW_PROGRESS)
execute_process(COMMAND
${CMAKE_COMMAND} -E tar xvf ../${package}.7z WORKING_DIRECTORY qtcreator)
endforeach()
- name: Build - name: Build
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
@@ -238,58 +283,10 @@ jobs:
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
name: ${{ env.PLUGIN_NAME}}-${{ 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
- name: Upload plugin json
if: matrix.config.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./build/build/${{ env.PLUGIN_NAME }}.json
update_json:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Download the JSON file
uses: actions/download-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./${{ env.PLUGIN_NAME }}-origin
- name: Store Release upload_url
run: |
RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}")
echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV
- name: Run the Node.js script to update JSON
env:
QT_TOKEN: ${{ secrets.TOKEN }}
API_URL: ${{ secrets.API_URL }}
run: |
node .github/scripts/registerPlugin.js ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} ${{ env.QT_TOKEN }} ${{ env.API_URL }}
- name: Delete previous json artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.PLUGIN_NAME }}*-json
- name: Upload the modified JSON file as an artifact
uses: actions/upload-artifact@v4
with:
name: plugin-json
path: .github/scripts/${{ env.PLUGIN_NAME }}.json
release: release:
if: contains(github.ref, 'tags/v') if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build, update_json] needs: build
steps: steps:
- name: Download artifacts - name: Download artifacts

View File

@@ -16,7 +16,6 @@ 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_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
@@ -46,30 +45,28 @@ add_qtc_plugin(QodeAssist
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/Ollama.hpp templates/Ollama.hpp
templates/BasicChat.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
providers/Providers.hpp providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.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/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
QodeAssistClient.hpp QodeAssistClient.cpp QodeAssistClient.hpp QodeAssistClient.cpp
DocumentContextReader.hpp DocumentContextReader.cpp
utils/CounterTooltip.hpp utils/CounterTooltip.cpp
core/ChangesManager.h core/ChangesManager.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.cpp chat/ChatOutputPane.h chat/ChatOutputPane.cpp
chat/NavigationPanel.hpp chat/NavigationPanel.cpp chat/NavigationPanel.hpp chat/NavigationPanel.cpp
ConfigurationManager.hpp ConfigurationManager.cpp ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
) )

View File

@@ -1,8 +1,8 @@
qt_add_library(QodeAssistChatView STATIC) qt_add_library(QodeAssistChatView STATIC)
qt_policy(SET QTP0001 NEW) qt_policy(SET QTP0001 NEW)
qt_policy(SET QTP0004 NEW)
# URI name should match the subdirectory name to suppress the warning
qt_add_qml_module(QodeAssistChatView qt_add_qml_module(QodeAssistChatView
URI ChatView URI ChatView
VERSION 1.0 VERSION 1.0
@@ -13,17 +13,6 @@ qt_add_qml_module(QodeAssistChatView
qml/Badge.qml qml/Badge.qml
qml/dialog/CodeBlock.qml qml/dialog/CodeBlock.qml
qml/dialog/TextBlock.qml qml/dialog/TextBlock.qml
qml/controls/QoAButton.qml
qml/parts/TopBar.qml
qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml
RESOURCES
icons/attach-file-light.svg
icons/attach-file-dark.svg
icons/close-dark.svg
icons/close-light.svg
icons/link-file-light.svg
icons/link-file-dark.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp ChatModel.hpp ChatModel.cpp
@@ -31,7 +20,6 @@ qt_add_qml_module(QodeAssistChatView
ClientInterface.hpp ClientInterface.cpp ClientInterface.hpp ClientInterface.cpp
MessagePart.hpp MessagePart.hpp
ChatUtils.h ChatUtils.cpp ChatUtils.h ChatUtils.cpp
ChatSerializer.hpp ChatSerializer.cpp
) )
target_link_libraries(QodeAssistChatView target_link_libraries(QodeAssistChatView
@@ -44,7 +32,6 @@ target_link_libraries(QodeAssistChatView
QtCreator::Utils QtCreator::Utils
LLMCore LLMCore
QodeAssistSettings QodeAssistSettings
Context
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView

View File

@@ -28,6 +28,7 @@ namespace QodeAssist::Chat {
ChatModel::ChatModel(QObject *parent) ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_totalTokens(0)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
@@ -54,13 +55,6 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
case Roles::Content: { case Roles::Content: {
return message.content; return message.content;
} }
case Roles::Attachments: {
QStringList filenames;
for (const auto &attachment : message.attachments) {
filenames << attachment.filename;
}
return filenames;
}
default: default:
return QVariant(); return QVariant();
} }
@@ -71,37 +65,29 @@ QHash<int, QByteArray> ChatModel::roleNames() const
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[Roles::RoleType] = "roleType"; roles[Roles::RoleType] = "roleType";
roles[Roles::Content] = "content"; roles[Roles::Content] = "content";
roles[Roles::Attachments] = "attachments";
return roles; return roles;
} }
void ChatModel::addMessage( void ChatModel::addMessage(const QString &content, ChatRole role, const QString &id)
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments)
{ {
QString fullContent = content; int tokenCount = estimateTokenCount(content);
if (!attachments.isEmpty()) {
fullContent += "\n\nAttached files list:";
for (const auto &attachment : attachments) {
fullContent += QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
}
}
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) { if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
Message &lastMessage = m_messages.last(); Message &lastMessage = m_messages.last();
int oldTokenCount = lastMessage.tokenCount;
lastMessage.content = content; lastMessage.content = content;
lastMessage.attachments = attachments; lastMessage.tokenCount = tokenCount;
m_totalTokens += (tokenCount - oldTokenCount);
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1)); emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else { } else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size()); beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{role, content, id}; m_messages.append({role, content, tokenCount, id});
newMessage.attachments = attachments; m_totalTokens += tokenCount;
m_messages.append(newMessage);
endInsertRows(); endInsertRows();
} }
trim();
emit totalTokensChanged();
} }
QVector<ChatModel::Message> ChatModel::getChatHistory() const QVector<ChatModel::Message> ChatModel::getChatHistory() const
@@ -109,12 +95,32 @@ QVector<ChatModel::Message> ChatModel::getChatHistory() const
return m_messages; return m_messages;
} }
void ChatModel::trim()
{
while (m_totalTokens > tokensThreshold()) {
if (!m_messages.isEmpty()) {
m_totalTokens -= m_messages.first().tokenCount;
beginRemoveRows(QModelIndex(), 0, 0);
m_messages.removeFirst();
endRemoveRows();
} else {
break;
}
}
}
int ChatModel::estimateTokenCount(const QString &text) const
{
return text.length() / 4;
}
void ChatModel::clear() void ChatModel::clear()
{ {
beginResetModel(); beginResetModel();
m_messages.clear(); m_messages.clear();
m_totalTokens = 0;
endResetModel(); endResetModel();
emit modelReseted(); emit totalTokensChanged();
} }
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
@@ -149,6 +155,7 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
{ {
QJsonArray messages; QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}}); messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
for (const auto &message : m_messages) { for (const auto &message : m_messages) {
@@ -163,27 +170,17 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
default: default:
continue; continue;
} }
messages.append(QJsonObject{{"role", role}, {"content", message.content}});
QString content
= message.attachments.isEmpty()
? message.content
: message.content + "\n\nAttached files list:"
+ std::accumulate(
message.attachments.begin(),
message.attachments.end(),
QString(),
[](QString acc, const Context::ContentFile &attachment) {
return acc
+ QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
});
messages.append(QJsonObject{{"role", role}, {"content", content}});
} }
return messages; return messages;
} }
int ChatModel::totalTokens() const
{
return m_totalTokens;
}
int ChatModel::tokensThreshold() const int ChatModel::tokensThreshold() const
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();

View File

@@ -26,29 +26,27 @@
#include <QJsonArray> #include <QJsonArray>
#include <QtQmlIntegration> #include <QtQmlIntegration>
#include "context/ContentFile.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel : public QAbstractListModel class ChatModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(int totalTokens READ totalTokens NOTIFY totalTokensChanged FINAL)
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL) Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
enum Roles { RoleType = Qt::UserRole, Content };
enum ChatRole { System, User, Assistant }; enum ChatRole { System, User, Assistant };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
struct Message struct Message
{ {
ChatRole role; ChatRole role;
QString content; QString content;
int tokenCount;
QString id; QString id;
QList<Context::ContentFile> attachments;
}; };
explicit ChatModel(QObject *parent = nullptr); explicit ChatModel(QObject *parent = nullptr);
@@ -57,28 +55,29 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addMessage( Q_INVOKABLE void addMessage(const QString &content, ChatRole role, const QString &id);
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {});
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const; QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const; QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
int totalTokens() const;
int tokensThreshold() const; int tokensThreshold() const;
QString currentModel() const; QString currentModel() const;
QString lastMessageId() const; QString lastMessageId() const;
signals: signals:
void totalTokensChanged();
void tokensThresholdChanged(); void tokensThresholdChanged();
void modelReseted();
private: private:
void trim();
int estimateTokenCount(const QString &text) const;
QVector<Message> m_messages; QVector<Message> m_messages;
int m_totalTokens = 0;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -18,30 +18,12 @@
*/ */
#include "ChatRootView.hpp" #include "ChatRootView.hpp"
#include <QtGui/qclipboard.h>
#include <QClipboard>
#include <QDesktopServices>
#include <QFileDialog>
#include <QMessageBox>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.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>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "context/ContextManager.hpp"
#include "context/FileChunker.hpp"
#include "context/RAGManager.hpp"
#include "context/TokenUtils.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -50,13 +32,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_clientInterface(new ClientInterface(m_chatModel, this)) , m_clientInterface(new ClientInterface(m_chatModel, this))
{ {
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
this,
[this](){
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
});
auto &settings = Settings::generalSettings(); auto &settings = Settings::generalSettings();
connect(&settings.caModel, connect(&settings.caModel,
@@ -64,44 +39,12 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::currentTemplateChanged); &ChatRootView::currentTemplateChanged);
connect( connect(&Settings::chatAssistantSettings().sharingCurrentFile,
m_clientInterface, &Utils::BaseAspect::changed,
&ClientInterface::messageReceivedCompletely,
this, this,
&ChatRootView::autosave); &ChatRootView::isSharingCurrentFileChanged);
connect( generateColors();
m_clientInterface,
&ClientInterface::messageReceivedCompletely,
this,
&ChatRootView::updateInputTokensCount);
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
connect(&Settings::chatAssistantSettings().useSystemPrompt, &Utils::BaseAspect::changed,
this, &ChatRootView::updateInputTokensCount);
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
this, &ChatRootView::updateInputTokensCount);
auto editors = Core::EditorManager::instance();
connect(editors, &Core::EditorManager::editorCreated, this, &ChatRootView::onEditorCreated);
connect(
editors,
&Core::EditorManager::editorAboutToClose,
this,
&ChatRootView::onEditorAboutToClose);
connect(editors, &Core::EditorManager::currentEditorAboutToChange, this, [this]() {
if (m_isSyncOpenFiles) {
for (auto editor : std::as_const(m_currentEditors)) {
onAppendLinkFileFromEditor(editor);
}
}
});
updateInputTokensCount();
} }
ChatModel *ChatRootView::chatModel() const ChatModel *ChatRootView::chatModel() const
@@ -109,26 +52,14 @@ ChatModel *ChatRootView::chatModel() const
return m_chatModel; return m_chatModel;
} }
void ChatRootView::sendMessage(const QString &message) QColor ChatRootView::backgroundColor() const
{ {
if (m_inputTokensCount > m_chatModel->tokensThreshold()) { return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
QMessageBox::StandardButton reply = QMessageBox::question(
Core::ICore::dialogParent(),
tr("Token Limit Exceeded"),
tr("The chat history has exceeded the token limit.\n"
"Would you like to create new chat?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
autosave();
m_chatModel->clear();
setRecentFilePath(QString{});
return;
}
} }
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles); void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const
clearAttachmentFiles(); {
m_clientInterface->sendMessage(message, sharingCurrentFile);
} }
void ChatRootView::copyToClipboard(const QString &text) void ChatRootView::copyToClipboard(const QString &text)
@@ -141,40 +72,47 @@ void ChatRootView::cancelRequest()
m_clientInterface->cancelRequest(); m_clientInterface->cancelRequest();
} }
void ChatRootView::clearAttachmentFiles() void ChatRootView::generateColors()
{ {
if (!m_attachmentFiles.isEmpty()) { QColor baseColor = backgroundColor();
m_attachmentFiles.clear(); bool isDarkTheme = baseColor.lightness() < 128;
emit attachmentFilesChanged();
}
}
void ChatRootView::clearLinkedFiles() if (isDarkTheme) {
{ m_primaryColor = generateColor(baseColor, 0.1, 1.2, 1.4);
if (!m_linkedFiles.isEmpty()) { m_secondaryColor = generateColor(baseColor, -0.1, 1.1, 1.2);
m_linkedFiles.clear(); m_codeColor = generateColor(baseColor, 0.05, 0.8, 1.1);
emit linkedFilesChanged();
}
}
QString ChatRootView::getChatsHistoryDir() const
{
QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toString();
} else { } else {
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString()); m_primaryColor = generateColor(baseColor, 0.05, 1.05, 1.1);
m_secondaryColor = generateColor(baseColor, -0.05, 1.1, 1.2);
m_codeColor = generateColor(baseColor, 0.02, 0.95, 1.05);
}
} }
QDir dir(path); QColor ChatRootView::generateColor(const QColor &baseColor,
if (!dir.exists() && !dir.mkpath(".")) { float hueShift,
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path)); float saturationMod,
return QString(); float lightnessMod)
{
float h, s, l, a;
baseColor.getHslF(&h, &s, &l, &a);
bool isDarkTheme = l < 0.5;
h = fmod(h + hueShift + 1.0, 1.0);
s = qBound(0.0f, s * saturationMod, 1.0f);
if (isDarkTheme) {
l = qBound(0.0f, l * lightnessMod, 1.0f);
} else {
l = qBound(0.0f, l / lightnessMod, 1.0f);
} }
return path; h = qBound(0.0f, h, 1.0f);
s = qBound(0.0f, s, 1.0f);
l = qBound(0.0f, l, 1.0f);
a = qBound(0.0f, a, 1.0f);
return QColor::fromHslF(h, s, l, a);
} }
QString ChatRootView::currentTemplate() const QString ChatRootView::currentTemplate() const
@@ -183,417 +121,24 @@ QString ChatRootView::currentTemplate() const
return settings.caModel(); return settings.caModel();
} }
void ChatRootView::saveHistory(const QString &filePath) QColor ChatRootView::primaryColor() const
{ {
auto result = ChatSerializer::saveToFile(m_chatModel, filePath); return m_primaryColor;
if (!result.success) {
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
} else {
setRecentFilePath(filePath);
}
} }
void ChatRootView::loadHistory(const QString &filePath) QColor ChatRootView::secondaryColor() const
{ {
auto result = ChatSerializer::loadFromFile(m_chatModel, filePath); return m_secondaryColor;
if (!result.success) {
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
} else {
setRecentFilePath(filePath);
}
updateInputTokensCount();
} }
void ChatRootView::showSaveDialog() QColor ChatRootView::codeColor() const
{ {
QString initialDir = getChatsHistoryDir(); return m_codeColor;
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
dialog->setAcceptMode(QFileDialog::AcceptSave);
dialog->setFileMode(QFileDialog::AnyFile);
dialog->setNameFilter(tr("JSON files (*.json)"));
dialog->setDefaultSuffix("json");
if (!initialDir.isEmpty()) {
dialog->setDirectory(initialDir);
dialog->selectFile(getSuggestedFileName() + ".json");
} }
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) { bool ChatRootView::isSharingCurrentFile() const
if (result == QFileDialog::Accepted) {
QStringList files = dialog->selectedFiles();
if (!files.isEmpty()) {
saveHistory(files.first());
}
}
dialog->deleteLater();
});
dialog->open();
}
void ChatRootView::showLoadDialog()
{ {
QString initialDir = getChatsHistoryDir(); return Settings::chatAssistantSettings().sharingCurrentFile();
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
dialog->setAcceptMode(QFileDialog::AcceptOpen);
dialog->setFileMode(QFileDialog::ExistingFile);
dialog->setNameFilter(tr("JSON files (*.json)"));
if (!initialDir.isEmpty()) {
dialog->setDirectory(initialDir);
}
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
if (result == QFileDialog::Accepted) {
QStringList files = dialog->selectedFiles();
if (!files.isEmpty()) {
loadHistory(files.first());
}
}
dialog->deleteLater();
});
dialog->open();
}
QString ChatRootView::getSuggestedFileName() const
{
QStringList parts;
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
static const QRegularExpression underSymbols = QRegularExpression("_+");
if (m_chatModel->rowCount() > 0) {
QString firstMessage
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
QString sanitizedMessage = shortMessage;
sanitizedMessage.replace(saitizeSymbols, "_");
sanitizedMessage.replace(underSymbols, "_");
sanitizedMessage = sanitizedMessage.trimmed();
if (!sanitizedMessage.isEmpty()) {
if (sanitizedMessage.startsWith('_')) {
sanitizedMessage.remove(0, 1);
}
if (sanitizedMessage.endsWith('_')) {
sanitizedMessage.chop(1);
}
QString targetDir = getChatsHistoryDir();
QString fullPath = QDir(targetDir).filePath(sanitizedMessage);
QFileInfo fileInfo(fullPath);
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
parts << sanitizedMessage;
}
}
}
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
QString fileName = parts.join("_");
QString fullPath = QDir(getChatsHistoryDir()).filePath(fileName);
QFileInfo finalCheck(fullPath);
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
fileName = QString("chat_%1").arg(
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
}
return fileName;
}
void ChatRootView::autosave()
{
if (m_chatModel->rowCount() == 0 || !Settings::chatAssistantSettings().autosave()) {
return;
}
QString filePath = getAutosaveFilePath();
if (!filePath.isEmpty()) {
ChatSerializer::saveToFile(m_chatModel, filePath);
setRecentFilePath(filePath);
}
}
QString ChatRootView::getAutosaveFilePath() const
{
if (!m_recentFilePath.isEmpty()) {
return m_recentFilePath;
}
QString dir = getChatsHistoryDir();
if (dir.isEmpty()) {
return QString();
}
return QDir(dir).filePath(getSuggestedFileName() + ".json");
}
QStringList ChatRootView::attachmentFiles() const
{
return m_attachmentFiles;
}
QStringList ChatRootView::linkedFiles() const
{
return m_linkedFiles;
}
void ChatRootView::showAttachFilesDialog()
{
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toString());
}
if (dialog.exec() == QDialog::Accepted) {
QStringList newFilePaths = dialog.selectedFiles();
if (!newFilePaths.isEmpty()) {
bool filesAdded = false;
for (const QString &filePath : std::as_const(newFilePaths)) {
if (!m_attachmentFiles.contains(filePath)) {
m_attachmentFiles.append(filePath);
filesAdded = true;
}
}
if (filesAdded) {
emit attachmentFilesChanged();
}
}
}
}
void ChatRootView::removeFileFromAttachList(int index)
{
if (index >= 0 && index < m_attachmentFiles.size()) {
m_attachmentFiles.removeAt(index);
emit attachmentFilesChanged();
}
}
void ChatRootView::showLinkFilesDialog()
{
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toString());
}
if (dialog.exec() == QDialog::Accepted) {
QStringList newFilePaths = dialog.selectedFiles();
if (!newFilePaths.isEmpty()) {
bool filesAdded = false;
for (const QString &filePath : std::as_const(newFilePaths)) {
if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath);
filesAdded = true;
}
}
if (filesAdded) {
emit linkedFilesChanged();
}
}
}
}
void ChatRootView::removeFileFromLinkList(int index)
{
if (index >= 0 && index < m_linkedFiles.size()) {
m_linkedFiles.removeAt(index);
emit linkedFilesChanged();
}
}
void ChatRootView::calculateMessageTokensCount(const QString &message)
{
m_messageTokensCount = Context::TokenUtils::estimateTokens(message);
updateInputTokensCount();
}
void ChatRootView::setIsSyncOpenFiles(bool state)
{
if (m_isSyncOpenFiles != state) {
m_isSyncOpenFiles = state;
emit isSyncOpenFilesChanged();
}
if (m_isSyncOpenFiles) {
for (auto editor : std::as_const(m_currentEditors)) {
onAppendLinkFileFromEditor(editor);
}
}
}
void ChatRootView::openChatHistoryFolder()
{
QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toString();
} else {
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
}
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
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()
{
int inputTokens = m_messageTokensCount;
auto& settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
}
if (!m_attachmentFiles.isEmpty()) {
auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
}
if (!m_linkedFiles.isEmpty()) {
auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
}
const auto& history = m_chatModel->getChatHistory();
for (const auto& message : history) {
inputTokens += Context::TokenUtils::estimateTokens(message.content);
inputTokens += 4; // + role
}
m_inputTokensCount = inputTokens;
emit inputTokensCountChanged();
}
int ChatRootView::inputTokensCount() const
{
return m_inputTokensCount;
}
bool ChatRootView::isSyncOpenFiles() const
{
return m_isSyncOpenFiles;
}
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toString();
m_linkedFiles.removeOne(filePath);
emit linkedFilesChanged();
}
if (editor) {
m_currentEditors.removeOne(editor);
}
}
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toString();
if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath);
emit linkedFilesChanged();
}
}
}
void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath)
{
if (editor && editor->document()) {
m_currentEditors.append(editor);
}
}
QString ChatRootView::chatFileName() const
{
return QFileInfo(m_recentFilePath).baseName();
}
void ChatRootView::setRecentFilePath(const QString &filePath)
{
if (m_recentFilePath != filePath) {
m_recentFilePath = filePath;
emit chatFileNameChanged();
}
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -23,21 +23,24 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatRootView : public QQuickItem class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
// Possibly Qt bug: QTBUG-131004
// The class type name must be fully qualified
// including the namespace.
// Otherwise qmlls can't find it.
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL) Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL) Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL) Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL) Q_PROPERTY(QColor primaryColor READ primaryColor CONSTANT FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QColor secondaryColor READ secondaryColor CONSTANT FINAL)
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL) Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL) Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
isSharingCurrentFileChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
@@ -46,70 +49,38 @@ public:
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const; QString currentTemplate() const;
void saveHistory(const QString &filePath); QColor backgroundColor() const;
void loadHistory(const QString &filePath); QColor primaryColor() const;
QColor secondaryColor() const;
Q_INVOKABLE void showSaveDialog(); QColor codeColor() const;
Q_INVOKABLE void showLoadDialog();
void autosave(); bool isSharingCurrentFile() const;
QString getAutosaveFilePath() const;
QStringList attachmentFiles() const;
QStringList linkedFiles() const;
Q_INVOKABLE void showAttachFilesDialog();
Q_INVOKABLE void removeFileFromAttachList(int index);
Q_INVOKABLE void showLinkFilesDialog();
Q_INVOKABLE void removeFileFromLinkList(int index);
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void testRAG(const QString &message);
Q_INVOKABLE void testChunking();
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
bool isSyncOpenFiles() const;
void onEditorAboutToClose(Core::IEditor *editor);
void onAppendLinkFileFromEditor(Core::IEditor *editor);
void onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath);
QString chatFileName() const;
void setRecentFilePath(const QString &filePath);
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message, bool sharingCurrentFile = false) const;
void copyToClipboard(const QString &text); void copyToClipboard(const QString &text);
void cancelRequest(); void cancelRequest();
void clearAttachmentFiles();
void clearLinkedFiles();
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged(); void currentTemplateChanged();
void attachmentFilesChanged();
void linkedFilesChanged(); void isSharingCurrentFileChanged();
void inputTokensCountChanged();
void isSyncOpenFilesChanged();
void chatFileNameChanged();
private: private:
QString getChatsHistoryDir() const; void generateColors();
QString getSuggestedFileName() const; QColor generateColor(const QColor &baseColor,
float hueShift,
float saturationMod,
float lightnessMod);
ChatModel *m_chatModel; ChatModel *m_chatModel;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
QString m_currentTemplate; QString m_currentTemplate;
QString m_recentFilePath; QColor m_primaryColor;
QStringList m_attachmentFiles; QColor m_secondaryColor;
QStringList m_linkedFiles; QColor m_codeColor;
int m_messageTokensCount{0};
int m_inputTokensCount{0};
bool m_isSyncOpenFiles;
QList<Core::IEditor *> m_currentEditors;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,142 +0,0 @@
/*
* 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 "ChatSerializer.hpp"
#include "Logger.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
namespace QodeAssist::Chat {
const QString ChatSerializer::VERSION = "0.1";
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
{
if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"};
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
}
QJsonObject root = serializeChat(model);
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
}
return {true, QString()};
}
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
return {false, QString("JSON parse error: %1").arg(error.errorString())};
}
QJsonObject root = doc.object();
QString version = root["version"].toString();
if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)};
}
if (!deserializeChat(model, root)) {
return {false, "Failed to deserialize chat data"};
}
return {true, QString()};
}
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.id = json["id"].toString();
return message;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
{
QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) {
messagesArray.append(serializeMessage(message));
}
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
return root;
}
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
{
QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
for (const auto &messageValue : messagesArray) {
messages.append(deserializeMessage(messageValue.toObject()));
}
model->clear();
for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id);
}
return true;
}
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
{
QFileInfo fileInfo(filePath);
QDir dir = fileInfo.dir();
return dir.exists() || dir.mkpath(".");
}
bool ChatSerializer::validateVersion(const QString &version)
{
return version == VERSION;
}
} // namespace QodeAssist::Chat

View File

@@ -1,56 +0,0 @@
/*
* 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 "ChatModel.hpp"
namespace QodeAssist::Chat {
struct SerializationResult
{
bool success{false};
QString errorMessage;
};
class ChatSerializer
{
public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
// Public for testing purposes
static QJsonObject serializeMessage(const ChatModel::Message &message);
static ChatModel::Message deserializeMessage(const QJsonObject &json);
static QJsonObject serializeChat(const ChatModel *model);
static bool deserializeChat(ChatModel *model, const QJsonObject &json);
private:
static const QString VERSION;
static constexpr int CURRENT_VERSION = 1;
static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version);
};
} // namespace QodeAssist::Chat

View File

@@ -23,6 +23,7 @@
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
// Q_NAMESPACE
class ChatUtils : public QObject class ChatUtils : public QObject
{ {

View File

@@ -33,7 +33,6 @@
#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 "PromptTemplateManager.hpp"
@@ -65,13 +64,11 @@ ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
ClientInterface::~ClientInterface() = default; ClientInterface::~ClientInterface() = default;
void ClientInterface::sendMessage( void ClientInterface::sendMessage(const QString &message, bool includeCurrentFile)
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
{ {
cancelRequest(); cancelRequest();
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments); m_chatModel->addMessage(message, ChatModel::ChatRole::User, "");
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
auto &chatAssistantSettings = Settings::chatAssistantSettings(); auto &chatAssistantSettings = Settings::chatAssistantSettings();
@@ -100,13 +97,16 @@ void ClientInterface::sendMessage(
if (chatAssistantSettings.useSystemPrompt()) if (chatAssistantSettings.useSystemPrompt())
systemPrompt = chatAssistantSettings.systemPrompt(); systemPrompt = chatAssistantSettings.systemPrompt();
if (!linkedFiles.isEmpty()) { if (includeCurrentFile) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles); QString fileContext = getCurrentFileContext();
if (!fileContext.isEmpty()) {
systemPrompt = systemPrompt.append(fileContext);
}
} }
QJsonObject providerRequest; QJsonObject providerRequest;
providerRequest["model"] = Settings::generalSettings().caModel(); providerRequest["model"] = Settings::generalSettings().caModel();
providerRequest["stream"] = chatAssistantSettings.stream(); providerRequest["stream"] = true;
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt); providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
if (promptTemplate) if (promptTemplate)
@@ -126,18 +126,11 @@ void ClientInterface::sendMessage(
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint()); config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest = providerRequest; config.providerRequest = providerRequest;
config.multiLineCompletion = false; config.multiLineCompletion = false;
config.apiKey = provider->apiKey(); config.apiKey = Settings::chatAssistantSettings().apiKey();
QJsonObject request; QJsonObject request;
request["id"] = QUuid::createUuid().toString(); 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;
}
m_requestHandler->sendLLMRequest(config, request); m_requestHandler->sendLLMRequest(config, request);
} }
@@ -166,7 +159,6 @@ void ClientInterface::handleLLMResponse(const QString &response,
if (isComplete) { if (isComplete) {
LOG_MESSAGE( LOG_MESSAGE(
"Message completed. Final response for message " + messageId + ": " + response); "Message completed. Final response for message " + messageId + ": " + response);
emit messageReceivedCompletely();
} }
} }
} }
@@ -195,21 +187,4 @@ QString ClientInterface::getCurrentFileContext() const
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(const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
.arg(file.filename, file.content);
}
}
return updatedPrompt;
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -36,23 +36,16 @@ public:
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr); explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void sendMessage( void sendMessage(const QString &message, bool includeCurrentFile = false);
const QString &message,
const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {});
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
signals: signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely();
private: 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(
const QString &basePrompt,
const QList<QString> &linkedFiles) const;
LLMCore::RequestHandler *m_requestHandler; LLMCore::RequestHandler *m_requestHandler;
ChatModel *m_chatModel; ChatModel *m_chatModel;

View File

@@ -1,11 +0,0 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_37_14)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_37_14">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

View File

@@ -1,11 +0,0 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_20)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_20">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

View File

@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 353 B

View File

@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 353 B

View File

@@ -1,12 +0,0 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_49_24)">
<path d="M10 12L10 32L10 12Z" fill="black"/>
<path d="M10 12L10 32" stroke="black" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_49_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 513 B

View File

@@ -1,12 +0,0 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_24)">
<path d="M10 12L10 32Z" fill="white"/>
<path d="M10 12L10 32" stroke="white" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 507 B

View File

@@ -23,18 +23,18 @@ Rectangle {
id: root id: root
property alias text: badgeText.text property alias text: badgeText.text
property alias fontColor: badgeText.color
implicitWidth: badgeText.implicitWidth + root.radius width: badgeText.implicitWidth + radius
implicitHeight: badgeText.implicitHeight + 6 height: badgeText.implicitHeight + 6
color: palette.button color: "lightgreen"
radius: root.height / 2 radius: height / 2
border.color: palette.mid
border.width: 1 border.width: 1
border.color: "gray"
Text { Text {
id: badgeText id: badgeText
anchors.centerIn: parent anchors.centerIn: parent
color: palette.buttonText
} }
} }

View File

@@ -17,25 +17,28 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/ */
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import ChatView import ChatView
import QtQuick.Layouts
import "./dialog" import "./dialog"
Rectangle { Rectangle {
id: root id: root
property alias msgModel: msgCreator.model property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model property color fontColor
property color codeBgColor
property color selectionColor
height: msgColumn.implicitHeight + 10 height: msgColumn.height
radius: 8 radius: 8
ColumnLayout { Column {
id: msgColumn id: msgColumn
width: parent.width
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width
spacing: 5 spacing: 5
Repeater { Repeater {
@@ -46,7 +49,7 @@ Rectangle {
// why does `required property MessagePart modelData` not work? // why does `required property MessagePart modelData` not work?
required property var modelData required property var modelData
Layout.preferredWidth: root.width width: parent.width
sourceComponent: { sourceComponent: {
// If `required property MessagePart modelData` is used // If `required property MessagePart modelData` is used
// and conversion to MessagePart fails, you're left // and conversion to MessagePart fails, you're left
@@ -77,40 +80,6 @@ Rectangle {
} }
} }
} }
Flow {
id: attachmentsFlow
Layout.fillWidth: true
visible: attachmentsModel.model && attachmentsModel.model.length > 0
leftPadding: 10
rightPadding: 10
spacing: 5
Repeater {
id: attachmentsModel
delegate: Rectangle {
required property int index
required property var modelData
height: attachText.implicitHeight + 8
width: attachText.implicitWidth + 16
radius: 4
color: palette.button
border.width: 1
border.color: palette.mid
Text {
id: attachText
anchors.centerIn: parent
text: modelData
color: palette.text
}
}
}
}
} }
component TextComponent : TextBlock { component TextComponent : TextBlock {
@@ -119,6 +88,8 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: 10 leftPadding: 10
text: itemData.text text: itemData.text
color: root.fontColor
selectionColor: root.selectionColor
} }
@@ -133,5 +104,8 @@ Rectangle {
code: itemData.text code: itemData.text
language: itemData.language language: itemData.language
color: root.codeBgColor
selectionColor: root.selectionColor
} }
} }

View File

@@ -17,66 +17,28 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/ */
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.Basic as QQC import QtQuick.Controls.Basic as QQC
import QtQuick.Layouts import QtQuick.Layouts
import ChatView import ChatView
import "./controls"
import "./parts"
ChatRootView { ChatRootView {
id: root id: root
property SystemPalette sysPalette: SystemPalette {
colorGroup: SystemPalette.Active
}
palette {
window: sysPalette.window
windowText: sysPalette.windowText
base: sysPalette.base
alternateBase: sysPalette.alternateBase
text: sysPalette.text
button: sysPalette.button
buttonText: sysPalette.buttonText
highlight: sysPalette.highlight
highlightedText: sysPalette.highlightedText
light: sysPalette.light
mid: sysPalette.mid
dark: sysPalette.dark
shadow: sysPalette.shadow
brightText: sysPalette.brightText
}
Rectangle { Rectangle {
id: bg id: bg
anchors.fill: parent anchors.fill: parent
color: palette.window color: root.backgroundColor
} }
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors {
spacing: 0 fill: parent
TopBar {
id: topBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
tokensBadge {
text: qsTr("tokens:%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
}
recentPath {
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
}
openChatHistory.onClicked: root.openChatHistoryFolder()
} }
spacing: 10
ListView { ListView {
id: chatListView id: chatListView
@@ -92,12 +54,14 @@ ChatRootView {
delegate: ChatItem { delegate: ChatItem {
required property var model required property var model
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 color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
color: model.roleType === ChatModel.User ? palette.alternateBase fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
: palette.base codeBgColor: root.codeColor
selectionColor: root.primaryColor.hslLightness > 0.5 ? Qt.darker(root.primaryColor, 1.5)
: Qt.lighter(root.primaryColor, 1.5)
} }
header: Item { header: Item {
@@ -105,7 +69,7 @@ ChatRootView {
height: 30 height: 30
} }
ScrollBar.vertical: QQC.ScrollBar { ScrollBar.vertical: ScrollBar {
id: scroll id: scroll
} }
@@ -131,28 +95,15 @@ ChatRootView {
id: messageInput id: messageInput
placeholderText: qsTr("Type your message here...") placeholderText: qsTr("Type your message here...")
placeholderTextColor: palette.mid placeholderTextColor: "#888"
color: palette.text color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
background: Rectangle { background: Rectangle {
radius: 2 radius: 2
color: palette.base color: root.primaryColor
border.color: messageInput.activeFocus ? palette.highlight : palette.button border.color: root.primaryColor.hslLightness > 0.5 ? Qt.lighter(root.primaryColor, 1.5)
: Qt.darker(root.primaryColor, 1.5)
border.width: 1 border.width: 1
Behavior on border.color {
ColorAnimation { duration: 150 }
} }
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: messageInput.hovered ? 0.1 : 0
radius: parent.radius
}
}
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
Keys.onPressed: function(event) { Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) { if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
root.sendChatMessage() root.sendChatMessage()
@@ -162,51 +113,65 @@ ChatRootView {
} }
} }
AttachedFilesPlace { RowLayout {
id: attachedFilesPlace
Layout.fillWidth: true Layout.fillWidth: true
attachedFilesModel: root.attachmentFiles spacing: 5
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg" Button {
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4)) id: sendButton
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
Layout.alignment: Qt.AlignBottom
text: qsTr("Send")
onClicked: root.sendChatMessage()
} }
AttachedFilesPlace { Button {
id: linkedFilesPlace id: stopButton
Layout.fillWidth: true Layout.alignment: Qt.AlignBottom
attachedFilesModel: root.linkedFiles text: qsTr("Stop")
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg" onClicked: root.cancelRequest()
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
} }
BottomBar { Button {
id: bottomBar id: clearButton
Layout.preferredWidth: parent.width Layout.alignment: Qt.AlignBottom
Layout.preferredHeight: 40 text: qsTr("Clear Chat")
onClicked: root.clearChat()
sendButton.onClicked: root.sendChatMessage()
stopButton.onClicked: root.cancelRequest()
syncOpenFiles {
checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
} }
attachFiles.onClicked: root.showAttachFilesDialog()
linkFiles.onClicked: root.showLinkFilesDialog() CheckBox {
testRag.onClicked: root.testRAG(messageInput.text) id: sharingCurrentFile
testChunks.onClicked: root.testChunking()
text: "Share current file with models"
checked: root.isSharingCurrentFile
}
}
}
Row {
id: bar
layoutDirection: Qt.RightToLeft
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: scroll.width
}
spacing: 10
Badge {
text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
color: root.codeColor
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
} }
} }
function clearChat() { function clearChat() {
root.chatModel.clear() root.chatModel.clear()
root.clearAttachmentFiles()
root.updateInputTokensCount()
} }
function scrollToBottom() { function scrollToBottom() {
@@ -214,7 +179,7 @@ ChatRootView {
} }
function sendChatMessage() { function sendChatMessage() {
root.sendMessage(messageInput.text) root.sendMessage(messageInput.text, sharingCurrentFile.checked)
messageInput.text = "" messageInput.text = ""
scrollToBottom() scrollToBottom()
} }

View File

@@ -1,54 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls.Basic
Button {
id: control
padding: 4
icon.width: 16
icon.height: 16
contentItem.height: 20
background: Rectangle {
id: bg
implicitHeight: 20
color: !control.enabled || !control.down ? control.palette.button : control.palette.dark
border.color: !control.enabled || (!control.hovered && !control.visualFocus) ? control.palette.mid : control.palette.highlight
border.width: 1
radius: 4
Rectangle {
anchors.fill: bg
radius: bg.radius
gradient: Gradient {
GradientStop { position: 0.0; color: Qt.alpha(control.palette.highlight, 0.4) }
GradientStop { position: 1.0; color: Qt.alpha(control.palette.highlight, 0.2) }
}
opacity: control.hovered ? 0.3 : 0.01
Behavior on opacity {NumberAnimation{duration: 250}}
}
}
}

View File

@@ -26,6 +26,7 @@ Rectangle {
property string code: "" property string code: ""
property string language: "" property string language: ""
property color selectionColor
readonly property string monospaceFont: { readonly property string monospaceFont: {
switch (Qt.platform.os) { switch (Qt.platform.os) {
@@ -40,7 +41,6 @@ Rectangle {
} }
} }
color: palette.alternateBase
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3) border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
: Qt.lighter(root.color, 1.3) : Qt.lighter(root.color, 1.3)
border.width: 2 border.width: 2
@@ -62,10 +62,10 @@ Rectangle {
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
font.family: root.monospaceFont font.family: root.monospaceFont
font.pointSize: Qt.application.font.pointSize font.pointSize: 12
color: parent.color.hslLightness > 0.5 ? "black" : "white" color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
selectionColor: palette.highlight selectionColor: root.selectionColor
} }
TextEdit { TextEdit {
@@ -80,7 +80,7 @@ Rectangle {
font.pointSize: 8 font.pointSize: 8
} }
QoAButton { Button {
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.margins: 5 anchors.margins: 5

View File

@@ -26,6 +26,4 @@ TextEdit {
selectByMouse: true selectByMouse: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
textFormat: Text.StyledText textFormat: Text.StyledText
selectionColor: palette.highlight
color: palette.text
} }

View File

@@ -1,109 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
Flow {
id: root
property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid
property string iconPath
signal removeFileFromListByIndex(index: int)
spacing: 5
leftPadding: 5
rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater {
id: attachRepeater
delegate: Rectangle {
required property int index
required property string modelData
height: 30
width: contentRow.width + 10
radius: 4
color: palette.button
border.width: 1
border.color: mouse.hovered ? palette.highlight : root.accentColor
HoverHandler {
id: mouse
}
Row {
id: contentRow
spacing: 5
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
Image {
id: icon
anchors.verticalCenter: parent.verticalCenter
source: root.iconPath
sourceSize.width: 8
sourceSize.height: 15
}
Text {
id: fileNameText
anchors.verticalCenter: parent.verticalCenter
color: palette.buttonText
text: {
const parts = modelData.split('/');
return parts[parts.length - 1];
}
}
MouseArea {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width + 5
height: closeButton.width + 5
onClicked: root.removeFileFromListByIndex(index)
Image {
id: closeIcon
anchors.centerIn: parent
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
width: 6
height: 6
}
}
}
}
}
}

View File

@@ -1,112 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
Rectangle {
id: root
property alias sendButton: sendButtonId
property alias stopButton: stopButtonId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias linkFiles: linkFilesId
property alias testRag: testRagId
property alias testChunks: testChunksId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
id: bottomBar
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: sendButtonId
text: qsTr("Send")
}
QoAButton {
id: stopButtonId
text: qsTr("Stop")
}
QoAButton {
id: attachFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Attach files")
}
QoAButton {
id: linkFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Link files")
}
CheckBox {
id: syncOpenFilesId
text: qsTr("Sync open files")
ToolTip.visible: syncOpenFilesId.hovered
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 {
Layout.fillWidth: true
}
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Layouts
import ChatView
Rectangle {
id: root
property alias saveButton: saveButtonId
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: saveButtonId
text: qsTr("Save")
}
QoAButton {
id: loadButtonId
text: qsTr("Load")
}
QoAButton {
id: clearButtonId
text: qsTr("Clear")
}
Text {
id: recentPathId
elide: Text.ElideMiddle
color: palette.text
}
QoAButton {
id: openChatHistoryId
text: qsTr("Show in system")
}
Item {
Layout.fillWidth: true
}
Badge {
id: tokensBadgeId
}
}
}

View File

@@ -1,128 +0,0 @@
/*
* 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 "CodeHandler.hpp"
#include <QHash>
namespace QodeAssist {
QString CodeHandler::processText(QString text)
{
QString result;
QStringList lines = text.split('\n');
bool inCodeBlock = false;
QString pendingComments;
QString currentLanguage;
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
currentLanguage = detectLanguage(line);
}
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
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";
}
}
pendingComments.clear();
}
result += line + "\n";
} else {
QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
pendingComments += trimmed + "\n";
} else {
pendingComments += "\n";
}
}
}
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;
}
QString CodeHandler::getCommentPrefix(const QString &language)
{
static const QHash<QString, QString> commentPrefixes
= {{"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::detectLanguage(const QString &line)
{
QString trimmed = line.trimmed();
if (trimmed.length() <= 3) { // Если только ```
return QString();
}
return trimmed.mid(3).trimmed();
}
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
{
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
} // namespace QodeAssist

View File

@@ -1,42 +0,0 @@
/*
* 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 <QObject>
#include <QRegularExpression>
#include <QString>
namespace QodeAssist {
class CodeHandler
{
public:
static QString processText(QString text);
private:
static QString getCommentPrefix(const QString &language);
static QString detectLanguage(const QString &line);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex();
};
} // namespace QodeAssist

View File

@@ -57,13 +57,6 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate); connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
} }
void ConfigurationManager::selectProvider() void ConfigurationManager::selectProvider()
@@ -76,8 +69,6 @@ void ConfigurationManager::selectProvider()
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider) auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
? m_generalSettings.ccProvider ? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider
: m_generalSettings.caProvider; : m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] { QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
@@ -95,19 +86,14 @@ void ConfigurationManager::selectModel()
return; return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue() const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue(); : m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue() const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: m_generalSettings.caUrl.volatileValue(); : m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : m_generalSettings.caModel;
: isPreset1 ? m_generalSettings.ccPreset1Model
: m_generalSettings.caModel;
if (auto provider = m_providersManager.getProviderByName(providerName)) { if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) { if (!provider->supportsModelListing()) {
@@ -136,13 +122,11 @@ void ConfigurationManager::selectTemplate()
return; return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames() const auto templateList = isCodeCompletion ? m_templateManger.fimTemplatesNames()
: m_templateManger.chatTemplatesNames(); : m_templateManger.chatTemplatesNames();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: 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]() {
@@ -166,9 +150,8 @@ void ConfigurationManager::selectUrl()
urls.append(url); urls.append(url);
} }
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
: settingsButton == &m_generalSettings.ccPreset1SetUrl ? m_generalSettings.ccUrl
? m_generalSettings.ccPreset1Url
: m_generalSettings.caUrl; : m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {

View File

@@ -23,9 +23,8 @@
#include <QTextBlock> #include <QTextBlock>
#include <languageserverprotocol/lsptypes.h> #include <languageserverprotocol/lsptypes.h>
#include "CodeCompletionSettings.hpp" #include "core/ChangesManager.h"
#include "settings/CodeCompletionSettings.hpp"
#include "ChangesManager.h"
const QRegularExpression &getYearRegex() const QRegularExpression &getYearRegex()
{ {
@@ -47,7 +46,7 @@ const QRegularExpression &getCommentRegex()
return commentRegex; return commentRegex;
} }
namespace QodeAssist::Context { namespace QodeAssist {
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument) DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
: m_textDocument(textDocument) : m_textDocument(textDocument)
@@ -247,4 +246,4 @@ QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPositio
} }
} }
} // namespace QodeAssist::Context } // namespace QodeAssist

View File

@@ -19,12 +19,12 @@
#pragma once #pragma once
#include <texteditor/textdocument.h>
#include <QTextDocument> #include <QTextDocument>
#include <texteditor/textdocument.h>
#include <llmcore/ContextData.hpp> #include <llmcore/ContextData.hpp>
namespace QodeAssist::Context { namespace QodeAssist {
struct CopyrightInfo struct CopyrightInfo
{ {
@@ -61,4 +61,4 @@ private:
CopyrightInfo m_copyrightInfo; CopyrightInfo m_copyrightInfo;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist

View File

@@ -26,9 +26,7 @@
#include <llmcore/RequestConfig.hpp> #include <llmcore/RequestConfig.hpp>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include "CodeHandler.hpp" #include "DocumentContextReader.hpp"
#include "context/DocumentContextReader.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"
@@ -146,41 +144,20 @@ 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)
{ {
const auto updatedContext = prepareContext(request); auto updatedContext = prepareContext(request);
auto &completeSettings = Settings::codeCompletionSettings(); auto &completeSettings = Settings::codeCompletionSettings();
auto &generalSettings = Settings::generalSettings();
bool isPreset1Active = isSpecifyCompletion(request); auto providerName = Settings::generalSettings().ccProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
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 provider = LLMCore::ProvidersManager::instance().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 ? generalSettings.ccTemplate() auto templateName = Settings::generalSettings().ccTemplate();
: generalSettings.ccPreset1Template();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName( auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName); templateName);
@@ -189,56 +166,33 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return; return;
} }
// TODO refactor to dynamic presets system
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::CodeCompletion; config.requestType = LLMCore::RequestType::Fim;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
config.url = QUrl(QString("%1%2").arg( config.url = QUrl(
url, QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint() config.apiKey = Settings::codeCompletionSettings().apiKey();
: provider->chatEndpoint()));
config.apiKey = provider->apiKey();
config.providerRequest = {{"model", modelName}, {"stream", completeSettings.stream()}}; config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
config.multiLineCompletion = completeSettings.multiLineCompletion(); config.multiLineCompletion = completeSettings.multiLineCompletion();
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords;
QString systemPrompt; QString systemPrompt;
if (completeSettings.useSystemPrompt()) if (completeSettings.useSystemPrompt())
systemPrompt.append(completeSettings.systemPrompt()); systemPrompt.append(completeSettings.systemPrompt());
if (!updatedContext.fileContext.isEmpty()) if (!updatedContext.fileContext.isEmpty())
systemPrompt.append(updatedContext.fileContext); systemPrompt.append(updatedContext.fileContext);
if (!systemPrompt.isEmpty())
config.providerRequest["system"] = systemPrompt;
QString userMessage; const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) { if (!stopWords.isEmpty())
userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix); config.providerRequest["stop"] = stopWords;
} else {
userMessage = updatedContext.prefix;
}
auto message = LLMCore::MessageBuilder() config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
.addSystemMessage(systemPrompt) config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
.addUserMessage(userMessage)
.addSuffix(updatedContext.suffix)
.addTokenizer(promptTemplate);
message.saveTo(
config.providerRequest,
providerName == "Ollama" ? LLMCore::ProvidersApi::Ollama : LLMCore::ProvidersApi::OpenAI);
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::CodeCompletion);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for fim request:");
LOG_MESSAGES(errors);
return;
}
m_requestHandler.sendLLMRequest(config, request); m_requestHandler.sendLLMRequest(config, request);
} }
@@ -262,61 +216,27 @@ LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &reque
int cursorPosition = position["character"].toInt(); int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt(); int lineNumber = position["line"].toInt();
Context::DocumentContextReader reader(textDocument); DocumentContextReader reader(textDocument);
return reader.prepareContext(lineNumber, cursorPosition); return reader.prepareContext(lineNumber, cursorPosition);
} }
Context::ProgrammingLanguage LLMClientInterface::getDocumentLanguage(const QJsonObject &request) const
{
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(const QString &completion, void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QJsonObject &request, const QJsonObject &request,
bool isComplete) bool isComplete)
{ {
bool isPreset1Active = isSpecifyCompletion(request);
auto templateName = !isPreset1Active ? Settings::generalSettings().ccTemplate()
: Settings::generalSettings().ccPreset1Template();
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();
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"]; response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject result; QJsonObject result;
QJsonArray completions; QJsonArray completions;
QJsonObject completionItem; QJsonObject completionItem;
completionItem[LanguageServerProtocol::textKey] = completion;
QString processedCompletion
= promptTemplate->type() == LLMCore::TemplateType::Chat
&& Settings::codeCompletionSettings().smartProcessInstuctText()
? CodeHandler::processText(completion)
: completion;
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range; QJsonObject range;
range["start"] = position; range["start"] = position;
QJsonObject end = position; QJsonObject end = position;
end["character"] = position["character"].toInt() + processedCompletion.length(); end["character"] = position["character"].toInt() + completion.length();
range["end"] = end; range["end"] = end;
completionItem[LanguageServerProtocol::rangeKey] = range; completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position; completionItem[LanguageServerProtocol::positionKey] = position;

View File

@@ -22,7 +22,6 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp> #include <llmcore/ContextData.hpp>
#include <llmcore/RequestHandler.hpp> #include <llmcore/RequestHandler.hpp>
@@ -59,10 +58,8 @@ private:
void handleExit(const QJsonObject &request); void handleExit(const QJsonObject &request);
void handleCancelRequest(const QJsonObject &request); void handleCancelRequest(const QJsonObject &request);
LLMCore::ContextData prepareContext( LLMCore::ContextData prepareContext(const QJsonObject &request,
const QJsonObject &request, const QStringView &accumulatedCompletion = QString{}); const QStringView &accumulatedCompletion = QString{});
Context::ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
bool isSpecifyCompletion(const QJsonObject &request);
LLMCore::RequestHandler m_requestHandler; LLMCore::RequestHandler m_requestHandler;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;

View File

@@ -1,13 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify * QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -23,25 +18,30 @@
*/ */
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include <QTextCursor>
#include <QtWidgets/qtoolbar.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <utils/stringutils.h> #include <utils/stringutils.h>
#include <utils/tooltip/tooltip.h> #include <utils/tooltip/tooltip.h>
namespace QodeAssist { namespace QodeAssist {
LLMSuggestion::LLMSuggestion( LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin)
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion) : m_completion(completion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion) , m_linesCount(0)
{ {
const auto &data = suggestions[currentCompletion]; int startPos = completion.range().start().toPositionInDocument(origin);
int endPos = completion.range().end().toPositionInDocument(origin);
int startPos = data.range.begin.toPositionInDocument(sourceDocument); startPos = qBound(0, startPos, origin->characterCount() - 1);
int endPos = data.range.end.toPositionInDocument(sourceDocument); endPos = qBound(startPos, endPos, origin->characterCount() - 1);
startPos = qBound(0, startPos, sourceDocument->characterCount() - 1); m_start = QTextCursor(origin);
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1); m_start.setPosition(startPos);
m_start.setKeepPositionOnInsert(true);
QTextCursor cursor(sourceDocument); QTextCursor cursor(origin);
cursor.setPosition(startPos); cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor); cursor.setPosition(endPos, QTextCursor::KeepAnchor);
@@ -51,57 +51,74 @@ LLMSuggestion::LLMSuggestion(
int startPosInBlock = startPos - block.position(); int startPosInBlock = startPos - block.position();
int endPosInBlock = endPos - block.position(); int endPosInBlock = endPos - block.position();
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text); blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, completion.text());
replacementDocument()->setPlainText(blockText);
document()->setPlainText(blockText);
setCurrentPosition(m_start.position());
}
bool LLMSuggestion::apply()
{
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
cursor.beginEditBlock();
cursor.removeSelectedText();
cursor.insertText(m_completion.text());
cursor.endEditBlock();
return true;
} }
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
{ {
return applyPart(Word, widget); return applyNextLine(widget);
} }
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyNextLine(TextEditor::TextEditorWidget *widget)
{ {
return applyPart(Line, widget); const QString text = m_completion.text();
QStringList lines = text.split('\n');
if (m_linesCount < lines.size())
m_linesCount++;
showTooltip(widget, m_linesCount);
return m_linesCount == lines.size() && !Utils::ToolTip::isVisible();
} }
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget) void LLMSuggestion::onCounterFinished(int count)
{ {
const Utils::Text::Range range = suggestions()[currentSuggestion()].range; Utils::ToolTip::hide();
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument()); m_linesCount = 0;
QTextCursor currentCursor = widget->textCursor(); QTextCursor cursor = m_completion.range().toSelection(m_start.document());
const QString text = suggestions()[currentSuggestion()].text; cursor.beginEditBlock();
cursor.removeSelectedText();
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock() QStringList lines = m_completion.text().split('\n');
+ (cursor.selectionEnd() - cursor.selectionStart()); QString textToInsert = lines.mid(0, count).join('\n');
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos); cursor.insertText(textToInsert);
cursor.endEditBlock();
if (next == -1)
return apply();
if (part == Line)
++next;
QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty())
return false;
currentCursor.insertText(subText);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Utils::Text::Position
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
} }
void LLMSuggestion::reset()
{
m_start.removeSelectedText();
} }
return false;
int LLMSuggestion::position()
{
return m_start.position();
}
void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
{
Utils::ToolTip::hide();
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
pos += QPoint(-10, -50);
m_counterTooltip = new CounterTooltip(count);
Utils::ToolTip::show(pos, m_counterTooltip, widget);
connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
} }
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,13 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify * QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -24,21 +19,37 @@
#pragma once #pragma once
#include <texteditor/texteditor.h> #include <QObject>
#include <texteditor/textsuggestion.h> #include "LSPCompletion.hpp"
#include <texteditor/textdocumentlayout.h>
#include "utils/CounterTooltip.hpp"
namespace QodeAssist { namespace QodeAssist {
class LLMSuggestion : public TextEditor::CyclicSuggestion class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion
{ {
Q_OBJECT
public: public:
enum Part { Word, Line }; LLMSuggestion(const Completion &completion, QTextDocument *origin);
LLMSuggestion( bool apply() final;
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0); bool applyWord(TextEditor::TextEditorWidget *widget) final;
bool applyNextLine(TextEditor::TextEditorWidget *widget);
void reset() final;
int position() final;
bool applyWord(TextEditor::TextEditorWidget *widget) override; const Completion &completion() const { return m_completion; }
bool applyLine(TextEditor::TextEditorWidget *widget) override;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget); void showTooltip(TextEditor::TextEditorWidget *widget, int count);
void onCounterFinished(int count);
private:
Completion m_completion;
QTextCursor m_start;
int m_linesCount;
CounterTooltip *m_counterTooltip = nullptr;
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,13 +1,16 @@
{ {
"Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.4.13", "Version" : "0.3.10",
"CompatVersion" : "${IDE_VERSION_COMPAT}",
"Vendor" : "Petr Mironychev", "Vendor" : "Petr Mironychev",
"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" : "GNU General Public License Usage
"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).",
Alternatively, this file may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this file. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.",
"Description" : ["QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code",
"Prerequisites:",
"- One of the supported LLM providers installed (e.g., Ollama or LM Studio)",
"- 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" : "",
${IDE_PLUGIN_DEPENDENCIES} ${IDE_PLUGIN_DEPENDENCIES}
} }

View File

@@ -31,10 +31,9 @@
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include "core/ChangesManager.h"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include <context/ChangesManager.h>
using namespace LanguageServerProtocol; using namespace LanguageServerProtocol;
using namespace TextEditor; using namespace TextEditor;
@@ -71,11 +70,11 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return; return;
Client::openDocument(document); Client::openDocument(document);
connect( connect(document,
document,
&TextDocument::contentsChangedWithPosition, &TextDocument::contentsChangedWithPosition,
this, this,
[this, document](int position, int charsRemoved, int charsAdded) { [this, document](int position, int charsRemoved, int charsAdded) {
Q_UNUSED(charsRemoved)
if (!Settings::codeCompletionSettings().autoCompletion()) if (!Settings::codeCompletionSettings().autoCompletion())
return; return;
@@ -88,33 +87,18 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return; return;
if (Settings::codeCompletionSettings().useProjectChangesCache()) if (Settings::codeCompletionSettings().useProjectChangesCache())
Context::ChangesManager::instance() ChangesManager::instance().addChange(document,
.addChange(document, position, charsRemoved, charsAdded); position,
charsRemoved,
charsAdded);
TextEditorWidget *widget = textEditor->editorWidget(); TextEditorWidget *widget = textEditor->editorWidget();
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
return; return;
const int cursorPosition = widget->textCursor().position(); const int cursorPosition = widget->textCursor().position();
if (cursorPosition < position || cursorPosition > position + charsAdded) if (cursorPosition < position || cursorPosition > position + charsAdded)
return; return;
if (charsRemoved > 0 || charsAdded <= 0) {
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
QTextCursor cursor = widget->textCursor();
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
QString lastChar = cursor.selectedText();
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
m_recentCharCount += charsAdded; m_recentCharCount += charsAdded;
if (m_typingTimer.elapsed() if (m_typingTimer.elapsed()
@@ -209,8 +193,8 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
auto isValidCompletion = [](const Completion &completion) { auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty(); return completion.isValid() && !completion.text().trimmed().isEmpty();
}; };
QList<Completion> completions QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(),
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion); isValidCompletion);
// remove trailing whitespaces from the end of the completions // remove trailing whitespaces from the end of the completions
for (Completion &completion : completions) { for (Completion &completion : completions) {
@@ -227,18 +211,10 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
if (delta > 0) if (delta > 0)
completion.setText(completionText.chopped(delta)); completion.setText(completionText.chopped(delta));
} }
auto suggestions = Utils::transform(completions, [](const Completion &c) {
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()};
};
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
if (completions.isEmpty()) if (completions.isEmpty())
return; return;
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document())); editor->insertSuggestion(
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
} }
} }
@@ -253,11 +229,7 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
{ {
if (!project)
return Settings::generalSettings().enableQodeAssist(); return Settings::generalSettings().enableQodeAssist();
Settings::ProjectSettings settings(project);
return settings.isEnabled();
} }
void QodeAssistClient::setupConnections() void QodeAssistClient::setupConnections()

206
README.md
View File

@@ -1,57 +1,34 @@
# QodeAssist - AI-powered coding assistant plugin for Qt Creator # QodeAssist - AI-powered coding assistant plugin for Qt Creator
[![Discord](https://dcbadge.limes.pink/api/server/https://discord.gg/DGgMtTteAD?style=flat?theme=discord)](https://discord.gg/DGgMtTteAD)
[![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-15.0.1-brightgreen) ![Static Badge](https://img.shields.io/badge/QtCreator-14.0.2-brightgreen)
[![](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 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.
⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
> - These services will consume API tokens which may result in charges to your account
> - The QodeAssist developer bears no responsibility for any charges incurred
> - 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. [Installation](#installation)
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude) 3. [Configure Plugin](#configure-plugin)
4. [Configure for OpenAI](#configure-for-openai) 4. [Supported LLM Providers](#supported-llm-providers)
5. [Configure for using Ollama](#configure-for-using-ollama) 5. [Recommended Models](#recommended-models)
6. [System Prompt Configuration](#system-prompt-configuration) - [Ollama](#ollama)
7. [File Context Features](#file-context-features) - [LM Studio](#lm-studio)
8. [Template-Model Compatibility](#template-model-compatibility) 6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
9. [QtCreator Version Compatibility](#qtcreator-version-compatibility) 7. [Development Progress](#development-progress)
10. [Development Progress](#development-progress) 8. [Hotkeys](#hotkeys)
11. [Hotkeys](#hotkeys) 9. [Troubleshooting](#troubleshooting)
12. [Troubleshooting](#troubleshooting) 10. [Support the Development](#support-the-development-of-qodeassist)
13. [Support the Development](#support-the-development-of-qodeassist) 11. [How to Build](#how-to-build)
14. [How to Build](#how-to-build)
## Overview ## Overview
- AI-powered code completion - AI-powered code completion
- Chat functionality: - Chat functionality:
- Side and Bottom panels - Side and Bottom panels
- Chat history autosave and restore
- Token usage monitoring and management
- Attach files for one-time code analysis
- Link files for persistent context with auto update in conversations
- Automatic syncing with open editor files (optional)
- Support for multiple LLM providers: - Support for multiple LLM providers:
- Ollama - Ollama
- OpenAI
- Anthropic Claude
- LM Studio - LM Studio
- OpenAI-compatible providers(eg. https://openrouter.ai) - OpenAI-compatible providers(eg. https://openrouter.ai)
- Extensive library of model-specific templates - Extensive library of model-specific templates
@@ -63,11 +40,6 @@
<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>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
</details>
<details> <details>
<summary>Chat with LLM models in side panels: (click to expand)</summary> <summary>Chat with LLM models in side panels: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/ead5a5d9-b40a-4f17-af05-77fa2bcb3a61" width="600" alt="QodeAssistChat"> <img src="https://github.com/user-attachments/assets/ead5a5d9-b40a-4f17-af05-77fa2bcb3a61" width="600" alt="QodeAssistChat">
@@ -78,52 +50,11 @@
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b"> <img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details> </details>
<details> ## Installation
<summary>Automatic syncing with open editor files: (click to expand)</summary>
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
</details>
## Install plugin to QtCreator
1. Install Latest QtCreator 1. Install Latest QtCreator
2. Download the QodeAssist plugin for your Qt Creator 2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
- Remove old version plugin if already was installed 3. Install a language models in Ollama via terminal. For example, you can run:
3. Launch Qt Creator and install the plugin:
- Go to:
- MacOS: Qt Creator -> About Plugins...
- Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure for Anthropic Claude
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Claude api key
3. Return to General tab and configure:
- Set "Claude" as the provider for code completion or/and chat assistant
- Set the Claude URL (https://api.anthropic.com)
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
- Choose the Claude template for code completion or/and chat
<details>
<summary>Example of Claude settings: (click to expand)</summary>
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
</details>
## Configure for OpenAI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure OpenAI api key
3. Return to General tab and configure:
- Set "OpenAI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.openai.com)
- Select your preferred model (e.g., gpt-4o)
- Choose the OpenAI template for code completion or/and chat
<details>
<summary>Example of OpenAI settings: (click to expand)</summary>
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
</details>
## Configure for using Ollama
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:
For standard computers (minimum 8GB RAM): For standard computers (minimum 8GB RAM):
``` ```
@@ -137,6 +68,16 @@ For high-end systems (32GB+ RAM):
``` ```
ollama run qwen2.5-coder:32b ollama run qwen2.5-coder:32b
``` ```
4. Download the QodeAssist plugin for your QtCreator.
5. Launch Qt Creator and install the plugin:
- Go to MacOS: Qt Creator -> About Plugins...
Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure Plugin
QodeAssist comes with default settings that should work immediately after installing a language model. The plugin is pre-configured to use Ollama with standard templates, so you may only need to verify the settings.
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 "Qode Assist" tab 2. Navigate to the "Qode Assist" tab
@@ -144,72 +85,65 @@ ollama run qwen2.5-coder:32b
- 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
- Your installed model appears in the model selection - Your installed model appears in the model selection
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct - The prompt template is Ollama Auto FIM
4. Click Apply if you made any changes 4. Click Apply if you made any changes
You're all set! QodeAssist is now ready to use in Qt Creator. You're all set! QodeAssist is now ready to use in Qt Creator.
<details>
<summary>Example of Ollama settings: (click to expand)</summary>
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
</details>
## System Prompt Configuration ## Supported LLM Providers
QodeAssist currently supports the following LLM (Large Language Model) providers:
- [Ollama](https://ollama.com)
- [LM Studio](https://lmstudio.ai) (experimental)
- OpenAI compatible providers (experimental)
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. ## Recommended Models:
QodeAssist has been thoroughly tested and optimized for use with the following language models:
## File Context Features - Qwen2.5-coder
- CodeLlama
- StarCoder2
- DeepSeek-Coder-V2
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant. ### Ollama:
### For autocomplete(FIM)
```
ollama run codellama:7b-code
ollama run starcoder2:7b
ollama run qwen2.5-coder:7b-base
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
```
### For chat
```
ollama run codellama:7b-instruct
ollama run starcoder2:instruct
ollama run qwen2.5-coder:7b-instruct
ollama run deepseek-coder-v2
```
### Attached Files ### Template-Model Compatibility
Attachments are designed for one-time code analysis and specific queries:
- Files are included only in the current message
- Content is discarded after the message is processed
- Ideal for:
- Getting specific feedback on code changes
- Code review requests
- Analyzing isolated code segments
- Quick implementation questions
- Files can be attached using the paperclip icon in the chat interface
- Multiple files can be attached to a single message
### Linked Files
Linked files provide persistent context throughout the conversation:
- Files remain accessible for the entire chat session
- Content is included in every message exchange
- Files are automatically refreshed - always using latest content from disk
- Perfect for:
- Long-term refactoring discussions
- Complex architectural changes
- Multi-file implementations
- Maintaining context across related questions
- Can be managed using the link icon in the chat interface
- Supports automatic syncing with open editor files (can be enabled in settings)
- Files can be added/removed at any time during the conversation
## Template-Model Compatibility
| Template | Compatible Models | Purpose | | Template | Compatible Models | Purpose |
|----------|------------------|----------| |----------|------------------|----------|
| CodeLlama FIM | `codellama:code` | Code completion | | CodeLlama FIM | `codellama:code` | Code completion |
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion | | DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
| Ollama Auto FIM | `Any Ollama base/fim models` | Code completion | | Ollama Auto FIM | `Any Ollama base model` | Code completion |
| Qwen FIM | `Qwen 2.5 models(exclude instruct)` | Code completion | | Qwen FIM | `Qwen 2.5 models` | Code completion |
| StarCoder2 FIM | `starcoder2 base model` | Code completion | | StarCoder2 FIM | `starcoder2 base model` | Code completion |
| Alpaca | `starcoder2:instruct` | Chat assistance | | Alpaca | `starcoder2:instruct` | Chat assistance |
| Basic Chat| `Messages without tokens` | Chat assistance | | Basic Chat| `Messages without tokens` | Chat assistance |
| ChatML | `Qwen 2.5 models(exclude base models)` | Chat assistance | | ChatML | `Qwen 2.5 models` | Chat assistance |
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance | | Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
| Llama3 | `llama3 model family` | Chat assistance | | Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat/instruct models` | Chat assistance | | Ollama Auto Chat | `Any Ollama chat model` | Chat assistance |
> Note:
> - FIM (Fill-in-Middle) templates are optimized for code completion
> - Chat templates are designed for interactive dialogue
> - The Ollama Auto templates automatically adapt to most Ollama models
> - Custom Template allows you to define your own prompt format
## QtCreator Version Compatibility ## QtCreator Version Compatibility
- QtCreator 15.0.1 - 0.4.8 - 0.4.x
- 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
@@ -228,7 +162,9 @@ Linked files provide persistent context throughout the conversation:
- on Mac: Option + Command + Q - on Mac: Option + Command + Q
- on Windows: Ctrl + Alt + Q - on Windows: 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 line by line, you can use the "Move cursor word right" shortcut:
- On Mac: Option + Right Arrow
- On Windows: Alt + Right Arrow
## Troubleshooting ## Troubleshooting
@@ -291,7 +227,3 @@ relative or absolute path to this plugin directory.
QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
C++ code style: check use .clang-fortmat in project C++ code style: check use .clang-fortmat in project
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

View File

@@ -1,72 +0,0 @@
/*
* 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 "UpdateStatusWidget.hpp"
namespace QodeAssist {
UpdateStatusWidget::UpdateStatusWidget(QWidget *parent)
: QFrame(parent)
{
setFrameStyle(QFrame::NoFrame);
auto layout = new QHBoxLayout(this);
layout->setContentsMargins(4, 0, 4, 0);
layout->setSpacing(4);
m_actionButton = new QToolButton(this);
m_actionButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_versionLabel = new QLabel(this);
m_versionLabel->setVisible(false);
m_updateButton = new QPushButton(tr("Update"), this);
m_updateButton->setVisible(false);
m_updateButton->setStyleSheet("QPushButton { padding: 2px 8px; }");
layout->addWidget(m_actionButton);
layout->addWidget(m_versionLabel);
layout->addWidget(m_updateButton);
}
void UpdateStatusWidget::setDefaultAction(QAction *action)
{
m_actionButton->setDefaultAction(action);
}
void UpdateStatusWidget::showUpdateAvailable(const QString &version)
{
m_versionLabel->setText(tr("new version: v%1").arg(version));
m_versionLabel->setVisible(true);
m_updateButton->setVisible(true);
m_updateButton->setToolTip(tr("Update QodeAssist to version %1").arg(version));
}
void UpdateStatusWidget::hideUpdateInfo()
{
m_versionLabel->setVisible(false);
m_updateButton->setVisible(false);
}
QPushButton *UpdateStatusWidget::updateButton() const
{
return m_updateButton;
}
} // namespace QodeAssist

View File

@@ -1,31 +0,0 @@
add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp
ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp
ContentFile.hpp
TokenUtils.hpp TokenUtils.cpp
ProgrammingLanguage.hpp ProgrammingLanguage.cpp
RAGManager.hpp RAGManager.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
PUBLIC
Qt::Core
Qt::Sql
QtCreator::Core
QtCreator::TextEditor
QtCreator::Utils
QtCreator::ProjectExplorer
PRIVATE
LLMCore
QodeAssistSettings
)
target_include_directories(Context PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR})

View File

@@ -1,32 +0,0 @@
/*
* 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 <QString>
namespace QodeAssist::Context {
struct ContentFile
{
QString filename;
QString content;
};
} // namespace QodeAssist::Context

View File

@@ -1,176 +0,0 @@
/*
* 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 "ContextManager.hpp"
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <projectexplorer/project.h>
#include <projectexplorer/projectnodes.h>
#include "FileChunker.hpp"
namespace QodeAssist::Context {
ContextManager &ContextManager::instance()
{
static ContextManager manager;
return manager;
}
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
{}
QString ContextManager::readFile(const QString &filePath) const
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return QString();
QTextStream in(&file);
return in.readAll();
}
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
{
QList<ContentFile> files;
for (const QString &path : filePaths) {
ContentFile contentFile = createContentFile(path);
files.append(contentFile);
}
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 sourceFiles;
if (!project)
return sourceFiles;
auto projectNode = project->rootProjectNode();
if (!projectNode)
return sourceFiles;
projectNode->forEachNode(
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
if (fileNode) {
QString filePath = fileNode->filePath().toString();
if (shouldProcessFile(filePath) && !isInBuildDirectory(filePath)) {
sourceFiles.append(filePath);
}
}
},
nullptr);
return sourceFiles;
}
bool ContextManager::shouldProcessFile(const QString &filePath) const
{
static const QStringList supportedExtensions
= {"cpp", "hpp", "c", "h", "cc", "hh", "cxx", "hxx", "qml", "js", "py"};
QFileInfo fileInfo(filePath);
return supportedExtensions.contains(fileInfo.suffix().toLower());
}
void ContextManager::testProjectChunks(
ProjectExplorer::Project *project, const FileChunker::ChunkingConfig &config)
{
if (!project) {
qDebug() << "No project provided";
return;
}
qDebug() << "\nStarting test chunking for project:" << project->displayName();
// Get source files
QStringList sourceFiles = getProjectSourceFiles(project);
qDebug() << "Found" << sourceFiles.size() << "source files";
// Create chunker
auto chunker = new FileChunker(config, this);
// Connect progress and error signals
connect(chunker, &FileChunker::progressUpdated, this, [](int processed, int total) {
qDebug() << "Progress:" << processed << "/" << total << "files";
});
connect(chunker, &FileChunker::error, this, [](const QString &error) {
qDebug() << "Error:" << error;
});
// Start chunking and handle results
auto future = chunker->chunkFiles(sourceFiles);
// Используем QFutureWatcher для обработки результатов
auto watcher = new QFutureWatcher<QList<FileChunk>>(this);
connect(watcher, &QFutureWatcher<QList<FileChunk>>::finished, this, [watcher, chunker]() {
// Очистка
watcher->deleteLater();
chunker->deleteLater();
});
watcher->setFuture(future);
}
} // namespace QodeAssist::Context

View File

@@ -1,60 +0,0 @@
/*
* 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 "ContentFile.hpp"
#include <QObject>
#include <QString>
#include "FileChunker.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class ContextManager : public QObject
{
Q_OBJECT
public:
static ContextManager &instance();
QString readFile(const QString &filePath) const;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const;
void testProjectChunks(
ProjectExplorer::Project *project, const FileChunker::ChunkingConfig &config);
private:
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

View File

@@ -1,265 +0,0 @@
#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

@@ -1,74 +0,0 @@
#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

View File

@@ -1,198 +0,0 @@
// 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

View File

@@ -1,68 +0,0 @@
// 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,70 +0,0 @@
/*
* 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 "ProgrammingLanguage.hpp"
namespace QodeAssist::Context {
ProgrammingLanguage ProgrammingLanguageUtils::fromMimeType(const QString &mimeType)
{
if (mimeType == "text/x-qml" || mimeType == "application/javascript"
|| mimeType == "text/javascript" || mimeType == "text/x-javascript") {
return ProgrammingLanguage::QML;
}
if (mimeType == "text/x-c++src" || mimeType == "text/x-c++hdr" || mimeType == "text/x-csrc"
|| mimeType == "text/x-chdr") {
return ProgrammingLanguage::Cpp;
}
if (mimeType == "text/x-python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
QString ProgrammingLanguageUtils::toString(ProgrammingLanguage language)
{
switch (language) {
case ProgrammingLanguage::Cpp:
return "c/c++";
case ProgrammingLanguage::QML:
return "qml";
case ProgrammingLanguage::Python:
return "python";
case ProgrammingLanguage::Unknown:
default:
return QString();
}
}
ProgrammingLanguage ProgrammingLanguageUtils::fromString(const QString &str)
{
QString lower = str.toLower();
if (lower == "c/c++") {
return ProgrammingLanguage::Cpp;
}
if (lower == "qml") {
return ProgrammingLanguage::QML;
}
if (lower == "python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
} // namespace QodeAssist::Context

View File

@@ -1,43 +0,0 @@
/*
* 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 <QString>
namespace QodeAssist::Context {
enum class ProgrammingLanguage {
QML,
Cpp,
Python,
Unknown,
};
namespace ProgrammingLanguageUtils {
ProgrammingLanguage fromMimeType(const QString &mimeType);
QString toString(ProgrammingLanguage language);
ProgrammingLanguage fromString(const QString &str);
} // namespace ProgrammingLanguageUtils
} // namespace QodeAssist::Context

View File

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

View File

@@ -1,443 +0,0 @@
/*
* 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

View File

@@ -1,119 +0,0 @@
/*
* 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

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

View File

@@ -1,64 +0,0 @@
#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

@@ -1,67 +0,0 @@
/*
* 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,174 +0,0 @@
/*
* 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

View File

@@ -1,116 +0,0 @@
/*
* 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,51 +0,0 @@
/*
* 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 <QFuture>
#include <QNetworkAccessManager>
#include <QObject>
#include <RAGData.hpp>
namespace QodeAssist::Context {
class RAGVectorizer : public QObject
{
Q_OBJECT
public:
explicit RAGVectorizer(
const QString &providerUrl = "http://localhost:11434",
const QString &modelName = "all-minilm:33m-l12-v2-fp16",
QObject *parent = nullptr);
~RAGVectorizer();
QFuture<RAGVector> vectorizeText(const QString &text);
private:
QJsonObject prepareEmbeddingRequest(const QString &text) const;
RAGVector parseEmbeddingResponse(const QByteArray &response) const;
QNetworkAccessManager *m_network;
QString m_embedProviderUrl;
QString m_model;
};
} // namespace QodeAssist::Context

View File

@@ -1,54 +0,0 @@
/*
* 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 "TokenUtils.hpp"
namespace QodeAssist::Context {
int TokenUtils::estimateTokens(const QString& text)
{
if (text.isEmpty()) {
return 0;
}
// TODO: need to improve
return text.length() / 4;
}
int TokenUtils::estimateFileTokens(const Context::ContentFile& file)
{
int total = 0;
total += estimateTokens(file.filename);
total += estimateTokens(file.content);
total += 5;
return total;
}
int TokenUtils::estimateFilesTokens(const QList<Context::ContentFile>& files)
{
int total = 0;
for (const auto& file : files) {
total += estimateFileTokens(file);
}
return total;
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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 <QString>
#include "ContentFile.hpp"
#include <QList>
namespace QodeAssist::Context {
class TokenUtils
{
public:
static int estimateTokens(const QString& text);
static int estimateFileTokens(const Context::ContentFile& file);
static int estimateFilesTokens(const QList<Context::ContentFile>& files);
};
}

View File

@@ -18,9 +18,9 @@
*/ */
#include "ChangesManager.h" #include "ChangesManager.h"
#include "CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
namespace QodeAssist::Context { namespace QodeAssist {
ChangesManager &ChangesManager::instance() ChangesManager &ChangesManager::instance()
{ {
@@ -79,4 +79,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
return context; return context;
} }
} // namespace QodeAssist::Context } // namespace QodeAssist

View File

@@ -25,7 +25,7 @@
#include <QTimer> #include <QTimer>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
namespace QodeAssist::Context { namespace QodeAssist {
class ChangesManager : public QObject class ChangesManager : public QObject
{ {
@@ -58,4 +58,4 @@ private:
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges; QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist

View File

@@ -7,10 +7,6 @@ add_library(LLMCore STATIC
PromptTemplateManager.hpp PromptTemplateManager.cpp PromptTemplateManager.hpp PromptTemplateManager.cpp
RequestConfig.hpp RequestConfig.hpp
RequestHandler.hpp RequestHandler.cpp RequestHandler.hpp RequestHandler.cpp
OllamaMessage.hpp OllamaMessage.cpp
OpenAIMessage.hpp OpenAIMessage.cpp
ValidationUtils.hpp ValidationUtils.cpp
MessageBuilder.hpp MessageBuilder.cpp
) )
target_link_libraries(LLMCore target_link_libraries(LLMCore

View File

@@ -1,93 +0,0 @@
/*
* 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

@@ -1,68 +0,0 @@
/*
* 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,102 +0,0 @@
/*
* 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 "OllamaMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
QJsonObject OllamaMessage::parseJsonFromData(const QByteArray &data)
{
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(line, &error);
if (!doc.isNull() && error.error == QJsonParseError::NoError) {
return doc.object();
}
}
return QJsonObject();
}
OllamaMessage OllamaMessage::fromJson(const QByteArray &data, Type type)
{
OllamaMessage msg;
QJsonObject obj = parseJsonFromData(data);
if (obj.isEmpty()) {
msg.error = "Invalid JSON response";
return msg;
}
msg.model = obj["model"].toString();
msg.createdAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate);
msg.done = obj["done"].toBool();
msg.doneReason = obj["done_reason"].toString();
msg.error = obj["error"].toString();
if (type == Type::Generate) {
auto &genResponse = msg.response.emplace<GenerateResponse>();
genResponse.response = obj["response"].toString();
if (msg.done && obj.contains("context")) {
const auto array = obj["context"].toArray();
genResponse.context.reserve(array.size());
for (const auto &val : array) {
genResponse.context.append(val.toInt());
}
}
} else {
auto &chatResponse = msg.response.emplace<ChatResponse>();
const auto msgObj = obj["message"].toObject();
chatResponse.role = msgObj["role"].toString();
chatResponse.content = msgObj["content"].toString();
}
if (msg.done) {
msg.metrics
= {obj["total_duration"].toVariant().toLongLong(),
obj["load_duration"].toVariant().toLongLong(),
obj["prompt_eval_count"].toVariant().toLongLong(),
obj["prompt_eval_duration"].toVariant().toLongLong(),
obj["eval_count"].toVariant().toLongLong(),
obj["eval_duration"].toVariant().toLongLong()};
}
return msg;
}
QString OllamaMessage::getContent() const
{
if (std::holds_alternative<GenerateResponse>(response)) {
return std::get<GenerateResponse>(response).response;
}
return std::get<ChatResponse>(response).content;
}
bool OllamaMessage::hasError() const
{
return !error.isEmpty();
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,71 +0,0 @@
/*
* 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 <QDateTime>
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::LLMCore {
class OllamaMessage
{
public:
enum class Type { Generate, Chat };
struct Metrics
{
qint64 totalDuration{0};
qint64 loadDuration{0};
qint64 promptEvalCount{0};
qint64 promptEvalDuration{0};
qint64 evalCount{0};
qint64 evalDuration{0};
};
struct GenerateResponse
{
QString response;
QVector<int> context;
};
struct ChatResponse
{
QString role;
QString content;
};
QString model;
QDateTime createdAt;
std::variant<GenerateResponse, ChatResponse> response;
bool done{false};
QString doneReason;
QString error;
Metrics metrics;
static OllamaMessage fromJson(const QByteArray &data, Type type);
QString getContent() const;
bool hasError() const;
private:
static QJsonObject parseJsonFromData(const QByteArray &data);
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,82 +0,0 @@
/*
* 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 "OpenAIMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
OpenAIMessage OpenAIMessage::fromJson(const QJsonObject &obj)
{
OpenAIMessage msg;
if (obj.contains("error")) {
msg.error = obj["error"].toObject()["message"].toString();
return msg;
}
if (obj.contains("choices")) {
auto choices = obj["choices"].toArray();
if (!choices.isEmpty()) {
auto choiceObj = choices[0].toObject();
if (choiceObj.contains("delta")) {
QJsonObject delta = choiceObj["delta"].toObject();
msg.choice.content = delta["content"].toString();
} else if (choiceObj.contains("message")) {
QJsonObject message = choiceObj["message"].toObject();
msg.choice.content = message["content"].toString();
}
msg.choice.finishReason = choiceObj["finish_reason"].toString();
if (!msg.choice.finishReason.isEmpty()) {
msg.done = true;
}
}
}
if (obj.contains("usage")) {
QJsonObject usage = obj["usage"].toObject();
msg.usage.promptTokens = usage["prompt_tokens"].toInt();
msg.usage.completionTokens = usage["completion_tokens"].toInt();
msg.usage.totalTokens = usage["total_tokens"].toInt();
}
return msg;
}
QString OpenAIMessage::getContent() const
{
return choice.content;
}
bool OpenAIMessage::hasError() const
{
return !error.isEmpty();
}
bool OpenAIMessage::isDone() const
{
return done
|| (!choice.finishReason.isEmpty()
&& (choice.finishReason == "stop" || choice.finishReason == "length"));
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,56 +0,0 @@
/*
* 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 <QByteArray>
#include <QJsonObject>
#include <QString>
namespace QodeAssist::LLMCore {
class OpenAIMessage
{
public:
struct Choice
{
QString content;
QString finishReason;
};
struct Usage
{
int promptTokens{0};
int completionTokens{0};
int totalTokens{0};
};
Choice choice;
QString error;
bool done{false};
Usage usage;
QString getContent() const;
bool hasError() const;
bool isDone() const;
static OpenAIMessage fromJson(const QJsonObject &obj);
};
} // namespace QodeAssist::LLMCore

View File

@@ -40,6 +40,7 @@ QStringList PromptTemplateManager::chatTemplatesNames() const
PromptTemplateManager::~PromptTemplateManager() PromptTemplateManager::~PromptTemplateManager()
{ {
qDeleteAll(m_fimTemplates); qDeleteAll(m_fimTemplates);
qDeleteAll(m_chatTemplates);
} }
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName) PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)

View File

@@ -39,8 +39,9 @@ public:
"T must inherit from PromptTemplate"); "T must inherit from PromptTemplate");
T *template_ptr = new T(); T *template_ptr = new T();
QString name = template_ptr->name(); QString name = template_ptr->name();
if (template_ptr->type() == TemplateType::Fim) {
m_fimTemplates[name] = template_ptr; m_fimTemplates[name] = template_ptr;
if (template_ptr->type() == TemplateType::Chat) { } else if (template_ptr->type() == TemplateType::Chat) {
m_chatTemplates[name] = template_ptr; m_chatTemplates[name] = template_ptr;
} }
} }

View File

@@ -19,12 +19,9 @@
#pragma once #pragma once
#include <utils/environment.h>
#include <QNetworkRequest>
#include <QString> #include <QString>
#include "PromptTemplate.hpp"
#include "RequestType.hpp" #include "RequestType.hpp"
#include <utils/environment.h>
class QNetworkReply; class QNetworkReply;
class QJsonObject; class QJsonObject;
@@ -45,9 +42,6 @@ public:
virtual void prepareRequest(QJsonObject &request, RequestType type) = 0; virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0; virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0;
virtual QList<QString> getInstalledModels(const QString &url) = 0; virtual QList<QString> getInstalledModels(const QString &url) = 0;
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
virtual QString apiKey() const = 0;
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -38,7 +38,7 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented)))); QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
QNetworkRequest networkRequest(config.url); QNetworkRequest networkRequest(config.url);
config.provider->prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest, config.apiKey);
QNetworkReply *reply = m_manager->post(networkRequest, QNetworkReply *reply = m_manager->post(networkRequest,
QJsonDocument(config.providerRequest).toJson()); QJsonDocument(config.providerRequest).toJson());
@@ -75,7 +75,7 @@ void RequestHandler::handleLLMResponse(QNetworkReply *reply,
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse); bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
if (config.requestType == RequestType::CodeCompletion) { if (config.requestType == RequestType::Fim) {
if (!config.multiLineCompletion if (!config.multiLineCompletion
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) { && processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
return; return;
@@ -84,6 +84,7 @@ void RequestHandler::handleLLMResponse(QNetworkReply *reply,
if (isComplete) { if (isComplete) {
auto cleanedCompletion = removeStopWords(accumulatedResponse, auto cleanedCompletion = removeStopWords(accumulatedResponse,
config.promptTemplate->stopWords()); config.promptTemplate->stopWords());
removeCodeBlockWrappers(cleanedCompletion);
emit completionReceived(cleanedCompletion, request, true); emit completionReceived(cleanedCompletion, request, true);
} }
@@ -108,6 +109,16 @@ bool RequestHandler::cancelRequest(const QString &id)
return false; return false;
} }
void RequestHandler::prepareNetworkRequest(
QNetworkRequest &networkRequest, const QString &apiKey) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey.isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());
}
}
bool RequestHandler::processSingleLineCompletion( bool RequestHandler::processSingleLineCompletion(
QNetworkReply *reply, QNetworkReply *reply,
const QJsonObject &request, const QJsonObject &request,
@@ -115,6 +126,7 @@ bool RequestHandler::processSingleLineCompletion(
const LLMConfig &config) const LLMConfig &config)
{ {
QString cleanedResponse = accumulatedResponse; QString cleanedResponse = accumulatedResponse;
removeCodeBlockWrappers(cleanedResponse);
int newlinePos = cleanedResponse.indexOf('\n'); int newlinePos = cleanedResponse.indexOf('\n');
if (newlinePos != -1) { if (newlinePos != -1) {

View File

@@ -52,6 +52,7 @@ private:
QMap<QString, QNetworkReply *> m_activeRequests; QMap<QString, QNetworkReply *> m_activeRequests;
QMap<QNetworkReply *, QString> m_accumulatedResponses; QMap<QNetworkReply *, QString> m_accumulatedResponses;
void prepareNetworkRequest(QNetworkRequest &networkRequest, const QString &apiKey) const;
bool processSingleLineCompletion(QNetworkReply *reply, bool processSingleLineCompletion(QNetworkReply *reply,
const QJsonObject &request, const QJsonObject &request,
const QString &accumulatedResponse, const QString &accumulatedResponse,

View File

@@ -21,5 +21,5 @@
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
enum RequestType { CodeCompletion, Chat }; enum RequestType { Fim, Chat };
} }

View File

@@ -1,57 +0,0 @@
/*
* 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 "ValidationUtils.hpp"
#include <QJsonArray>
namespace QodeAssist::LLMCore {
QStringList ValidationUtils::validateRequestFields(
const QJsonObject &request, const QJsonObject &templateObj)
{
QStringList errors;
validateFields(request, templateObj, errors);
validateNestedObjects(request, templateObj, errors);
return errors;
}
void ValidationUtils::validateFields(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (!templateObj.contains(it.key())) {
errors << QString("unknown field '%1'").arg(it.key());
}
}
}
void ValidationUtils::validateNestedObjects(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (templateObj.contains(it.key()) && it.value().isObject()
&& templateObj[it.key()].isObject()) {
validateFields(it.value().toObject(), templateObj[it.key()].toObject(), errors);
validateNestedObjects(it.value().toObject(), templateObj[it.key()].toObject(), errors);
}
}
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,41 +0,0 @@
/*
* 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 <QJsonObject>
#include <QStringList>
namespace QodeAssist::LLMCore {
class ValidationUtils
{
public:
static QStringList validateRequestFields(
const QJsonObject &request, const QJsonObject &templateObj);
private:
static void validateFields(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
static void validateNestedObjects(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,238 +0,0 @@
/*
* 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 "ClaudeProvider.hpp"
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QUrlQuery>
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {
ClaudeProvider::ClaudeProvider() {}
QString ClaudeProvider::name() const
{
return "Claude";
}
QString ClaudeProvider::url() const
{
return "https://api.anthropic.com";
}
QString ClaudeProvider::completionEndpoint() const
{
return "/v1/messages";
}
QString ClaudeProvider::chatEndpoint() const
{
return "/v1/messages";
}
bool ClaudeProvider::supportsModelListing() const
{
return true;
}
void ClaudeProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
{
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
QJsonArray messages;
if (req.contains("messages")) {
QJsonArray origMessages = req["messages"].toArray();
for (const auto &msg : origMessages) {
QJsonObject message = msg.toObject();
if (message["role"].toString() == "system") {
req["system"] = message["content"];
} else {
messages.append(message);
}
}
} else {
if (req.contains("system")) {
req["system"] = req["system"].toString();
}
if (req.contains("prompt")) {
messages.append(
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
}
}
return messages;
};
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
request["stream"] = true;
};
QJsonArray messages = prepareMessages(request);
if (!messages.isEmpty()) {
request["messages"] = std::move(messages);
}
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool ClaudeProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
bool isComplete = false;
QString tempResponse;
while (reply->canReadLine()) {
QByteArray line = reply->readLine().trimmed();
if (line.isEmpty()) {
continue;
}
if (!line.startsWith("data:")) {
continue;
}
line = line.mid(6);
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
if (jsonResponse.isNull()) {
continue;
}
QJsonObject responseObj = jsonResponse.object();
QString eventType = responseObj["type"].toString();
if (eventType == "message_delta") {
if (responseObj.contains("delta")) {
QJsonObject delta = responseObj["delta"].toObject();
if (delta.contains("stop_reason")) {
isComplete = true;
}
}
} else if (eventType == "content_block_delta") {
QJsonObject delta = responseObj["delta"].toObject();
if (delta["type"].toString() == "text_delta") {
tempResponse += delta["text"].toString();
}
}
}
if (!tempResponse.isEmpty()) {
accumulatedResponse += tempResponse;
}
return isComplete;
}
QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
{
QList<QString> models;
QNetworkAccessManager manager;
QUrl url(baseUrl + "/v1/models");
QUrlQuery query;
query.addQueryItem("limit", "1000");
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("anthropic-version", "2023-06-01");
if (!apiKey().isEmpty()) {
request.setRawHeader("x-api-key", apiKey().toUtf8());
}
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) {
QJsonArray modelArray = jsonObject["data"].toArray();
for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject();
if (modelObject.contains("id")) {
QString modelId = modelObject["id"].toString();
models.append(modelId);
}
}
}
} else {
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
{
const auto templateReq = QJsonObject{
{"model", {}},
{"system", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"temperature", {}},
{"max_tokens", {}},
{"anthropic-version", {}},
{"top_p", {}},
{"top_k", {}},
{"stop", QJsonArray{}},
{"stream", {}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString ClaudeProvider::apiKey() const
{
return Settings::providerSettings().claudeApiKey();
}
void ClaudeProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
networkRequest.setRawHeader("x-api-key", apiKey().toUtf8());
networkRequest.setRawHeader("anthropic-version", "2023-06-01");
}
}
} // namespace QodeAssist::Providers

View File

@@ -1,44 +0,0 @@
/*
* 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 "llmcore/Provider.hpp"
namespace QodeAssist::Providers {
class ClaudeProvider : public LLMCore::Provider
{
public:
ClaudeProvider();
QString name() const override;
QString url() const override;
QString completionEndpoint() const override;
QString chatEndpoint() const override;
bool supportsModelListing() const override;
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
};
} // namespace QodeAssist::Providers

View File

@@ -25,8 +25,6 @@
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include "llmcore/OpenAIMessage.hpp"
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp" #include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
@@ -94,7 +92,7 @@ void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType
request["messages"] = std::move(messages); request["messages"] = std::move(messages);
} }
if (type == LLMCore::RequestType::CodeCompletion) { if (type == LLMCore::RequestType::Fim) {
applyModelParams(Settings::codeCompletionSettings()); applyModelParams(Settings::codeCompletionSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());
@@ -103,53 +101,43 @@ void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType
bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse) bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{ {
QByteArray data = reply->readAll(); bool isComplete = false;
if (data.isEmpty()) { while (reply->canReadLine()) {
return false; QByteArray line = reply->readLine().trimmed();
} if (line.isEmpty()) {
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue; continue;
} }
if (line == "data: [DONE]") { if (line == "data: [DONE]") {
isDone = true; isComplete = true;
continue; break;
} }
QByteArray jsonData = line;
if (line.startsWith("data: ")) { if (line.startsWith("data: ")) {
jsonData = line.mid(6); line = line.mid(6); // Remove "data: " prefix
} }
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
QJsonParseError error; if (jsonResponse.isNull()) {
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); qWarning() << "Invalid JSON response from LM Studio:" << line;
if (doc.isNull()) {
continue; continue;
} }
QJsonObject responseObj = jsonResponse.object();
if (responseObj.contains("choices")) {
QJsonArray choices = responseObj["choices"].toArray();
if (!choices.isEmpty()) {
QJsonObject choice = choices.first().toObject();
QJsonObject delta = choice["delta"].toObject();
if (delta.contains("content")) {
QString completion = delta["content"].toString();
auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); accumulatedResponse += completion;
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
} }
if (choice["finish_reason"].toString() == "stop") {
QString content = message.getContent(); isComplete = true;
if (!content.isEmpty()) { break;
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
} }
} }
}
return isDone; }
return isComplete;
} }
QList<QString> LMStudioProvider::getInstalledModels(const QString &url) QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
@@ -183,32 +171,4 @@ QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
return models; return models;
} }
QList<QString> LMStudioProvider::validateRequest(
const QJsonObject &request, LLMCore::TemplateType type)
{
const auto templateReq = QJsonObject{
{"model", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"temperature", {}},
{"max_tokens", {}},
{"top_p", {}},
{"top_k", {}},
{"frequency_penalty", {}},
{"presence_penalty", {}},
{"stop", QJsonArray{}},
{"stream", {}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString LMStudioProvider::apiKey() const
{
return {};
}
void LMStudioProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -36,9 +36,6 @@ public:
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QList<QString> getInstalledModels(const QString &url) override; QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -25,8 +25,6 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QtCore/qeventloop.h> #include <QtCore/qeventloop.h>
#include "llmcore/OllamaMessage.hpp"
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp" #include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
@@ -66,7 +64,6 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
QJsonObject options; QJsonObject options;
options["num_predict"] = settings.maxTokens(); options["num_predict"] = settings.maxTokens();
options["temperature"] = settings.temperature(); options["temperature"] = settings.temperature();
options["stop"] = request.take("stop");
if (settings.useTopP()) if (settings.useTopP())
options["top_p"] = settings.topP(); options["top_p"] = settings.topP();
@@ -81,7 +78,7 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
request["keep_alive"] = settings.ollamaLivetime(); request["keep_alive"] = settings.ollamaLivetime();
}; };
if (type == LLMCore::RequestType::CodeCompletion) { if (type == LLMCore::RequestType::Fim) {
applySettings(Settings::codeCompletionSettings()); applySettings(Settings::codeCompletionSettings());
} else { } else {
applySettings(Settings::chatAssistantSettings()); applySettings(Settings::chatAssistantSettings());
@@ -90,41 +87,53 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse) bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{ {
QByteArray data = reply->readAll(); QString endpoint = reply->url().path();
if (data.isEmpty()) {
bool isComplete = false;
while (reply->canReadLine()) {
QByteArray line = reply->readLine().trimmed();
if (line.isEmpty()) {
continue;
}
QJsonDocument doc = QJsonDocument::fromJson(line);
if (doc.isNull()) {
LOG_MESSAGE("Invalid JSON response from Ollama: " + QString::fromUtf8(line));
continue;
}
QJsonObject responseObj = doc.object();
if (responseObj.contains("error")) {
QString errorMessage = responseObj["error"].toString();
LOG_MESSAGE("Error in Ollama response: " + errorMessage);
return false; return false;
} }
QByteArrayList lines = data.split('\n'); if (endpoint == completionEndpoint()) {
bool isDone = false; if (responseObj.contains("response")) {
QString completion = responseObj["response"].toString();
for (const QByteArray &line : lines) { accumulatedResponse += completion;
if (line.trimmed().isEmpty()) {
continue;
} }
} else if (endpoint == chatEndpoint()) {
const QString endpoint = reply->url().path(); if (responseObj.contains("message")) {
auto messageType = endpoint == completionEndpoint() QJsonObject message = responseObj["message"].toObject();
? LLMCore::OllamaMessage::Type::Generate if (message.contains("content")) {
: LLMCore::OllamaMessage::Type::Chat; QString content = message["content"].toString();
auto message = LLMCore::OllamaMessage::fromJson(line, messageType);
if (message.hasError()) {
LOG_MESSAGE("Error in Ollama response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content; accumulatedResponse += content;
} }
}
} else {
LOG_MESSAGE("Unknown endpoint: " + endpoint);
}
if (message.done) { if (responseObj.contains("done") && responseObj["done"].toBool()) {
isDone = true; isComplete = true;
break;
} }
} }
return isDone; return isComplete;
} }
QList<QString> OllamaProvider::getInstalledModels(const QString &url) QList<QString> OllamaProvider::getInstalledModels(const QString &url)
@@ -157,52 +166,4 @@ QList<QString> OllamaProvider::getInstalledModels(const QString &url)
return models; return models;
} }
QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
{
const auto fimReq = QJsonObject{
{"keep_alive", {}},
{"model", {}},
{"stream", {}},
{"prompt", {}},
{"suffix", {}},
{"system", {}},
{"options",
QJsonObject{
{"temperature", {}},
{"stop", {}},
{"top_p", {}},
{"top_k", {}},
{"num_predict", {}},
{"frequency_penalty", {}},
{"presence_penalty", {}}}}};
const auto messageReq = QJsonObject{
{"keep_alive", {}},
{"model", {}},
{"stream", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"options",
QJsonObject{
{"temperature", {}},
{"stop", {}},
{"top_p", {}},
{"top_k", {}},
{"num_predict", {}},
{"frequency_penalty", {}},
{"presence_penalty", {}}}}};
return LLMCore::ValidationUtils::validateRequestFields(
request, type == LLMCore::TemplateType::Fim ? fimReq : messageReq);
}
QString OllamaProvider::apiKey() const
{
return {};
}
void OllamaProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
};
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -36,9 +36,6 @@ public:
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QList<QString> getInstalledModels(const QString &url) override; QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -18,18 +18,14 @@
*/ */
#include "OpenAICompatProvider.hpp" #include "OpenAICompatProvider.hpp"
#include "settings/ChatAssistantSettings.hpp" #include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include "llmcore/OpenAIMessage.hpp"
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@@ -95,7 +91,7 @@ void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::Request
request["messages"] = std::move(messages); request["messages"] = std::move(messages);
} }
if (type == LLMCore::RequestType::CodeCompletion) { if (type == LLMCore::RequestType::Fim) {
applyModelParams(Settings::codeCompletionSettings()); applyModelParams(Settings::codeCompletionSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());
@@ -104,53 +100,74 @@ void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::Request
bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse) bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{ {
QByteArray data = reply->readAll(); bool isComplete = false;
if (data.isEmpty()) { QString tempResponse = accumulatedResponse;
while (reply->canReadLine()) {
QByteArray line = reply->readLine().trimmed();
if (line.isEmpty()) {
continue;
}
if (!line.startsWith("data:")) {
continue;
}
line = line.mid(6);
if (line == "[DONE]") {
isComplete = true;
break;
}
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
if (jsonResponse.isNull()) {
LOG_MESSAGE(
"Invalid JSON response from OpenAI compatible provider: " + QString::fromUtf8(line));
continue;
}
QJsonObject responseObj = jsonResponse.object();
if (responseObj.contains("error")) {
LOG_MESSAGE(
"OpenAI compatible provider error: "
+ QString::fromUtf8(QJsonDocument(responseObj).toJson(QJsonDocument::Indented)));
return false; return false;
} }
bool isDone = false; if (responseObj.contains("choices")) {
QByteArrayList lines = data.split('\n'); QJsonArray choices = responseObj["choices"].toArray();
if (!choices.isEmpty()) {
for (const QByteArray &line : lines) { QJsonObject choice = choices.first().toObject();
if (line.trimmed().isEmpty()) { QJsonObject delta = choice["delta"].toObject();
continue; if (delta.contains("content")) {
QString completion = delta["content"].toString();
if (!completion.isEmpty()) {
tempResponse += completion;
} }
if (line == "data: [DONE]") {
isDone = true;
continue;
} }
QString finishReason = choice["finish_reason"].toString();
QByteArray jsonData = line; if (!finishReason.isNull() && finishReason == "stop") {
if (line.startsWith("data: ")) { isComplete = true;
jsonData = line.mid(6);
} }
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
} }
} }
return isDone; if (responseObj.contains("usage")) {
QJsonObject usage = responseObj["usage"].toObject();
LOG_MESSAGE(QString("Token usage - Prompt: %1, Completion: %2, Total: %3")
.arg(usage["prompt_tokens"].toInt())
.arg(usage["completion_tokens"].toInt())
.arg(usage["total_tokens"].toInt()));
}
}
if (!tempResponse.isEmpty()) {
accumulatedResponse = tempResponse;
}
return isComplete;
} }
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url) QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
@@ -158,36 +175,4 @@ QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
return QStringList(); return QStringList();
} }
QList<QString> OpenAICompatProvider::validateRequest(
const QJsonObject &request, LLMCore::TemplateType type)
{
const auto templateReq = QJsonObject{
{"model", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"temperature", {}},
{"max_tokens", {}},
{"top_p", {}},
{"top_k", {}},
{"frequency_penalty", {}},
{"presence_penalty", {}},
{"stop", QJsonArray{}},
{"stream", {}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString OpenAICompatProvider::apiKey() const
{
return Settings::providerSettings().openAiCompatApiKey();
}
void OpenAICompatProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
}
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -36,9 +36,6 @@ public:
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QList<QString> getInstalledModels(const QString &url) override; QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -1,229 +0,0 @@
/*
* 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 "OpenAIProvider.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include "llmcore/OpenAIMessage.hpp"
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp"
namespace QodeAssist::Providers {
OpenAIProvider::OpenAIProvider() {}
QString OpenAIProvider::name() const
{
return "OpenAI";
}
QString OpenAIProvider::url() const
{
return "https://api.openai.com";
}
QString OpenAIProvider::completionEndpoint() const
{
return "/v1/chat/completions";
}
QString OpenAIProvider::chatEndpoint() const
{
return "/v1/chat/completions";
}
bool OpenAIProvider::supportsModelListing() const
{
return true;
}
void OpenAIProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
{
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
QJsonArray messages;
if (req.contains("system")) {
messages.append(
QJsonObject{{"role", "system"}, {"content", req.take("system").toString()}});
}
if (req.contains("prompt")) {
messages.append(
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
}
return messages;
};
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
QJsonArray messages = prepareMessages(request);
if (!messages.isEmpty()) {
request["messages"] = std::move(messages);
}
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool OpenAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return isDone;
}
QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
{
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/v1/models").arg(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) {
QJsonArray modelArray = jsonObject["data"].toArray();
for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject();
if (modelObject.contains("id")) {
QString modelId = modelObject["id"].toString();
if (modelId.startsWith("gpt")) {
models.append(modelId);
}
}
}
}
} else {
LOG_MESSAGE(QString("Error fetching ChatGPT models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
{
const auto templateReq = QJsonObject{
{"model", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"temperature", {}},
{"max_tokens", {}},
{"top_p", {}},
{"top_k", {}},
{"frequency_penalty", {}},
{"presence_penalty", {}},
{"stop", QJsonArray{}},
{"stream", {}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString OpenAIProvider::apiKey() const
{
return Settings::providerSettings().openAiApiKey();
}
void OpenAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
}
} // namespace QodeAssist::Providers

View File

@@ -1,44 +0,0 @@
/*
* 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 "llmcore/Provider.hpp"
namespace QodeAssist::Providers {
class OpenAIProvider : public LLMCore::Provider
{
public:
OpenAIProvider();
QString name() const override;
QString url() const override;
QString completionEndpoint() const override;
QString chatEndpoint() const override;
bool supportsModelListing() const override;
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
};
} // namespace QodeAssist::Providers

View File

@@ -1,145 +0,0 @@
/*
* 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 "OpenRouterAIProvider.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include "llmcore/OpenAIMessage.hpp"
#include "logger/Logger.hpp"
namespace QodeAssist::Providers {
OpenRouterProvider::OpenRouterProvider() {}
QString OpenRouterProvider::name() const
{
return "OpenRouter";
}
QString OpenRouterProvider::url() const
{
return "https://openrouter.ai/api";
}
void OpenRouterProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
{
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
QJsonArray messages;
if (req.contains("system")) {
messages.append(
QJsonObject{{"role", "system"}, {"content", req.take("system").toString()}});
}
if (req.contains("prompt")) {
messages.append(
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
}
return messages;
};
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
QJsonArray messages = prepareMessages(request);
if (!messages.isEmpty()) {
request["messages"] = std::move(messages);
}
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty() || line.contains("OPENROUTER PROCESSING")) {
continue;
}
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return isDone;
}
QString OpenRouterProvider::apiKey() const
{
return Settings::providerSettings().openRouterApiKey();
}
} // namespace QodeAssist::Providers

View File

@@ -1,39 +0,0 @@
/*
* 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 "llmcore/Provider.hpp"
#include "providers/OpenAICompatProvider.hpp"
namespace QodeAssist::Providers {
class OpenRouterProvider : public OpenAICompatProvider
{
public:
OpenRouterProvider();
QString name() const override;
QString url() const override;
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QString apiKey() const override;
};
} // namespace QodeAssist::Providers

View File

@@ -20,12 +20,9 @@
#pragma once #pragma once
#include "llmcore/ProvidersManager.hpp" #include "llmcore/ProvidersManager.hpp"
#include "providers/ClaudeProvider.hpp"
#include "providers/LMStudioProvider.hpp" #include "providers/LMStudioProvider.hpp"
#include "providers/OllamaProvider.hpp" #include "providers/OllamaProvider.hpp"
#include "providers/OpenAICompatProvider.hpp" #include "providers/OpenAICompatProvider.hpp"
#include "providers/OpenAIProvider.hpp"
#include "providers/OpenRouterAIProvider.hpp"
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@@ -35,9 +32,6 @@ inline void registerProviders()
providerManager.registerProvider<OllamaProvider>(); providerManager.registerProvider<OllamaProvider>();
providerManager.registerProvider<LMStudioProvider>(); providerManager.registerProvider<LMStudioProvider>();
providerManager.registerProvider<OpenAICompatProvider>(); providerManager.registerProvider<OpenAICompatProvider>();
providerManager.registerProvider<OpenRouterProvider>();
providerManager.registerProvider<ClaudeProvider>();
providerManager.registerProvider<OpenAIProvider>();
} }
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -19,8 +19,6 @@
#include "QodeAssistConstants.hpp" #include "QodeAssistConstants.hpp"
#include "QodeAssisttr.h" #include "QodeAssisttr.h"
#include "settings/PluginUpdater.hpp"
#include "settings/UpdateDialog.hpp"
#include <coreplugin/actionmanager/actioncontainer.h> #include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/actionmanager.h>
@@ -34,21 +32,18 @@
#include <extensionsystem/iplugin.h> #include <extensionsystem/iplugin.h>
#include <languageclient/languageclientmanager.h> #include <languageclient/languageclientmanager.h>
#include <texteditor/texteditor.h>
#include <utils/icon.h>
#include <QAction> #include <QAction>
#include <QMainWindow> #include <QMainWindow>
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
#include <texteditor/texteditor.h>
#include <utils/icon.h>
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
#include "chat/ChatOutputPane.h" #include "chat/ChatOutputPane.h"
#include "chat/NavigationPanel.hpp" #include "chat/NavigationPanel.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettingsPanel.hpp"
#include "UpdateStatusWidget.hpp"
#include "providers/Providers.hpp" #include "providers/Providers.hpp"
#include "templates/Templates.hpp" #include "templates/Templates.hpp"
@@ -65,8 +60,8 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin
public: public:
QodeAssistPlugin() QodeAssistPlugin()
: m_updater(new PluginUpdater(this)) {
{} }
~QodeAssistPlugin() final ~QodeAssistPlugin() final
{ {
@@ -100,36 +95,31 @@ public:
} }
}); });
m_statusWidget = new UpdateStatusWidget; auto toggleButton = new QToolButton;
m_statusWidget->setDefaultAction(requestAction.contextAction()); toggleButton->setDefaultAction(requestAction.contextAction());
StatusBarManager::addStatusBarWidget(m_statusWidget, StatusBarManager::RightCorner); StatusBarManager::addStatusBarWidget(toggleButton, StatusBarManager::RightCorner);
connect(m_statusWidget->updateButton(), &QPushButton::clicked, this, [this]() {
UpdateDialog::checkForUpdatesAndShow(Core::ICore::mainWindow());
});
m_chatOutputPane = new Chat::ChatOutputPane(this); m_chatOutputPane = new Chat::ChatOutputPane(this);
m_navigationPanel = new Chat::NavigationPanel(); m_navigationPanel = new Chat::NavigationPanel();
Settings::setupProjectPanel();
ConfigurationManager::instance().init(); ConfigurationManager::instance().init();
if (Settings::generalSettings().enableCheckUpdate()) {
QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates);
}
} }
void extensionsInitialized() final {} void extensionsInitialized() final
{
}
void restartClient() void restartClient()
{ {
LanguageClient::LanguageClientManager::shutdownClient(m_qodeAssistClient); LanguageClient::LanguageClientManager::shutdownClient(m_qodeAssistClient);
m_qodeAssistClient = new QodeAssistClient(); m_qodeAssistClient = new QodeAssistClient();
} }
bool delayedInitialize() final bool delayedInitialize() final
{ {
restartClient(); restartClient();
return true; return true;
} }
@@ -137,38 +127,17 @@ public:
{ {
if (!m_qodeAssistClient) if (!m_qodeAssistClient)
return SynchronousShutdown; return SynchronousShutdown;
connect(m_qodeAssistClient, &QObject::destroyed, this, &IPlugin::asynchronousShutdownFinished); connect(m_qodeAssistClient,
&QObject::destroyed,
this,
&IPlugin::asynchronousShutdownFinished);
return AsynchronousShutdown; return AsynchronousShutdown;
} }
private: private:
void checkForUpdates()
{
connect(
m_updater,
&PluginUpdater::updateCheckFinished,
this,
&QodeAssistPlugin::handleUpdateCheckResult,
Qt::UniqueConnection);
m_updater->checkForUpdates();
}
void handleUpdateCheckResult(const PluginUpdater::UpdateInfo &info)
{
if (!info.isUpdateAvailable
|| QVersionNumber::fromString(info.currentIdeVersion)
> QVersionNumber::fromString(info.targetIdeVersion))
return;
if (m_statusWidget)
m_statusWidget->showUpdateAvailable(info.version);
}
QPointer<QodeAssistClient> m_qodeAssistClient; QPointer<QodeAssistClient> m_qodeAssistClient;
QPointer<Chat::ChatOutputPane> m_chatOutputPane; QPointer<Chat::ChatOutputPane> m_chatOutputPane;
QPointer<Chat::NavigationPanel> m_navigationPanel; QPointer<Chat::NavigationPanel> m_navigationPanel;
QPointer<PluginUpdater> m_updater;
UpdateStatusWidget *m_statusWidget{nullptr};
}; };
} // namespace QodeAssist::Internal } // namespace QodeAssist::Internal

View File

@@ -0,0 +1,19 @@
{
"prompt": "{{QODE_INSTRUCTIONS}}<fim_prefix>{{QODE_PREFIX}}<fim_suffix>{{QODE_SUFFIX}}<fim_middle>",
"options": {
"temperature": 0.7,
"top_p": 0.95,
"top_k": 40,
"num_predict": 175,
"stop": [
"<|endoftext|>",
"<file_sep>",
"<fim_prefix>",
"<fim_suffix>",
"<fim_middle>"
],
"frequency_penalty": 0,
"presence_penalty": 0
},
"stream": true
}

View File

@@ -0,0 +1,16 @@
{
"max_tokens": 150,
"messages": [
{
"content": "{{QODE_INSTRUCTIONS}}\n### Instruction:{{QODE_PREFIX}}{{QODE_SUFFIX}} ### Response:\n",
"role": "user"
}
],
"stop": [
"### Instruction:",
"### Response:",
"\n\n### "
],
"stream": true,
"temperature": 0.2
}

View File

@@ -32,29 +32,14 @@ public:
: Utils::BaseAspect(container) : Utils::BaseAspect(container)
{} {}
void addToLayoutImpl(Layouting::Layout &parent) override void addToLayout(Layouting::Layout &parent) override
{ {
auto button = new QPushButton(m_buttonText); auto button = new QPushButton(m_buttonText);
button->setVisible(m_visible);
connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked); connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked);
connect(this, &ButtonAspect::visibleChanged, button, &QPushButton::setVisible);
parent.addItem(button); parent.addItem(button);
} }
void updateVisibility(bool visible)
{
if (m_visible == visible)
return;
m_visible = visible;
emit visibleChanged(visible);
}
QString m_buttonText; QString m_buttonText;
signals: signals:
void clicked(); void clicked();
void visibleChanged(bool state);
private:
bool m_visible = true;
}; };

View File

@@ -8,11 +8,6 @@ add_library(QodeAssistSettings STATIC
CodeCompletionSettings.hpp CodeCompletionSettings.cpp CodeCompletionSettings.hpp CodeCompletionSettings.cpp
ChatAssistantSettings.hpp ChatAssistantSettings.cpp ChatAssistantSettings.hpp ChatAssistantSettings.cpp
SettingsDialog.hpp SettingsDialog.cpp SettingsDialog.hpp SettingsDialog.cpp
ProjectSettings.hpp ProjectSettings.cpp
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
ProviderSettings.hpp ProviderSettings.cpp
PluginUpdater.hpp PluginUpdater.cpp
UpdateDialog.hpp UpdateDialog.cpp
) )
target_link_libraries(QodeAssistSettings target_link_libraries(QodeAssistSettings

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