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
127 changed files with 1153 additions and 7146 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

@@ -8,7 +8,6 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
find_package(QtCreator REQUIRED COMPONENTS Core) find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
@@ -17,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
@@ -44,37 +42,31 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp templates/Templates.hpp
templates/CodeLlamaFim.hpp templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp templates/CustomFimTemplate.hpp
templates/Qwen.hpp templates/Qwen.hpp
templates/OpenAICompatible.hpp
templates/Ollama.hpp
templates/BasicChat.hpp
templates/Llama3.hpp templates/Llama3.hpp
templates/ChatML.hpp templates/ChatML.hpp
templates/Alpaca.hpp templates/Alpaca.hpp
templates/Llama2.hpp templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
providers/Providers.hpp providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.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,27 +18,12 @@
*/ */
#include "ChatRootView.hpp" #include "ChatRootView.hpp"
#include <QtGui/qclipboard.h>
#include <QClipboard>
#include <QDesktopServices>
#include <QFileDialog>
#include <QMessageBox>
#include <coreplugin/icore.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include <coreplugin/editormanager/editormanager.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "context/TokenUtils.hpp"
#include "context/ContextManager.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -47,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,
@@ -61,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::isSharingCurrentFileChanged);
&ChatRootView::autosave);
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
@@ -106,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) { void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const
autosave(); {
m_chatModel->clear(); m_clientInterface->sendMessage(message, sharingCurrentFile);
setRecentFilePath(QString{});
return;
}
}
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
clearAttachmentFiles();
} }
void ChatRootView::copyToClipboard(const QString &text) void ChatRootView::copyToClipboard(const QString &text)
@@ -138,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);
}
}
QColor ChatRootView::generateColor(const QColor &baseColor,
float hueShift,
float saturationMod,
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);
} }
QDir dir(path); h = qBound(0.0f, h, 1.0f);
if (!dir.exists() && !dir.mkpath(".")) { s = qBound(0.0f, s, 1.0f);
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path)); l = qBound(0.0f, l, 1.0f);
return QString(); a = qBound(0.0f, a, 1.0f);
}
return path; return QColor::fromHslF(h, s, l, a);
} }
QString ChatRootView::currentTemplate() const QString ChatRootView::currentTemplate() const
@@ -180,354 +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) {
if (result == QFileDialog::Accepted) {
QStringList files = dialog->selectedFiles();
if (!files.isEmpty()) {
saveHistory(files.first());
}
}
dialog->deleteLater();
});
dialog->open();
} }
void ChatRootView::showLoadDialog() bool ChatRootView::isSharingCurrentFile() const
{ {
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);
}
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,68 +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 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();
@@ -93,47 +90,47 @@ void ClientInterface::sendMessage(
} }
LLMCore::ContextData context; LLMCore::ContextData context;
context.prefix = message;
context.suffix = "";
if (chatAssistantSettings.useSystemPrompt()) { QString systemPrompt;
QString systemPrompt = chatAssistantSettings.systemPrompt(); if (chatAssistantSettings.useSystemPrompt())
if (!linkedFiles.isEmpty()) { systemPrompt = chatAssistantSettings.systemPrompt();
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
if (includeCurrentFile) {
QString fileContext = getCurrentFileContext();
if (!fileContext.isEmpty()) {
systemPrompt = systemPrompt.append(fileContext);
} }
context.systemPrompt = systemPrompt;
} }
QVector<LLMCore::Message> messages; QJsonObject providerRequest;
for (const auto &msg : m_chatModel->getChatHistory()) { providerRequest["model"] = Settings::generalSettings().caModel();
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content}); providerRequest["stream"] = true;
} providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
context.history = messages;
if (promptTemplate)
promptTemplate->prepareRequest(providerRequest, context);
else
qWarning("No prompt template found");
if (provider)
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
else
qWarning("No provider found");
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat; config.requestType = LLMCore::RequestType::Chat;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"} config.providerRequest = providerRequest;
: QString{"generateContent?"}; config.multiLineCompletion = false;
config.url = QUrl(QString("%1/models/%2:%3") config.apiKey = Settings::chatAssistantSettings().apiKey();
.arg(
Settings::generalSettings().caUrl(),
Settings::generalSettings().caModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().caModel()},
{"stream", chatAssistantSettings.stream()}};
}
config.apiKey = provider->apiKey(); QJsonObject request;
request["id"] = QUuid::createUuid().toString();
config.provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QJsonObject request{{"id", QUuid::createUuid().toString()}};
m_requestHandler->sendLLMRequest(config, request); m_requestHandler->sendLLMRequest(config, request);
} }
@@ -162,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();
} }
} }
} }
@@ -191,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,29 +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 bool isUserMessage: false property color codeBgColor
property color selectionColor
height: msgColumn.implicitHeight + 10 height: msgColumn.height
radius: 8 radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
ColumnLayout { Column {
id: msgColumn id: msgColumn
x: 5
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width
spacing: 5 spacing: 5
Repeater { Repeater {
@@ -50,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
@@ -81,51 +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
}
}
}
}
}
Rectangle {
id: userMessageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: "#92BD6C"
radius: root.radius
visible: root.isUserMessage
} }
component TextComponent : TextBlock { component TextComponent : TextBlock {
@@ -134,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
} }
@@ -148,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,11 +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
isUserMessage: model.roleType === ChatModel.User fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
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 {
@@ -104,7 +69,7 @@ ChatRootView {
height: 30 height: 30
} }
ScrollBar.vertical: QQC.ScrollBar { ScrollBar.vertical: ScrollBar {
id: scroll id: scroll
} }
@@ -130,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()
@@ -161,49 +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"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
}
AttachedFilesPlace { Button {
id: linkedFilesPlace id: sendButton
Layout.fillWidth: true Layout.alignment: Qt.AlignBottom
attachedFilesModel: root.linkedFiles text: qsTr("Send")
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg" onClicked: root.sendChatMessage()
: "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 {
id: bottomBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
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() Button {
id: stopButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Stop")
onClicked: root.cancelRequest()
}
Button {
id: clearButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Clear Chat")
onClicked: root.clearChat()
}
CheckBox {
id: sharingCurrentFile
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() {
@@ -211,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,98 +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
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")
}
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

@@ -35,28 +35,6 @@ ConfigurationManager &ConfigurationManager::instance()
void ConfigurationManager::init() void ConfigurationManager::init()
{ {
setupConnections(); setupConnections();
updateAllTemplateDescriptions();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
} }
ConfigurationManager::ConfigurationManager(QObject *parent) ConfigurationManager::ConfigurationManager(QObject *parent)
@@ -79,21 +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);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
} }
void ConfigurationManager::selectProvider() void ConfigurationManager::selectProvider()
@@ -106,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] {
@@ -125,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()) {
@@ -166,18 +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 QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1 const auto templateList = isCodeCompletion ? m_templateManger.fimTemplatesNames()
? m_templateManger.getFimTemplatesForProvider(providerID) : m_templateManger.chatTemplatesNames();
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: m_generalSettings.caTemplate; : m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
@@ -201,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

@@ -36,9 +36,6 @@ public:
void init(); void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
public slots: public slots:
void selectProvider(); void selectProvider();
void selectModel(); void selectModel();

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)
@@ -210,13 +209,14 @@ LLMCore::ContextData DocumentContextReader::prepareContext(int lineNumber, int c
QString contextAfter = getContextAfter(lineNumber, cursorPosition); QString contextAfter = getContextAfter(lineNumber, cursorPosition);
QString fileContext; QString fileContext;
fileContext.append("\n ").append(getLanguageAndFileInfo()); if (Settings::codeCompletionSettings().useFilePathInContext())
fileContext.append("\n ").append(getLanguageAndFileInfo());
if (Settings::codeCompletionSettings().useProjectChangesCache()) if (Settings::codeCompletionSettings().useProjectChangesCache())
fileContext.append("\n ").append( fileContext.append("\n ").append(
ChangesManager::instance().getRecentChangesContext(m_textDocument)); ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext}; return {contextBefore, contextAfter, fileContext};
} }
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
@@ -246,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/ContextManager.hpp"
#include "context/DocumentContextReader.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"
@@ -150,26 +148,16 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
{ {
auto updatedContext = prepareContext(request); auto updatedContext = prepareContext(request);
auto &completeSettings = Settings::codeCompletionSettings(); auto &completeSettings = Settings::codeCompletionSettings();
auto &generalSettings = Settings::generalSettings();
bool isPreset1Active = Context::ContextManager::instance().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);
@@ -178,68 +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;
// TODO refactor networking config.url = QUrl(
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
QString stream = completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"} config.apiKey = Settings::codeCompletionSettings().apiKey();
: QString{"generateContent?"};
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream)); config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
} else {
config.url = QUrl(QString("%1%2").arg(
url,
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
: provider->chatEndpoint()));
config.providerRequest = {{"model", modelName}, {"stream", completeSettings.stream()}};
}
config.apiKey = provider->apiKey();
config.multiLineCompletion = completeSettings.multiLineCompletion(); config.multiLineCompletion = completeSettings.multiLineCompletion();
QString systemPrompt;
if (completeSettings.useSystemPrompt())
systemPrompt.append(completeSettings.systemPrompt());
if (!updatedContext.fileContext.isEmpty())
systemPrompt.append(updatedContext.fileContext);
if (!systemPrompt.isEmpty())
config.providerRequest["system"] = systemPrompt;
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords()); const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty()) if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords; config.providerRequest["stop"] = stopWords;
QString systemPrompt; config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
if (completeSettings.useSystemPrompt()) config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
systemPrompt.append(completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == LLMCore::TemplateType::Chat
? completeSettings.systemPromptForNonFimModels()
: completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
QString userMessage;
if (completeSettings.useUserMessageTemplateForCC()) {
userMessage = completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
config.provider->prepareRequest(
config.providerRequest,
promptTemplate,
updatedContext,
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);
} }
@@ -263,7 +216,7 @@ 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);
} }
@@ -271,36 +224,20 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QJsonObject &request, const QJsonObject &request,
bool isComplete) bool isComplete)
{ {
bool isPreset1Active = Context::ContextManager::instance().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;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(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;
range["end"] = position; QJsonObject end = position;
end["character"] = position["character"].toInt() + completion.length();
range["end"] = end;
completionItem[LanguageServerProtocol::rangeKey] = range; completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position; completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem); completions.append(completionItem);
@@ -313,7 +250,7 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE(QString("Full response: \n%1") LOG_MESSAGE(QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
endTimeMeasurement(requestId); endTimeMeasurement(requestId);

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,8 +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{});
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) void LLMSuggestion::reset()
return apply(); {
m_start.removeSelectedText();
}
if (part == Line) int LLMSuggestion::position()
++next; {
return m_start.position();
}
QString subText = text.mid(startPos, next - startPos); void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
if (subText.isEmpty()) {
return false; Utils::ToolTip::hide();
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
currentCursor.insertText(subText); pos += QPoint(-10, -50);
m_counterTooltip = new CounterTooltip(count);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) { Utils::ToolTip::show(pos, m_counterTooltip, widget);
const QString newCompletionText = text.mid(startPos + seperatorPos + 1); connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
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));
}
}
return false;
} }
} // 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.5.0", "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,63 +70,48 @@ 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;
auto project = ProjectManager::projectForFile(document->filePath()); auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
auto textEditor = BaseTextEditor::currentTextEditor(); auto textEditor = BaseTextEditor::currentTextEditor();
if (!textEditor || textEditor->document() != document) if (!textEditor || textEditor->document() != 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();
if (cursorPosition < position || cursorPosition > position + charsAdded)
return;
const int cursorPosition = widget->textCursor().position(); m_recentCharCount += charsAdded;
if (cursorPosition < position || cursorPosition > position + charsAdded)
return;
if (charsRemoved > 0 || charsAdded <= 0) { if (m_typingTimer.elapsed()
m_recentCharCount = 0; > Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
m_typingTimer.restart(); m_recentCharCount = charsAdded;
return; m_typingTimer.restart();
} }
QTextCursor cursor = widget->textCursor(); if (m_recentCharCount
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); > Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
QString lastChar = cursor.selectedText(); scheduleRequest(widget);
}
if (lastChar.isEmpty() || lastChar[0].isPunct()) { });
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
m_recentCharCount += charsAdded;
if (m_typingTimer.elapsed()
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
m_recentCharCount = charsAdded;
m_typingTimer.restart();
}
if (m_recentCharCount
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
scheduleRequest(widget);
}
});
} }
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project) bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@@ -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()

