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

View File

@@ -16,7 +16,6 @@ add_subdirectory(llmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context)
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
@@ -46,7 +45,10 @@ add_qtc_plugin(QodeAssist
templates/StarCoder2Fim.hpp templates/StarCoder2Fim.hpp
templates/DeepSeekCoderFim.hpp templates/DeepSeekCoderFim.hpp
templates/CustomFimTemplate.hpp templates/CustomFimTemplate.hpp
templates/Qwen.hpp templates/Qwen.hpp
templates/Ollama.hpp templates/Ollama.hpp
templates/BasicChat.hpp templates/BasicChat.hpp
templates/Llama3.hpp templates/Llama3.hpp
@@ -54,18 +56,17 @@ add_qtc_plugin(QodeAssist
templates/Alpaca.hpp templates/Alpaca.hpp
templates/Llama2.hpp templates/Llama2.hpp
providers/Providers.hpp providers/Providers.hpp
templates/Claude.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
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
) )

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,14 +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.svg
icons/close-dark.svg
icons/close-light.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp ChatModel.hpp ChatModel.cpp
@@ -28,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
@@ -41,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

@@ -55,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();
} }
@@ -72,43 +65,28 @@ 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);
}
}
int tokenCount = estimateTokenCount(fullContent);
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; int oldTokenCount = lastMessage.tokenCount;
lastMessage.content = content; lastMessage.content = content;
lastMessage.attachments = attachments;
lastMessage.tokenCount = tokenCount; lastMessage.tokenCount = tokenCount;
m_totalTokens += (tokenCount - oldTokenCount); 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, tokenCount, id}; m_messages.append({role, content, tokenCount, id});
newMessage.attachments = attachments;
m_messages.append(newMessage);
m_totalTokens += tokenCount; m_totalTokens += tokenCount;
endInsertRows(); endInsertRows();
} }
trim();
emit totalTokensChanged(); emit totalTokensChanged();
} }
@@ -117,6 +95,20 @@ 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 int ChatModel::estimateTokenCount(const QString &text) const
{ {
return text.length() / 4; return text.length() / 4;
@@ -129,7 +121,6 @@ void ChatModel::clear()
m_totalTokens = 0; m_totalTokens = 0;
endResetModel(); endResetModel();
emit totalTokensChanged(); emit totalTokensChanged();
emit modelReseted();
} }
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
@@ -164,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) {
@@ -178,22 +170,7 @@ 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;

View File

@@ -26,8 +26,6 @@
#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
@@ -38,19 +36,17 @@ class ChatModel : public QAbstractListModel
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; int tokenCount;
QString id; QString id;
QList<Context::ContentFile> attachments;
}; };
explicit ChatModel(QObject *parent = nullptr); explicit ChatModel(QObject *parent = nullptr);
@@ -59,11 +55,7 @@ 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;
@@ -79,9 +71,9 @@ public:
signals: signals:
void totalTokensChanged(); void totalTokensChanged();
void tokensThresholdChanged(); void tokensThresholdChanged();
void modelReseted();
private: private:
void trim();
int estimateTokenCount(const QString &text) const; int estimateTokenCount(const QString &text) const;
QVector<Message> m_messages; QVector<Message> m_messages;

View File

@@ -18,23 +18,12 @@
*/ */
#include "ChatRootView.hpp" #include "ChatRootView.hpp"
#include <QtGui/qclipboard.h>
#include <QClipboard>
#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 "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -55,14 +44,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::isSharingCurrentFileChanged); &ChatRootView::isSharingCurrentFileChanged);
connect(
m_clientInterface,
&ClientInterface::messageReceivedCompletely,
this,
&ChatRootView::autosave);
connect(m_chatModel, &ChatModel::modelReseted, [this]() { m_recentFilePath = QString(); });
generateColors(); generateColors();
} }
@@ -76,26 +57,9 @@ QColor ChatRootView::backgroundColor() const
return Utils::creatorColor(Utils::Theme::BackgroundColorNormal); return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
} }
void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const
{ {
if (m_chatModel->totalTokens() > m_chatModel->tokensThreshold()) { m_clientInterface->sendMessage(message, sharingCurrentFile);
QMessageBox::StandardButton reply = QMessageBox::question(
Core::ICore::dialogParent(),
tr("Token Limit Exceeded"),
tr("The chat history has exceeded the token limit.\n"
"Would you like to create new chat?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
autosave();
m_chatModel->clear();
m_recentFilePath = QString{};
return;
}
}
m_clientInterface->sendMessage(message, m_attachmentFiles, sharingCurrentFile);
clearAttachmentFiles();
} }
void ChatRootView::copyToClipboard(const QString &text) void ChatRootView::copyToClipboard(const QString &text)
@@ -108,14 +72,6 @@ void ChatRootView::cancelRequest()
m_clientInterface->cancelRequest(); m_clientInterface->cancelRequest();
} }
void ChatRootView::clearAttachmentFiles()
{
if (!m_attachmentFiles.isEmpty()) {
m_attachmentFiles.clear();
emit attachmentFilesChanged();
}
}
void ChatRootView::generateColors() void ChatRootView::generateColors()
{ {
QColor baseColor = backgroundColor(); QColor baseColor = backgroundColor();
@@ -159,26 +115,6 @@ QColor ChatRootView::generateColor(const QColor &baseColor,
return QColor::fromHslF(h, s, l, a); return QColor::fromHslF(h, s, l, a);
} }
QString ChatRootView::getChatsHistoryDir() const
{
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(".")) {
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
return QString();
}
return path;
}
QString ChatRootView::currentTemplate() const QString ChatRootView::currentTemplate() const
{ {
auto &settings = Settings::generalSettings(); auto &settings = Settings::generalSettings();
@@ -205,136 +141,4 @@ bool ChatRootView::isSharingCurrentFile() const
return Settings::chatAssistantSettings().sharingCurrentFile(); return Settings::chatAssistantSettings().sharingCurrentFile();
} }
void ChatRootView::saveHistory(const QString &filePath)
{
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
if (!result.success) {
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
}
}
void ChatRootView::loadHistory(const QString &filePath)
{
auto result = ChatSerializer::loadFromFile(m_chatModel, filePath);
if (!result.success) {
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
} else {
m_recentFilePath = filePath;
}
}
void ChatRootView::showSaveDialog()
{
QString initialDir = getChatsHistoryDir();
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()
{
QString initialDir = getChatsHistoryDir();
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;
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);
shortMessage.replace(QRegularExpression("[^a-zA-Z0-9_-]"), "_");
parts << shortMessage;
}
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
return parts.join("_");
}
void ChatRootView::autosave()
{
if (m_chatModel->rowCount() == 0 || !Settings::chatAssistantSettings().autosave()) {
return;
}
QString filePath = getAutosaveFilePath();
if (!filePath.isEmpty()) {
ChatSerializer::saveToFile(m_chatModel, filePath);
m_recentFilePath = 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");
}
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 filePaths = dialog.selectedFiles();
if (!filePaths.isEmpty()) {
m_attachmentFiles = filePaths;
emit attachmentFilesChanged();
}
}
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -29,6 +29,10 @@ 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(QColor backgroundColor READ backgroundColor CONSTANT FINAL) Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
@@ -37,8 +41,6 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL) Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
isSharingCurrentFileChanged FINAL) isSharingCurrentFileChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles MEMBER m_attachmentFiles NOTIFY attachmentFilesChanged)
QML_ELEMENT QML_ELEMENT
public: public:
@@ -55,28 +57,16 @@ public:
bool isSharingCurrentFile() const; bool isSharingCurrentFile() const;
void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath);
Q_INVOKABLE void showSaveDialog();
Q_INVOKABLE void showLoadDialog();
void autosave();
QString getAutosaveFilePath() const;
Q_INVOKABLE void showAttachFilesDialog();
public slots: public slots:
void sendMessage(const QString &message, bool sharingCurrentFile = false); 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();
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged(); void currentTemplateChanged();
void isSharingCurrentFileChanged(); void isSharingCurrentFileChanged();
void attachmentFilesChanged();
private: private:
void generateColors(); void generateColors();
@@ -85,17 +75,12 @@ private:
float saturationMod, float saturationMod,
float lightnessMod); float lightnessMod);
QString getChatsHistoryDir() const;
QString getSuggestedFileName() const;
ChatModel *m_chatModel; ChatModel *m_chatModel;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
QString m_currentTemplate; QString m_currentTemplate;
QColor m_primaryColor; QColor m_primaryColor;
QColor m_secondaryColor; QColor m_secondaryColor;
QColor m_codeColor; QColor m_codeColor;
QString m_recentFilePath;
QStringList m_attachmentFiles;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,145 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "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["tokenCount"] = message.tokenCount;
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.tokenCount = json["tokenCount"].toInt();
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;
root["totalTokens"] = model->totalTokens();
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, bool includeCurrentFile)
{ {
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();
@@ -109,7 +106,7 @@ void ClientInterface::sendMessage(
QJsonObject providerRequest; QJsonObject providerRequest;
providerRequest["model"] = Settings::generalSettings().caModel(); providerRequest["model"] = Settings::generalSettings().caModel();
providerRequest["stream"] = chatAssistantSettings.stream(); providerRequest["stream"] = true;
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt); providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
if (promptTemplate) if (promptTemplate)
@@ -129,18 +126,11 @@ void ClientInterface::sendMessage(
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint()); config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest = providerRequest; config.providerRequest = providerRequest;
config.multiLineCompletion = false; config.multiLineCompletion = false;
config.apiKey = provider->apiKey(); config.apiKey = Settings::chatAssistantSettings().apiKey();
QJsonObject request; QJsonObject request;
request["id"] = QUuid::createUuid().toString(); request["id"] = QUuid::createUuid().toString();
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for chat request:");
LOG_MESSAGES(errors);
return;
}
m_requestHandler->sendLLMRequest(config, request); m_requestHandler->sendLLMRequest(config, request);
} }
@@ -169,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();
} }
} }
} }

