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
168 changed files with 1747 additions and 10102 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,64 +0,0 @@
{
"name": "QodeAssist",
"vendor": "Petr Mironychev",
"tags": [
"code assistant",
"llm",
"ai"
],
"compatibility": "Qt 6.8.2",
"platforms": [
"Windows",
"macOS",
"Linux"
],
"license": "GPLv3",
"version": "0.5.3",
"status": "draft",
"is_pack": false,
"released_at": null,
"version_history": [
{
"version": "0.4.0",
"is_latest": false,
"released_at": "2024-01-24T15:00:00Z"
},
{
"version": "0.5.2",
"is_latest": false,
"released_at": "2025-03-13T17:00:00Z"
},
{
"version": "0.5.3",
"is_latest": true,
"released_at": "2025-03-14T11: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.2 QT_VERSION: 6.7.3
QT_CREATOR_VERSION: 16.0.0 QT_CREATOR_VERSION: 14.0.2
QT_CREATOR_VERSION_INTERNAL: 16.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,46 +30,76 @@ 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 22.04 GCC", artifact: "Linux-x64", name: "Ubuntu Latest GCC", artifact: "Linux-x64",
os: ubuntu-22.04, 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}")
- name: Install dependencies 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
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
if ("${{ runner.os }}" STREQUAL "Linux") if ("${{ runner.os }}" STREQUAL "Linux")
@@ -78,13 +107,7 @@ jobs:
COMMAND sudo apt update COMMAND sudo apt update
) )
execute_process( execute_process(
COMMAND sudo apt install COMMAND sudo apt install libgl1-mesa-dev libcups2-dev
# build dependencies
libgl1-mesa-dev libgtest-dev libgmock-dev
# runtime dependencies for tests (Qt is downloaded outside package manager,
# thus minimal dependencies must be installed explicitly)
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb-xkb1 libxkbcommon-x11-0 xvfb
RESULT_VARIABLE result RESULT_VARIABLE result
) )
if (NOT result EQUAL 0) if (NOT result EQUAL 0)
@@ -101,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")
@@ -112,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)
@@ -130,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}")
@@ -149,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")
@@ -167,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: |
@@ -244,65 +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: startsWith(matrix.config.os, 'ubuntu')
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./build/build/${{ env.PLUGIN_NAME }}.json
- name: Run unit tests
if: startsWith(matrix.config.os, 'ubuntu')
run: |
xvfb-run ./build/build/test/QodeAssistTest
update_json:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-22.04
needs: build
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- 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

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

3
.gitmodules vendored
View File

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

1
3rdparty/inja vendored

Submodule 3rdparty/inja deleted from 384a6bef3f

View File

@@ -8,38 +8,14 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
find_package(QtCreator REQUIRED COMPONENTS Core) find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
find_package(GTest)
# IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
set(QODEASSIST_QT_CREATOR_VERSION_MINOR ${CMAKE_MATCH_2})
set(QODEASSIST_QT_CREATOR_VERSION_PATCH ${CMAKE_MATCH_3})
if(NOT version_match)
message(FATAL_ERROR "Failed to parse Qt Creator version string: ${IDE_VERSION}")
endif()
message(STATUS "Qt Creator Version: ${QODEASSIST_QT_CREATOR_VERSION_MAJOR}.${QODEASSIST_QT_CREATOR_VERSION_MINOR}.${QODEASSIST_QT_CREATOR_VERSION_PATCH}")
add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
)
add_subdirectory(llmcore) add_subdirectory(llmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context)
if(GTest_FOUND)
add_subdirectory(test)
endif()
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
@@ -66,39 +42,31 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp templates/Templates.hpp
templates/CodeLlamaFim.hpp templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp templates/CustomFimTemplate.hpp
templates/Qwen.hpp templates/Qwen.hpp
templates/OpenAICompatible.hpp
templates/Ollama.hpp
templates/BasicChat.hpp
templates/Llama3.hpp templates/Llama3.hpp
templates/ChatML.hpp templates/ChatML.hpp
templates/Alpaca.hpp templates/Alpaca.hpp
templates/Llama2.hpp templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
providers/Providers.hpp providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
QodeAssistClient.hpp QodeAssistClient.cpp QodeAssistClient.hpp QodeAssistClient.cpp
DocumentContextReader.hpp DocumentContextReader.cpp
utils/CounterTooltip.hpp utils/CounterTooltip.cpp
core/ChangesManager.h core/ChangesManager.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.cpp chat/ChatOutputPane.h chat/ChatOutputPane.cpp
chat/NavigationPanel.hpp chat/NavigationPanel.cpp chat/NavigationPanel.hpp chat/NavigationPanel.cpp
ConfigurationManager.hpp ConfigurationManager.cpp ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
) )

View File

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

View File

@@ -18,9 +18,9 @@
*/ */
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include <utils/aspects.h>
#include <QtCore/qjsonobject.h> #include <QtCore/qjsonobject.h>
#include <QtQml> #include <QtQml>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
@@ -28,11 +28,11 @@ namespace QodeAssist::Chat {
ChatModel::ChatModel(QObject *parent) ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_totalTokens(0)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
connect( connect(&settings.chatTokensThreshold,
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
this, this,
&ChatModel::tokensThresholdChanged); &ChatModel::tokensThresholdChanged);
@@ -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,37 +65,29 @@ QHash<int, QByteArray> ChatModel::roleNames() const
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[Roles::RoleType] = "roleType"; roles[Roles::RoleType] = "roleType";
roles[Roles::Content] = "content"; roles[Roles::Content] = "content";
roles[Roles::Attachments] = "attachments";
return roles; return roles;
} }
void ChatModel::addMessage( void ChatModel::addMessage(const QString &content, ChatRole role, const QString &id)
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments)
{ {
QString fullContent = content; int tokenCount = estimateTokenCount(content);
if (!attachments.isEmpty()) {
fullContent += "\n\nAttached files list:";
for (const auto &attachment : attachments) {
fullContent += QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
}
}
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) { if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
Message &lastMessage = m_messages.last(); Message &lastMessage = m_messages.last();
int oldTokenCount = lastMessage.tokenCount;
lastMessage.content = content; lastMessage.content = content;
lastMessage.attachments = attachments; lastMessage.tokenCount = tokenCount;
m_totalTokens += (tokenCount - oldTokenCount);
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1)); emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else { } else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size()); beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{role, content, id}; m_messages.append({role, content, tokenCount, id});
newMessage.attachments = attachments; m_totalTokens += tokenCount;
m_messages.append(newMessage);
endInsertRows(); endInsertRows();
} }
trim();
emit totalTokensChanged();
} }
QVector<ChatModel::Message> ChatModel::getChatHistory() const QVector<ChatModel::Message> ChatModel::getChatHistory() const
@@ -110,12 +95,32 @@ QVector<ChatModel::Message> ChatModel::getChatHistory() const
return m_messages; return m_messages;
} }
void ChatModel::trim()
{
while (m_totalTokens > tokensThreshold()) {
if (!m_messages.isEmpty()) {
m_totalTokens -= m_messages.first().tokenCount;
beginRemoveRows(QModelIndex(), 0, 0);
m_messages.removeFirst();
endRemoveRows();
} else {
break;
}
}
}
int ChatModel::estimateTokenCount(const QString &text) const
{
return text.length() / 4;
}
void ChatModel::clear() void ChatModel::clear()
{ {
beginResetModel(); beginResetModel();
m_messages.clear(); m_messages.clear();
m_totalTokens = 0;
endResetModel(); endResetModel();
emit modelReseted(); emit totalTokensChanged();
} }
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
@@ -128,8 +133,7 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
while (blockMatches.hasNext()) { while (blockMatches.hasNext()) {
auto match = blockMatches.next(); auto match = blockMatches.next();
if (match.capturedStart() > lastIndex) { if (match.capturedStart() > lastIndex) {
QString textBetween QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) { if (!textBetween.isEmpty()) {
parts.append({MessagePart::Text, textBetween, ""}); parts.append({MessagePart::Text, textBetween, ""});
} }
@@ -151,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) {
@@ -165,27 +170,17 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
default: default:
continue; continue;
} }
messages.append(QJsonObject{{"role", role}, {"content", message.content}});
QString content
= message.attachments.isEmpty()
? message.content
: message.content + "\n\nAttached files list:"
+ std::accumulate(
message.attachments.begin(),
message.attachments.end(),
QString(),
[](QString acc, const Context::ContentFile &attachment) {
return acc
+ QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
});
messages.append(QJsonObject{{"role", role}, {"content", content}});
} }
return messages; return messages;
} }
int ChatModel::totalTokens() const
{
return m_totalTokens;
}
int ChatModel::tokensThreshold() const int ChatModel::tokensThreshold() const
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();

View File

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

View File

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

View File

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

View File

@@ -1,142 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ChatSerializer.hpp"
#include "Logger.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
namespace QodeAssist::Chat {
const QString ChatSerializer::VERSION = "0.1";
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
{
if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"};
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
}
QJsonObject root = serializeChat(model);
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
}
return {true, QString()};
}
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
return {false, QString("JSON parse error: %1").arg(error.errorString())};
}
QJsonObject root = doc.object();
QString version = root["version"].toString();
if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)};
}
if (!deserializeChat(model, root)) {
return {false, "Failed to deserialize chat data"};
}
return {true, QString()};
}
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.id = json["id"].toString();
return message;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
{
QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) {
messagesArray.append(serializeMessage(message));
}
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
return root;
}
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
{
QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
for (const auto &messageValue : messagesArray) {
messages.append(deserializeMessage(messageValue.toObject()));
}
model->clear();
for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id);
}
return true;
}
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
{
QFileInfo fileInfo(filePath);
QDir dir = fileInfo.dir();
return dir.exists() || dir.mkpath(".");
}
bool ChatSerializer::validateVersion(const QString &version)
{
return version == VERSION;
}
} // namespace QodeAssist::Chat

View File

@@ -1,56 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "ChatModel.hpp"
namespace QodeAssist::Chat {
struct SerializationResult
{
bool success{false};
QString errorMessage;
};
class ChatSerializer
{
public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
// Public for testing purposes
static QJsonObject serializeMessage(const ChatModel::Message &message);
static ChatModel::Message deserializeMessage(const QJsonObject &json);
static QJsonObject serializeChat(const ChatModel *model);
static bool deserializeChat(ChatModel *model, const QJsonObject &json);
private:
static const QString VERSION;
static constexpr int CURRENT_VERSION = 1;
static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version);
};
} // namespace QodeAssist::Chat

View File

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

View File

@@ -40,4 +40,4 @@ void ChatWidget::scrollToBottom()
{ {
QMetaObject::invokeMethod(rootObject(), "scrollToBottom"); QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
} }
} // namespace QodeAssist::Chat }

View File

@@ -38,4 +38,4 @@ signals:
void clearPressed(); void clearPressed();
}; };
} // namespace QodeAssist::Chat }

View File

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

View File