208
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 Qt Creator 2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Download the QodeAssist plugin for your Qt Creator 3. Install a language models in Ollama via terminal. For example, you can run:
- Remove old version plugin if already was installed
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,22 +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
)
target_link_libraries(Context
PUBLIC
Qt::Core
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,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 "ContextManager.hpp"
#include <QFile>
#include <QFileInfo>
#include <QJsonObject>
#include <QTextStream>
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include <texteditor/textdocument.h>
#include <utils/filepath.h>
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;
}
ProgrammingLanguage ContextManager::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());
}
bool ContextManager::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;
}
} // namespace QodeAssist::Context

View File

@@ -1,49 +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 <QString>
#include "ContentFile.hpp"
#include "ProgrammingLanguage.hpp"
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;
ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
bool isSpecifyCompletion(const QJsonObject &request);
private:
explicit ContextManager(QObject *parent = nullptr);
~ContextManager() = default;
ContextManager(const ContextManager &) = delete;
ContextManager &operator=(const ContextManager &) = delete;
ContentFile createContentFile(const QString &filePath) const;
};
} // 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, // QML/JavaScript
Cpp, // C/C++
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,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

@@ -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
ProviderID.hpp
) )
target_link_libraries(LLMCore target_link_libraries(LLMCore

View File

@@ -20,23 +20,14 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QVector>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
struct Message
{
QString role;
QString content;
};
struct ContextData struct ContextData
{ {
std::optional<QString> systemPrompt; QString prefix;
std::optional<QString> prefix; QString suffix;
std::optional<QString> suffix; QString fileContext;
std::optional<QString> fileContext;
std::optional<QVector<Message>> history;
}; };
} // namespace QodeAssist::LLMCore } // 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

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

View File

@@ -37,48 +37,19 @@ QStringList PromptTemplateManager::chatTemplatesNames() const
return m_chatTemplates.keys(); return m_chatTemplates.keys();
} }
QStringList PromptTemplateManager::getFimTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_fimTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
QStringList PromptTemplateManager::getChatTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_chatTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
PromptTemplateManager::~PromptTemplateManager() PromptTemplateManager::~PromptTemplateManager()
{ {
qDeleteAll(m_fimTemplates); qDeleteAll(m_fimTemplates);
qDeleteAll(m_chatTemplates);
} }
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName) PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
{ {
if (!m_fimTemplates.contains(templateName))
return m_fimTemplates.first();
return m_fimTemplates[templateName]; return m_fimTemplates[templateName];
} }
PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName) PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName)
{ {
if (!m_chatTemplates.contains(templateName))
return m_chatTemplates.first();
return m_chatTemplates[templateName]; return m_chatTemplates[templateName];
} }

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();
m_fimTemplates[name] = template_ptr; if (template_ptr->type() == TemplateType::Fim) {
if (template_ptr->type() == TemplateType::Chat) { m_fimTemplates[name] = template_ptr;
} else if (template_ptr->type() == TemplateType::Chat) {
m_chatTemplates[name] = template_ptr; m_chatTemplates[name] = template_ptr;
} }
} }
@@ -51,9 +52,6 @@ public:
QStringList fimTemplatesNames() const; QStringList fimTemplatesNames() const;
QStringList chatTemplatesNames() const; QStringList chatTemplatesNames() const;
QStringList getFimTemplatesForProvider(ProviderID id);
QStringList getChatTemplatesForProvider(ProviderID id);
private: private:
PromptTemplateManager() = default; PromptTemplateManager() = default;
PromptTemplateManager(const PromptTemplateManager &) = delete; PromptTemplateManager(const PromptTemplateManager &) = delete;

View File

@@ -19,13 +19,9 @@
#pragma once #pragma once
#include <utils/environment.h>
#include <QNetworkRequest>
#include <QString> #include <QString>
#include "ContextData.hpp"
#include "PromptTemplate.hpp"
#include "RequestType.hpp" #include "RequestType.hpp"
#include <utils/environment.h>
class QNetworkReply; class QNetworkReply;
class QJsonObject; class QJsonObject;
@@ -42,18 +38,10 @@ public:
virtual QString completionEndpoint() const = 0; virtual QString completionEndpoint() const = 0;
virtual QString chatEndpoint() const = 0; virtual QString chatEndpoint() const = 0;
virtual bool supportsModelListing() const = 0; virtual bool supportsModelListing() const = 0;
virtual void prepareRequest(
QJsonObject &request, virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::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;
virtual ProviderID providerID() const = 0;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,33 +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/>.
*/
namespace QodeAssist::LLMCore {
enum class ProviderID {
Any,
Ollama,
LMStudio,
Claude,
OpenAI,
OpenAICompatible,
MistralAI,
OpenRouter,
GoogleAI
};
}

View File

@@ -39,8 +39,6 @@ ProvidersManager::~ProvidersManager()
Provider *ProvidersManager::getProviderByName(const QString &providerName) Provider *ProvidersManager::getProviderByName(const QString &providerName)
{ {
if (!m_providers.contains(providerName))
return m_providers.first();
return m_providers[providerName]; return m_providers[providerName];
} }

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());
@@ -58,10 +58,7 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
reply->deleteLater(); reply->deleteLater();
m_activeRequests.remove(requestId); m_activeRequests.remove(requestId);
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
LOG_MESSAGE(QString("Error details: %1\nStatus code: %2\nResponse: %3") LOG_MESSAGE(QString("Error in QodeAssist request: %1").arg(reply->errorString()));
.arg(reply->errorString())
.arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())
.arg(QString(reply->readAll())));
emit requestFinished(requestId, false, reply->errorString()); emit requestFinished(requestId, false, reply->errorString());
} else { } else {
LOG_MESSAGE("Request finished successfully"); LOG_MESSAGE("Request finished successfully");
@@ -78,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;
@@ -87,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);
} }
@@ -111,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,
@@ -118,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, Embedding }; 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,221 +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/ProviderSettings.hpp"
namespace QodeAssist::Providers {
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::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
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;
};
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");
}
}
LLMCore::ProviderID ClaudeProvider::providerID() const
{
return LLMCore::ProviderID::Claude;
}
} // namespace QodeAssist::Providers

View File

@@ -1,47 +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:
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::PromptTemplate *prompt,
LLMCore::ContextData context,
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;
LLMCore::ProviderID providerID() const override;
};
} // namespace QodeAssist::Providers

View File