View File

@@ -36,16 +36,12 @@ 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 = {},
bool includeCurrentFile = false);
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);

View File

@@ -1,10 +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"/>
</g>
<defs>
<clipPath id="clip0_37_14">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 555 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

@@ -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,28 +17,28 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/ */
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import ChatView import ChatView
import QtQuick.Layouts
import "./dialog" import "./dialog"
Rectangle { Rectangle {
id: root id: root
property alias msgModel: msgCreator.model property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model
property color fontColor property color fontColor
property color codeBgColor property color codeBgColor
property color selectionColor property color selectionColor
height: msgColumn.implicitHeight + 10 height: msgColumn.height
radius: 8 radius: 8
ColumnLayout { Column {
id: msgColumn id: msgColumn
width: parent.width
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width
spacing: 5 spacing: 5
Repeater { Repeater {
@@ -80,38 +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: root.codeBgColor
Text {
id: attachText
anchors.centerIn: parent
text: modelData
color: root.fontColor
}
}
}
}
} }
component TextComponent : TextBlock { component TextComponent : TextBlock {

View File

@@ -17,61 +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 {
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.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
}
} }
spacing: 10
ListView { ListView {
id: chatListView id: chatListView
@@ -87,10 +54,8 @@ ChatRootView {
delegate: ChatItem { delegate: ChatItem {
required property var model required property var model
width: ListView.view.width - scroll.width width: ListView.view.width - scroll.width
msgModel: root.chatModel.processMessageContent(model.content) msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white" fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
codeBgColor: root.codeColor codeBgColor: root.codeColor
@@ -148,23 +113,60 @@ ChatRootView {
} }
} }
AttachedFilesPlace { RowLayout {
id: attachedFilesPlace
Layout.fillWidth: true Layout.fillWidth: true
attachedFilesModel: root.attachmentFiles spacing: 5
Button {
id: sendButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Send")
onClicked: root.sendChatMessage()
}
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
}
} }
}
BottomBar { Row {
id: bottomBar id: bar
Layout.preferredWidth: parent.width layoutDirection: Qt.RightToLeft
Layout.preferredHeight: 40
sendButton.onClicked: root.sendChatMessage() anchors {
stopButton.onClicked: root.cancelRequest() left: parent.left
sharingCurrentFile.checked: root.isSharingCurrentFile leftMargin: 5
attachFiles.onClicked: root.showAttachFilesDialog() 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"
} }
} }
@@ -177,7 +179,7 @@ ChatRootView {
} }
function sendChatMessage() { function sendChatMessage() {
root.sendMessage(messageInput.text, bottomBar.sharingCurrentFile.checked) 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

@@ -1,90 +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: attachFilesPlace
property alias attachedFilesModel: attachRepeater.model
spacing: 5
leftPadding: 5
rightPadding: 5
Repeater {
id: attachRepeater
delegate: Rectangle {
required property int index
required property string modelData
height: 30
width: fileNameText.width + closeButton.width + 20
radius: 4
color: palette.button
Row {
spacing: 5
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
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
height: closeButton.width
onClicked: {
const newList = [...root.attachmentFiles];
newList.splice(index, 1);
root.attachmentFiles = newList;
}
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,83 +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 sharingCurrentFile: sharingCurrentFileId
property alias attachFiles: attachFilesId
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")
}
CheckBox {
id: sharingCurrentFileId
text: qsTr("Share current file with models")
}
QoAButton {
id: attachFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/attach-file.svg"
height: 15
width: 8
}
text: qsTr("Attach files")
}
Item {
Layout.fillWidth: true
}
}
}

View File

@@ -1,73 +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
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")
}
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

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

View File

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

View File