@@ -25,7 +25,6 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "RequestHandler.hpp" #include "RequestHandler.hpp"
#include "llmcore/IPromptProvider.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -34,30 +33,22 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void sendMessage( void sendMessage(const QString &message, bool includeCurrentFile = false);
const QString &message,
const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {});
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
signals: signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely();
private: private:
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel;
LLMCore::RequestHandler *m_requestHandler; LLMCore::RequestHandler *m_requestHandler;
ChatModel *m_chatModel;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

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

Before

Width:  |  Height:  |  Size: 869 B

View File

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

Before

Width:  |  Height:  |  Size: 869 B

View File

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

Before

Width:  |  Height:  |  Size: 353 B

View File

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

Before

Width:  |  Height:  |  Size: 353 B

View File

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

Before

Width:  |  Height:  |  Size: 513 B

View File

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

Before

Width:  |  Height:  |  Size: 507 B

View File

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

View File

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

View File

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

View File

@@ -1,54 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls.Basic
Button {
id: control
padding: 4
icon.width: 16
icon.height: 16
contentItem.height: 20
background: Rectangle {
id: bg
implicitHeight: 20
color: !control.enabled || !control.down ? control.palette.button : control.palette.dark
border.color: !control.enabled || (!control.hovered && !control.visualFocus) ? control.palette.mid : control.palette.highlight
border.width: 1
radius: 4
Rectangle {
anchors.fill: bg
radius: bg.radius
gradient: Gradient {
GradientStop { position: 0.0; color: Qt.alpha(control.palette.highlight, 0.4) }
GradientStop { position: 1.0; color: Qt.alpha(control.palette.highlight, 0.2) }
}
opacity: control.hovered ? 0.3 : 0.01
Behavior on opacity {NumberAnimation{duration: 250}}
}
}
}

View File

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

View File

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

View File

@@ -1,109 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
Flow {
id: root
property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid
property string iconPath
signal removeFileFromListByIndex(index: int)
spacing: 5
leftPadding: 5
rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater {
id: attachRepeater
delegate: Rectangle {
required property int index
required property string modelData
height: 30
width: contentRow.width + 10
radius: 4
color: palette.button
border.width: 1
border.color: mouse.hovered ? palette.highlight : root.accentColor
HoverHandler {
id: mouse
}
Row {
id: contentRow
spacing: 5
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
Image {
id: icon
anchors.verticalCenter: parent.verticalCenter
source: root.iconPath
sourceSize.width: 8
sourceSize.height: 15
}
Text {
id: fileNameText
anchors.verticalCenter: parent.verticalCenter
color: palette.buttonText
text: {
const parts = modelData.split('/');
return parts[parts.length - 1];
}
}
MouseArea {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width + 5
height: closeButton.width + 5
onClicked: root.removeFileFromListByIndex(index)
Image {
id: closeIcon
anchors.centerIn: parent
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
width: 6
height: 6
}
}
}
}
}
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
Rectangle {
id: root
property alias sendButton: sendButtonId
property alias stopButton: stopButtonId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias linkFiles: linkFilesId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
id: bottomBar
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: sendButtonId
text: qsTr("Send")
}
QoAButton {
id: stopButtonId
text: qsTr("Stop")
}
QoAButton {
id: attachFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Attach files")
}
QoAButton {
id: linkFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Link files")
}
CheckBox {
id: syncOpenFilesId
text: qsTr("Sync open files")
ToolTip.visible: syncOpenFilesId.hovered
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
}
Item {
Layout.fillWidth: true
}
}
}

View File

@@ -1,88 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Layouts
import ChatView
Rectangle {
id: root
property alias saveButton: saveButtonId
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: saveButtonId
text: qsTr("Save")
}
QoAButton {
id: loadButtonId
text: qsTr("Load")
}
QoAButton {
id: clearButtonId
text: qsTr("Clear")
}
Text {
id: recentPathId
elide: Text.ElideMiddle
color: palette.text
}
QoAButton {
id: openChatHistoryId
text: qsTr("Show in system")
}
Item {
Layout.fillWidth: true
}
Badge {
id: tokensBadgeId
}
}
}

View File

@@ -1,193 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "CodeHandler.hpp"
#include <QFileInfo>
#include <QHash>
namespace QodeAssist {
struct LanguageProperties
{
QString name;
QString commentStyle;
QVector<QString> namesFromModel;
QVector<QString> fileExtensions;
};
const QVector<LanguageProperties> &getKnownLanguages()
{
static QVector<LanguageProperties> knownLanguages = {
{"python", "#", {"python", "py"}, {"py"}},
{"lua", "--", {"lua"}, {"lua"}},
{"js", "//", {"js", "javascript"}, {"js", "jsx"}},
{"ts", "//", {"ts", "typescript"}, {"ts", "tsx"}},
{"c-like", "//", {"c", "c++", "cpp"}, {"c", "h", "cpp", "hpp"}},
{"java", "//", {"java"}, {"java"}},
{"c#", "//", {"cs", "csharp"}, {"cs"}},
{"php", "//", {"php"}, {"php"}},
{"ruby", "#", {"rb", "ruby"}, {"rb"}},
{"go", "//", {"go"}, {"go"}},
{"swift", "//", {"swift"}, {"swift"}},
{"kotlin", "//", {"kt", "kotlin"}, {"kt", "kotlin"}},
{"scala", "//", {"scala"}, {"scala"}},
{"r", "#", {"r"}, {"r"}},
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
{"perl", "#", {"pl", "perl"}, {"pl"}},
{"hs", "--", {"hs", "haskell"}, {"hs"}},
{"qml", "//", {"qml"}, {"qml"}},
};
return knownLanguages;
}
static QHash<QString, QString> buildLanguageToCommentPrefixMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
result[languageProps.name] = languageProps.commentStyle;
}
return result;
}
static QHash<QString, QString> buildExtensionToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &extension : languageProps.fileExtensions) {
result[extension] = languageProps.name;
}
}
return result;
}
static QHash<QString, QString> buildModelLanguageNameToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &nameFromModel : languageProps.namesFromModel) {
result[nameFromModel] = languageProps.name;
}
}
return result;
}
QString CodeHandler::processText(QString text, QString currentFilePath)
{
QString result;
QStringList lines = text.split('\n');
bool inCodeBlock = false;
QString pendingComments;
auto currentFileExtension = QFileInfo(currentFilePath).suffix();
auto currentLanguage = detectLanguageFromExtension(currentFileExtension);
auto addPendingCommentsIfAny = [&]() {
if (pendingComments.isEmpty()) {
return;
}
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();
};
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
auto lineLanguage = detectLanguageFromLine(line);
if (!lineLanguage.isEmpty()) {
currentLanguage = lineLanguage;
}
addPendingCommentsIfAny();
if (lineLanguage.isEmpty()) {
// language not detected, so add direct output from model, if any
result += line.trimmed().mid(3) + "\n"; // add the remainder of line after ```
}
}
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
result += line + "\n";
} else {
QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
pendingComments += trimmed + "\n";
} else {
pendingComments += "\n";
}
}
}
addPendingCommentsIfAny();
return result;
}
QString CodeHandler::getCommentPrefix(const QString &language)
{
static const auto commentPrefixes = buildLanguageToCommentPrefixMap();
return commentPrefixes.value(language, "//");
}
QString CodeHandler::detectLanguageFromLine(const QString &line)
{
static const auto modelNameToLanguage = buildModelLanguageNameToLanguageMap();
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), "");
}
QString CodeHandler::detectLanguageFromExtension(const QString &extension)
{
static const auto extensionToLanguage = buildExtensionToLanguageMap();
return extensionToLanguage.value(extension.toLower(), "");
}
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
{
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
} // namespace QodeAssist

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QRegularExpression>
#include <QString>
namespace QodeAssist {
class CodeHandler
{
public:
static QString processText(QString text, QString currentFileName);
/**
* Detects language from line, or returns empty string if this was not possible
*/
static QString detectLanguageFromLine(const QString &line);
/**
* Detects language file name, or returns empty string if this was not possible
*/
static QString detectLanguageFromExtension(const QString &extension);
private:
static QString getCommentPrefix(const QString &language);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex();
};
} // namespace QodeAssist

View File

@@ -19,8 +19,8 @@
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
#include <settings/ButtonAspect.hpp>
#include <QTimer> #include <QTimer>
#include <settings/ButtonAspect.hpp>
#include "QodeAssisttr.h" #include "QodeAssisttr.h"
@@ -35,28 +35,6 @@ ConfigurationManager &ConfigurationManager::instance()
void ConfigurationManager::init() void ConfigurationManager::init()
{ {
setupConnections(); setupConnections();
updateAllTemplateDescriptions();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
} }
ConfigurationManager::ConfigurationManager(QObject *parent) ConfigurationManager::ConfigurationManager(QObject *parent)
@@ -79,21 +57,6 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate); connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
} }
void ConfigurationManager::selectProvider() void ConfigurationManager::selectProvider()
@@ -106,13 +69,13 @@ void ConfigurationManager::selectProvider()
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider) auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
? m_generalSettings.ccProvider ? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider
: m_generalSettings.caProvider; : m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] { QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(providersList,
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:")); targetSettings,
Tr::tr("Select LLM Provider"),
Tr::tr("Providers:"));
}); });
} }
@@ -123,19 +86,14 @@ void ConfigurationManager::selectModel()
return; return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue() const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue(); : m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue() const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: m_generalSettings.caUrl.volatileValue(); : m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : m_generalSettings.caModel;
: isPreset1 ? m_generalSettings.ccPreset1Model
: m_generalSettings.caModel;
if (auto provider = m_providersManager.getProviderByName(providerName)) { if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) { if (!provider->supportsModelListing()) {
@@ -164,23 +122,18 @@ void ConfigurationManager::selectTemplate()
return; return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1 const auto templateList = isCodeCompletion ? m_templateManger.fimTemplatesNames()
? m_templateManger.getFimTemplatesForProvider(providerID) : m_templateManger.chatTemplatesNames();
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: m_generalSettings.caTemplate; : m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(templateList,
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:")); targetSettings,
Tr::tr("Select Template"),
Tr::tr("Templates:"));
}); });
} }
@@ -197,9 +150,8 @@ void ConfigurationManager::selectUrl()
urls.append(url); urls.append(url);
} }
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
: settingsButton == &m_generalSettings.ccPreset1SetUrl ? m_generalSettings.ccUrl
? m_generalSettings.ccPreset1Url
: m_generalSettings.caUrl; : m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {

View File

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

249
DocumentContextReader.cpp Normal file
View File

@@ -0,0 +1,249 @@
/*
* 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 "DocumentContextReader.hpp"
#include <QFileInfo>
#include <QTextBlock>
#include <languageserverprotocol/lsptypes.h>
#include "core/ChangesManager.h"
#include "settings/CodeCompletionSettings.hpp"
const QRegularExpression &getYearRegex()
{
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
return yearRegex;
}
const QRegularExpression &getNameRegex()
{
static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b");
return nameRegex;
}
const QRegularExpression &getCommentRegex()
{
static const QRegularExpression
commentRegex(R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))",
QRegularExpression::MultilineOption);
return commentRegex;
}
namespace QodeAssist {
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
: m_textDocument(textDocument)
, m_document(textDocument->document())
{
m_copyrightInfo = findCopyright();
}
QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) const
{
if (!m_document || lineNumber < 0)
return QString();
QTextBlock block = m_document->begin();
int currentLine = 0;
while (block.isValid()) {
if (currentLine == lineNumber) {
QString text = block.text();
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
text = text.left(cursorPosition);
}
return text;
}
block = block.next();
currentLine++;
}
return QString();
}
QString DocumentContextReader::getContextBefore(int lineNumber,
int cursorPosition,
int linesCount) const
{
int effectiveStartLine;
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - linesCount);
} else {
effectiveStartLine = qMax(0, lineNumber - linesCount);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
QString DocumentContextReader::getContextAfter(int lineNumber,
int cursorPosition,
int linesCount) const
{
int endLine = qMin(m_document->blockCount() - 1, lineNumber + linesCount);
return getContextBetween(lineNumber + 1, endLine, cursorPosition);
}
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
{
int startLine = 0;
if (m_copyrightInfo.found) {
startLine = m_copyrightInfo.endLine + 1;
}
startLine = qMin(startLine, lineNumber);
QString result = getContextBetween(startLine, lineNumber, cursorPosition);
return result;
}
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
{
return getContextBetween(lineNumber, m_document->blockCount() - 1, cursorPosition);
}
QString DocumentContextReader::getLanguageAndFileInfo() const
{
if (!m_textDocument)
return QString();
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(
m_textDocument->mimeType());
QString mimeType = m_textDocument->mimeType();
QString filePath = m_textDocument->filePath().toString();
QString fileExtension = QFileInfo(filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, mimeType, filePath, fileExtension);
}
CopyrightInfo DocumentContextReader::findCopyright()
{
CopyrightInfo result = {-1, -1, false};
QString text = m_document->toPlainText();
QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text);
QList<CopyrightInfo> copyrightBlocks;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower();
if (matchedText.contains("copyright") || matchedText.contains("(C)")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
int startPos = match.capturedStart();
int endPos = match.capturedEnd();
CopyrightInfo info;
info.startLine = m_document->findBlock(startPos).blockNumber();
info.endLine = m_document->findBlock(endPos).blockNumber();
info.found = true;
copyrightBlocks.append(info);
}
}
for (int i = 0; i < copyrightBlocks.size() - 1; ++i) {
if (copyrightBlocks[i].endLine + 1 >= copyrightBlocks[i + 1].startLine) {
copyrightBlocks[i].endLine = copyrightBlocks[i + 1].endLine;
copyrightBlocks.removeAt(i + 1);
--i;
}
}
if (!copyrightBlocks.isEmpty()) { // temproary solution, need cache
return copyrightBlocks.first();
}
return result;
}
QString DocumentContextReader::getContextBetween(int startLine,
int endLine,
int cursorPosition) const
{
QString context;
for (int i = startLine; i <= endLine; ++i) {
QTextBlock block = m_document->findBlockByNumber(i);
if (!block.isValid()) {
break;
}
if (i == endLine) {
context += block.text().left(cursorPosition);
} else {
context += block.text() + "\n";
}
}
return context;
}
CopyrightInfo DocumentContextReader::copyrightInfo() const
{
return m_copyrightInfo;
}
LLMCore::ContextData DocumentContextReader::prepareContext(int lineNumber, int cursorPosition) const
{
QString contextBefore = getContextBefore(lineNumber, cursorPosition);
QString contextAfter = getContextAfter(lineNumber, cursorPosition);
QString fileContext;
if (Settings::codeCompletionSettings().useFilePathInContext())
fileContext.append("\n ").append(getLanguageAndFileInfo());
if (Settings::codeCompletionSettings().useProjectChangesCache())
fileContext.append("\n ").append(
ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {contextBefore, contextAfter, fileContext};
}
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileBefore(lineNumber, cursorPosition);
} else {
int effectiveStartLine;
int beforeCursor = Settings::codeCompletionSettings().readStringsBeforeCursor();
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - beforeCursor);
} else {
effectiveStartLine = qMax(0, lineNumber - beforeCursor);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
}
QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileAfter(lineNumber, cursorPosition);
} else {
int endLine = qMin(m_document->blockCount() - 1,
lineNumber + Settings::codeCompletionSettings().readStringsAfterCursor());
return getContextBetween(lineNumber + 1, endLine, -1);
}
}
} // namespace QodeAssist

View File

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

View File

@@ -23,37 +23,22 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include "CodeHandler.hpp" #include <llmcore/RequestConfig.hpp>
#include "context/ContextManager.hpp" #include <texteditor/textdocument.h>
#include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp" #include "DocumentContextReader.hpp"
#include "llmcore/PromptTemplateManager.hpp" #include "llmcore/PromptTemplateManager.hpp"
#include "llmcore/ProvidersManager.hpp" #include "llmcore/ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <llmcore/RequestConfig.hpp>
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface()
const Settings::GeneralSettings &generalSettings, : m_requestHandler(this)
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
LLMCore::RequestHandlerBase &requestHandler,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_requestHandler(requestHandler)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
{ {
connect( connect(&m_requestHandler,
&m_requestHandler,
&LLMCore::RequestHandler::completionReceived, &LLMCore::RequestHandler::completionReceived,
this, this,
&LLMClientInterface::sendCompletionToClient); &LLMClientInterface::sendCompletionToClient);
@@ -88,7 +73,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
handleTextDocumentDidOpen(request); handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") { } else if (method == "getCompletionsCycling") {
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId); startTimeMeasurement(requestId);
handleCompletion(request); handleCompletion(request);
} else if (method == "$/cancelRequest") { } else if (method == "$/cancelRequest") {
handleCancelRequest(request); handleCancelRequest(request);
@@ -161,157 +146,98 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
void LLMClientInterface::handleCompletion(const QJsonObject &request) void LLMClientInterface::handleCompletion(const QJsonObject &request)
{ {
auto filePath = Context::extractFilePathFromRequest(request); auto updatedContext = prepareContext(request);
auto documentInfo = m_documentReader.readDocument(filePath); auto &completeSettings = Settings::codeCompletionSettings();
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available for" + filePath);
return;
}
auto updatedContext = prepareContext(request, documentInfo); auto providerName = Settings::generalSettings().ccProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
bool isPreset1Active
= Context::ContextManager::isSpecifyCompletion(documentInfo, m_generalSettings);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) { if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
return; return;
} }
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() auto templateName = Settings::generalSettings().ccTemplate();
: m_generalSettings.ccPreset1Template(); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) { if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
return; return;
} }
// TODO refactor to dynamic presets system
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::CodeCompletion; config.requestType = LLMCore::RequestType::Fim;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
// TODO refactor networking config.url = QUrl(
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"} config.apiKey = Settings::codeCompletionSettings().apiKey();
: QString{"generateContent?"};
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream)); config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
} else {
config.url = QUrl(QString("%1%2").arg( config.multiLineCompletion = completeSettings.multiLineCompletion();
url,
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint() QString systemPrompt;
: provider->chatEndpoint())); if (completeSettings.useSystemPrompt())
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}}; systemPrompt.append(completeSettings.systemPrompt());
} if (!updatedContext.fileContext.isEmpty())
config.apiKey = provider->apiKey(); systemPrompt.append(updatedContext.fileContext);
config.multiLineCompletion = m_completeSettings.multiLineCompletion(); if (!systemPrompt.isEmpty())
config.providerRequest["system"] = systemPrompt;
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords()); const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty()) if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords; config.providerRequest["stop"] = stopWords;
QString systemPrompt; config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
if (m_completeSettings.useSystemPrompt()) config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == LLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
config.provider->prepareRequest(
config.providerRequest,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for fim request:");
LOG_MESSAGES(errors);
return;
}
m_requestHandler.sendLLMRequest(config, request); m_requestHandler.sendLLMRequest(config, request);
} }
LLMCore::ContextData LLMClientInterface::prepareContext( LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
const QJsonObject &request, const Context::DocumentInfo &documentInfo) const QStringView &accumulatedCompletion)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject(); QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].toObject(); QJsonObject position = doc["position"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
return LLMCore::ContextData{};
}
int cursorPosition = position["character"].toInt(); int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt(); int lineNumber = position["line"].toInt();
Context::DocumentContextReader DocumentContextReader reader(textDocument);
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath); return reader.prepareContext(lineNumber, cursorPosition);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
} }
void LLMClientInterface::sendCompletionToClient( void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QString &completion, const QJsonObject &request, bool isComplete) const QJsonObject &request,
bool isComplete)
{ {
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active
= Context::ContextManager::isSpecifyCompletion(documentInfo, m_generalSettings);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"]; response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject result; QJsonObject result;
QJsonArray completions; QJsonArray completions;
QJsonObject completionItem; QJsonObject completionItem;
completionItem[LanguageServerProtocol::textKey] = completion;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
QString processedCompletion
= promptTemplate->type() == LLMCore::TemplateType::Chat
&& m_completeSettings.smartProcessInstuctText()
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
: completion;
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range; QJsonObject range;
range["start"] = position; range["start"] = position;
range["end"] = position; QJsonObject end = position;
end["character"] = position["character"].toInt() + completion.length();
range["end"] = end;
completionItem[LanguageServerProtocol::rangeKey] = range; completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position; completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem); completions.append(completionItem);
@@ -323,15 +249,37 @@ void LLMClientInterface::sendCompletionToClient(
QString("Completions: \n%1") QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE( LOG_MESSAGE(QString("Full response: \n%1")
QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId); endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
} }
void LLMClientInterface::startTimeMeasurement(const QString &requestId)
{
m_requestStartTimes[requestId] = QDateTime::currentMSecsSinceEpoch();
}
void LLMClientInterface::endTimeMeasurement(const QString &requestId)
{
if (m_requestStartTimes.contains(requestId)) {
qint64 startTime = m_requestStartTimes[requestId];
qint64 endTime = QDateTime::currentMSecsSinceEpoch();
qint64 totalTime = endTime - startTime;
logPerformance(requestId, "TotalCompletionTime", totalTime);
m_requestStartTimes.remove(requestId);
}
}
void LLMClientInterface::logPerformance(const QString &requestId,
const QString &operation,
qint64 elapsedMs)
{
LOG_MESSAGE(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
}
void LLMClientInterface::parseCurrentMessage() {} void LLMClientInterface::parseCurrentMessage() {}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -22,15 +22,8 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp> #include <llmcore/ContextData.hpp>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include <llmcore/RequestHandler.hpp> #include <llmcore/RequestHandler.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
class QNetworkReply; class QNetworkReply;
class QNetworkAccessManager; class QNetworkAccessManager;
@@ -42,27 +35,19 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
Q_OBJECT Q_OBJECT
public: public:
LLMClientInterface( LLMClientInterface();
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
LLMCore::RequestHandlerBase &requestHandler,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
Utils::FilePath serverDeviceTemplate() const override; Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient( void sendCompletionToClient(const QString &completion,
const QString &completion, const QJsonObject &request, bool isComplete); const QJsonObject &request,
bool isComplete);
void handleCompletion(const QJsonObject &request); void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override;
protected: protected:
void startImpl() override; void startImpl() override;
void sendData(const QByteArray &data) override;
void parseCurrentMessage() override; void parseCurrentMessage() override;
private: private:
@@ -73,17 +58,16 @@ private:
void handleExit(const QJsonObject &request); void handleExit(const QJsonObject &request);
void handleCancelRequest(const QJsonObject &request); void handleCancelRequest(const QJsonObject &request);
LLMCore::ContextData prepareContext( LLMCore::ContextData prepareContext(const QJsonObject &request,
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QStringView &accumulatedCompletion = QString{});
const Settings::CodeCompletionSettings &m_completeSettings; LLMCore::RequestHandler m_requestHandler;
const Settings::GeneralSettings &m_generalSettings;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
LLMCore::IProviderRegistry &m_providerRegistry;
LLMCore::RequestHandlerBase &m_requestHandler;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;
QMap<QString, qint64> m_requestStartTimes;
void startTimeMeasurement(const QString &requestId);
void endTimeMeasurement(const QString &requestId);
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

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

View File

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

View File

@@ -68,8 +68,7 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
public: public:
static constexpr LanguageServerProtocol::Key docKey{"doc"}; static constexpr LanguageServerProtocol::Key docKey{"doc"};
GetCompletionParams( GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
const LanguageServerProtocol::TextDocumentIdentifier &document,
int version, int version,
const LanguageServerProtocol::Position &position) const LanguageServerProtocol::Position &position)
{ {

View File

@@ -1,13 +1,16 @@
{ {
"Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.5.3", "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" : "https://github.com/Palm1r/QodeAssist",
${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;
@@ -44,8 +43,8 @@ using namespace Core;
namespace QodeAssist { namespace QodeAssist {
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(clientInterface) : LanguageClient::Client(new LLMClientInterface())
, m_recentCharCount(0) , m_recentCharCount(0)
{ {
setName("Qode Assist"); setName("Qode Assist");
@@ -71,11 +70,11 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return; return;
Client::openDocument(document); Client::openDocument(document);
connect( connect(document,
document,
&TextDocument::contentsChangedWithPosition, &TextDocument::contentsChangedWithPosition,
this, this,
[this, document](int position, int charsRemoved, int charsAdded) { [this, document](int position, int charsRemoved, int charsAdded) {
Q_UNUSED(charsRemoved)
if (!Settings::codeCompletionSettings().autoCompletion()) if (!Settings::codeCompletionSettings().autoCompletion())
return; return;
@@ -88,33 +87,18 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return; return;
if (Settings::codeCompletionSettings().useProjectChangesCache()) if (Settings::codeCompletionSettings().useProjectChangesCache())
Context::ChangesManager::instance() ChangesManager::instance().addChange(document,
.addChange(document, position, charsRemoved, charsAdded); position,
charsRemoved,
charsAdded);
TextEditorWidget *widget = textEditor->editorWidget(); TextEditorWidget *widget = textEditor->editorWidget();
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
return; return;
const int cursorPosition = widget->textCursor().position(); const int cursorPosition = widget->textCursor().position();
if (cursorPosition < position || cursorPosition > position + charsAdded) if (cursorPosition < position || cursorPosition > position + charsAdded)
return; return;
if (charsRemoved > 0 || charsAdded <= 0) {
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
QTextCursor cursor = widget->textCursor();
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
QString lastChar = cursor.selectedText();
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
m_recentCharCount += charsAdded; m_recentCharCount += charsAdded;
if (m_typingTimer.elapsed() if (m_typingTimer.elapsed()
@@ -147,8 +131,7 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
return; return;
const FilePath filePath = editor->textDocument()->filePath(); const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{ GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath), documentVersion(filePath),
Position(cursor.mainCursor())}}; Position(cursor.mainCursor())}};
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)]( request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
@@ -189,8 +172,8 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position()); it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer()); it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
} }
void QodeAssistClient::handleCompletions( void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor) TextEditor::TextEditorWidget *editor)
{ {
if (response.error()) if (response.error())
log(*response.error()); log(*response.error());
@@ -210,8 +193,8 @@ void QodeAssistClient::handleCompletions(
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) {
@@ -228,18 +211,10 @@ void QodeAssistClient::handleCompletions(
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()));
} }
} }
@@ -254,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()
@@ -268,11 +239,16 @@ void QodeAssistClient::setupConnections()
openDocument(textDocument); openDocument(textDocument);
}; };
m_documentOpenedConnection m_documentOpenedConnection = connect(EditorManager::instance(),
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc); &EditorManager::documentOpened,
m_documentClosedConnection = connect( this,
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) { openDoc);
if (auto textDocument = qobject_cast<TextDocument *>(document)) m_documentClosedConnection = connect(EditorManager::instance(),
&EditorManager::documentClosed,
this,
[this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(
document))
closeDocument(textDocument); closeDocument(textDocument);
}); });

View File

@@ -24,18 +24,16 @@
#pragma once #pragma once
#include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp> #include "LSPCompletion.hpp"
namespace QodeAssist { namespace QodeAssist {
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
public: public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient();
~QodeAssistClient() override; ~QodeAssistClient() override;
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
@@ -45,8 +43,8 @@ public:
private: private:
void scheduleRequest(TextEditor::TextEditorWidget *editor); void scheduleRequest(TextEditor::TextEditorWidget *editor);
void handleCompletions( void handleCompletions(const GetCompletionRequest::Response &response,
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor); TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor); void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
bool isEnabled(ProjectExplorer::Project *project) const; bool isEnabled(ProjectExplorer::Project *project) const;

265
README.md
View File

@@ -1,80 +1,45 @@
# QodeAssist - AI-powered coding assistant plugin for Qt Creator # QodeAssist - AI-powered coding assistant plugin for Qt Creator
[![Discord](https://dcbadge.limes.pink/api/server/https://discord.gg/DGgMtTteAD?style=flat?theme=discord)](https://discord.gg/DGgMtTteAD)
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml) [![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist) ![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.0-brightgreen) ![Static Badge](https://img.shields.io/badge/QtCreator-14.0.2-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment. QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
> - These services will consume API tokens which may result in charges to your account
> - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use
⚠️ **Commercial Support and Custom Development**
> The QodeAssist developer offers commercial services for:
> - Adapting the plugin for specific Qt Creator versions
> - Custom development for particular operating systems
> - Integration with specific language models
> - Implementing custom features and modifications
>
> For commercial inquiries, please contact: qodeassist.dev@pm.me
## Table of Contents ## Table of Contents
1. [Overview](#overview) 1. [Overview](#overview)
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator) 2. [Installation](#installation)
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude) 3. [Configure Plugin](#configure-plugin)
4. [Configure for OpenAI](#configure-for-openai) 4. [Supported LLM Providers](#supported-llm-providers)
5. [Configure for Mistral AI](#configure-for-mistral-ai) 5. [Recommended Models](#recommended-models)
6. [Configure for Google AI](#configure-for-google-ai) - [Ollama](#ollama)
7. [Configure for Ollama](#configure-for-ollama) - [LM Studio](#lm-studio)
8. [Configure for llama.cpp](#configure-for-llamacpp) 6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
9. [System Prompt Configuration](#system-prompt-configuration) 7. [Development Progress](#development-progress)
10. [File Context Features](#file-context-features) 8. [Hotkeys](#hotkeys)
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility) 9. [Troubleshooting](#troubleshooting)
12. [Development Progress](#development-progress) 10. [Support the Development](#support-the-development-of-qodeassist)
13. [Hotkeys](#hotkeys) 11. [How to Build](#how-to-build)
14. [Troubleshooting](#troubleshooting)
15. [Support the Development](#support-the-development-of-qodeassist)
16. [How to Build](#how-to-build)
## Overview ## Overview
- AI-powered code completion - AI-powered code completion
- Chat functionality: - Chat functionality:
- Side and Bottom panels - Side and Bottom panels
- Chat history autosave and restore
- Token usage monitoring and management
- Attach files for one-time code analysis
- Link files for persistent context with auto update in conversations
- Automatic syncing with open editor files (optional)
- Support for multiple LLM providers: - Support for multiple LLM providers:
- Ollama - Ollama
- llama.cpp
- OpenAI
- Anthropic Claude
- LM Studio - LM Studio
- Mistral AI - OpenAI-compatible providers(eg. https://openrouter.ai)
- Google AI
- OpenAI-compatible providers(eg. llama.cpp, https://openrouter.ai)
- Extensive library of model-specific templates - Extensive library of model-specific templates
- Custom template support - Custom template support
- Easy configuration and model selection - Easy configuration and model selection
Join our Discord Community: Have questions or want to discuss QodeAssist? Join our [Discord server](https://discord.gg/BGMkUsXUgf) to connect with other users and get support!
<details> <details>
<summary>Code completion: (click to expand)</summary> <summary>Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
</details> </details>
<details>
<summary>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">
@@ -85,78 +50,11 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b"> <img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details> </details>
<details> ## Installation
<summary>Automatic syncing with open editor files: (click to expand)</summary>
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
</details>
## Install plugin to QtCreator
1. Install Latest QtCreator 1. Install Latest QtCreator
2. Download the QodeAssist plugin for your Qt Creator 2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
- Remove old version plugin if already was installed 3. Install a language models in Ollama via terminal. For example, you can run:
3. Launch Qt Creator and install the plugin:
- Go to:
- MacOS: Qt Creator -> About Plugins...
- Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure for Anthropic Claude
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Claude api key
3. Return to General tab and configure:
- Set "Claude" as the provider for code completion or/and chat assistant
- Set the Claude URL (https://api.anthropic.com)
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
- Choose the Claude template for code completion or/and chat
<details>
<summary>Example of Claude settings: (click to expand)</summary>
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
</details>
## Configure for OpenAI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure OpenAI api key
3. Return to General tab and configure:
- Set "OpenAI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.openai.com)
- Select your preferred model (e.g., gpt-4o)
- Choose the OpenAI template for code completion or/and chat
<details>
<summary>Example of OpenAI settings: (click to expand)</summary>
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
</details>
## Configure for Mistral AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Mistral AI api key
3. Return to General tab and configure:
- Set "Mistral AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.mistral.ai)
- Select your preferred model (e.g., mistral-large-latest)
- Choose the Mistral AI template for code completion or/and chat
<details>
<summary>Example of Mistral AI settings: (click to expand)</summary>
<img width="829" alt="Mistral AI Settings" src="https://github.com/user-attachments/assets/1c5ed13b-a29b-43f7-b33f-2e05fdea540c" />
</details>
## Configure for Google AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Google AI api key
3. Return to General tab and configure:
- Set "Google AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://generativelanguage.googleapis.com/v1beta)
- Select your preferred model (e.g., gemini-2.0-flash)
- Choose the Google AI template
<details>
<summary>Example of Google AI settings: (click to expand)</summary>
<img width="829" alt="Google AI Settings" src="https://github.com/user-attachments/assets/046ede65-a94d-496c-bc6c-41f3750be12a" />
</details>
## Configure for Ollama
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Install a language models in Ollama via terminal. For example, you can run:
For standard computers (minimum 8GB RAM): For standard computers (minimum 8GB RAM):
``` ```
@@ -170,6 +68,16 @@ For high-end systems (32GB+ RAM):
``` ```
ollama run qwen2.5-coder:32b ollama run qwen2.5-coder:32b
``` ```
4. Download the QodeAssist plugin for your QtCreator.
5. Launch Qt Creator and install the plugin:
- Go to MacOS: Qt Creator -> About Plugins...
Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure Plugin
QodeAssist comes with default settings that should work immediately after installing a language model. The plugin is pre-configured to use Ollama with standard templates, so you may only need to verify the settings.
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS) 1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "Qode Assist" tab 2. Navigate to the "Qode Assist" tab
@@ -177,69 +85,65 @@ ollama run qwen2.5-coder:32b
- Ollama is selected as your LLM provider - Ollama is selected as your LLM provider
- The URL is set to http://localhost:11434 - The URL is set to http://localhost:11434
- Your installed model appears in the model selection - Your installed model appears in the model selection
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct - The prompt template is Ollama Auto FIM
4. Click Apply if you made any changes 4. Click Apply if you made any changes
You're all set! QodeAssist is now ready to use in Qt Creator. You're all set! QodeAssist is now ready to use in Qt Creator.
<details>
<summary>Example of Ollama settings: (click to expand)</summary>
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
</details>
## Configure for llama.cpp ## Supported LLM Providers
1. Open Qt Creator settings and navigate to the QodeAssist section QodeAssist currently supports the following LLM (Large Language Model) providers:
2. Go to General tab and configure: - [Ollama](https://ollama.com)
- Set "llama.cpp" as the provider for code completion or/and chat assistant - [LM Studio](https://lmstudio.ai) (experimental)
- Set the llama.cpp URL (e.g. http://localhost:8080) - OpenAI compatible providers (experimental)
- Fill in model name
- Choose template for model(e.g. llama.cpp FIM for any model with FIM support)
<details>
<summary>Example of llama.cpp settings: (click to expand)</summary>
<img width="829" alt="llama.cpp Settings" src="https://github.com/user-attachments/assets/8c75602c-60f3-49ed-a7a9-d3c972061ea2" />
</details>
## System Prompt Configuration ## Recommended Models:
QodeAssist has been thoroughly tested and optimized for use with the following language models:
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings. - Qwen2.5-coder
- CodeLlama
- StarCoder2
- DeepSeek-Coder-V2
## File Context Features ### Ollama:
### For autocomplete(FIM)
```
ollama run codellama:7b-code
ollama run starcoder2:7b
ollama run qwen2.5-coder:7b-base
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
```
### For chat
```
ollama run codellama:7b-instruct
ollama run starcoder2:instruct
ollama run qwen2.5-coder:7b-instruct
ollama run deepseek-coder-v2
```
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant. ### Template-Model Compatibility
### Attached Files | Template | Compatible Models | Purpose |
|----------|------------------|----------|
| CodeLlama FIM | `codellama:code` | Code completion |
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
| Ollama Auto FIM | `Any Ollama base model` | Code completion |
| Qwen FIM | `Qwen 2.5 models` | Code completion |
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
| Alpaca | `starcoder2:instruct` | Chat assistance |
| Basic Chat| `Messages without tokens` | Chat assistance |
| ChatML | `Qwen 2.5 models` | Chat assistance |
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
| Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat model` | Chat assistance |
Attachments are designed for one-time code analysis and specific queries: > Note:
- Files are included only in the current message > - FIM (Fill-in-Middle) templates are optimized for code completion
- Content is discarded after the message is processed > - Chat templates are designed for interactive dialogue
- Ideal for: > - The Ollama Auto templates automatically adapt to most Ollama models
- Getting specific feedback on code changes > - Custom Template allows you to define your own prompt format
- Code review requests
- Analyzing isolated code segments
- Quick implementation questions
- Files can be attached using the paperclip icon in the chat interface
- Multiple files can be attached to a single message
### Linked Files
Linked files provide persistent context throughout the conversation:
- Files remain accessible for the entire chat session
- Content is included in every message exchange
- Files are automatically refreshed - always using latest content from disk
- Perfect for:
- Long-term refactoring discussions
- Complex architectural changes
- Multi-file implementations
- Maintaining context across related questions
- Can be managed using the link icon in the chat interface
- Supports automatic syncing with open editor files (can be enabled in settings)
- Files can be added/removed at any time during the conversation
## QtCreator Version Compatibility ## QtCreator Version Compatibility
- QtCreator 16.0.0 - 0.5.2 - 0.5.x
- QtCreator 15.0.1 - 0.4.8 - 0.5.1
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
- QtCreator 14.0.2 - 0.2.3 - 0.3.x - QtCreator 14.0.2 - 0.2.3 - 0.3.x
- QtCreator 14.0.1 - 0.2.2 plugin version and below - QtCreator 14.0.1 - 0.2.2 plugin version and below
@@ -258,7 +162,9 @@ Linked files provide persistent context throughout the conversation:
- on Mac: Option + Command + Q - on Mac: Option + Command + Q
- on Windows: Ctrl + Alt + Q - on Windows: Ctrl + Alt + Q
- To insert the full suggestion, you can use the TAB key - To insert the full suggestion, you can use the TAB key
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac - To insert line by line, you can use the "Move cursor word right" shortcut:
- On Mac: Option + Right Arrow
- On Windows: Alt + Right Arrow
## Troubleshooting ## Troubleshooting
@@ -269,14 +175,17 @@ If QodeAssist is having problems connecting to the LLM provider, please check th
- For Ollama, the default is usually http://localhost:11434 - For Ollama, the default is usually http://localhost:11434
- For LM Studio, the default is usually http://localhost:1234 - For LM Studio, the default is usually http://localhost:1234
2. Confirm that the selected model and template are compatible: 2. Check the endpoint:
Make sure the endpoint in the settings matches the one required by your provider
- For Ollama, it should be /api/generate
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
3. Confirm that the selected model and template are compatible:
Ensure you've chosen the correct model in the "Select Models" option Ensure you've chosen the correct model in the "Select Models" option
Verify that the selected prompt template matches the model you're using Verify that the selected prompt template matches the model you're using
3. On Linux the prebuilt binaries support only ubuntu 22.04+ or simililliar os.
If you need compatiblity with another os, you have to build manualy. our experiments and resolution you can check here: https://github.com/Palm1r/QodeAssist/issues/48
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values: If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
1. Open Qt Creator settings 1. Open Qt Creator settings
2. Navigate to the "Qode Assist" tab 2. Navigate to the "Qode Assist" tab
@@ -318,7 +227,3 @@ relative or absolute path to this plugin directory.
QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
C++ code style: check use .clang-fortmat in project C++ code style: check use .clang-fortmat in project
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

View File

@@ -1,72 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "UpdateStatusWidget.hpp"
namespace QodeAssist {
UpdateStatusWidget::UpdateStatusWidget(QWidget *parent)
: QFrame(parent)
{
setFrameStyle(QFrame::NoFrame);
auto layout = new QHBoxLayout(this);
layout->setContentsMargins(4, 0, 4, 0);
layout->setSpacing(4);
m_actionButton = new QToolButton(this);
m_actionButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_versionLabel = new QLabel(this);
m_versionLabel->setVisible(false);
m_updateButton = new QPushButton(tr("Update"), this);
m_updateButton->setVisible(false);
m_updateButton->setStyleSheet("QPushButton { padding: 2px 8px; }");
layout->addWidget(m_actionButton);
layout->addWidget(m_versionLabel);
layout->addWidget(m_updateButton);
}
void UpdateStatusWidget::setDefaultAction(QAction *action)
{
m_actionButton->setDefaultAction(action);
}
void UpdateStatusWidget::showUpdateAvailable(const QString &version)
{
m_versionLabel->setText(tr("new version: v%1").arg(version));
m_versionLabel->setVisible(true);
m_updateButton->setVisible(true);
m_updateButton->setToolTip(tr("Update QodeAssist to version %1").arg(version));
}
void UpdateStatusWidget::hideUpdateInfo()
{
m_versionLabel->setVisible(false);
m_updateButton->setVisible(false);
}
QPushButton *UpdateStatusWidget::updateButton() const
{
return m_updateButton;
}
} // namespace QodeAssist

View File

@@ -1,28 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QtGlobal>
#define QODEASSIST_QT_CREATOR_VERSION \
QT_VERSION_CHECK( \
QODEASSIST_QT_CREATOR_VERSION_MAJOR, \
QODEASSIST_QT_CREATOR_VERSION_MINOR, \
QODEASSIST_QT_CREATOR_VERSION_PATCH)

View File

@@ -23,15 +23,16 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
NavigationPanel::NavigationPanel() NavigationPanel::NavigationPanel() {
{
setDisplayName(tr("QodeAssist Chat")); setDisplayName(tr("QodeAssist Chat"));
setPriority(500); setPriority(500);
setId("QodeAssistChat"); setId("QodeAssistChat");
setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C)); setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C));
} }
NavigationPanel::~NavigationPanel() {} NavigationPanel::~NavigationPanel()
{
}
Core::NavigationView NavigationPanel::createWidget() Core::NavigationView NavigationPanel::createWidget()
{ {
@@ -41,4 +42,4 @@ Core::NavigationView NavigationPanel::createWidget()
return view; return view;
} }
} // namespace QodeAssist::Chat }

View File

@@ -19,8 +19,8 @@
#pragma once #pragma once
#include <coreplugin/inavigationwidgetfactory.h>
#include <QObject> #include <QObject>
#include <coreplugin/inavigationwidgetfactory.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -34,4 +34,4 @@ public:
Core::NavigationView createWidget() override; Core::NavigationView createWidget() override;
}; };
} // namespace QodeAssist::Chat }

View File

@@ -1,24 +0,0 @@
add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp
ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp
ContentFile.hpp
DocumentReaderQtCreator.hpp
IDocumentReader.hpp
TokenUtils.hpp TokenUtils.cpp
ProgrammingLanguage.hpp ProgrammingLanguage.cpp
)
target_link_libraries(Context
PUBLIC
Qt::Core
QtCreator::Core
QtCreator::TextEditor
QtCreator::Utils
QtCreator::ProjectExplorer
PRIVATE
LLMCore
QodeAssistSettings
)
target_include_directories(Context PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR})

View File

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

View File

@@ -1,91 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ContextManager.hpp"
#include <QFile>
#include <QFileInfo>
#include <QJsonObject>
#include <QTextStream>
#include "GeneralSettings.hpp"
#include "Logger.hpp"
namespace QodeAssist::Context {
ContextManager &ContextManager::instance()
{
static ContextManager manager;
return manager;
}
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
{}
QString ContextManager::readFile(const QString &filePath) const
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return QString();
QTextStream in(&file);
return in.readAll();
}
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
{
QList<ContentFile> files;
for (const QString &path : filePaths) {
ContentFile contentFile = createContentFile(path);
files.append(contentFile);
}
return files;
}
ContentFile ContextManager::createContentFile(const QString &filePath) const
{
ContentFile contentFile;
QFileInfo fileInfo(filePath);
contentFile.filename = fileInfo.fileName();
contentFile.content = readFile(filePath);
return contentFile;
}
ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &documentInfo)
{
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available for" + documentInfo.filePath);
return Context::ProgrammingLanguage::Unknown;
}
return Context::ProgrammingLanguageUtils::fromMimeType(documentInfo.mimeType);
}
bool ContextManager::isSpecifyCompletion(
const DocumentInfo &documentInfo, const Settings::GeneralSettings &generalSettings)
{
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
}
} // namespace QodeAssist::Context

View File

@@ -1,53 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QString>
#include "ContentFile.hpp"
#include "IDocumentReader.hpp"
#include "ProgrammingLanguage.hpp"
#include "settings/GeneralSettings.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;
static ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo);
static bool isSpecifyCompletion(
const DocumentInfo &documentInfo, const Settings::GeneralSettings &generalSettings);
private:
explicit ContextManager(QObject *parent = nullptr);
~ContextManager() = default;
ContextManager(const ContextManager &) = delete;
ContextManager &operator=(const ContextManager &) = delete;
ContentFile createContentFile(const QString &filePath) const;
};
} // namespace QodeAssist::Context

View File

@@ -1,284 +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 "DocumentContextReader.hpp"
#include <languageserverprotocol/lsptypes.h>
#include <QFileInfo>
#include <QTextBlock>
#include "CodeCompletionSettings.hpp"
#include "ChangesManager.h"
const QRegularExpression &getYearRegex()
{
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
return yearRegex;
}
const QRegularExpression &getNameRegex()
{
static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b");
return nameRegex;
}
const QRegularExpression &getCommentRegex()
{
static const QRegularExpression commentRegex(
R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))", QRegularExpression::MultilineOption);
return commentRegex;
}
namespace QodeAssist::Context {
DocumentContextReader::DocumentContextReader(
QTextDocument *document, const QString &mimeType, const QString &filePath)
: m_document(document)
, m_mimeType(mimeType)
, m_filePath(filePath)
{
m_copyrightInfo = findCopyright();
}
QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) const
{
if (!m_document || lineNumber < 0)
return QString();
QTextBlock block = m_document->begin();
int currentLine = 0;
while (block.isValid()) {
if (currentLine == lineNumber) {
QString text = block.text();
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
text = text.left(cursorPosition);
}
return text;
}
block = block.next();
currentLine++;
}
return QString();
}
QString DocumentContextReader::getContextBefore(
int lineNumber, int cursorPosition, int linesCount) const
{
int startLine = lineNumber - linesCount + 1;
if (m_copyrightInfo.found) {
startLine = qMax(m_copyrightInfo.endLine + 1, startLine);
}
return getContextBetween(startLine, -1, lineNumber, cursorPosition);
}
QString DocumentContextReader::getContextAfter(
int lineNumber, int cursorPosition, int linesCount) const
{
int endLine = lineNumber + linesCount - 1;
if (m_copyrightInfo.found && m_copyrightInfo.endLine >= lineNumber) {
lineNumber = m_copyrightInfo.endLine + 1;
cursorPosition = -1;
}
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
}
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
{
int startLine = 0;
if (m_copyrightInfo.found) {
startLine = m_copyrightInfo.endLine + 1;
}
return getContextBetween(startLine, -1, lineNumber, cursorPosition);
}
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
{
int endLine = m_document->blockCount() - 1;
if (m_copyrightInfo.found && m_copyrightInfo.endLine >= lineNumber) {
lineNumber = m_copyrightInfo.endLine + 1;
cursorPosition = -1;
}
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
}
QString DocumentContextReader::getLanguageAndFileInfo() const
{
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_mimeType);
QString fileExtension = QFileInfo(m_filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, m_mimeType, m_filePath, fileExtension);
}
CopyrightInfo DocumentContextReader::findCopyright()
{
CopyrightInfo result = {-1, -1, false};
QString text = m_document->toPlainText();
QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text);
QList<CopyrightInfo> copyrightBlocks;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower();
if (matchedText.contains("copyright") || matchedText.contains("(C)")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
int startPos = match.capturedStart();
int endPos = match.capturedEnd();
CopyrightInfo info;
info.startLine = m_document->findBlock(startPos).blockNumber();
info.endLine = m_document->findBlock(endPos).blockNumber();
info.found = true;
copyrightBlocks.append(info);
}
}
for (int i = 0; i < copyrightBlocks.size() - 1; ++i) {
if (copyrightBlocks[i].endLine + 1 >= copyrightBlocks[i + 1].startLine) {
copyrightBlocks[i].endLine = copyrightBlocks[i + 1].endLine;
copyrightBlocks.removeAt(i + 1);
--i;
}
}
if (!copyrightBlocks.isEmpty()) { // temproary solution, need cache
return copyrightBlocks.first();
}
return result;
}
QString DocumentContextReader::getContextBetween(
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const
{
QString context;
startLine = qMax(startLine, 0);
endLine = qMin(endLine, m_document->blockCount() - 1);
if (startLine > endLine) {
return context;
}
if (startLine == endLine) {
auto block = m_document->findBlockByNumber(startLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (startCursorPosition < 0) {
startCursorPosition = 0;
}
if (endCursorPosition < 0) {
endCursorPosition = text.size();
}
if (startCursorPosition >= endCursorPosition) {
return context;
}
return text.mid(startCursorPosition, endCursorPosition - startCursorPosition);
}
// first line
{
auto block = m_document->findBlockByNumber(startLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (startCursorPosition < 0) {
context += text + "\n";
} else {
context += text.right(text.size() - startCursorPosition) + "\n";
}
}
// intermediate lines, if any
for (int i = startLine + 1; i <= endLine - 1; ++i) {
auto block = m_document->findBlockByNumber(i);
if (!block.isValid()) {
return context;
}
context += block.text() + "\n";
}
// last line
{
auto block = m_document->findBlockByNumber(endLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (endCursorPosition < 0) {
context += text;
} else {
context += text.left(endCursorPosition);
}
}
return context;
}
CopyrightInfo DocumentContextReader::copyrightInfo() const
{
return m_copyrightInfo;
}
LLMCore::ContextData DocumentContextReader::prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
{
QString contextBefore;
QString contextAfter;
if (settings.readFullFile()) {
contextBefore = readWholeFileBefore(lineNumber, cursorPosition);
contextAfter = readWholeFileAfter(lineNumber, cursorPosition);
} else {
// Note that readStrings{After,Before}Cursor include current line, but linesCount argument of
// getContext{After,Before} do not
contextBefore
= getContextBefore(lineNumber, cursorPosition, settings.readStringsBeforeCursor() + 1);
contextAfter
= getContextAfter(lineNumber, cursorPosition, settings.readStringsAfterCursor() + 1);
}
QString fileContext;
fileContext.append("\n ").append(getLanguageAndFileInfo());
if (settings.useProjectChangesCache())
fileContext.append("Recent Project Changes Context:\n ")
.append(ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext};
}
} // namespace QodeAssist::Context

View File

@@ -1,45 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas
*
* 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 "IDocumentReader.hpp"
#include <texteditor/textdocument.h>
namespace QodeAssist::Context {
class DocumentReaderQtCreator : public IDocumentReader
{
public:
DocumentInfo readDocument(const QString &path) const override
{
auto *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
Utils::FilePath::fromString(path));
if (!textDocument) {
return {};
}
return {
.document = textDocument->document(),
.mimeType = textDocument->mimeType(),
.filePath = path};
}
};
} // namespace QodeAssist::Context

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas
*
* 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 <QTextDocument>
namespace QodeAssist::Context {
struct DocumentInfo
{
QTextDocument *document = nullptr; // not owned
QString mimeType;
QString filePath;
};
class IDocumentReader
{
public:
virtual ~IDocumentReader() = default;
virtual DocumentInfo readDocument(const QString &path) const = 0;
};
} // namespace QodeAssist::Context

View File

@@ -1,70 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ProgrammingLanguage.hpp"
namespace QodeAssist::Context {
ProgrammingLanguage ProgrammingLanguageUtils::fromMimeType(const QString &mimeType)
{
if (mimeType == "text/x-qml" || mimeType == "application/javascript"
|| mimeType == "text/javascript" || mimeType == "text/x-javascript") {
return ProgrammingLanguage::QML;
}
if (mimeType == "text/x-c++src" || mimeType == "text/x-c++hdr" || mimeType == "text/x-csrc"
|| mimeType == "text/x-chdr") {
return ProgrammingLanguage::Cpp;
}
if (mimeType == "text/x-python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
QString ProgrammingLanguageUtils::toString(ProgrammingLanguage language)
{
switch (language) {
case ProgrammingLanguage::Cpp:
return "c/c++";
case ProgrammingLanguage::QML:
return "qml";
case ProgrammingLanguage::Python:
return "python";
case ProgrammingLanguage::Unknown:
default:
return QString();
}
}
ProgrammingLanguage ProgrammingLanguageUtils::fromString(const QString &str)
{
QString lower = str.toLower();
if (lower == "c/c++") {
return ProgrammingLanguage::Cpp;
}
if (lower == "qml") {
return ProgrammingLanguage::QML;
}
if (lower == "python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
} // namespace QodeAssist::Context

View File

@@ -1,43 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
namespace QodeAssist::Context {
enum class ProgrammingLanguage {
QML, // QML/JavaScript
Cpp, // C/C++
Python,
Unknown,
};
namespace ProgrammingLanguageUtils {
ProgrammingLanguage fromMimeType(const QString &mimeType);
QString toString(ProgrammingLanguage language);
ProgrammingLanguage fromString(const QString &str);
} // namespace ProgrammingLanguageUtils
} // namespace QodeAssist::Context

View File

@@ -1,54 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "TokenUtils.hpp"
namespace QodeAssist::Context {
int TokenUtils::estimateTokens(const QString &text)
{
if (text.isEmpty()) {
return 0;
}
// TODO: need to improve
return text.length() / 4;
}
int TokenUtils::estimateFileTokens(const Context::ContentFile &file)
{
int total = 0;
total += estimateTokens(file.filename);
total += estimateTokens(file.content);
total += 5;
return total;
}
int TokenUtils::estimateFilesTokens(const QList<Context::ContentFile> &files)
{
int total = 0;
for (const auto &file : files) {
total += estimateFileTokens(file);
}
return total;
}
} // namespace QodeAssist::Context

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
namespace QodeAssist::Context {
inline QString extractFilePathFromRequest(const QJsonObject &request)
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QString uri = doc["uri"].toString();
return QUrl(uri).toLocalFile();
}
} // namespace QodeAssist::Context

View File

@@ -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()
{ {
@@ -30,12 +30,17 @@ ChangesManager &ChangesManager::instance()
ChangesManager::ChangesManager() ChangesManager::ChangesManager()
: QObject(nullptr) : QObject(nullptr)
{} {
}
ChangesManager::~ChangesManager() {} ChangesManager::~ChangesManager()
{
}
void ChangesManager::addChange( void ChangesManager::addChange(TextEditor::TextDocument *document,
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded) int position,
int charsRemoved,
int charsAdded)
{ {
auto &documentQueue = m_documentChanges[document]; auto &documentQueue = m_documentChanges[document];
@@ -46,10 +51,9 @@ void ChangesManager::addChange(
ChangeInfo change{fileName, lineNumber, lineContent}; ChangeInfo change{fileName, lineNumber, lineContent};
auto it auto it = std::find_if(documentQueue.begin(),
= std::find_if(documentQueue.begin(), documentQueue.end(), [lineNumber](const ChangeInfo &c) { documentQueue.end(),
return c.lineNumber == lineNumber; [lineNumber](const ChangeInfo &c) { return c.lineNumber == lineNumber; });
});
if (it != documentQueue.end()) { if (it != documentQueue.end()) {
it->lineContent = lineContent; it->lineContent = lineContent;
@@ -75,4 +79,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
return context; return context;
} }
} // namespace QodeAssist::Context } // namespace QodeAssist

View File

@@ -19,13 +19,13 @@
#pragma once #pragma once
#include <texteditor/textdocument.h>
#include <QDateTime> #include <QDateTime>
#include <QHash> #include <QHash>
#include <QQueue> #include <QQueue>
#include <QTimer> #include <QTimer>
#include <texteditor/textdocument.h>
namespace QodeAssist::Context { namespace QodeAssist {
class ChangesManager : public QObject class ChangesManager : public QObject
{ {
@@ -41,8 +41,10 @@ public:
static ChangesManager &instance(); static ChangesManager &instance();
void addChange( void addChange(TextEditor::TextDocument *document,
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded); int position,
int charsRemoved,
int charsAdded);
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const; QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
private: private:
@@ -56,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

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

View File

@@ -20,29 +20,14 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QVector>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
struct Message
{
QString role;
QString content;
// clang-format off
bool operator==(const Message&) const = default;
// clang-format on
};
struct ContextData struct ContextData
{ {
std::optional<QString> systemPrompt; QString prefix;
std::optional<QString> prefix; QString suffix;
std::optional<QString> suffix; QString fileContext;
std::optional<QString> fileContext;
std::optional<QVector<Message>> history;
bool operator==(const ContextData &) const = default;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "PromptTemplate.hpp"
#include <QString>
namespace QodeAssist::LLMCore {
class IPromptProvider
{
public:
virtual ~IPromptProvider() = default;
virtual PromptTemplate *getTemplateByName(const QString &templateName) const = 0;
virtual QStringList templatesNames() const = 0;
virtual QStringList getTemplatesForProvider(ProviderID id) const = 0;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "Provider.hpp"
namespace QodeAssist::LLMCore {
class IProviderRegistry
{
public:
virtual ~IProviderRegistry() = default;
virtual Provider *getProviderByName(const QString &providerName) = 0;
virtual QStringList providersNames() const = 0;
};
} // namespace QodeAssist::LLMCore

View File

@@ -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,53 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplate.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderChat : public IPromptProvider
{
public:
explicit PromptProviderChat(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderChat() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getChatTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.chatTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getChatTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,52 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderFim : public IPromptProvider
{
public:
explicit PromptProviderFim(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderFim() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getFimTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.fimTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getFimTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

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

View File

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

View File

@@ -35,11 +35,13 @@ public:
template<typename T> template<typename T>
void registerTemplate() void registerTemplate()
{ {
static_assert(std::is_base_of<PromptTemplate, T>::value, "T must inherit from PromptTemplate"); static_assert(std::is_base_of<PromptTemplate, T>::value,
"T must inherit from PromptTemplate");
T *template_ptr = new T(); T *template_ptr = new T();
QString name = template_ptr->name(); QString name = template_ptr->name();
if (template_ptr->type() == TemplateType::Fim) {
m_fimTemplates[name] = template_ptr; m_fimTemplates[name] = template_ptr;
if (template_ptr->type() == TemplateType::Chat) { } else if (template_ptr->type() == TemplateType::Chat) {
m_chatTemplates[name] = template_ptr; m_chatTemplates[name] = template_ptr;
} }
} }
@@ -50,9 +52,6 @@ public:
QStringList fimTemplatesNames() const; QStringList fimTemplatesNames() const;
QStringList chatTemplatesNames() const; QStringList chatTemplatesNames() const;
QStringList getFimTemplatesForProvider(ProviderID id);
QStringList getChatTemplatesForProvider(ProviderID id);
private: private:
PromptTemplateManager() = default; PromptTemplateManager() = default;
PromptTemplateManager(const PromptTemplateManager &) = delete; PromptTemplateManager(const PromptTemplateManager &) = delete;

View File

@@ -19,13 +19,9 @@
#pragma once #pragma once
#include <utils/environment.h>
#include <QNetworkRequest>
#include <QString> #include <QString>
#include "ContextData.hpp"
#include "PromptTemplate.hpp"
#include "RequestType.hpp" #include "RequestType.hpp"
#include <utils/environment.h>
class QNetworkReply; class QNetworkReply;
class QJsonObject; class QJsonObject;
@@ -42,18 +38,10 @@ public:
virtual QString completionEndpoint() const = 0; virtual QString completionEndpoint() const = 0;
virtual QString chatEndpoint() const = 0; virtual QString chatEndpoint() const = 0;
virtual bool supportsModelListing() const = 0; virtual bool supportsModelListing() const = 0;
virtual void prepareRequest(
QJsonObject &request, virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
= 0;
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0; virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0;
virtual QList<QString> getInstalledModels(const QString &url) = 0; virtual QList<QString> getInstalledModels(const QString &url) = 0;
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
virtual QString apiKey() const = 0;
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
virtual ProviderID providerID() const = 0;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
namespace QodeAssist::LLMCore {
enum class ProviderID {
Any,
Ollama,
LMStudio,
Claude,
OpenAI,
OpenAICompatible,
MistralAI,
OpenRouter,
GoogleAI,
LlamaCpp
};
}

View File

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

View File

@@ -21,12 +21,12 @@
#include <QString> #include <QString>
#include "IProviderRegistry.hpp"
#include <QMap> #include <QMap>
#include "Provider.hpp"
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
class ProvidersManager : public IProviderRegistry class ProvidersManager
{ {
public: public:
static ProvidersManager &instance(); static ProvidersManager &instance();
@@ -41,9 +41,9 @@ public:
m_providers[name] = provider; m_providers[name] = provider;
} }
Provider *getProviderByName(const QString &providerName) override; Provider *getProviderByName(const QString &providerName);
QStringList providersNames() const override; QStringList providersNames() const;
private: private:
ProvidersManager() = default; ProvidersManager() = default;

View File

@@ -19,11 +19,11 @@
#pragma once #pragma once
#include <QJsonObject>
#include <QUrl>
#include "PromptTemplate.hpp" #include "PromptTemplate.hpp"
#include "Provider.hpp" #include "Provider.hpp"
#include "RequestType.hpp" #include "RequestType.hpp"
#include <QJsonObject>
#include <QUrl>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {

View File

@@ -26,23 +26,22 @@
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
RequestHandler::RequestHandler(QObject *parent) RequestHandler::RequestHandler(QObject *parent)
: RequestHandlerBase(parent) : QObject(parent)
, m_manager(new QNetworkAccessManager(this)) , m_manager(new QNetworkAccessManager(this))
{} {}
void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request) void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request)
{ {
LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2") LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
.arg( .arg(config.url.toString(),
config.url.toString(),
QString::fromUtf8( QString::fromUtf8(
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 QNetworkReply *reply = m_manager->post(networkRequest,
= m_manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson()); QJsonDocument(config.providerRequest).toJson());
if (!reply) { if (!reply) {
LOG_MESSAGE("Error: Failed to create network reply"); LOG_MESSAGE("Error: Failed to create network reply");
return; return;
@@ -59,10 +58,7 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
reply->deleteLater(); reply->deleteLater();
m_activeRequests.remove(requestId); m_activeRequests.remove(requestId);
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
LOG_MESSAGE(QString("Error details: %1\nStatus code: %2\nResponse: %3") LOG_MESSAGE(QString("Error in QodeAssist request: %1").arg(reply->errorString()));
.arg(reply->errorString())
.arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())
.arg(QString(reply->readAll())));
emit requestFinished(requestId, false, reply->errorString()); emit requestFinished(requestId, false, reply->errorString());
} else { } else {
LOG_MESSAGE("Request finished successfully"); LOG_MESSAGE("Request finished successfully");
@@ -71,22 +67,24 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
}); });
} }
void RequestHandler::handleLLMResponse( void RequestHandler::handleLLMResponse(QNetworkReply *reply,
QNetworkReply *reply, const QJsonObject &request, const LLMConfig &config) const QJsonObject &request,
const LLMConfig &config)
{ {
QString &accumulatedResponse = m_accumulatedResponses[reply]; QString &accumulatedResponse = m_accumulatedResponses[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;
} }
if (isComplete) { if (isComplete) {
auto cleanedCompletion auto cleanedCompletion = removeStopWords(accumulatedResponse,
= removeStopWords(accumulatedResponse, config.promptTemplate->stopWords()); config.promptTemplate->stopWords());
removeCodeBlockWrappers(cleanedCompletion);
emit completionReceived(cleanedCompletion, request, true); emit completionReceived(cleanedCompletion, request, true);
} }
@@ -111,6 +109,16 @@ bool RequestHandler::cancelRequest(const QString &id)
return false; return false;
} }
void RequestHandler::prepareNetworkRequest(
QNetworkRequest &networkRequest, const QString &apiKey) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey.isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());
}
}
bool RequestHandler::processSingleLineCompletion( bool RequestHandler::processSingleLineCompletion(
QNetworkReply *reply, QNetworkReply *reply,
const QJsonObject &request, const QJsonObject &request,
@@ -118,6 +126,7 @@ bool RequestHandler::processSingleLineCompletion(
const LLMConfig &config) const LLMConfig &config)
{ {
QString cleanedResponse = accumulatedResponse; QString cleanedResponse = accumulatedResponse;
removeCodeBlockWrappers(cleanedResponse);
int newlinePos = cleanedResponse.indexOf('\n'); int newlinePos = cleanedResponse.indexOf('\n');
if (newlinePos != -1) { if (newlinePos != -1) {
@@ -143,4 +152,36 @@ QString RequestHandler::removeStopWords(const QStringView &completion, const QSt
return filteredCompletion; return filteredCompletion;
} }
void RequestHandler::removeCodeBlockWrappers(QString &response)
{
static const QRegularExpression
fullCodeBlockRegex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
static const QRegularExpression
partialStartBlockRegex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
static const QRegularExpression
partialEndBlockRegex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
QRegularExpressionMatchIterator matchIterator = fullCodeBlockRegex.globalMatch(response);
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString codeBlock = match.captured(0);
QString codeContent = match.captured(1).trimmed();
response.replace(codeBlock, codeContent);
}
QRegularExpressionMatch startMatch = partialStartBlockRegex.match(response);
if (startMatch.hasMatch()) {
QString partialBlock = startMatch.captured(0);
QString codeContent = startMatch.captured(1).trimmed();
response.replace(partialBlock, codeContent);
}
QRegularExpressionMatch endMatch = partialEndBlockRegex.match(response);
if (endMatch.hasMatch()) {
QString partialBlock = endMatch.captured(0);
QString codeContent = endMatch.captured(1).trimmed();
response.replace(partialBlock, codeContent);
}
}
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -24,32 +24,41 @@
#include <QObject> #include <QObject>
#include "RequestConfig.hpp" #include "RequestConfig.hpp"
#include "RequestHandlerBase.hpp"
class QNetworkReply; class QNetworkReply;
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
class RequestHandler : public RequestHandlerBase class RequestHandler : public QObject
{ {
Q_OBJECT
public: public:
explicit RequestHandler(QObject *parent = nullptr); explicit RequestHandler(QObject *parent = nullptr);
void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) override; void sendLLMRequest(const LLMConfig &config, const QJsonObject &request);
bool cancelRequest(const QString &id) override; void handleLLMResponse(QNetworkReply *reply,
void handleLLMResponse(QNetworkReply *reply, const QJsonObject &request, const LLMConfig &config); const QJsonObject &request,
const LLMConfig &config);
bool cancelRequest(const QString &id);
signals:
void completionReceived(const QString &completion, const QJsonObject &request, bool isComplete);
void requestFinished(const QString &requestId, bool success, const QString &errorString);
void requestCancelled(const QString &id);
private: private:
QNetworkAccessManager *m_manager; QNetworkAccessManager *m_manager;
QMap<QString, QNetworkReply *> m_activeRequests; QMap<QString, QNetworkReply *> m_activeRequests;
QMap<QNetworkReply *, QString> m_accumulatedResponses; QMap<QNetworkReply *, QString> m_accumulatedResponses;
bool processSingleLineCompletion( void prepareNetworkRequest(QNetworkRequest &networkRequest, const QString &apiKey) const;
QNetworkReply *reply, bool processSingleLineCompletion(QNetworkReply *reply,
const QJsonObject &request, const QJsonObject &request,
const QString &accumulatedResponse, const QString &accumulatedResponse,
const LLMConfig &config); const LLMConfig &config);
QString removeStopWords(const QStringView &completion, const QStringList &stopWords); QString removeStopWords(const QStringView &completion, const QStringList &stopWords);
void removeCodeBlockWrappers(QString &response);
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,30 +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 "RequestHandlerBase.hpp"
namespace QodeAssist::LLMCore {
RequestHandlerBase::RequestHandlerBase(QObject *parent)
: QObject(parent)
{}
RequestHandlerBase::~RequestHandlerBase() = default;
} // namespace QodeAssist::LLMCore

View File

@@ -1,45 +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 "RequestConfig.hpp"
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::LLMCore {
class RequestHandlerBase : public QObject
{
Q_OBJECT
public:
explicit RequestHandlerBase(QObject *parent = nullptr);
~RequestHandlerBase() override;
virtual void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) = 0;
virtual bool cancelRequest(const QString &id) = 0;
signals:
void completionReceived(const QString &completion, const QJsonObject &request, bool isComplete);
void requestFinished(const QString &requestId, bool success, const QString &errorString);
void requestCancelled(const QString &id);
};
} // namespace QodeAssist::LLMCore

View File

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

View File

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

View File

@@ -1,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,9 +1,6 @@
add_library(QodeAssistLogger STATIC add_library(QodeAssistLogger STATIC
EmptyRequestPerformanceLogger.hpp
IRequestPerformanceLogger.hpp
Logger.cpp Logger.cpp
Logger.hpp Logger.hpp
RequestPerformanceLogger.hpp RequestPerformanceLogger.cpp
) )
target_link_libraries(QodeAssistLogger target_link_libraries(QodeAssistLogger

View File

@@ -1,32 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "IRequestPerformanceLogger.hpp"
namespace QodeAssist {
class EmptyRequestPerformanceLogger : public IRequestPerformanceLogger
{
public:
void startTimeMeasurement(const QString &requestId) override {}
void endTimeMeasurement(const QString &requestId) override {}
void logPerformance(const QString &requestId, qint64 elapsedMs) override {}
};
} // namespace QodeAssist

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
namespace QodeAssist {
class IRequestPerformanceLogger
{
public:
virtual ~IRequestPerformanceLogger() = default;
virtual void startTimeMeasurement(const QString &requestId) = 0;
virtual void endTimeMeasurement(const QString &requestId) = 0;
virtual void logPerformance(const QString &requestId, qint64 elapsedMs) = 0;
};
} // namespace QodeAssist

View File

@@ -1,55 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "RequestPerformanceLogger.hpp"
#include "Logger.hpp"
namespace QodeAssist {
void RequestPerformanceLogger::startTimeMeasurement(const QString &requestId)
{
m_requestStartTimes[requestId] = QDateTime::currentMSecsSinceEpoch();
}
void RequestPerformanceLogger::endTimeMeasurement(const QString &requestId)
{
if (!m_requestStartTimes.contains(requestId)) {
return;
}
qint64 startTime = m_requestStartTimes[requestId];
qint64 endTime = QDateTime::currentMSecsSinceEpoch();
qint64 totalTime = endTime - startTime;
logPerformance(requestId, totalTime);
m_requestStartTimes.remove(requestId);
}
void RequestPerformanceLogger::logPerformance(const QString &requestId, qint64 elapsedMs)
{
LOG_MESSAGE(
QString("Performance: %1 total completion time took %2 ms").arg(requestId).arg(elapsedMs));
}
void RequestPerformanceLogger::logPerformance(
const QString &requestId, const QString &operation, qint64 elapsedMs)
{
LOG_MESSAGE(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
}
} // namespace QodeAssist

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "IRequestPerformanceLogger.hpp"
#include <QDateTime>
#include <QMap>
namespace QodeAssist {
class RequestPerformanceLogger : public IRequestPerformanceLogger
{
public:
RequestPerformanceLogger() = default;
~RequestPerformanceLogger() override = default;
void startTimeMeasurement(const QString &requestId) override;
void endTimeMeasurement(const QString &requestId) override;
void logPerformance(const QString &requestId, qint64 elapsedMs) override;
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
private:
QMap<QString, qint64> m_requestStartTimes;
};
} // namespace QodeAssist

View File

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

View File

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

View File

@@ -1,303 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "GoogleAIProvider.hpp"
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QtCore/qurlquery.h>
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {
QString GoogleAIProvider::name() const
{
return "Google AI";
}
QString GoogleAIProvider::url() const
{
return "https://generativelanguage.googleapis.com/v1beta";
}
QString GoogleAIProvider::completionEndpoint() const
{
return {};
}
QString GoogleAIProvider::chatEndpoint() const
{
return {};
}
bool GoogleAIProvider::supportsModelListing() const
{
return true;
}
void GoogleAIProvider::prepareRequest(
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
QJsonObject generationConfig;
generationConfig["maxOutputTokens"] = settings.maxTokens();
generationConfig["temperature"] = settings.temperature();
if (settings.useTopP())
generationConfig["topP"] = settings.topP();
if (settings.useTopK())
generationConfig["topK"] = settings.topK();
request["generationConfig"] = generationConfig;
};
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool GoogleAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
if (reply->isFinished()) {
if (reply->bytesAvailable() > 0) {
QByteArray data = reply->readAll();
if (data.startsWith("data: ")) {
return handleStreamResponse(data, accumulatedResponse);
} else {
return handleRegularResponse(data, accumulatedResponse);
}
}
return true;
}
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
if (data.startsWith("data: ")) {
return handleStreamResponse(data, accumulatedResponse);
} else {
return handleRegularResponse(data, accumulatedResponse);
}
}
QList<QString> GoogleAIProvider::getInstalledModels(const QString &url)
{
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/models?key=%2").arg(url, apiKey()));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("models")) {
QJsonArray modelArray = jsonObject["models"].toArray();
models.clear();
for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject();
if (modelObject.contains("name")) {
QString modelName = modelObject["name"].toString();
if (modelName.contains("/")) {
modelName = modelName.split("/").last();
}
models.append(modelName);
}
}
}
} else {
LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> GoogleAIProvider::validateRequest(
const QJsonObject &request, LLMCore::TemplateType type)
{
QJsonObject templateReq;
templateReq = QJsonObject{
{"contents", QJsonArray{}},
{"system_instruction", QJsonArray{}},
{"generationConfig",
QJsonObject{{"temperature", {}}, {"maxOutputTokens", {}}, {"topP", {}}, {"topK", {}}}},
{"safetySettings", QJsonArray{}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString GoogleAIProvider::apiKey() const
{
return Settings::providerSettings().googleAiApiKey();
}
void GoogleAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl url = networkRequest.url();
QUrlQuery query(url.query());
query.addQueryItem("key", apiKey());
url.setQuery(query);
networkRequest.setUrl(url);
}
LLMCore::ProviderID GoogleAIProvider::providerID() const
{
return LLMCore::ProviderID::GoogleAI;
}
bool GoogleAIProvider::handleStreamResponse(const QByteArray &data, QString &accumulatedResponse)
{
QByteArrayList lines = data.split('\n');
bool isDone = false;
for (const QByteArray &line : lines) {
QByteArray trimmedLine = line.trimmed();
if (trimmedLine.isEmpty()) {
continue;
}
if (trimmedLine == "data: [DONE]") {
isDone = true;
continue;
}
if (trimmedLine.startsWith("data: ")) {
QByteArray jsonData = trimmedLine.mid(6); // Remove "data: " prefix
QJsonDocument doc = QJsonDocument::fromJson(jsonData);
if (doc.isNull() || !doc.isObject()) {
continue;
}
QJsonObject responseObj = doc.object();
if (responseObj.contains("error")) {
QJsonObject error = responseObj["error"].toObject();
LOG_MESSAGE("Error in Google AI stream response: " + error["message"].toString());
continue;
}
if (responseObj.contains("candidates")) {
QJsonArray candidates = responseObj["candidates"].toArray();
if (!candidates.isEmpty()) {
QJsonObject candidate = candidates.first().toObject();
if (candidate.contains("finishReason")
&& !candidate["finishReason"].toString().isEmpty()) {
isDone = true;
}
if (candidate.contains("content")) {
QJsonObject content = candidate["content"].toObject();
if (content.contains("parts")) {
QJsonArray parts = content["parts"].toArray();
for (const auto &part : parts) {
QJsonObject partObj = part.toObject();
if (partObj.contains("text")) {
accumulatedResponse += partObj["text"].toString();
}
}
}
}
}
}
}
}
return isDone;
}
bool GoogleAIProvider::handleRegularResponse(const QByteArray &data, QString &accumulatedResponse)
{
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull() || !doc.isObject()) {
LOG_MESSAGE("Invalid JSON response from Google AI API");
return false;
}
QJsonObject response = doc.object();
if (response.contains("error")) {
QJsonObject error = response["error"].toObject();
LOG_MESSAGE("Error in Google AI response: " + error["message"].toString());
return false;
}
if (!response.contains("candidates") || response["candidates"].toArray().isEmpty()) {
return false;
}
QJsonObject candidate = response["candidates"].toArray().first().toObject();
if (!candidate.contains("content")) {
return false;
}
QJsonObject content = candidate["content"].toObject();
if (!content.contains("parts")) {
return false;
}
QJsonArray parts = content["parts"].toArray();
for (const auto &part : parts) {
QJsonObject partObj = part.toObject();
if (partObj.contains("text")) {
accumulatedResponse += partObj["text"].toString();
}
}
return true;
}
} // namespace QodeAssist::Providers

View File

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

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