@@ -1,303 +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 "GoogleAIProvider.hpp"
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QtCore/qurlquery.h>
#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 {
QString GoogleAIProvider::name() const
{
return "Google AI";
}
QString GoogleAIProvider::url() const
{
return "https://generativelanguage.googleapis.com/v1beta";
}
QString GoogleAIProvider::completionEndpoint() const
{
return {};
}
QString GoogleAIProvider::chatEndpoint() const
{
return {};
}
bool GoogleAIProvider::supportsModelListing() const
{
return true;
}
void GoogleAIProvider::prepareRequest(
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
QJsonObject generationConfig;
generationConfig["maxOutputTokens"] = settings.maxTokens();
generationConfig["temperature"] = settings.temperature();
if (settings.useTopP())
generationConfig["topP"] = settings.topP();
if (settings.useTopK())
generationConfig["topK"] = settings.topK();
request["generationConfig"] = generationConfig;
};
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool GoogleAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
if (reply->isFinished()) {
if (reply->bytesAvailable() > 0) {
QByteArray data = reply->readAll();
if (data.startsWith("data: ")) {
return handleStreamResponse(data, accumulatedResponse);
} else {
return handleRegularResponse(data, accumulatedResponse);
}
}
return true;
}
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
if (data.startsWith("data: ")) {
return handleStreamResponse(data, accumulatedResponse);
} else {
return handleRegularResponse(data, accumulatedResponse);
}
}
QList<QString> GoogleAIProvider::getInstalledModels(const QString &url)
{
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/models?key=%2").arg(url, apiKey()));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
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("models")) {
QJsonArray modelArray = jsonObject["models"].toArray();
models.clear();
for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject();
if (modelObject.contains("name")) {
QString modelName = modelObject["name"].toString();
if (modelName.contains("/")) {
modelName = modelName.split("/").last();
}
models.append(modelName);
}
}
}
} else {
LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> GoogleAIProvider::validateRequest(
const QJsonObject &request, LLMCore::TemplateType type)
{
QJsonObject templateReq;
templateReq = QJsonObject{
{"contents", QJsonArray{}},
{"system_instruction", QJsonArray{}},
{"generationConfig",
QJsonObject{{"temperature", {}}, {"maxOutputTokens", {}}, {"topP", {}}, {"topK", {}}}},
{"safetySettings", QJsonArray{}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString GoogleAIProvider::apiKey() const
{
return Settings::providerSettings().googleAiApiKey();
}
void GoogleAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl url = networkRequest.url();
QUrlQuery query(url.query());
query.addQueryItem("key", apiKey());
url.setQuery(query);
networkRequest.setUrl(url);
}
LLMCore::ProviderID GoogleAIProvider::providerID() const
{
return LLMCore::ProviderID::GoogleAI;
}
bool GoogleAIProvider::handleStreamResponse(const QByteArray &data, QString &accumulatedResponse)
{
QByteArrayList lines = data.split('\n');
bool isDone = false;
for (const QByteArray &line : lines) {
QByteArray trimmedLine = line.trimmed();
if (trimmedLine.isEmpty()) {
continue;
}
if (trimmedLine == "data: [DONE]") {
isDone = true;
continue;
}
if (trimmedLine.startsWith("data: ")) {
QByteArray jsonData = trimmedLine.mid(6); // Remove "data: " prefix
QJsonDocument doc = QJsonDocument::fromJson(jsonData);
if (doc.isNull() || !doc.isObject()) {
continue;
}
QJsonObject responseObj = doc.object();
if (responseObj.contains("error")) {
QJsonObject error = responseObj["error"].toObject();
LOG_MESSAGE("Error in Google AI stream response: " + error["message"].toString());
continue;
}
if (responseObj.contains("candidates")) {
QJsonArray candidates = responseObj["candidates"].toArray();
if (!candidates.isEmpty()) {
QJsonObject candidate = candidates.first().toObject();
if (candidate.contains("finishReason")
&& !candidate["finishReason"].toString().isEmpty()) {
isDone = true;
}
if (candidate.contains("content")) {
QJsonObject content = candidate["content"].toObject();
if (content.contains("parts")) {
QJsonArray parts = content["parts"].toArray();
for (const auto &part : parts) {
QJsonObject partObj = part.toObject();
if (partObj.contains("text")) {
accumulatedResponse += partObj["text"].toString();
}
}
}
}
}
}
}
}
return isDone;
}
bool GoogleAIProvider::handleRegularResponse(const QByteArray &data, QString &accumulatedResponse)
{
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull() || !doc.isObject()) {
LOG_MESSAGE("Invalid JSON response from Google AI API");
return false;
}
QJsonObject response = doc.object();
if (response.contains("error")) {
QJsonObject error = response["error"].toObject();
LOG_MESSAGE("Error in Google AI response: " + error["message"].toString());
return false;
}
if (!response.contains("candidates") || response["candidates"].toArray().isEmpty()) {
return false;
}
QJsonObject candidate = response["candidates"].toArray().first().toObject();
if (!candidate.contains("content")) {
return false;
}
QJsonObject content = candidate["content"].toObject();
if (!content.contains("parts")) {
return false;
}
QJsonArray parts = content["parts"].toArray();
for (const auto &part : parts) {
QJsonObject partObj = part.toObject();
if (partObj.contains("text")) {
accumulatedResponse += partObj["text"].toString();
}
}
return true;
}
} // namespace QodeAssist::Providers

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 "llmcore/Provider.hpp"
namespace QodeAssist::Providers {
class GoogleAIProvider : public LLMCore::Provider
{
public:
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::PromptTemplate *prompt,
LLMCore::ContextData context,
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;
LLMCore::ProviderID providerID() const override;
private:
bool handleStreamResponse(const QByteArray &data, QString &accumulatedResponse);
bool handleRegularResponse(const QByteArray &data, QString &accumulatedResponse);
};
} // namespace QodeAssist::Providers

View File

@@ -25,14 +25,14 @@
#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"
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
LMStudioProvider::LMStudioProvider() {}
QString LMStudioProvider::name() const QString LMStudioProvider::name() const
{ {
return "LM Studio"; return "LM Studio";
@@ -58,55 +58,86 @@ bool LMStudioProvider::supportsModelListing() const
return true; return true;
} }
void LMStudioProvider::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::Fim) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
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); if (choice["finish_reason"].toString() == "stop") {
continue; isComplete = true;
} break;
}
QString content = message.getContent(); }
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
} }
} }
return isComplete;
return isDone;
} }
QList<QString> LMStudioProvider::getInstalledModels(const QString &url) QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
@@ -140,70 +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");
}
LLMCore::ProviderID LMStudioProvider::providerID() const
{
return LLMCore::ProviderID::LMStudio;
}
void QodeAssist::Providers::LMStudioProvider::prepareRequest(
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
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();
};
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -26,22 +26,16 @@ namespace QodeAssist::Providers {
class LMStudioProvider : public LLMCore::Provider class LMStudioProvider : public LLMCore::Provider
{ {
public: public:
LMStudioProvider();
QString name() const override; QString name() const override;
QString url() const override; QString url() const override;
QString completionEndpoint() const override; QString completionEndpoint() const override;
QString chatEndpoint() const override; QString chatEndpoint() const override;
bool supportsModelListing() const override; bool supportsModelListing() const override;
void prepareRequest( void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
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;
LLMCore::ProviderID providerID() const override;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -1,220 +0,0 @@
#include "MistralAIProvider.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QtCore/qeventloop.h>
#include "llmcore/OpenAIMessage.hpp"
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp"
namespace QodeAssist::Providers {
QString MistralAIProvider::name() const
{
return "Mistral AI";
}
QString MistralAIProvider::url() const
{
return "https://api.mistral.ai";
}
QString MistralAIProvider::completionEndpoint() const
{
return "/v1/fim/completions";
}
QString MistralAIProvider::chatEndpoint() const
{
return "/v1/chat/completions";
}
bool MistralAIProvider::supportsModelListing() const
{
return true;
}
bool MistralAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return isDone;
}
QList<QString> MistralAIProvider::getInstalledModels(const QString &url)
{
QList<QString> models;
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") && jsonObject["object"].toString() == "list") {
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 Mistral AI models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> MistralAIProvider::validateRequest(
const QJsonObject &request, LLMCore::TemplateType type)
{
const auto fimReq = QJsonObject{
{"model", {}},
{"max_tokens", {}},
{"stream", {}},
{"temperature", {}},
{"prompt", {}},
{"suffix", {}}};
const auto templateReq = QJsonObject{
{"model", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"temperature", {}},
{"max_tokens", {}},
{"top_p", {}},
{"frequency_penalty", {}},
{"presence_penalty", {}},
{"stop", QJsonArray{}},
{"stream", {}}};
return LLMCore::ValidationUtils::validateRequestFields(
request, type == LLMCore::TemplateType::FIM ? fimReq : templateReq);
}
QString MistralAIProvider::apiKey() const
{
return Settings::providerSettings().mistralAiApiKey();
}
void MistralAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
}
LLMCore::ProviderID MistralAIProvider::providerID() const
{
return LLMCore::ProviderID::MistralAI;
}
void MistralAIProvider::prepareRequest(
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
if (type == LLMCore::RequestType::Chat) {
auto &settings = Settings::chatAssistantSettings();
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
// request["random_seed"] = "";
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
} else {
auto &settings = Settings::codeCompletionSettings();
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
// request["random_seed"] = "";
}
}
} // namespace QodeAssist::Providers

View File

@@ -1,47 +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 MistralAIProvider : public LLMCore::Provider
{
public:
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::PromptTemplate *prompt,
LLMCore::ContextData context,
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;
LLMCore::ProviderID providerID() const override;
};
} // namespace QodeAssist::Providers

View File

@@ -25,14 +25,14 @@
#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"
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
OllamaProvider::OllamaProvider() {}
QString OllamaProvider::name() const QString OllamaProvider::name() const
{ {
return "Ollama"; return "Ollama";
@@ -58,23 +58,12 @@ bool OllamaProvider::supportsModelListing() const
return true; return true;
} }
void OllamaProvider::prepareRequest( void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{ {
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applySettings = [&request](const auto &settings) { auto applySettings = [&request](const auto &settings) {
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();
@@ -89,7 +78,7 @@ void OllamaProvider::prepareRequest(
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());
@@ -98,41 +87,53 @@ void OllamaProvider::prepareRequest(
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()) {
return false;
}
QByteArrayList lines = data.split('\n'); bool isComplete = false;
bool isDone = false; while (reply->canReadLine()) {
QByteArray line = reply->readLine().trimmed();
for (const QByteArray &line : lines) { if (line.isEmpty()) {
if (line.trimmed().isEmpty()) {
continue; continue;
} }
const QString endpoint = reply->url().path(); QJsonDocument doc = QJsonDocument::fromJson(line);
auto messageType = endpoint == completionEndpoint() if (doc.isNull()) {
? LLMCore::OllamaMessage::Type::Generate LOG_MESSAGE("Invalid JSON response from Ollama: " + QString::fromUtf8(line));
: LLMCore::OllamaMessage::Type::Chat;
auto message = LLMCore::OllamaMessage::fromJson(line, messageType);
if (message.hasError()) {
LOG_MESSAGE("Error in Ollama response: " + message.error);
continue; continue;
} }
QString content = message.getContent(); QJsonObject responseObj = doc.object();
if (!content.isEmpty()) {
accumulatedResponse += content; if (responseObj.contains("error")) {
QString errorMessage = responseObj["error"].toString();
LOG_MESSAGE("Error in Ollama response: " + errorMessage);
return false;
} }
if (message.done) { if (endpoint == completionEndpoint()) {
isDone = true; if (responseObj.contains("response")) {
QString completion = responseObj["response"].toString();
accumulatedResponse += completion;
}
} else if (endpoint == chatEndpoint()) {
if (responseObj.contains("message")) {
QJsonObject message = responseObj["message"].toObject();
if (message.contains("content")) {
QString content = message["content"].toString();
accumulatedResponse += content;
}
}
} else {
LOG_MESSAGE("Unknown endpoint: " + endpoint);
}
if (responseObj.contains("done") && responseObj["done"].toBool()) {
isComplete = true;
break;
} }
} }
return isDone; return isComplete;
} }
QList<QString> OllamaProvider::getInstalledModels(const QString &url) QList<QString> OllamaProvider::getInstalledModels(const QString &url)
@@ -165,57 +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");
}
LLMCore::ProviderID OllamaProvider::providerID() const
{
return LLMCore::ProviderID::Ollama;
}
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -26,22 +26,16 @@ namespace QodeAssist::Providers {
class OllamaProvider : public LLMCore::Provider class OllamaProvider : public LLMCore::Provider
{ {
public: public:
OllamaProvider();
QString name() const override; QString name() const override;
QString url() const override; QString url() const override;
QString completionEndpoint() const override; QString completionEndpoint() const override;
QString chatEndpoint() const override; QString chatEndpoint() const override;
bool supportsModelListing() const override; bool supportsModelListing() const override;
void prepareRequest( void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
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;
LLMCore::ProviderID providerID() const override;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -18,22 +18,20 @@
*/ */
#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 {
OpenAICompatProvider::OpenAICompatProvider() {}
QString OpenAICompatProvider::name() const QString OpenAICompatProvider::name() const
{ {
return "OpenAI Compatible"; return "OpenAI Compatible";
@@ -59,17 +57,20 @@ bool OpenAICompatProvider::supportsModelListing() const
return false; return false;
} }
void OpenAICompatProvider::prepareRequest( void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{ {
if (!prompt->isSupportProvider(providerID())) { auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); QJsonArray messages;
} if (req.contains("system")) {
messages.append(
prompt->prepareRequest(request, context); 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) { auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens(); request["max_tokens"] = settings.maxTokens();
@@ -85,7 +86,12 @@ void OpenAICompatProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == LLMCore::RequestType::CodeCompletion) { QJsonArray messages = prepareMessages(request);
if (!messages.isEmpty()) {
request["messages"] = std::move(messages);
}
if (type == LLMCore::RequestType::Fim) {
applyModelParams(Settings::codeCompletionSettings()); applyModelParams(Settings::codeCompletionSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());
@@ -94,53 +100,74 @@ void OpenAICompatProvider::prepareRequest(
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;
return false;
}
bool isDone = false; while (reply->canReadLine()) {
QByteArrayList lines = data.split('\n'); QByteArray line = reply->readLine().trimmed();
if (line.isEmpty()) {
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue; continue;
} }
if (line == "data: [DONE]") { if (!line.startsWith("data:")) {
isDone = true;
continue; continue;
} }
QByteArray jsonData = line; line = line.mid(6);
if (line.startsWith("data: ")) {
jsonData = line.mid(6); if (line == "[DONE]") {
isComplete = true;
break;
} }
QJsonParseError error; QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); if (jsonResponse.isNull()) {
LOG_MESSAGE(
if (doc.isNull()) { "Invalid JSON response from OpenAI compatible provider: " + QString::fromUtf8(line));
continue; continue;
} }
auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); QJsonObject responseObj = jsonResponse.object();
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error); if (responseObj.contains("error")) {
continue; LOG_MESSAGE(
"OpenAI compatible provider error: "
+ QString::fromUtf8(QJsonDocument(responseObj).toJson(QJsonDocument::Indented)));
return false;
} }
QString content = message.getContent(); if (responseObj.contains("choices")) {
if (!content.isEmpty()) { QJsonArray choices = responseObj["choices"].toArray();
accumulatedResponse += content; if (!choices.isEmpty()) {
QJsonObject choice = choices.first().toObject();
QJsonObject delta = choice["delta"].toObject();
if (delta.contains("content")) {
QString completion = delta["content"].toString();
if (!completion.isEmpty()) {
tempResponse += completion;
}
}
QString finishReason = choice["finish_reason"].toString();
if (!finishReason.isNull() && finishReason == "stop") {
isComplete = true;
}
}
} }
if (message.isDone()) { if (responseObj.contains("usage")) {
isDone = true; 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()));
} }
} }
return isDone; if (!tempResponse.isEmpty()) {
accumulatedResponse = tempResponse;
}
return isComplete;
} }
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url) QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
@@ -148,41 +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());
}
}
LLMCore::ProviderID OpenAICompatProvider::providerID() const
{
return LLMCore::ProviderID::OpenAICompatible;
}
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -26,22 +26,16 @@ namespace QodeAssist::Providers {
class OpenAICompatProvider : public LLMCore::Provider class OpenAICompatProvider : public LLMCore::Provider
{ {
public: public:
OpenAICompatProvider();
QString name() const override; QString name() const override;
QString url() const override; QString url() const override;
QString completionEndpoint() const override; QString completionEndpoint() const override;
QString chatEndpoint() const override; QString chatEndpoint() const override;
bool supportsModelListing() const override; bool supportsModelListing() const override;
void prepareRequest( void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
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;
LLMCore::ProviderID providerID() const override;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@@ -1,224 +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 {
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::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
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();
};
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());
}
}
LLMCore::ProviderID OpenAIProvider::providerID() const
{
return LLMCore::ProviderID::OpenAI;
}
} // namespace QodeAssist::Providers

View File

@@ -1,47 +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:
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::PromptTemplate *prompt,
LLMCore::ContextData context,
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;
LLMCore::ProviderID providerID() const override;
};
} // namespace QodeAssist::Providers

View File

@@ -1,105 +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/ProviderSettings.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include "llmcore/OpenAIMessage.hpp"
#include "logger/Logger.hpp"
namespace QodeAssist::Providers {
QString OpenRouterProvider::name() const
{
return "OpenRouter";
}
QString OpenRouterProvider::url() const
{
return "https://openrouter.ai/api";
}
bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty() || line.contains("OPENROUTER PROCESSING")) {
continue;
}
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return isDone;
}
QString OpenRouterProvider::apiKey() const
{
return Settings::providerSettings().openRouterApiKey();
}
LLMCore::ProviderID OpenRouterProvider::providerID() const
{
return LLMCore::ProviderID::OpenRouter;
}
} // namespace QodeAssist::Providers

View File

@@ -1,37 +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:
QString name() const override;
QString url() const override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QString apiKey() const override;
LLMCore::ProviderID providerID() const override;
};
} // namespace QodeAssist::Providers

View File

@@ -20,14 +20,9 @@
#pragma once #pragma once
#include "llmcore/ProvidersManager.hpp" #include "llmcore/ProvidersManager.hpp"
#include "providers/ClaudeProvider.hpp"
#include "providers/GoogleAIProvider.hpp"
#include "providers/LMStudioProvider.hpp" #include "providers/LMStudioProvider.hpp"
#include "providers/MistralAIProvider.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,13 +30,8 @@ inline void registerProviders()
{ {
auto &providerManager = LLMCore::ProvidersManager::instance(); auto &providerManager = LLMCore::ProvidersManager::instance();
providerManager.registerProvider<OllamaProvider>(); providerManager.registerProvider<OllamaProvider>();
providerManager.registerProvider<ClaudeProvider>();
providerManager.registerProvider<OpenAIProvider>();
providerManager.registerProvider<OpenAICompatProvider>();
providerManager.registerProvider<LMStudioProvider>(); providerManager.registerProvider<LMStudioProvider>();
providerManager.registerProvider<OpenRouterProvider>(); providerManager.registerProvider<OpenAICompatProvider>();
providerManager.registerProvider<MistralAIProvider>();
providerManager.registerProvider<GoogleAIProvider>();
} }
} // 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,38 +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]() { m_chatOutputPane = new Chat::ChatOutputPane(this);
UpdateDialog::checkForUpdatesAndShow(Core::ICore::mainWindow()); m_navigationPanel = new Chat::NavigationPanel();
});
if (Settings::generalSettings().enableChat()) {
m_chatOutputPane = new Chat::ChatOutputPane(this);
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;
} }
@@ -139,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

View File

@@ -44,23 +44,15 @@ ChatAssistantSettings::ChatAssistantSettings()
// Chat Settings // Chat Settings
chatTokensThreshold.setSettingsKey(Constants::CA_TOKENS_THRESHOLD); chatTokensThreshold.setSettingsKey(Constants::CA_TOKENS_THRESHOLD);
chatTokensThreshold.setLabelText(Tr::tr("Chat history token limit:")); chatTokensThreshold.setLabelText(Tr::tr("Chat History Token Limit:"));
chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When " chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When "
"exceeded, oldest messages will be removed.")); "exceeded, oldest messages will be removed."));
chatTokensThreshold.setRange(1, 900000); chatTokensThreshold.setRange(1000, 16000);
chatTokensThreshold.setDefaultValue(8000); chatTokensThreshold.setDefaultValue(8000);
linkOpenFiles.setSettingsKey(Constants::CA_LINK_OPEN_FILES); sharingCurrentFile.setSettingsKey(Constants::CA_SHARING_CURRENT_FILE);
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default")); sharingCurrentFile.setLabelText(Tr::tr("Share Current File With Assistant by Default"));
linkOpenFiles.setDefaultValue(false); sharingCurrentFile.setDefaultValue(true);
stream.setSettingsKey(Constants::CA_STREAM);
stream.setDefaultValue(true);
stream.setLabelText(Tr::tr("Enable stream option"));
autosave.setSettingsKey(Constants::CA_AUTOSAVE);
autosave.setDefaultValue(true);
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
// General Parameters Settings // General Parameters Settings
temperature.setSettingsKey(Constants::CA_TEMPERATURE); temperature.setSettingsKey(Constants::CA_TEMPERATURE);
@@ -139,7 +131,7 @@ ChatAssistantSettings::ChatAssistantSettings()
// API Configuration Settings // API Configuration Settings
apiKey.setSettingsKey(Constants::CA_API_KEY); apiKey.setSettingsKey(Constants::CA_API_KEY);
apiKey.setLabelText(Tr::tr("[Deprecated, see Provider Settings]API Key:")); apiKey.setLabelText(Tr::tr("API Key:"));
apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay); apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
apiKey.setPlaceHolderText(Tr::tr("Enter your API key here")); apiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
@@ -166,30 +158,28 @@ ChatAssistantSettings::ChatAssistantSettings()
ollamaGrid.addRow({ollamaLivetime}); ollamaGrid.addRow({ollamaLivetime});
ollamaGrid.addRow({contextWindow}); ollamaGrid.addRow({contextWindow});
return Column{ return Column{Row{Stretch{1}, resetToDefaults},
Row{Stretch{1}, resetToDefaults}, Space{8},
Space{8}, Group{title(Tr::tr("Chat Settings")),
Group{ Column{Row{chatTokensThreshold, Stretch{1}}, sharingCurrentFile}},
title(Tr::tr("Chat Settings")), Space{8},
Column{Row{chatTokensThreshold, Stretch{1}}, linkOpenFiles, stream, autosave}}, Group{
Space{8}, title(Tr::tr("General Parameters")),
Group{ Row{genGrid, Stretch{1}},
title(Tr::tr("General Parameters")), },
Row{genGrid, Stretch{1}}, Space{8},
}, Group{title(Tr::tr("Advanced Parameters")),
Space{8}, Column{Row{advancedGrid, Stretch{1}}}},
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}}, Space{8},
Space{8}, Group{title(Tr::tr("Context Settings")),
Group{ Column{
title(Tr::tr("Context Settings")), Row{useSystemPrompt, Stretch{1}},
Column{ systemPrompt,
Row{useSystemPrompt, Stretch{1}}, }},
systemPrompt, Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
}}, Space{8},
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}}, Group{title(Tr::tr("API Configuration")), Column{apiKey}},
Space{8}, Stretch{1}};
Group{title(Tr::tr("API Configuration")), Column{apiKey}},
Stretch{1}};
}); });
} }
@@ -211,7 +201,6 @@ void ChatAssistantSettings::resetSettingsToDefaults()
QMessageBox::Yes | QMessageBox::No); QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
resetAspect(stream);
resetAspect(chatTokensThreshold); resetAspect(chatTokensThreshold);
resetAspect(temperature); resetAspect(temperature);
resetAspect(maxTokens); resetAspect(maxTokens);
@@ -227,7 +216,6 @@ void ChatAssistantSettings::resetSettingsToDefaults()
resetAspect(systemPrompt); resetAspect(systemPrompt);
resetAspect(ollamaLivetime); resetAspect(ollamaLivetime);
resetAspect(contextWindow); resetAspect(contextWindow);
resetAspect(linkOpenFiles);
} }
} }

View File

@@ -34,9 +34,7 @@ public:
// Chat settings // Chat settings
Utils::IntegerAspect chatTokensThreshold{this}; Utils::IntegerAspect chatTokensThreshold{this};
Utils::BoolAspect linkOpenFiles{this}; Utils::BoolAspect sharingCurrentFile{this};
Utils::BoolAspect stream{this};
Utils::BoolAspect autosave{this};
// General Parameters Settings // General Parameters Settings
Utils::DoubleAspect temperature{this}; Utils::DoubleAspect temperature{this};

View File

@@ -48,21 +48,13 @@ CodeCompletionSettings::CodeCompletionSettings()
autoCompletion.setDefaultValue(true); autoCompletion.setDefaultValue(true);
multiLineCompletion.setSettingsKey(Constants::CC_MULTILINE_COMPLETION); multiLineCompletion.setSettingsKey(Constants::CC_MULTILINE_COMPLETION);
multiLineCompletion.setDefaultValue(true); multiLineCompletion.setDefaultValue(false);
multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion")); multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion(experimental)"));
stream.setSettingsKey(Constants::CC_STREAM);
stream.setDefaultValue(true);
stream.setLabelText(Tr::tr("Enable stream option"));
smartProcessInstuctText.setSettingsKey(Constants::CC_SMART_PROCESS_INSTRUCT_TEXT);
smartProcessInstuctText.setDefaultValue(true);
smartProcessInstuctText.setLabelText(Tr::tr("Enable smart process text from instruct model"));
startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER); startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER);
startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)")); startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)"));
startSuggestionTimer.setRange(10, 10000); startSuggestionTimer.setRange(10, 10000);
startSuggestionTimer.setDefaultValue(350); startSuggestionTimer.setDefaultValue(500);
autoCompletionCharThreshold.setSettingsKey(Constants::СС_AUTO_COMPLETION_CHAR_THRESHOLD); autoCompletionCharThreshold.setSettingsKey(Constants::СС_AUTO_COMPLETION_CHAR_THRESHOLD);
autoCompletionCharThreshold.setLabelText(Tr::tr("AI suggestion triggers after typing")); autoCompletionCharThreshold.setLabelText(Tr::tr("AI suggestion triggers after typing"));
@@ -70,7 +62,7 @@ CodeCompletionSettings::CodeCompletionSettings()
Tr::tr("The number of characters that need to be typed within the typing interval " Tr::tr("The number of characters that need to be typed within the typing interval "
"before an AI suggestion request is sent.")); "before an AI suggestion request is sent."));
autoCompletionCharThreshold.setRange(0, 10); autoCompletionCharThreshold.setRange(0, 10);
autoCompletionCharThreshold.setDefaultValue(1); autoCompletionCharThreshold.setDefaultValue(0);
autoCompletionTypingInterval.setSettingsKey(Constants::СС_AUTO_COMPLETION_TYPING_INTERVAL); autoCompletionTypingInterval.setSettingsKey(Constants::СС_AUTO_COMPLETION_TYPING_INTERVAL);
autoCompletionTypingInterval.setLabelText(Tr::tr("character(s) within(ms)")); autoCompletionTypingInterval.setLabelText(Tr::tr("character(s) within(ms)"));
@@ -78,7 +70,7 @@ CodeCompletionSettings::CodeCompletionSettings()
Tr::tr("The time window (in milliseconds) during which the character threshold " Tr::tr("The time window (in milliseconds) during which the character threshold "
"must be met to trigger an AI suggestion request.")); "must be met to trigger an AI suggestion request."));
autoCompletionTypingInterval.setRange(500, 5000); autoCompletionTypingInterval.setRange(500, 5000);
autoCompletionTypingInterval.setDefaultValue(1200); autoCompletionTypingInterval.setDefaultValue(2000);
// General Parameters Settings // General Parameters Settings
temperature.setSettingsKey(Constants::CC_TEMPERATURE); temperature.setSettingsKey(Constants::CC_TEMPERATURE);
@@ -89,8 +81,8 @@ CodeCompletionSettings::CodeCompletionSettings()
maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS); maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS);
maxTokens.setLabelText(Tr::tr("Max Tokens:")); maxTokens.setLabelText(Tr::tr("Max Tokens:"));
maxTokens.setRange(-1, 900000); maxTokens.setRange(-1, 10000);
maxTokens.setDefaultValue(100); maxTokens.setDefaultValue(50);
// Advanced Parameters // Advanced Parameters
useTopP.setSettingsKey(Constants::CC_USE_TOP_P); useTopP.setSettingsKey(Constants::CC_USE_TOP_P);
@@ -151,47 +143,12 @@ CodeCompletionSettings::CodeCompletionSettings()
systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT); systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT);
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay); systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
systemPromptForNonFimModels.setLabelText(Tr::tr("System prompt for FIM models:")); systemPrompt.setDefaultValue("You are an expert C++, Qt, and QML code completion AI. ANSWER "
systemPrompt.setDefaultValue( "should be SHORT and in CODE");
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
"precise and contextually appropriate code completions.\n\n");
useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE); useFilePathInContext.setSettingsKey(Constants::CC_USE_FILE_PATH_IN_CONTEXT);
useUserMessageTemplateForCC.setDefaultValue(true); useFilePathInContext.setDefaultValue(true);
useUserMessageTemplateForCC.setLabelText( useFilePathInContext.setLabelText(Tr::tr("Use File Path in Context"));
Tr::tr("Use special system prompt and user message for non FIM models"));
systemPromptForNonFimModels.setSettingsKey(Constants::CC_SYSTEM_PROMPT_FOR_NON_FIM);
systemPromptForNonFimModels.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
systemPromptForNonFimModels.setLabelText(Tr::tr("System prompt for non FIM models:"));
systemPromptForNonFimModels.setDefaultValue(
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
"precise and contextually appropriate code completions.\n\n"
"Core Requirements:\n"
"1. Continue code exactly from the cursor position, ensuring it properly connects with any "
"existing code after the cursor\n"
"2. Never repeat existing code before or after the cursor\n"
"Specific Guidelines:\n"
"- For function calls: Complete parameters with appropriate types and names\n"
"- For class members: Respect access modifiers and class conventions\n"
"- Respect existing indentation and formatting\n"
"- Consider scope and visibility of referenced symbols\n"
"- Ensure seamless integration with code both before and after the cursor\n\n"
"Context Format:\n"
"<code_context>\n"
"{{code before cursor}}<cursor>{{code after cursor}}\n"
"</code_context>\n\n"
"Response Format:\n"
"- No explanations or comments\n"
"- Only include new characters needed to create valid code\n"
"- Should be codeblock with language\n");
userMessageTemplateForCC.setSettingsKey(Constants::CC_USER_TEMPLATE);
userMessageTemplateForCC.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
userMessageTemplateForCC.setLabelText(Tr::tr("User message for non FIM models:"));
userMessageTemplateForCC.setDefaultValue(
"Here is the code context with insertion points:\n"
"<code_context>\n${prefix}<cursor>${suffix}\n</code_context>\n\n");
useProjectChangesCache.setSettingsKey(Constants::CC_USE_PROJECT_CHANGES_CACHE); useProjectChangesCache.setSettingsKey(Constants::CC_USE_PROJECT_CHANGES_CACHE);
useProjectChangesCache.setDefaultValue(true); useProjectChangesCache.setDefaultValue(true);
@@ -217,7 +174,7 @@ CodeCompletionSettings::CodeCompletionSettings()
// API Configuration Settings // API Configuration Settings
apiKey.setSettingsKey(Constants::CC_API_KEY); apiKey.setSettingsKey(Constants::CC_API_KEY);
apiKey.setLabelText(Tr::tr("[Deprecated, see Provider Settings]API Key:")); apiKey.setLabelText(Tr::tr("API Key:"));
apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay); apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
apiKey.setPlaceHolderText(Tr::tr("Enter your API key here")); apiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
@@ -252,46 +209,35 @@ CodeCompletionSettings::CodeCompletionSettings()
auto contextItem = Column{Row{contextGrid, Stretch{1}}, auto contextItem = Column{Row{contextGrid, Stretch{1}},
Row{useSystemPrompt, Stretch{1}}, Row{useSystemPrompt, Stretch{1}},
Group{title(Tr::tr("Prompts for FIM models")), systemPrompt,
Column{systemPrompt}}, Row{useFilePathInContext, Stretch{1}},
Group{title(Tr::tr("Prompts for Non FIM models")),
Column{
Row{useUserMessageTemplateForCC, Stretch{1}},
systemPromptForNonFimModels,
userMessageTemplateForCC,
}},
Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}}; Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
return Column{ return Column{Row{Stretch{1}, resetToDefaults},
Row{Stretch{1}, resetToDefaults}, Space{8},
Space{8}, Group{title(Tr::tr("Auto Completion Settings")),
Group{ Column{autoCompletion,
title(TrConstants::AUTO_COMPLETION_SETTINGS), Space{8},
Column{ multiLineCompletion,
autoCompletion, Row{autoCompletionCharThreshold,
Space{8}, autoCompletionTypingInterval,
multiLineCompletion, startSuggestionTimer,
stream, Stretch{1}}}},
smartProcessInstuctText, Space{8},
Row{autoCompletionCharThreshold, Group{title(Tr::tr("General Parameters")),
autoCompletionTypingInterval, Column{
startSuggestionTimer, Row{genGrid, Stretch{1}},
Stretch{1}}}}, }},
Space{8}, Space{8},
Group{ Group{title(Tr::tr("Advanced Parameters")),
title(Tr::tr("General Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
Column{ Space{8},
Row{genGrid, Stretch{1}}, Group{title(Tr::tr("Context Settings")), contextItem},
}}, Space{8},
Space{8}, Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}}, Space{8},
Space{8}, Group{title(Tr::tr("API Configuration")), Column{apiKey}},
Group{title(Tr::tr("Context Settings")), contextItem}, Stretch{1}};
Space{8},
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
Space{8},
Group{title(Tr::tr("API Configuration")), Column{apiKey}},
Stretch{1}};
}); });
} }
@@ -329,8 +275,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
resetAspect(autoCompletion); resetAspect(autoCompletion);
resetAspect(multiLineCompletion); resetAspect(multiLineCompletion);
resetAspect(stream);
resetAspect(smartProcessInstuctText);
resetAspect(temperature); resetAspect(temperature);
resetAspect(maxTokens); resetAspect(maxTokens);
resetAspect(useTopP); resetAspect(useTopP);
@@ -347,24 +291,14 @@ void CodeCompletionSettings::resetSettingsToDefaults()
resetAspect(readStringsAfterCursor); resetAspect(readStringsAfterCursor);
resetAspect(useSystemPrompt); resetAspect(useSystemPrompt);
resetAspect(systemPrompt); resetAspect(systemPrompt);
resetAspect(useFilePathInContext);
resetAspect(useProjectChangesCache); resetAspect(useProjectChangesCache);
resetAspect(maxChangesCacheSize); resetAspect(maxChangesCacheSize);
resetAspect(ollamaLivetime); resetAspect(ollamaLivetime);
resetAspect(contextWindow); resetAspect(contextWindow);
resetAspect(useUserMessageTemplateForCC);
resetAspect(userMessageTemplateForCC);
resetAspect(systemPromptForNonFimModels);
} }
} }
QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix)
{
QString result = userMessageTemplateForCC();
result.replace("${prefix}", prefix);
result.replace("${suffix}", suffix);
return result;
}
class CodeCompletionSettingsPage : public Core::IOptionsPage class CodeCompletionSettingsPage : public Core::IOptionsPage
{ {
public: public:

View File

@@ -35,8 +35,6 @@ public:
// Auto Completion Settings // Auto Completion Settings
Utils::BoolAspect autoCompletion{this}; Utils::BoolAspect autoCompletion{this};
Utils::BoolAspect multiLineCompletion{this}; Utils::BoolAspect multiLineCompletion{this};
Utils::BoolAspect stream{this};
Utils::BoolAspect smartProcessInstuctText{this};
Utils::IntegerAspect startSuggestionTimer{this}; Utils::IntegerAspect startSuggestionTimer{this};
Utils::IntegerAspect autoCompletionCharThreshold{this}; Utils::IntegerAspect autoCompletionCharThreshold{this};
@@ -66,9 +64,7 @@ public:
Utils::IntegerAspect readStringsAfterCursor{this}; Utils::IntegerAspect readStringsAfterCursor{this};
Utils::BoolAspect useSystemPrompt{this}; Utils::BoolAspect useSystemPrompt{this};
Utils::StringAspect systemPrompt{this}; Utils::StringAspect systemPrompt{this};
Utils::BoolAspect useUserMessageTemplateForCC{this}; Utils::BoolAspect useFilePathInContext{this};
Utils::StringAspect systemPromptForNonFimModels{this};
Utils::StringAspect userMessageTemplateForCC{this};
Utils::BoolAspect useProjectChangesCache{this}; Utils::BoolAspect useProjectChangesCache{this};
Utils::IntegerAspect maxChangesCacheSize{this}; Utils::IntegerAspect maxChangesCacheSize{this};
@@ -79,8 +75,6 @@ public:
// API Configuration Settings // API Configuration Settings
Utils::StringAspect apiKey{this}; Utils::StringAspect apiKey{this};
QString processMessageToFIM(const QString &prefix, const QString &suffix);
private: private:
void setupConnections(); void setupConnections();
void resetSettingsToDefaults(); void resetSettingsToDefaults();

View File

@@ -21,7 +21,6 @@
#include <coreplugin/dialogs/ioptionspage.h> #include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <utils/detailswidget.h>
#include <utils/layoutbuilder.h> #include <utils/layoutbuilder.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include <QInputDialog> #include <QInputDialog>
@@ -38,7 +37,6 @@
#include "SettingsDialog.hpp" #include "SettingsDialog.hpp"
#include "SettingsTr.hpp" #include "SettingsTr.hpp"
#include "SettingsUtils.hpp" #include "SettingsUtils.hpp"
#include "UpdateDialog.hpp"
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
@@ -62,16 +60,7 @@ GeneralSettings::GeneralSettings()
enableLogging.setLabelText(TrConstants::ENABLE_LOG); enableLogging.setLabelText(TrConstants::ENABLE_LOG);
enableLogging.setDefaultValue(false); enableLogging.setDefaultValue(false);
enableCheckUpdate.setSettingsKey(Constants::ENABLE_CHECK_UPDATE);
enableCheckUpdate.setLabelText(TrConstants::ENABLE_CHECK_UPDATE_ON_START);
enableCheckUpdate.setDefaultValue(true);
enableChat.setSettingsKey(Constants::ENABLE_CHAT);
enableChat.setLabelText(TrConstants::ENABLE_CHAT);
enableChat.setDefaultValue(true);
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS; resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama"); initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccProvider.setReadOnly(true); ccProvider.setReadOnly(true);
@@ -94,44 +83,6 @@ GeneralSettings::GeneralSettings()
ccStatus.setDefaultValue(""); ccStatus.setDefaultValue("");
ccTest.m_buttonText = TrConstants::TEST; ccTest.m_buttonText = TrConstants::TEST;
ccTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
ccTemplateDescription.setReadOnly(true);
ccTemplateDescription.setDefaultValue("");
ccTemplateDescription.setLabelText(TrConstants::CURRENT_TEMPLATE_DESCRIPTION);
// preset1
specifyPreset1.setSettingsKey(Constants::CC_SPECIFY_PRESET1);
specifyPreset1.setLabelText(TrConstants::ADD_NEW_PRESET);
specifyPreset1.setDefaultValue(false);
preset1Language.setSettingsKey(Constants::CC_PRESET1_LANGUAGE);
preset1Language.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
// see ProgrammingLanguageUtils
preset1Language.addOption("qml");
preset1Language.addOption("c/c++");
preset1Language.addOption("python");
initStringAspect(
ccPreset1Provider, Constants::CC_PRESET1_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccPreset1Provider.setReadOnly(true);
ccPreset1SelectProvider.m_buttonText = TrConstants::SELECT;
initStringAspect(
ccPreset1Url, Constants::CC_PRESET1_URL, TrConstants::URL, "http://localhost:11434");
ccPreset1Url.setHistoryCompleter(Constants::CC_PRESET1_URL_HISTORY);
ccPreset1SetUrl.m_buttonText = TrConstants::SELECT;
initStringAspect(
ccPreset1Model, Constants::CC_PRESET1_MODEL, TrConstants::MODEL, "qwen2.5-coder:7b");
ccPreset1Model.setHistoryCompleter(Constants::CC_PRESET1_MODEL_HISTORY);
ccPreset1SelectModel.m_buttonText = TrConstants::SELECT;
initStringAspect(
ccPreset1Template, Constants::CC_PRESET1_TEMPLATE, TrConstants::TEMPLATE, "Ollama Auto FIM");
ccPreset1Template.setReadOnly(true);
ccPreset1SelectTemplate.m_buttonText = TrConstants::SELECT;
// chat assistance
initStringAspect(caProvider, Constants::CA_PROVIDER, TrConstants::PROVIDER, "Ollama"); initStringAspect(caProvider, Constants::CA_PROVIDER, TrConstants::PROVIDER, "Ollama");
caProvider.setReadOnly(true); caProvider.setReadOnly(true);
caSelectProvider.m_buttonText = TrConstants::SELECT; caSelectProvider.m_buttonText = TrConstants::SELECT;
@@ -154,19 +105,12 @@ GeneralSettings::GeneralSettings()
caStatus.setDefaultValue(""); caStatus.setDefaultValue("");
caTest.m_buttonText = TrConstants::TEST; caTest.m_buttonText = TrConstants::TEST;
caTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
caTemplateDescription.setReadOnly(true);
caTemplateDescription.setDefaultValue("");
caTemplateDescription.setLabelText(TrConstants::CURRENT_TEMPLATE_DESCRIPTION);
readSettings(); readSettings();
Logger::instance().setLoggingEnabled(enableLogging()); Logger::instance().setLoggingEnabled(enableLogging());
setupConnections(); setupConnections();
updatePreset1Visiblity(specifyPreset1.value());
setLayouter([this]() { setLayouter([this]() {
using namespace Layouting; using namespace Layouting;
@@ -175,46 +119,34 @@ GeneralSettings::GeneralSettings()
ccGrid.addRow({ccUrl, ccSetUrl}); ccGrid.addRow({ccUrl, ccSetUrl});
ccGrid.addRow({ccModel, ccSelectModel}); ccGrid.addRow({ccModel, ccSelectModel});
ccGrid.addRow({ccTemplate, ccSelectTemplate}); ccGrid.addRow({ccTemplate, ccSelectTemplate});
ccGrid.addRow({ccStatus, ccTest});
auto ccPreset1Grid = Grid{};
ccPreset1Grid.addRow({ccPreset1Provider, ccPreset1SelectProvider});
ccPreset1Grid.addRow({ccPreset1Url, ccPreset1SetUrl});
ccPreset1Grid.addRow({ccPreset1Model, ccPreset1SelectModel});
ccPreset1Grid.addRow({ccPreset1Template, ccPreset1SelectTemplate});
auto caGrid = Grid{}; auto caGrid = Grid{};
caGrid.addRow({caProvider, caSelectProvider}); caGrid.addRow({caProvider, caSelectProvider});
caGrid.addRow({caUrl, caSetUrl}); caGrid.addRow({caUrl, caSetUrl});
caGrid.addRow({caModel, caSelectModel}); caGrid.addRow({caModel, caSelectModel});
caGrid.addRow({caTemplate, caSelectTemplate}); caGrid.addRow({caTemplate, caSelectTemplate});
caGrid.addRow({caStatus, caTest});
auto ccGroup = Group{ auto ccGroup = Group{title(TrConstants::CODE_COMPLETION), ccGrid};
title(TrConstants::CODE_COMPLETION), auto caGroup = Group{title(TrConstants::CHAT_ASSISTANT), caGrid};
Column{
ccGrid,
ccTemplateDescription,
Row{specifyPreset1, preset1Language, Stretch{1}},
ccPreset1Grid}};
auto caGroup
= Group{title(TrConstants::CHAT_ASSISTANT), Column{caGrid, caTemplateDescription}};
auto rootLayout = Column{ auto rootLayout = Column{Row{enableQodeAssist, Stretch{1}, resetToDefaults},
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}}, Row{enableLogging, Stretch{1}},
Row{enableLogging, Stretch{1}}, Space{8},
Row{enableCheckUpdate, Stretch{1}}, ccGroup,
Row{enableChat, Stretch{1}}, Space{8},
Space{8}, caGroup,
ccGroup, Stretch{1}};
Space{8},
caGroup,
Stretch{1}};
return rootLayout; return rootLayout;
}); });
} }
void GeneralSettings::showSelectionDialog( void GeneralSettings::showSelectionDialog(const QStringList &data,
const QStringList &data, Utils::StringAspect &aspect, const QString &title, const QString &text) Utils::StringAspect &aspect,
const QString &title,
const QString &text)
{ {
if (data.isEmpty()) if (data.isEmpty())
return; return;
@@ -258,7 +190,6 @@ void GeneralSettings::showModelsNotFoundDialog(Utils::StringAspect &aspect)
auto selectProviderBtn = new QPushButton(TrConstants::SELECT_PROVIDER); auto selectProviderBtn = new QPushButton(TrConstants::SELECT_PROVIDER);
auto selectUrlBtn = new QPushButton(TrConstants::SELECT_URL); auto selectUrlBtn = new QPushButton(TrConstants::SELECT_URL);
auto enterManuallyBtn = new QPushButton(TrConstants::ENTER_MODEL_MANUALLY); auto enterManuallyBtn = new QPushButton(TrConstants::ENTER_MODEL_MANUALLY);
auto configureApiKeyBtn = new QPushButton(TrConstants::CONFIGURE_API_KEY);
connect(selectProviderBtn, &QPushButton::clicked, &dialog, [this, providerButton, &dialog]() { connect(selectProviderBtn, &QPushButton::clicked, &dialog, [this, providerButton, &dialog]() {
dialog.close(); dialog.close();
@@ -275,15 +206,9 @@ void GeneralSettings::showModelsNotFoundDialog(Utils::StringAspect &aspect)
showModelsNotSupportedDialog(aspect); showModelsNotSupportedDialog(aspect);
}); });
connect(configureApiKeyBtn, &QPushButton::clicked, &dialog, [&dialog]() {
dialog.close();
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
dialog.buttonLayout()->addWidget(selectProviderBtn); dialog.buttonLayout()->addWidget(selectProviderBtn);
dialog.buttonLayout()->addWidget(selectUrlBtn); dialog.buttonLayout()->addWidget(selectUrlBtn);
dialog.buttonLayout()->addWidget(enterManuallyBtn); dialog.buttonLayout()->addWidget(enterManuallyBtn);
dialog.buttonLayout()->addWidget(configureApiKeyBtn);
} }
auto closeBtn = new QPushButton(TrConstants::CLOSE); auto closeBtn = new QPushButton(TrConstants::CLOSE);
@@ -336,11 +261,9 @@ void GeneralSettings::showUrlSelectionDialog(
dialog.addSpacing(); dialog.addSpacing();
QStringList allUrls = predefinedUrls; QStringList allUrls = predefinedUrls;
QString key = QString("CompleterHistory/") QString key
.append( = QString("CompleterHistory/")
(&aspect == &ccUrl) ? Constants::CC_URL_HISTORY .append((&aspect == &ccUrl) ? Constants::CC_URL_HISTORY : Constants::CA_URL_HISTORY);
: (&aspect == &ccPreset1Url) ? Constants::CC_PRESET1_URL_HISTORY
: Constants::CA_URL_HISTORY);
QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList(); QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList();
allUrls.append(historyList); allUrls.append(historyList);
allUrls.removeDuplicates(); allUrls.removeDuplicates();
@@ -368,31 +291,12 @@ void GeneralSettings::showUrlSelectionDialog(
dialog.exec(); dialog.exec();
} }
void GeneralSettings::updatePreset1Visiblity(bool state)
{
ccPreset1Provider.setVisible(specifyPreset1.volatileValue());
ccPreset1SelectProvider.updateVisibility(specifyPreset1.volatileValue());
ccPreset1Url.setVisible(specifyPreset1.volatileValue());
ccPreset1SetUrl.updateVisibility(specifyPreset1.volatileValue());
ccPreset1Model.setVisible(specifyPreset1.volatileValue());
ccPreset1SelectModel.updateVisibility(specifyPreset1.volatileValue());
ccPreset1Template.setVisible(specifyPreset1.volatileValue());
ccPreset1SelectTemplate.updateVisibility(specifyPreset1.volatileValue());
}
void GeneralSettings::setupConnections() void GeneralSettings::setupConnections()
{ {
connect(&enableLogging, &Utils::BoolAspect::volatileValueChanged, this, [this]() { connect(&enableLogging, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
Logger::instance().setLoggingEnabled(enableLogging.volatileValue()); Logger::instance().setLoggingEnabled(enableLogging.volatileValue());
}); });
connect(&resetToDefaults, &ButtonAspect::clicked, this, &GeneralSettings::resetPageToDefaults); connect(&resetToDefaults, &ButtonAspect::clicked, this, &GeneralSettings::resetPageToDefaults);
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
});
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
updatePreset1Visiblity(specifyPreset1.volatileValue());
});
} }
void GeneralSettings::resetPageToDefaults() void GeneralSettings::resetPageToDefaults()
@@ -414,13 +318,6 @@ void GeneralSettings::resetPageToDefaults()
resetAspect(caModel); resetAspect(caModel);
resetAspect(caTemplate); resetAspect(caTemplate);
resetAspect(caUrl); resetAspect(caUrl);
resetAspect(enableCheckUpdate);
resetAspect(specifyPreset1);
resetAspect(preset1Language);
resetAspect(ccPreset1Provider);
resetAspect(ccPreset1Model);
resetAspect(ccPreset1Template);
resetAspect(ccPreset1Url);
writeSettings(); writeSettings();
} }
} }

View File

@@ -35,10 +35,6 @@ public:
Utils::BoolAspect enableQodeAssist{this}; Utils::BoolAspect enableQodeAssist{this};
Utils::BoolAspect enableLogging{this}; Utils::BoolAspect enableLogging{this};
Utils::BoolAspect enableCheckUpdate{this};
Utils::BoolAspect enableChat{this};
ButtonAspect checkUpdate{this};
ButtonAspect resetToDefaults{this}; ButtonAspect resetToDefaults{this};
// code completion setttings // code completion setttings
@@ -57,25 +53,6 @@ public:
Utils::StringAspect ccStatus{this}; Utils::StringAspect ccStatus{this};
ButtonAspect ccTest{this}; ButtonAspect ccTest{this};
Utils::StringAspect ccTemplateDescription{this};
// TODO create dynamic presets system
// preset1 for code completion settings
Utils::BoolAspect specifyPreset1{this};
Utils::SelectionAspect preset1Language{this};
Utils::StringAspect ccPreset1Provider{this};
ButtonAspect ccPreset1SelectProvider{this};
Utils::StringAspect ccPreset1Url{this};
ButtonAspect ccPreset1SetUrl{this};
Utils::StringAspect ccPreset1Model{this};
ButtonAspect ccPreset1SelectModel{this};
Utils::StringAspect ccPreset1Template{this};
ButtonAspect ccPreset1SelectTemplate{this};
// chat assistant settings // chat assistant settings
Utils::StringAspect caProvider{this}; Utils::StringAspect caProvider{this};
ButtonAspect caSelectProvider{this}; ButtonAspect caSelectProvider{this};
@@ -92,8 +69,6 @@ public:
Utils::StringAspect caStatus{this}; Utils::StringAspect caStatus{this};
ButtonAspect caTest{this}; ButtonAspect caTest{this};
Utils::StringAspect caTemplateDescription{this};
void showSelectionDialog(const QStringList &data, void showSelectionDialog(const QStringList &data,
Utils::StringAspect &aspect, Utils::StringAspect &aspect,
const QString &title = {}, const QString &title = {},
@@ -105,8 +80,6 @@ public:
void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls); void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls);
void updatePreset1Visiblity(bool state);
private: private:
void setupConnections(); void setupConnections();
void resetPageToDefaults(); void resetPageToDefaults();

View File

@@ -1,188 +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 "PluginUpdater.hpp"
#include <coreplugin/coreconstants.h>
#include <coreplugin/coreplugin.h>
#include <extensionsystem/pluginmanager.h>
#include <extensionsystem/pluginspec.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QStandardPaths>
namespace QodeAssist {
PluginUpdater::PluginUpdater(QObject *parent)
: QObject(parent)
, m_networkManager(new QNetworkAccessManager(this))
{}
void PluginUpdater::checkForUpdates()
{
if (m_isCheckingUpdate)
return;
m_isCheckingUpdate = true;
QNetworkRequest request((QUrl(getUpdateUrl())));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
handleUpdateResponse(reply);
m_isCheckingUpdate = false;
reply->deleteLater();
});
}
void PluginUpdater::handleUpdateResponse(QNetworkReply *reply)
{
UpdateInfo info;
if (reply->error() != QNetworkReply::NoError) {
emit downloadError(reply->errorString());
return;
}
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
QJsonObject obj = doc.object();
info.version = obj["tag_name"].toString();
if (info.version.startsWith('v'))
info.version.remove(0, 1);
QString qtcVersionStr = Core::ICore::versionString().split(' ').last();
QVersionNumber qtcVersion = QVersionNumber::fromString(qtcVersionStr);
info.currentIdeVersion = qtcVersionStr;
auto assets = obj["assets"].toArray();
for (const auto &asset : assets) {
QString name = asset.toObject()["name"].toString();
if (name.startsWith("QodeAssist-")) {
QString assetVersionStr = name.section('-', 1, 1);
QVersionNumber assetVersion = QVersionNumber::fromString(assetVersionStr);
info.targetIdeVersion = assetVersionStr;
if (assetVersion != qtcVersion) {
continue;
}
#if defined(Q_OS_WIN)
if (name.contains("Windows"))
#elif defined(Q_OS_MACOS)
if (name.contains("macOS"))
#else
if (name.contains("Linux") && !name.contains("experimental"))
#endif
{
info.downloadUrl = asset.toObject()["browser_download_url"].toString();
info.fileName = name;
break;
}
}
}
if (info.downloadUrl.isEmpty()) {
info.incompatibleIdeVersion = true;
emit updateCheckFinished(info);
return;
}
info.changeLog = obj["body"].toString();
info.isUpdateAvailable = QVersionNumber::fromString(info.version)
> QVersionNumber::fromString(currentVersion());
m_lastUpdateInfo = info;
emit updateCheckFinished(info);
}
void PluginUpdater::downloadUpdate(const QString &url)
{
QNetworkRequest request(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::downloadProgress, this, &PluginUpdater::handleDownloadProgress);
connect(reply, &QNetworkReply::finished, this, &PluginUpdater::handleDownloadFinished);
}
QString PluginUpdater::currentVersion() const
{
const auto pluginSpecs = ExtensionSystem::PluginManager::plugins();
for (const ExtensionSystem::PluginSpec *spec : pluginSpecs) {
if (spec->name() == QLatin1String("QodeAssist"))
return spec->version();
}
return QString();
}
bool PluginUpdater::isUpdateAvailable() const
{
return m_lastUpdateInfo.isUpdateAvailable;
}
QString PluginUpdater::getUpdateUrl() const
{
return "https://api.github.com/repos/Palm1r/qodeassist/releases/latest";
}
void PluginUpdater::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
emit downloadProgress(bytesReceived, bytesTotal);
}
void PluginUpdater::handleDownloadFinished()
{
auto reply = qobject_cast<QNetworkReply *>(sender());
QTC_ASSERT(reply, return);
if (reply->error() != QNetworkReply::NoError) {
emit downloadError(reply->errorString());
reply->deleteLater();
return;
}
QString downloadPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)
+ QDir::separator() + "QodeAssist_v" + m_lastUpdateInfo.version;
QDir().mkpath(downloadPath);
QString filePath = downloadPath + QDir::separator() + m_lastUpdateInfo.fileName;
if (QFile::exists(filePath)) {
emit downloadError(tr("Update file already exists: %1").arg(filePath));
reply->deleteLater();
return;
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
emit downloadError(tr("Could not save the update file"));
reply->deleteLater();
return;
}
file.write(reply->readAll());
file.close();
emit downloadFinished(filePath);
reply->deleteLater();
}
} // namespace QodeAssist

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/>.
*/
#pragma once
#include <coreplugin/icore.h>
#include <coreplugin/plugininstallwizard.h>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <QVersionNumber>
namespace QodeAssist {
class PluginUpdater : public QObject
{
Q_OBJECT
public:
struct UpdateInfo
{
QString version;
QString downloadUrl;
QString changeLog;
QString fileName;
bool isUpdateAvailable;
bool incompatibleIdeVersion{false};
QString targetIdeVersion;
QString currentIdeVersion;
};
explicit PluginUpdater(QObject *parent = nullptr);
~PluginUpdater() = default;
void checkForUpdates();
void downloadUpdate(const QString &url);
QString currentVersion() const;
bool isUpdateAvailable() const;
signals:
void updateCheckFinished(const UpdateInfo &info);
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void downloadFinished(const QString &filePath);
void downloadError(const QString &error);
private:
void handleUpdateResponse(QNetworkReply *reply);
void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void handleDownloadFinished();
QString getUpdateUrl() const;
QNetworkAccessManager *m_networkManager;
UpdateInfo m_lastUpdateInfo;
bool m_isCheckingUpdate{false};
};
} // namespace QodeAssist

View File

@@ -1,81 +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 "ProjectSettings.hpp"
#include "GeneralSettings.hpp"
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include <coreplugin/icore.h>
#include <projectexplorer/project.h>
namespace QodeAssist::Settings {
ProjectSettings::ProjectSettings(ProjectExplorer::Project *project)
{
setAutoApply(true);
useGlobalSettings.setSettingsKey(Constants::QODE_ASSIST_USE_GLOBAL_SETTINGS);
useGlobalSettings.setDefaultValue(true);
enableQodeAssist.setSettingsKey(Constants::QODE_ASSIST_ENABLE_IN_PROJECT);
enableQodeAssist.setDisplayName(Tr::tr("Enable Qode Assist"));
enableQodeAssist.setLabelText(Tr::tr("Enable Qode Assist"));
enableQodeAssist.setDefaultValue(false);
chatHistoryPath.setSettingsKey(Constants::QODE_ASSIST_CHAT_HISTORY_PATH);
chatHistoryPath.setExpectedKind(Utils::PathChooser::ExistingDirectory);
chatHistoryPath.setLabelText(Tr::tr("Chat History Path:"));
QString projectChatHistoryPath
= QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
chatHistoryPath.setDefaultValue(projectChatHistoryPath);
Utils::Store map = Utils::storeFromVariant(
project->namedSettings(Constants::QODE_ASSIST_PROJECT_SETTINGS_ID));
fromMap(map);
enableQodeAssist.addOnChanged(this, [this, project] { save(project); });
useGlobalSettings.addOnChanged(this, [this, project] { save(project); });
chatHistoryPath.addOnChanged(this, [this, project] { save(project); });
}
void ProjectSettings::setUseGlobalSettings(bool useGlobal)
{
useGlobalSettings.setValue(useGlobal);
}
bool ProjectSettings::isEnabled() const
{
if (useGlobalSettings())
return generalSettings().enableQodeAssist();
return enableQodeAssist();
}
void ProjectSettings::save(ProjectExplorer::Project *project)
{
Utils::Store map;
toMap(map);
project
->setNamedSettings(Constants::QODE_ASSIST_PROJECT_SETTINGS_ID, Utils::variantFromStore(map));
generalSettings().apply();
}
} // namespace QodeAssist::Settings

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