@@ -26,9 +26,7 @@
#include <llmcore/RequestConfig.hpp> #include <llmcore/RequestConfig.hpp>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include "CodeHandler.hpp" #include "DocumentContextReader.hpp"
#include "context/DocumentContextReader.hpp"
#include "llmcore/MessageBuilder.hpp"
#include "llmcore/PromptTemplateManager.hpp" #include "llmcore/PromptTemplateManager.hpp"
#include "llmcore/ProvidersManager.hpp" #include "llmcore/ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
@@ -169,56 +167,32 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
} }
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::CodeCompletion; config.requestType = LLMCore::RequestType::Fim;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
config.url = QUrl(QString("%1%2").arg( config.url = QUrl(
Settings::generalSettings().ccUrl(), QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint() config.apiKey = Settings::codeCompletionSettings().apiKey();
: provider->chatEndpoint()));
config.apiKey = provider->apiKey();
config.providerRequest config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
= {{"model", Settings::generalSettings().ccModel()},
{"stream", Settings::codeCompletionSettings().stream()}};
config.multiLineCompletion = completeSettings.multiLineCompletion(); config.multiLineCompletion = completeSettings.multiLineCompletion();
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords;
QString systemPrompt; QString systemPrompt;
if (completeSettings.useSystemPrompt()) if (completeSettings.useSystemPrompt())
systemPrompt.append(completeSettings.systemPrompt()); systemPrompt.append(completeSettings.systemPrompt());
if (!updatedContext.fileContext.isEmpty()) if (!updatedContext.fileContext.isEmpty())
systemPrompt.append(updatedContext.fileContext); systemPrompt.append(updatedContext.fileContext);
if (!systemPrompt.isEmpty())
config.providerRequest["system"] = systemPrompt;
QString userMessage; const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) { if (!stopWords.isEmpty())
userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix); config.providerRequest["stop"] = stopWords;
} else {
userMessage = updatedContext.prefix;
}
auto message = LLMCore::MessageBuilder() config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
.addSystemMessage(systemPrompt) config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
.addUserMessage(userMessage)
.addSuffix(updatedContext.suffix)
.addTokenizer(promptTemplate);
message.saveTo(
config.providerRequest,
providerName == "Ollama" ? LLMCore::ProvidersApi::Ollama : LLMCore::ProvidersApi::OpenAI);
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::CodeCompletion);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for fim request:");
LOG_MESSAGES(errors);
return;
}
m_requestHandler.sendLLMRequest(config, request); m_requestHandler.sendLLMRequest(config, request);
} }
@@ -242,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);
} }
@@ -250,31 +224,19 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QJsonObject &request, const QJsonObject &request,
bool isComplete) bool isComplete)
{ {
auto templateName = Settings::generalSettings().ccTemplate();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"]; response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject result; QJsonObject result;
QJsonArray completions; QJsonArray completions;
QJsonObject completionItem; QJsonObject completionItem;
completionItem[LanguageServerProtocol::textKey] = completion;
QString processedCompletion
= promptTemplate->type() == LLMCore::TemplateType::Chat
&& Settings::codeCompletionSettings().smartProcessInstuctText()
? CodeHandler::processText(completion)
: completion;
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range; QJsonObject range;
range["start"] = position; range["start"] = position;
QJsonObject end = position; QJsonObject end = position;
end["character"] = position["character"].toInt() + processedCompletion.length(); end["character"] = position["character"].toInt() + completion.length();
range["end"] = end; range["end"] = end;
completionItem[LanguageServerProtocol::rangeKey] = range; completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position; completionItem[LanguageServerProtocol::positionKey] = position;

View File

@@ -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.4.4", "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()

View File

@@ -3,25 +3,18 @@
[![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.0-brightgreen) ![Static Badge](https://img.shields.io/badge/QtCreator-14.0.2-brightgreen)
![Static Badge](https://img.shields.io/badge/donations:0-brightgreen)
![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
## Table of Contents ## Table of Contents
1. [Overview](#overview) 1. [Overview](#overview)
2. [Installation for using Ollama](#installation-for-using-Ollama) 2. [Installation](#installation)
3. [Installation for using Claude](#installation-for-using-Claude)
3. [Configure Plugin](#configure-plugin) 3. [Configure Plugin](#configure-plugin)
4. [Supported LLM Providers](#supported-llm-providers) 4. [Supported LLM Providers](#supported-llm-providers)
5. [Recommended Models](#recommended-models) 5. [Recommended Models](#recommended-models)
- [Ollama](#ollama) - [Ollama](#ollama)
- [LM Studio](#lm-studio)
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility) 6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
7. [Development Progress](#development-progress) 7. [Development Progress](#development-progress)
8. [Hotkeys](#hotkeys) 8. [Hotkeys](#hotkeys)
@@ -36,7 +29,6 @@
- Side and Bottom panels - Side and Bottom panels
- Support for multiple LLM providers: - Support for multiple LLM providers:
- Ollama - Ollama
- 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
@@ -48,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">
@@ -63,7 +50,7 @@
<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>
## Installation for using Ollama ## Installation
1. Install Latest QtCreator 1. Install Latest QtCreator
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation. 2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
@@ -88,21 +75,6 @@ ollama run qwen2.5-coder:32b
- Click on "Install Plugin..." - Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file - Select the downloaded QodeAssist plugin archive file
## Installation for using Claude
1. Install Latest QtCreator
2. Download the QodeAssist plugin for your QtCreator.
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
4. Select Claude provider
5. Select Claude api
6. Fill in api key for Claude
5. Select Claude templates for code completion and chat
6. Enjoy!
## Configure Plugin ## 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. 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.
@@ -121,9 +93,8 @@ You're all set! QodeAssist is now ready to use in Qt Creator.
## Supported LLM Providers ## Supported LLM Providers
QodeAssist currently supports the following LLM (Large Language Model) providers: QodeAssist currently supports the following LLM (Large Language Model) providers:
- [Ollama](https://ollama.com) - [Ollama](https://ollama.com)
- [LM Studio](https://lmstudio.ai) - [LM Studio](https://lmstudio.ai) (experimental)
- [OpenRouter](https://openrouter.ai) - OpenAI compatible providers (experimental)
- OpenAI compatible providers
## Recommended Models: ## Recommended Models:
QodeAssist has been thoroughly tested and optimized for use with the following language models: QodeAssist has been thoroughly tested and optimized for use with the following language models:
@@ -133,14 +104,6 @@ QodeAssist has been thoroughly tested and optimized for use with the following l
- StarCoder2 - StarCoder2
- DeepSeek-Coder-V2 - DeepSeek-Coder-V2
### Model Types
FIM models (codellama:7b-code, starcoder2:7b, etc.) - Optimized for code completion and suggestions
Instruct models (codellama:7b-instruct, starcoder2:instruct, etc.) - Better for chat assistance, explanations, and code review
For best results, use FIM models with code completion and Instruct models with chat features.
### Ollama: ### Ollama:
### For autocomplete(FIM) ### For autocomplete(FIM)
``` ```
@@ -149,7 +112,7 @@ ollama run starcoder2:7b
ollama run qwen2.5-coder:7b-base ollama run qwen2.5-coder:7b-base
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
``` ```
### For chat and instruct ### For chat
``` ```
ollama run codellama:7b-instruct ollama run codellama:7b-instruct
ollama run starcoder2:instruct ollama run starcoder2:instruct
@@ -181,7 +144,6 @@ ollama run deepseek-coder-v2
## QtCreator Version Compatibility ## QtCreator Version Compatibility
- QtCreator 15.0.0 - 0.4.x
- 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
@@ -200,7 +162,9 @@ ollama run deepseek-coder-v2
- 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
@@ -263,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,20 +0,0 @@
add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp
ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp
ContentFile.hpp
)
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,67 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ContextManager.hpp"
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
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;
}
} // namespace QodeAssist::Context

View File

@@ -1,46 +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"
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;
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

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

View File

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

View File

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

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "MessageBuilder.hpp"
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addSystemMessage(
const QString &content)
{
m_systemMessage = content;
return *this;
}
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addUserMessage(
const QString &content)
{
m_messages.append({MessageRole::User, content});
return *this;
}
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addSuffix(
const QString &content)
{
m_suffix = content;
return *this;
}
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addTokenizer(
PromptTemplate *promptTemplate)
{
m_promptTemplate = promptTemplate;
return *this;
}
QString QodeAssist::LLMCore::MessageBuilder::roleToString(MessageRole role) const
{
switch (role) {
case MessageRole::System:
return ROLE_SYSTEM;
case MessageRole::User:
return ROLE_USER;
case MessageRole::Assistant:
return ROLE_ASSISTANT;
default:
return ROLE_USER;
}
}
void QodeAssist::LLMCore::MessageBuilder::saveTo(QJsonObject &request, ProvidersApi api)
{
if (!m_promptTemplate) {
return;
}
ContextData context{
m_messages.isEmpty() ? QString() : m_messages.last().content, m_suffix, m_systemMessage};
if (api == ProvidersApi::Ollama) {
if (m_promptTemplate->type() == TemplateType::Fim) {
request["system"] = m_systemMessage;
m_promptTemplate->prepareRequest(request, context);
} else {
QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", m_systemMessage}});
messages.append(QJsonObject{{"role", "user"}, {"content", m_messages.last().content}});
request["messages"] = messages;
m_promptTemplate->prepareRequest(request, context);
}
} else if (api == ProvidersApi::OpenAI) {
QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", m_systemMessage}});
messages.append(QJsonObject{{"role", "user"}, {"content", m_messages.last().content}});
request["messages"] = messages;
m_promptTemplate->prepareRequest(request, context);
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
#include "PromptTemplate.hpp"
namespace QodeAssist::LLMCore {
enum class MessageRole { System, User, Assistant };
enum class OllamaFormat { Messages, Completions };
enum class ProvidersApi { Ollama, OpenAI, Claude };
static const QString ROLE_SYSTEM = "system";
static const QString ROLE_USER = "user";
static const QString ROLE_ASSISTANT = "assistant";
struct Message
{
MessageRole role;
QString content;
};
class MessageBuilder
{
public:
MessageBuilder &addSystemMessage(const QString &content);
MessageBuilder &addUserMessage(const QString &content);
MessageBuilder &addSuffix(const QString &content);
MessageBuilder &addTokenizer(PromptTemplate *promptTemplate);
QString roleToString(MessageRole role) const;
void saveTo(QJsonObject &request, ProvidersApi api);
private:
QString m_systemMessage;
QString m_suffix;
QVector<Message> m_messages;
PromptTemplate *m_promptTemplate;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,102 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OllamaMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
QJsonObject OllamaMessage::parseJsonFromData(const QByteArray &data)
{
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(line, &error);
if (!doc.isNull() && error.error == QJsonParseError::NoError) {
return doc.object();
}
}
return QJsonObject();
}
OllamaMessage OllamaMessage::fromJson(const QByteArray &data, Type type)
{
OllamaMessage msg;
QJsonObject obj = parseJsonFromData(data);
if (obj.isEmpty()) {
msg.error = "Invalid JSON response";
return msg;
}
msg.model = obj["model"].toString();
msg.createdAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate);
msg.done = obj["done"].toBool();
msg.doneReason = obj["done_reason"].toString();
msg.error = obj["error"].toString();
if (type == Type::Generate) {
auto &genResponse = msg.response.emplace<GenerateResponse>();
genResponse.response = obj["response"].toString();
if (msg.done && obj.contains("context")) {
const auto array = obj["context"].toArray();
genResponse.context.reserve(array.size());
for (const auto &val : array) {
genResponse.context.append(val.toInt());
}
}
} else {
auto &chatResponse = msg.response.emplace<ChatResponse>();
const auto msgObj = obj["message"].toObject();
chatResponse.role = msgObj["role"].toString();
chatResponse.content = msgObj["content"].toString();
}
if (msg.done) {
msg.metrics
= {obj["total_duration"].toVariant().toLongLong(),
obj["load_duration"].toVariant().toLongLong(),
obj["prompt_eval_count"].toVariant().toLongLong(),
obj["prompt_eval_duration"].toVariant().toLongLong(),
obj["eval_count"].toVariant().toLongLong(),
obj["eval_duration"].toVariant().toLongLong()};
}
return msg;
}
QString OllamaMessage::getContent() const
{
if (std::holds_alternative<GenerateResponse>(response)) {
return std::get<GenerateResponse>(response).response;
}
return std::get<ChatResponse>(response).content;
}
bool OllamaMessage::hasError() const
{
return !error.isEmpty();
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,71 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDateTime>
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::LLMCore {
class OllamaMessage
{
public:
enum class Type { Generate, Chat };
struct Metrics
{
qint64 totalDuration{0};
qint64 loadDuration{0};
qint64 promptEvalCount{0};
qint64 promptEvalDuration{0};
qint64 evalCount{0};
qint64 evalDuration{0};
};
struct GenerateResponse
{
QString response;
QVector<int> context;
};
struct ChatResponse
{
QString role;
QString content;
};
QString model;
QDateTime createdAt;
std::variant<GenerateResponse, ChatResponse> response;
bool done{false};
QString doneReason;
QString error;
Metrics metrics;
static OllamaMessage fromJson(const QByteArray &data, Type type);
QString getContent() const;
bool hasError() const;
private:
static QJsonObject parseJsonFromData(const QByteArray &data);
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,82 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OpenAIMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
OpenAIMessage OpenAIMessage::fromJson(const QJsonObject &obj)
{
OpenAIMessage msg;
if (obj.contains("error")) {
msg.error = obj["error"].toObject()["message"].toString();
return msg;
}
if (obj.contains("choices")) {
auto choices = obj["choices"].toArray();
if (!choices.isEmpty()) {
auto choiceObj = choices[0].toObject();
if (choiceObj.contains("delta")) {
QJsonObject delta = choiceObj["delta"].toObject();
msg.choice.content = delta["content"].toString();
} else if (choiceObj.contains("message")) {
QJsonObject message = choiceObj["message"].toObject();
msg.choice.content = message["content"].toString();
}
msg.choice.finishReason = choiceObj["finish_reason"].toString();
if (!msg.choice.finishReason.isEmpty()) {
msg.done = true;
}
}
}
if (obj.contains("usage")) {
QJsonObject usage = obj["usage"].toObject();
msg.usage.promptTokens = usage["prompt_tokens"].toInt();
msg.usage.completionTokens = usage["completion_tokens"].toInt();
msg.usage.totalTokens = usage["total_tokens"].toInt();
}
return msg;
}
QString OpenAIMessage::getContent() const
{
return choice.content;
}
bool OpenAIMessage::hasError() const
{
return !error.isEmpty();
}
bool OpenAIMessage::isDone() const
{
return done
|| (!choice.finishReason.isEmpty()
&& (choice.finishReason == "stop" || choice.finishReason == "length"));
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,56 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QByteArray>
#include <QJsonObject>
#include <QString>
namespace QodeAssist::LLMCore {
class OpenAIMessage
{
public:
struct Choice
{
QString content;
QString finishReason;
};
struct Usage
{
int promptTokens{0};
int completionTokens{0};
int totalTokens{0};
};
Choice choice;
QString error;
bool done{false};
Usage usage;
QString getContent() const;
bool hasError() const;
bool isDone() const;
static OpenAIMessage fromJson(const QJsonObject &obj);
};
} // namespace QodeAssist::LLMCore

View File

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

View File

@@ -39,8 +39,9 @@ public:
"T must inherit from PromptTemplate"); "T must inherit from PromptTemplate");
T *template_ptr = new T(); T *template_ptr = new T();
QString name = template_ptr->name(); QString name = template_ptr->name();
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;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,57 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ValidationUtils.hpp"
#include <QJsonArray>
namespace QodeAssist::LLMCore {
QStringList ValidationUtils::validateRequestFields(
const QJsonObject &request, const QJsonObject &templateObj)
{
QStringList errors;
validateFields(request, templateObj, errors);
validateNestedObjects(request, templateObj, errors);
return errors;
}
void ValidationUtils::validateFields(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (!templateObj.contains(it.key())) {
errors << QString("unknown field '%1'").arg(it.key());
}
}
}
void ValidationUtils::validateNestedObjects(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (templateObj.contains(it.key()) && it.value().isObject()
&& templateObj[it.key()].isObject()) {
validateFields(it.value().toObject(), templateObj[it.key()].toObject(), errors);
validateNestedObjects(it.value().toObject(), templateObj[it.key()].toObject(), errors);
}
}
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QStringList>
namespace QodeAssist::LLMCore {
class ValidationUtils
{
public:
static QStringList validateRequestFields(
const QJsonObject &request, const QJsonObject &templateObj);
private:
static void validateFields(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
static void validateNestedObjects(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,238 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ClaudeProvider.hpp"
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QUrlQuery>
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {
ClaudeProvider::ClaudeProvider() {}
QString ClaudeProvider::name() const
{
return "Claude";
}
QString ClaudeProvider::url() const
{
return "https://api.anthropic.com";
}
QString ClaudeProvider::completionEndpoint() const
{
return "/v1/messages";
}
QString ClaudeProvider::chatEndpoint() const
{
return "/v1/messages";
}
bool ClaudeProvider::supportsModelListing() const
{
return true;
}
void ClaudeProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
{
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
QJsonArray messages;
if (req.contains("messages")) {
QJsonArray origMessages = req["messages"].toArray();
for (const auto &msg : origMessages) {
QJsonObject message = msg.toObject();
if (message["role"].toString() == "system") {
req["system"] = message["content"];
} else {
messages.append(message);
}
}
} else {
if (req.contains("system")) {
req["system"] = req["system"].toString();
}
if (req.contains("prompt")) {
messages.append(
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
}
}
return messages;
};
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
request["stream"] = true;
};
QJsonArray messages = prepareMessages(request);
if (!messages.isEmpty()) {
request["messages"] = std::move(messages);
}
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool ClaudeProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
bool isComplete = false;
QString tempResponse;
while (reply->canReadLine()) {
QByteArray line = reply->readLine().trimmed();
if (line.isEmpty()) {
continue;
}
if (!line.startsWith("data:")) {
continue;
}
line = line.mid(6);
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
if (jsonResponse.isNull()) {
continue;
}
QJsonObject responseObj = jsonResponse.object();
QString eventType = responseObj["type"].toString();
if (eventType == "message_delta") {
if (responseObj.contains("delta")) {
QJsonObject delta = responseObj["delta"].toObject();
if (delta.contains("stop_reason")) {
isComplete = true;
}
}
} else if (eventType == "content_block_delta") {
QJsonObject delta = responseObj["delta"].toObject();
if (delta["type"].toString() == "text_delta") {
tempResponse += delta["text"].toString();
}
}
}
if (!tempResponse.isEmpty()) {
accumulatedResponse += tempResponse;
}
return isComplete;
}
QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
{
QList<QString> models;
QNetworkAccessManager manager;
QUrl url(baseUrl + "/v1/models");
QUrlQuery query;
query.addQueryItem("limit", "1000");
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("anthropic-version", "2023-06-01");
if (!apiKey().isEmpty()) {
request.setRawHeader("x-api-key", apiKey().toUtf8());
}
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) {
QJsonArray modelArray = jsonObject["data"].toArray();
for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject();
if (modelObject.contains("id")) {
QString modelId = modelObject["id"].toString();
models.append(modelId);
}
}
}
} else {
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
{
const auto templateReq = QJsonObject{
{"model", {}},
{"system", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"temperature", {}},
{"max_tokens", {}},
{"anthropic-version", {}},
{"top_p", {}},
{"top_k", {}},
{"stop", QJsonArray{}},
{"stream", {}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString ClaudeProvider::apiKey() const
{
return Settings::providerSettings().claudeApiKey();
}
void ClaudeProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
networkRequest.setRawHeader("x-api-key", apiKey().toUtf8());
networkRequest.setRawHeader("anthropic-version", "2023-06-01");
}
}
} // namespace QodeAssist::Providers

View File

@@ -1,44 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "llmcore/Provider.hpp"
namespace QodeAssist::Providers {
class ClaudeProvider : public LLMCore::Provider
{
public:
ClaudeProvider();
QString name() const override;
QString url() const override;
QString completionEndpoint() const override;
QString chatEndpoint() const override;
bool supportsModelListing() const override;
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
};
} // namespace QodeAssist::Providers

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,133 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OpenRouterAIProvider.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include "llmcore/OpenAIMessage.hpp"
#include "logger/Logger.hpp"
namespace QodeAssist::Providers {
OpenRouterProvider::OpenRouterProvider() {}
QString OpenRouterProvider::name() const
{
return "OpenRouter";
}
QString OpenRouterProvider::url() const
{
return "https://openrouter.ai/api";
}
void OpenRouterProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
{
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
QJsonArray messages;
if (req.contains("system")) {
messages.append(
QJsonObject{{"role", "system"}, {"content", req.take("system").toString()}});
}
if (req.contains("prompt")) {
messages.append(
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
}
return messages;
};
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
QJsonArray messages = prepareMessages(request);
if (!messages.isEmpty()) {
request["messages"] = std::move(messages);
}
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
QByteArrayList chunks = data.split('\n');
for (const QByteArray &chunk : chunks) {
if (chunk.trimmed().isEmpty() || chunk.contains("OPENROUTER PROCESSING")
|| chunk == "data: [DONE]") {
continue;
}
QByteArray jsonData = chunk;
if (chunk.startsWith("data: ")) {
jsonData = chunk.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 OpenRouter response: " + message.error);
continue;
}
accumulatedResponse += message.getContent();
return message.isDone();
}
return false;
}
QString OpenRouterProvider::apiKey() const
{
return Settings::providerSettings().openRouterApiKey();
}
} // namespace QodeAssist::Providers

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "llmcore/Provider.hpp"
#include "providers/OpenAICompatProvider.hpp"
namespace QodeAssist::Providers {
class OpenRouterProvider : public OpenAICompatProvider
{
public:
OpenRouterProvider();
QString name() const override;
QString url() const override;
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
QString apiKey() const override;
};
} // namespace QodeAssist::Providers

View File

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

View File

@@ -43,7 +43,6 @@
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
#include "chat/ChatOutputPane.h" #include "chat/ChatOutputPane.h"
#include "chat/NavigationPanel.hpp" #include "chat/NavigationPanel.hpp"
#include "settings/ProjectSettingsPanel.hpp"
#include "providers/Providers.hpp" #include "providers/Providers.hpp"
#include "templates/Templates.hpp" #include "templates/Templates.hpp"
@@ -103,8 +102,6 @@ public:
m_chatOutputPane = new Chat::ChatOutputPane(this); m_chatOutputPane = new Chat::ChatOutputPane(this);
m_navigationPanel = new Chat::NavigationPanel(); m_navigationPanel = new Chat::NavigationPanel();
Settings::setupProjectPanel();
ConfigurationManager::instance().init(); ConfigurationManager::instance().init();
} }

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,7 +32,7 @@ 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);
connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked); connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked);

View File

@@ -8,9 +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
) )
target_link_libraries(QodeAssistSettings target_link_libraries(QodeAssistSettings

View File

@@ -47,21 +47,13 @@ ChatAssistantSettings::ChatAssistantSettings()
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);
sharingCurrentFile.setSettingsKey(Constants::CA_SHARING_CURRENT_FILE); sharingCurrentFile.setSettingsKey(Constants::CA_SHARING_CURRENT_FILE);
sharingCurrentFile.setLabelText(Tr::tr("Share Current File With Assistant by Default")); sharingCurrentFile.setLabelText(Tr::tr("Share Current File With Assistant by Default"));
sharingCurrentFile.setDefaultValue(true); 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);
temperature.setLabelText(Tr::tr("Temperature:")); temperature.setLabelText(Tr::tr("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}}, sharingCurrentFile, 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);

View File

@@ -35,8 +35,6 @@ public:
// Chat settings // Chat settings
Utils::IntegerAspect chatTokensThreshold{this}; Utils::IntegerAspect chatTokensThreshold{this};
Utils::BoolAspect sharingCurrentFile{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,7 +81,7 @@ 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(50); maxTokens.setDefaultValue(50);
// Advanced Parameters // Advanced Parameters
@@ -151,27 +143,8 @@ CodeCompletionSettings::CodeCompletionSettings()
systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT); systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT);
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay); systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
systemPrompt.setDefaultValue( systemPrompt.setDefaultValue("You are an expert C++, Qt, and QML code completion AI. ANSWER "
"You are an expert in C++, Qt, and QML programming. Your task is to provide code " "should be SHORT and in CODE");
"suggestions that seamlessly integrate with existing code. You will receive a code context "
"with specified insertion points. Your goal is to complete only one logic expression "
"within these points."
"Here is the code context with insertion points:<code_context>Before: {{variable}}After: "
"{{variable}}</code_context> Instructions: 1. Carefully analyze the provided code context. "
"2. Consider the existing code and the specified insertion points.3. Generate a code "
"suggestion that completes one logic expression between the 'Before' and 'After' points. "
"4. Ensure your suggestion does not repeat any existing code. 5. Format your suggestion as "
"a code block using triple backticks. 6. Do not include any comments or descriptions with "
"your code suggestion. Remember to include only the new code to be inserted.");
useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE);
useUserMessageTemplateForCC.setDefaultValue(true);
useUserMessageTemplateForCC.setLabelText(Tr::tr("Use User Template for code completion message for non-FIM models"));
userMessageTemplateForCC.setSettingsKey(Constants::CC_USER_TEMPLATE);
userMessageTemplateForCC.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
userMessageTemplateForCC.setDefaultValue("Here is the code context with insertion points: <code_context>"
"\nBefore: %1After: %2\n </code_context>");
useFilePathInContext.setSettingsKey(Constants::CC_USE_FILE_PATH_IN_CONTEXT); useFilePathInContext.setSettingsKey(Constants::CC_USE_FILE_PATH_IN_CONTEXT);
useFilePathInContext.setDefaultValue(true); useFilePathInContext.setDefaultValue(true);
@@ -201,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"));
@@ -237,41 +210,34 @@ CodeCompletionSettings::CodeCompletionSettings()
auto contextItem = Column{Row{contextGrid, Stretch{1}}, auto contextItem = Column{Row{contextGrid, Stretch{1}},
Row{useSystemPrompt, Stretch{1}}, Row{useSystemPrompt, Stretch{1}},
systemPrompt, systemPrompt,
Row{useUserMessageTemplateForCC, Stretch{1}},
userMessageTemplateForCC,
Row{useFilePathInContext, Stretch{1}}, Row{useFilePathInContext, Stretch{1}},
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}};
}); });
} }
@@ -309,7 +275,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
resetAspect(autoCompletion); resetAspect(autoCompletion);
resetAspect(multiLineCompletion); resetAspect(multiLineCompletion);
resetAspect(stream);
resetAspect(temperature); resetAspect(temperature);
resetAspect(maxTokens); resetAspect(maxTokens);
resetAspect(useTopP); resetAspect(useTopP);

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,8 +64,6 @@ 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::StringAspect userMessageTemplateForCC{this};
Utils::BoolAspect useFilePathInContext{this}; Utils::BoolAspect useFilePathInContext{this};
Utils::BoolAspect useProjectChangesCache{this}; Utils::BoolAspect useProjectChangesCache{this};
Utils::IntegerAspect maxChangesCacheSize{this}; Utils::IntegerAspect maxChangesCacheSize{this};

View File

@@ -119,12 +119,14 @@ 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 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{title(TrConstants::CODE_COMPLETION), ccGrid}; auto ccGroup = Group{title(TrConstants::CODE_COMPLETION), ccGrid};
auto caGroup = Group{title(TrConstants::CHAT_ASSISTANT), caGrid}; auto caGroup = Group{title(TrConstants::CHAT_ASSISTANT), caGrid};

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

View File

@@ -1,44 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <utils/aspects.h>
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Settings {
class ProjectSettings : public Utils::AspectContainer
{
public:
explicit ProjectSettings(ProjectExplorer::Project *project);
void save(ProjectExplorer::Project *project);
void setUseGlobalSettings(bool useGlobalSettings);
bool isEnabled() const;
Utils::BoolAspect enableQodeAssist{this};
Utils::BoolAspect useGlobalSettings{this};
Utils::FilePathAspect chatHistoryPath{this};
};
} // namespace QodeAssist::Settings

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ProjectSettingsPanel.hpp"
#include <projectexplorer/project.h>
#include <projectexplorer/projectpanelfactory.h>
#include <projectexplorer/projectsettingswidget.h>
#include <utils/layoutbuilder.h>
#include "ProjectSettings.hpp"
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
using namespace ProjectExplorer;
namespace QodeAssist::Settings {
class ProjectSettingsWidget final : public ProjectExplorer::ProjectSettingsWidget
{
public:
ProjectSettingsWidget()
{
setGlobalSettingsId(Constants::QODE_ASSIST_GENERAL_OPTIONS_ID);
setUseGlobalSettingsCheckBoxVisible(true);
}
};
static ProjectSettingsWidget *createProjectPanel(Project *project)
{
using namespace Layouting;
auto widget = new ProjectSettingsWidget;
auto settings = new ProjectSettings(project);
settings->setParent(widget);
QObject::connect(
widget,
&ProjectSettingsWidget::useGlobalSettingsChanged,
settings,
&ProjectSettings::setUseGlobalSettings);
widget->setUseGlobalSettings(settings->useGlobalSettings());
widget->setEnabled(!settings->useGlobalSettings());
QObject::connect(
widget, &ProjectSettingsWidget::useGlobalSettingsChanged, widget, [widget](bool useGlobal) {
widget->setEnabled(!useGlobal);
});
Column{
settings->enableQodeAssist,
Space{8},
settings->chatHistoryPath,
}
.attachTo(widget);
return widget;
}
class ProjectPanelFactory final : public ProjectExplorer::ProjectPanelFactory
{
public:
ProjectPanelFactory()
{
setPriority(1000);
setDisplayName(Tr::tr("Qode Assist"));
setCreateWidgetFunction(&createProjectPanel);
}
};
void setupProjectPanel()
{
static ProjectPanelFactory theProjectPanelFactory;
}
} // namespace QodeAssist::Settings

View File

@@ -1,26 +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
namespace QodeAssist::Settings {
void setupProjectPanel();
} // namespace QodeAssist::Settings

View File

@@ -1,136 +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 "ProviderSettings.hpp"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <QMessageBox>
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include "SettingsUtils.hpp"
namespace QodeAssist::Settings {
ProviderSettings &providerSettings()
{
static ProviderSettings settings;
return settings;
}
ProviderSettings::ProviderSettings()
{
setAutoApply(false);
setDisplayName(Tr::tr("Provider Settings"));
// OpenRouter Settings
openRouterApiKey.setSettingsKey(Constants::OPEN_ROUTER_API_KEY);
openRouterApiKey.setLabelText(Tr::tr("OpenRouter API Key:"));
openRouterApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
openRouterApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
openRouterApiKey.setHistoryCompleter(Constants::OPEN_ROUTER_API_KEY_HISTORY);
openRouterApiKey.setDefaultValue("");
openRouterApiKey.setAutoApply(true);
// OpenAI Compatible Settings
openAiCompatApiKey.setSettingsKey(Constants::OPEN_AI_COMPAT_API_KEY);
openAiCompatApiKey.setLabelText(Tr::tr("OpenAI Compatible API Key:"));
openAiCompatApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
openAiCompatApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
openAiCompatApiKey.setHistoryCompleter(Constants::OPEN_AI_COMPAT_API_KEY_HISTORY);
openAiCompatApiKey.setDefaultValue("");
openAiCompatApiKey.setAutoApply(true);
// Claude Compatible Settings
claudeApiKey.setSettingsKey(Constants::CLAUDE_API_KEY);
claudeApiKey.setLabelText(Tr::tr("Claude API Key:"));
claudeApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
claudeApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
claudeApiKey.setHistoryCompleter(Constants::CLAUDE_API_KEY_HISTORY);
claudeApiKey.setDefaultValue("");
claudeApiKey.setAutoApply(true);
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
readSettings();
setupConnections();
setLayouter([this]() {
using namespace Layouting;
return Column{
Row{Stretch{1}, resetToDefaults},
Space{8},
Group{title(Tr::tr("OpenRouter Settings")), Column{openRouterApiKey}},
Space{8},
Group{title(Tr::tr("OpenAI Compatible Settings")), Column{openAiCompatApiKey}},
Space{8},
Group{title(Tr::tr("Claude Settings")), Column{claudeApiKey}},
Stretch{1}};
});
}
void ProviderSettings::setupConnections()
{
connect(
&resetToDefaults, &ButtonAspect::clicked, this, &ProviderSettings::resetSettingsToDefaults);
connect(&openRouterApiKey, &ButtonAspect::changed, this, [this]() {
openRouterApiKey.writeSettings();
});
connect(&openAiCompatApiKey, &ButtonAspect::changed, this, [this]() {
openAiCompatApiKey.writeSettings();
});
connect(&claudeApiKey, &ButtonAspect::changed, this, [this]() { claudeApiKey.writeSettings(); });
}
void ProviderSettings::resetSettingsToDefaults()
{
QMessageBox::StandardButton reply;
reply = QMessageBox::question(
Core::ICore::dialogParent(),
Tr::tr("Reset Settings"),
Tr::tr("Are you sure you want to reset all settings to default values?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
resetAspect(openRouterApiKey);
resetAspect(openAiCompatApiKey);
resetAspect(claudeApiKey);
}
}
class ProviderSettingsPage : public Core::IOptionsPage
{
public:
ProviderSettingsPage()
{
setId(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
setDisplayName(Tr::tr("Provider Settings"));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
setSettingsProvider([] { return &providerSettings(); });
}
};
const ProviderSettingsPage providerSettingsPage;
} // namespace QodeAssist::Settings

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 <utils/aspects.h>
#include "ButtonAspect.hpp"
namespace QodeAssist::Settings {
class ProviderSettings : public Utils::AspectContainer
{
public:
ProviderSettings();
ButtonAspect resetToDefaults{this};
// API Keys
Utils::StringAspect openRouterApiKey{this};
Utils::StringAspect openAiCompatApiKey{this};
Utils::StringAspect claudeApiKey{this};
private:
void setupConnections();
void resetSettingsToDefaults();
};
ProviderSettings &providerSettings();
} // namespace QodeAssist::Settings

View File

@@ -24,13 +24,6 @@ namespace QodeAssist::Constants {
const char ACTION_ID[] = "QodeAssist.Action"; const char ACTION_ID[] = "QodeAssist.Action";
const char MENU_ID[] = "QodeAssist.Menu"; const char MENU_ID[] = "QodeAssist.Menu";
// project settings
const char QODE_ASSIST_PROJECT_SETTINGS_ID[] = "QodeAssist.ProjectSettings";
const char QODE_ASSIST_USE_GLOBAL_SETTINGS[] = "QodeAssist.UseGlobalSettings";
const char QODE_ASSIST_ENABLE_IN_PROJECT[] = "QodeAssist.EnableInProject";
const char QODE_ASSIST_CHAT_HISTORY_PATH[] = "QodeAssist.ChatHistoryPath";
// new settings // new settings
const char CC_PROVIDER[] = "QodeAssist.ccProvider"; const char CC_PROVIDER[] = "QodeAssist.ccProvider";
const char CC_MODEL[] = "QodeAssist.ccModel"; const char CC_MODEL[] = "QodeAssist.ccModel";
@@ -56,13 +49,9 @@ const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCha
const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval"; const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold"; const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";
const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion"; const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion";
const char CC_STREAM[] = "QodeAssist.ccStream";
const char CC_SMART_PROCESS_INSTRUCT_TEXT[] = "QodeAssist.ccSmartProcessInstructText";
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate"; const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold"; const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold";
const char CA_SHARING_CURRENT_FILE[] = "QodeAssist.caSharingCurrentFile"; const char CA_SHARING_CURRENT_FILE[] = "QodeAssist.caSharingCurrentFile";
const char CA_STREAM[] = "QodeAssist.caStream";
const char CA_AUTOSAVE[] = "QodeAssist.caAutosave";
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions"; const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId"; const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
@@ -77,17 +66,6 @@ const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "Qode Assist";
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion"; const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
// Provider Settings Page ID
const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.5ProviderSettingsPageId";
// Provider API Keys
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
const char OPEN_ROUTER_API_KEY_HISTORY[] = "QodeAssist.openRouterApiKeyHistory";
const char OPEN_AI_COMPAT_API_KEY[] = "QodeAssist.openAiCompatApiKey";
const char OPEN_AI_COMPAT_API_KEY_HISTORY[] = "QodeAssist.openAiCompatApiKeyHistory";
const char CLAUDE_API_KEY[] = "QodeAssist.claudeApiKey";
const char CLAUDE_API_KEY_HISTORY[] = "QodeAssist.claudeApiKeyHistory";
// context settings // context settings
const char CC_READ_FULL_FILE[] = "QodeAssist.ccReadFullFile"; const char CC_READ_FULL_FILE[] = "QodeAssist.ccReadFullFile";
const char CC_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.ccReadStringsBeforeCursor"; const char CC_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.ccReadStringsBeforeCursor";
@@ -95,8 +73,6 @@ const char CC_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.ccReadStringsAfterCursor
const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt"; const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt";
const char CC_USE_FILE_PATH_IN_CONTEXT[] = "QodeAssist.ccUseFilePathInContext"; const char CC_USE_FILE_PATH_IN_CONTEXT[] = "QodeAssist.ccUseFilePathInContext";
const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt"; const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt";
const char CC_USE_USER_TEMPLATE[] = "QodeAssist.ccUseUserTemplate";
const char CC_USER_TEMPLATE[] = "QodeAssist.ccUserTemplate";
const char CC_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache"; const char CC_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache";
const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize"; const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize";
const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt"; const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt";

View File

@@ -79,8 +79,6 @@ inline const char PREDEFINED_URL[]
inline const char CUSTOM_URL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter custom URL"); inline const char CUSTOM_URL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter custom URL");
inline const char ENTER_MODEL_MANUALLY_BUTTON[] inline const char ENTER_MODEL_MANUALLY_BUTTON[]
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter Model Name Manually"); = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter Model Name Manually");
inline const char AUTO_COMPLETION_SETTINGS[]
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Auto Completion Settings");
} // namespace TrConstants } // namespace TrConstants

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonArray>
#include "llmcore/PromptTemplate.hpp"
namespace QodeAssist::Templates {
class Claude : public LLMCore::PromptTemplate
{
public:
LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
QString name() const override { return "Claude"; }
QString promptTemplate() const override { return {}; }
QStringList stopWords() const override { return QStringList(); }
void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override {}
QString description() const override { return "Claude"; }
};
} // namespace QodeAssist::Templates

View File

@@ -50,6 +50,14 @@ public:
void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
{ {
QJsonArray messages = request["messages"].toArray();
QJsonObject newMessage;
newMessage["role"] = "user";
newMessage["content"] = context.prefix;
messages.append(newMessage);
request["messages"] = messages;
} }
QString description() const override { return "template will take from ollama modelfile"; } QString description() const override { return "template will take from ollama modelfile"; }
}; };

View File

@@ -23,7 +23,6 @@
#include "templates/Alpaca.hpp" #include "templates/Alpaca.hpp"
#include "templates/BasicChat.hpp" #include "templates/BasicChat.hpp"
#include "templates/ChatML.hpp" #include "templates/ChatML.hpp"
#include "templates/Claude.hpp"
#include "templates/CodeLlamaFim.hpp" #include "templates/CodeLlamaFim.hpp"
#include "templates/CustomFimTemplate.hpp" #include "templates/CustomFimTemplate.hpp"
#include "templates/DeepSeekCoderFim.hpp" #include "templates/DeepSeekCoderFim.hpp"
@@ -50,7 +49,6 @@ inline void registerTemplates()
templateManager.registerTemplate<ChatML>(); templateManager.registerTemplate<ChatML>();
templateManager.registerTemplate<Alpaca>(); templateManager.registerTemplate<Alpaca>();
templateManager.registerTemplate<Llama2>(); templateManager.registerTemplate<Llama2>();
templateManager.registerTemplate<Claude>();
} }
} // namespace QodeAssist::Templates } // namespace QodeAssist::Templates

View File

@@ -17,26 +17,32 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/ */
#pragma once #include "CounterTooltip.hpp"
#include <QObject>
#include <QRegularExpression>
#include <QString>
namespace QodeAssist { namespace QodeAssist {
class CodeHandler CounterTooltip::CounterTooltip(int count)
: m_count(count)
{ {
public: m_label = new QLabel(this);
static QString processText(QString text); addWidget(m_label);
updateLabel();
private: m_timer = new QTimer(this);
static QString getCommentPrefix(const QString &language); m_timer->setSingleShot(true);
static QString detectLanguage(const QString &line); m_timer->setInterval(2000);
static const QRegularExpression &getFullCodeBlockRegex(); connect(m_timer, &QTimer::timeout, this, [this] { emit finished(m_count); });
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex(); m_timer->start();
}; }
CounterTooltip::~CounterTooltip() {}
void CounterTooltip::updateLabel()
{
const auto hotkey = QKeySequence(QKeySequence::MoveToNextWord).toString();
m_label->setText(QString("Insert Next %1 line(s) (%2)").arg(m_count).arg(hotkey));
}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,4 +1,4 @@
/* /*
* Copyright (C) 2024 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
@@ -19,14 +19,30 @@
#pragma once #pragma once
#include <QString> #include <QLabel>
#include <QTimer>
#include <QToolBar>
#include <QWidget>
namespace QodeAssist::Context { namespace QodeAssist {
struct ContentFile class CounterTooltip : public QToolBar
{ {
QString filename; Q_OBJECT
QString content;
public:
CounterTooltip(int count);
~CounterTooltip();
signals:
void finished(int count);
private:
void updateLabel();
QLabel *m_label;
QTimer *m_timer;
int m_count;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist