mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-15 10:49:18 -04:00
Compare commits
83 Commits
backport-t
...
dev-rag-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142afa725f | ||
|
|
f36db033e6 | ||
|
|
5dfcf74128 | ||
|
|
02101665ca | ||
|
|
77a03d42ed | ||
|
|
09c38c8b0e | ||
|
|
7b73d7af7b | ||
|
|
5a426b4d9f | ||
|
|
1fa6a225a4 | ||
|
|
31133e3378 | ||
|
|
a2f15fc843 | ||
|
|
2a0beb6c4c | ||
|
|
e836b86569 | ||
|
|
288fefebe5 | ||
|
|
528badbf1e | ||
|
|
b789e42602 | ||
|
|
4bf955462f | ||
|
|
5b99e68e53 | ||
|
|
0f1b277ef7 | ||
|
|
56995c9edf | ||
|
|
45aba6b6be | ||
|
|
1dfb3feb96 | ||
|
|
2c49d45297 | ||
|
|
31145f191b | ||
|
|
9096adde6f | ||
|
|
b8e578d2d7 | ||
|
|
4e45774bce | ||
|
|
928490d31f | ||
|
|
97163cf6c9 | ||
|
|
f85c162692 | ||
|
|
258053d826 | ||
|
|
bf63ae5714 | ||
|
|
ae76850e78 | ||
|
|
bf3c0b3aa0 | ||
|
|
9add61c805 | ||
|
|
add86d2e67 | ||
|
|
a6c909d34d | ||
|
|
2814dec3e5 | ||
|
|
1b86b60de8 | ||
|
|
4b7f638731 | ||
|
|
de046f0529 | ||
|
|
e975e143b1 | ||
|
|
c97c0f62e8 | ||
|
|
61fded34ea | ||
|
|
289a19ac1a | ||
|
|
43ac662671 | ||
|
|
1d64d2afc9 | ||
|
|
9db61119aa | ||
|
|
70481b3116 | ||
|
|
511f5b36eb | ||
|
|
35012865c7 | ||
|
|
f27429aa66 | ||
|
|
113d5adcf4 | ||
|
|
30ea89cdc2 | ||
|
|
13469edce6 | ||
|
|
ee2c3950e8 | ||
|
|
d04e5bc967 | ||
|
|
d8ef9d0120 | ||
|
|
e544e46d76 | ||
|
|
63f0900511 | ||
|
|
7dee6f62c0 | ||
|
|
dc06ea2ed5 | ||
|
|
fc5e1adc0d | ||
|
|
93e59fb2dc | ||
|
|
cd2a56cde0 | ||
|
|
09cde8fd3d | ||
|
|
ac8080542d | ||
|
|
7376a11a05 | ||
|
|
10e8b16caf | ||
|
|
a38debb140 | ||
|
|
844ac35a59 | ||
|
|
16b77a5722 | ||
|
|
c070fd5cfd | ||
|
|
882047d7b2 | ||
|
|
b692402897 | ||
|
|
8102ba95f9 | ||
|
|
f8bb9998ab | ||
|
|
6dab055ca2 | ||
|
|
7b31fff9f2 | ||
|
|
be9156fd0e | ||
|
|
657413344d | ||
|
|
5f3deb44b9 | ||
|
|
55e2b24b8d |
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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: ''
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -23,16 +23,6 @@ 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.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Log**
|
||||||
- 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.
|
||||||
|
|||||||
54
.github/scripts/plugin.json
vendored
Normal file
54
.github/scripts/plugin.json
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "QodeAssist",
|
||||||
|
"vendor": "Petr Mironychev",
|
||||||
|
"tags": [
|
||||||
|
"code assistant",
|
||||||
|
"llm",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"compatibility": "Qt 6.8.1",
|
||||||
|
"platforms": [
|
||||||
|
"Windows",
|
||||||
|
"macOS",
|
||||||
|
"Linux"
|
||||||
|
],
|
||||||
|
"license": "GPLv3",
|
||||||
|
"version": "0.4.0",
|
||||||
|
"status": "draft",
|
||||||
|
"is_pack": false,
|
||||||
|
"released_at": null,
|
||||||
|
"version_history": [
|
||||||
|
{
|
||||||
|
"version": "0.4.0",
|
||||||
|
"is_latest": true,
|
||||||
|
"released_at": "2024-01-24T15:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
|
||||||
|
"small_icon": "https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41",
|
||||||
|
"description_paragraphs": [
|
||||||
|
{
|
||||||
|
"header": "Description",
|
||||||
|
"text": [
|
||||||
|
"QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description_links": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Palm1r/QodeAssist",
|
||||||
|
"link_text": "Site"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description_images": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a",
|
||||||
|
"image_label": "Code Completion"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"copyright": "(C) Petr Mironychev",
|
||||||
|
"download_history": {
|
||||||
|
"download_count": 0
|
||||||
|
},
|
||||||
|
"plugin_sets": []
|
||||||
|
}
|
||||||
147
.github/scripts/registerPlugin.js
vendored
Normal file
147
.github/scripts/registerPlugin.js
vendored
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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'));
|
||||||
173
.github/workflows/build_cmake.yml
vendored
173
.github/workflows/build_cmake.yml
vendored
@@ -9,11 +9,12 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PLUGIN_NAME: QodeAssist
|
PLUGIN_NAME: QodeAssist
|
||||||
QT_VERSION: 6.7.3
|
QT_VERSION: 6.8.1
|
||||||
QT_CREATOR_VERSION: 14.0.2
|
QT_CREATOR_VERSION: 15.0.1
|
||||||
QT_CREATOR_SNAPSHOT: NO
|
QT_CREATOR_VERSION_INTERNAL: 15.0.1
|
||||||
MACOS_DEPLOYMENT_TARGET: "11.0"
|
MACOS_DEPLOYMENT_TARGET: "11.0"
|
||||||
CMAKE_VERSION: "3.29.6"
|
CMAKE_VERSION: "3.29.6"
|
||||||
NINJA_VERSION: "1.12.1"
|
NINJA_VERSION: "1.12.1"
|
||||||
@@ -30,74 +31,44 @@ jobs:
|
|||||||
- {
|
- {
|
||||||
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
||||||
os: windows-latest,
|
os: windows-latest,
|
||||||
|
platform: windows_x64,
|
||||||
cc: "cl", cxx: "cl",
|
cc: "cl", cxx: "cl",
|
||||||
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
|
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
|
||||||
os: ubuntu-latest,
|
os: ubuntu-latest,
|
||||||
|
platform: linux_x64,
|
||||||
cc: "gcc", cxx: "g++"
|
cc: "gcc", cxx: "g++"
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
name: "macOS Latest Clang", artifact: "macOS-universal",
|
name: "macOS Latest Clang", artifact: "macOS-universal",
|
||||||
os: macos-latest,
|
os: macos-latest,
|
||||||
|
platform: mac_x64,
|
||||||
cc: "clang", cxx: "clang++"
|
cc: "clang", cxx: "clang++"
|
||||||
}
|
}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
- name: Checkout submodules
|
- name: Checkout submodules
|
||||||
id: git
|
id: git
|
||||||
shell: cmake -P {0}
|
shell: cmake -P {0}
|
||||||
run: |
|
run: |
|
||||||
if (${{github.ref}} MATCHES "tags/v(.*)")
|
if (${{github.ref}} MATCHES "tags/v(.*)")
|
||||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}\n")
|
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
|
||||||
else()
|
else()
|
||||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\n")
|
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
- name: Download Ninja and CMake
|
- name: Download Ninja and CMake
|
||||||
shell: cmake -P {0}
|
uses: lukka/get-cmake@latest
|
||||||
run: |
|
with:
|
||||||
set(cmake_version "$ENV{CMAKE_VERSION}")
|
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||||
set(ninja_version "$ENV{NINJA_VERSION}")
|
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||||
|
|
||||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
|
||||||
set(ninja_suffix "win.zip")
|
|
||||||
set(cmake_suffix "windows-x86_64.zip")
|
|
||||||
set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin")
|
|
||||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
|
||||||
set(ninja_suffix "linux.zip")
|
|
||||||
set(cmake_suffix "linux-x86_64.tar.gz")
|
|
||||||
set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin")
|
|
||||||
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
|
||||||
set(ninja_suffix "mac.zip")
|
|
||||||
set(cmake_suffix "macos-universal.tar.gz")
|
|
||||||
set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
|
|
||||||
file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
|
|
||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)
|
|
||||||
|
|
||||||
set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
|
|
||||||
file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
|
|
||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)
|
|
||||||
|
|
||||||
# Add to PATH environment variable
|
|
||||||
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
|
|
||||||
set(path_separator ":")
|
|
||||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
|
||||||
set(path_separator ";")
|
|
||||||
endif()
|
|
||||||
file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}")
|
|
||||||
|
|
||||||
if (NOT "${{ runner.os }}" STREQUAL "Windows")
|
|
||||||
execute_process(
|
|
||||||
COMMAND chmod +x ninja
|
|
||||||
COMMAND chmod +x ${cmake_dir}/cmake
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
- name: Install system libs
|
- name: Install system libs
|
||||||
shell: cmake -P {0}
|
shell: cmake -P {0}
|
||||||
@@ -107,7 +78,7 @@ jobs:
|
|||||||
COMMAND sudo apt update
|
COMMAND sudo apt update
|
||||||
)
|
)
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND sudo apt install libgl1-mesa-dev libcups2-dev
|
COMMAND sudo apt install libgl1-mesa-dev
|
||||||
RESULT_VARIABLE result
|
RESULT_VARIABLE result
|
||||||
)
|
)
|
||||||
if (NOT result EQUAL 0)
|
if (NOT result EQUAL 0)
|
||||||
@@ -124,9 +95,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_msvc2019_64")
|
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||||
set(qt_dir_prefix "${qt_version}/msvc2019_64")
|
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||||
set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
|
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-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")
|
||||||
@@ -135,15 +106,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_8-GCC-Linux-RHEL_8_8-X86_64")
|
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-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_13-Clang-MacOS-MacOS_13-X86_64-ARM64")
|
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}")
|
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/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)
|
||||||
@@ -153,7 +124,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_prefix}" qt_dir)
|
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6" 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}")
|
||||||
@@ -172,11 +143,17 @@ jobs:
|
|||||||
|
|
||||||
foreach(package qt5compat qtshadertools)
|
foreach(package qt5compat qtshadertools)
|
||||||
downloadAndExtract(
|
downloadAndExtract(
|
||||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${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")
|
||||||
@@ -184,47 +161,25 @@ jobs:
|
|||||||
else()
|
else()
|
||||||
set(uic_suffix "Rhel8.6-x86_64")
|
set(uic_suffix "Rhel8.6-x86_64")
|
||||||
endif()
|
endif()
|
||||||
downloadAndExtract(
|
downloadAndExtractLibicu(
|
||||||
"${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: |
|
||||||
@@ -283,11 +238,59 @@ 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
|
||||||
|
|
||||||
release:
|
# The json is the same for all platforms, but we need to save one
|
||||||
|
- name: Upload plugin json
|
||||||
|
if: matrix.config.os == 'ubuntu-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||||
|
path: ./build/build/${{ env.PLUGIN_NAME }}.json
|
||||||
|
|
||||||
|
update_json:
|
||||||
if: contains(github.ref, 'tags/v')
|
if: contains(github.ref, 'tags/v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Download the JSON file
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||||
|
path: ./${{ env.PLUGIN_NAME }}-origin
|
||||||
|
|
||||||
|
- name: Store Release upload_url
|
||||||
|
run: |
|
||||||
|
RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}")
|
||||||
|
echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Run the Node.js script to update JSON
|
||||||
|
env:
|
||||||
|
QT_TOKEN: ${{ secrets.TOKEN }}
|
||||||
|
API_URL: ${{ secrets.API_URL }}
|
||||||
|
run: |
|
||||||
|
node .github/scripts/registerPlugin.js ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} ${{ env.QT_TOKEN }} ${{ env.API_URL }}
|
||||||
|
|
||||||
|
- name: Delete previous json artifacts
|
||||||
|
uses: geekyeggo/delete-artifact@v5
|
||||||
|
with:
|
||||||
|
name: ${{ env.PLUGIN_NAME }}*-json
|
||||||
|
|
||||||
|
- name: Upload the modified JSON file as an artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: plugin-json
|
||||||
|
path: .github/scripts/${{ env.PLUGIN_NAME }}.json
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: contains(github.ref, 'tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build, update_json]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ add_subdirectory(llmcore)
|
|||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
add_subdirectory(logger)
|
add_subdirectory(logger)
|
||||||
add_subdirectory(ChatView)
|
add_subdirectory(ChatView)
|
||||||
|
add_subdirectory(context)
|
||||||
|
|
||||||
add_qtc_plugin(QodeAssist
|
add_qtc_plugin(QodeAssist
|
||||||
PLUGIN_DEPENDS
|
PLUGIN_DEPENDS
|
||||||
@@ -45,28 +46,30 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/StarCoder2Fim.hpp
|
templates/StarCoder2Fim.hpp
|
||||||
templates/DeepSeekCoderFim.hpp
|
templates/DeepSeekCoderFim.hpp
|
||||||
templates/CustomFimTemplate.hpp
|
templates/CustomFimTemplate.hpp
|
||||||
|
|
||||||
|
|
||||||
templates/Qwen.hpp
|
templates/Qwen.hpp
|
||||||
|
|
||||||
templates/Ollama.hpp
|
templates/Ollama.hpp
|
||||||
templates/BasicChat.hpp
|
templates/BasicChat.hpp
|
||||||
templates/Llama3.hpp
|
templates/Llama3.hpp
|
||||||
templates/ChatML.hpp
|
templates/ChatML.hpp
|
||||||
templates/Alpaca.hpp
|
templates/Alpaca.hpp
|
||||||
templates/Llama2.hpp
|
templates/Llama2.hpp
|
||||||
|
templates/Claude.hpp
|
||||||
|
templates/OpenAI.hpp
|
||||||
|
templates/CodeLlamaQMLFim.hpp
|
||||||
providers/Providers.hpp
|
providers/Providers.hpp
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
||||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
||||||
|
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
||||||
|
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||||
|
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
QodeAssistClient.hpp QodeAssistClient.cpp
|
QodeAssistClient.hpp QodeAssistClient.cpp
|
||||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
|
||||||
utils/CounterTooltip.hpp utils/CounterTooltip.cpp
|
|
||||||
core/ChangesManager.h core/ChangesManager.cpp
|
|
||||||
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
||||||
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||||
|
CodeHandler.hpp CodeHandler.cpp
|
||||||
|
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,6 +13,17 @@ 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
|
||||||
@@ -20,6 +31,7 @@ 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
|
||||||
@@ -32,6 +44,7 @@ target_link_libraries(QodeAssistChatView
|
|||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
LLMCore
|
LLMCore
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
|
Context
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ 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();
|
||||||
|
|
||||||
@@ -55,6 +54,13 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -65,29 +71,37 @@ 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(const QString &content, ChatRole role, const QString &id)
|
void ChatModel::addMessage(
|
||||||
|
const QString &content,
|
||||||
|
ChatRole role,
|
||||||
|
const QString &id,
|
||||||
|
const QList<Context::ContentFile> &attachments)
|
||||||
{
|
{
|
||||||
int tokenCount = estimateTokenCount(content);
|
QString fullContent = 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.tokenCount = tokenCount;
|
lastMessage.attachments = attachments;
|
||||||
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());
|
||||||
m_messages.append({role, content, tokenCount, id});
|
Message newMessage{role, content, id};
|
||||||
m_totalTokens += tokenCount;
|
newMessage.attachments = attachments;
|
||||||
|
m_messages.append(newMessage);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
trim();
|
|
||||||
emit totalTokensChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
||||||
@@ -95,32 +109,12 @@ 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 totalTokensChanged();
|
emit modelReseted();
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||||
@@ -155,7 +149,6 @@ 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) {
|
||||||
@@ -170,17 +163,27 @@ 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();
|
||||||
|
|||||||
@@ -26,27 +26,29 @@
|
|||||||
#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);
|
||||||
@@ -55,29 +57,28 @@ 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(const QString &content, ChatRole role, const QString &id);
|
Q_INVOKABLE void addMessage(
|
||||||
|
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
|
||||||
|
|||||||
@@ -18,12 +18,30 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ChatRootView.hpp"
|
#include "ChatRootView.hpp"
|
||||||
#include <QtGui/qclipboard.h>
|
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectexplorer.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <projectexplorer/projecttree.h>
|
||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "ProjectSettings.hpp"
|
||||||
|
#include "context/ContextManager.hpp"
|
||||||
|
#include "context/FileChunker.hpp"
|
||||||
|
#include "context/RAGManager.hpp"
|
||||||
|
#include "context/TokenUtils.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -32,6 +50,13 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||||
{
|
{
|
||||||
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
|
connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
[this](){
|
||||||
|
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
|
||||||
|
});
|
||||||
|
|
||||||
auto &settings = Settings::generalSettings();
|
auto &settings = Settings::generalSettings();
|
||||||
|
|
||||||
connect(&settings.caModel,
|
connect(&settings.caModel,
|
||||||
@@ -39,12 +64,44 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::currentTemplateChanged);
|
&ChatRootView::currentTemplateChanged);
|
||||||
|
|
||||||
connect(&Settings::chatAssistantSettings().sharingCurrentFile,
|
connect(
|
||||||
&Utils::BaseAspect::changed,
|
m_clientInterface,
|
||||||
this,
|
&ClientInterface::messageReceivedCompletely,
|
||||||
&ChatRootView::isSharingCurrentFileChanged);
|
this,
|
||||||
|
&ChatRootView::autosave);
|
||||||
|
|
||||||
generateColors();
|
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,
|
||||||
|
this, &ChatRootView::updateInputTokensCount);
|
||||||
|
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
|
||||||
|
this, &ChatRootView::updateInputTokensCount);
|
||||||
|
|
||||||
|
auto editors = Core::EditorManager::instance();
|
||||||
|
|
||||||
|
connect(editors, &Core::EditorManager::editorCreated, this, &ChatRootView::onEditorCreated);
|
||||||
|
connect(
|
||||||
|
editors,
|
||||||
|
&Core::EditorManager::editorAboutToClose,
|
||||||
|
this,
|
||||||
|
&ChatRootView::onEditorAboutToClose);
|
||||||
|
|
||||||
|
connect(editors, &Core::EditorManager::currentEditorAboutToChange, this, [this]() {
|
||||||
|
if (m_isSyncOpenFiles) {
|
||||||
|
for (auto editor : std::as_const(m_currentEditors)) {
|
||||||
|
onAppendLinkFileFromEditor(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateInputTokensCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel *ChatRootView::chatModel() const
|
ChatModel *ChatRootView::chatModel() const
|
||||||
@@ -52,14 +109,26 @@ ChatModel *ChatRootView::chatModel() const
|
|||||||
return m_chatModel;
|
return m_chatModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::backgroundColor() const
|
void ChatRootView::sendMessage(const QString &message)
|
||||||
{
|
{
|
||||||
return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
if (m_inputTokensCount > m_chatModel->tokensThreshold()) {
|
||||||
}
|
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);
|
||||||
|
|
||||||
void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const
|
if (reply == QMessageBox::Yes) {
|
||||||
{
|
autosave();
|
||||||
m_clientInterface->sendMessage(message, sharingCurrentFile);
|
m_chatModel->clear();
|
||||||
|
setRecentFilePath(QString{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||||
|
clearAttachmentFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::copyToClipboard(const QString &text)
|
void ChatRootView::copyToClipboard(const QString &text)
|
||||||
@@ -72,47 +141,40 @@ void ChatRootView::cancelRequest()
|
|||||||
m_clientInterface->cancelRequest();
|
m_clientInterface->cancelRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::generateColors()
|
void ChatRootView::clearAttachmentFiles()
|
||||||
{
|
{
|
||||||
QColor baseColor = backgroundColor();
|
if (!m_attachmentFiles.isEmpty()) {
|
||||||
bool isDarkTheme = baseColor.lightness() < 128;
|
m_attachmentFiles.clear();
|
||||||
|
emit attachmentFilesChanged();
|
||||||
if (isDarkTheme) {
|
|
||||||
m_primaryColor = generateColor(baseColor, 0.1, 1.2, 1.4);
|
|
||||||
m_secondaryColor = generateColor(baseColor, -0.1, 1.1, 1.2);
|
|
||||||
m_codeColor = generateColor(baseColor, 0.05, 0.8, 1.1);
|
|
||||||
} else {
|
|
||||||
m_primaryColor = generateColor(baseColor, 0.05, 1.05, 1.1);
|
|
||||||
m_secondaryColor = generateColor(baseColor, -0.05, 1.1, 1.2);
|
|
||||||
m_codeColor = generateColor(baseColor, 0.02, 0.95, 1.05);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::generateColor(const QColor &baseColor,
|
void ChatRootView::clearLinkedFiles()
|
||||||
float hueShift,
|
|
||||||
float saturationMod,
|
|
||||||
float lightnessMod)
|
|
||||||
{
|
{
|
||||||
float h, s, l, a;
|
if (!m_linkedFiles.isEmpty()) {
|
||||||
baseColor.getHslF(&h, &s, &l, &a);
|
m_linkedFiles.clear();
|
||||||
bool isDarkTheme = l < 0.5;
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h = fmod(h + hueShift + 1.0, 1.0);
|
QString ChatRootView::getChatsHistoryDir() const
|
||||||
|
{
|
||||||
|
QString path;
|
||||||
|
|
||||||
s = qBound(0.0f, s * saturationMod, 1.0f);
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
if (isDarkTheme) {
|
path = projectSettings.chatHistoryPath().toString();
|
||||||
l = qBound(0.0f, l * lightnessMod, 1.0f);
|
|
||||||
} else {
|
} else {
|
||||||
l = qBound(0.0f, l / lightnessMod, 1.0f);
|
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
h = qBound(0.0f, h, 1.0f);
|
QDir dir(path);
|
||||||
s = qBound(0.0f, s, 1.0f);
|
if (!dir.exists() && !dir.mkpath(".")) {
|
||||||
l = qBound(0.0f, l, 1.0f);
|
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
||||||
a = qBound(0.0f, a, 1.0f);
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
return QColor::fromHslF(h, s, l, a);
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::currentTemplate() const
|
QString ChatRootView::currentTemplate() const
|
||||||
@@ -121,24 +183,417 @@ QString ChatRootView::currentTemplate() const
|
|||||||
return settings.caModel();
|
return settings.caModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::primaryColor() const
|
void ChatRootView::saveHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
return m_primaryColor;
|
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||||
|
if (!result.success) {
|
||||||
|
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
|
||||||
|
} else {
|
||||||
|
setRecentFilePath(filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::secondaryColor() const
|
void ChatRootView::loadHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
return m_secondaryColor;
|
auto result = ChatSerializer::loadFromFile(m_chatModel, filePath);
|
||||||
|
if (!result.success) {
|
||||||
|
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
||||||
|
} else {
|
||||||
|
setRecentFilePath(filePath);
|
||||||
|
}
|
||||||
|
updateInputTokensCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::codeColor() const
|
void ChatRootView::showSaveDialog()
|
||||||
{
|
{
|
||||||
return m_codeColor;
|
QString initialDir = getChatsHistoryDir();
|
||||||
|
|
||||||
|
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
|
||||||
|
dialog->setAcceptMode(QFileDialog::AcceptSave);
|
||||||
|
dialog->setFileMode(QFileDialog::AnyFile);
|
||||||
|
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||||
|
dialog->setDefaultSuffix("json");
|
||||||
|
if (!initialDir.isEmpty()) {
|
||||||
|
dialog->setDirectory(initialDir);
|
||||||
|
dialog->selectFile(getSuggestedFileName() + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||||
|
if (result == QFileDialog::Accepted) {
|
||||||
|
QStringList files = dialog->selectedFiles();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
saveHistory(files.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->open();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::isSharingCurrentFile() const
|
void ChatRootView::showLoadDialog()
|
||||||
{
|
{
|
||||||
return Settings::chatAssistantSettings().sharingCurrentFile();
|
QString initialDir = getChatsHistoryDir();
|
||||||
|
|
||||||
|
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
|
||||||
|
dialog->setAcceptMode(QFileDialog::AcceptOpen);
|
||||||
|
dialog->setFileMode(QFileDialog::ExistingFile);
|
||||||
|
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||||
|
if (!initialDir.isEmpty()) {
|
||||||
|
dialog->setDirectory(initialDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||||
|
if (result == QFileDialog::Accepted) {
|
||||||
|
QStringList files = dialog->selectedFiles();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
loadHistory(files.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::getSuggestedFileName() const
|
||||||
|
{
|
||||||
|
QStringList parts;
|
||||||
|
|
||||||
|
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
|
||||||
|
static const QRegularExpression underSymbols = QRegularExpression("_+");
|
||||||
|
|
||||||
|
if (m_chatModel->rowCount() > 0) {
|
||||||
|
QString firstMessage
|
||||||
|
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
||||||
|
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||||
|
|
||||||
|
QString sanitizedMessage = shortMessage;
|
||||||
|
sanitizedMessage.replace(saitizeSymbols, "_");
|
||||||
|
sanitizedMessage.replace(underSymbols, "_");
|
||||||
|
sanitizedMessage = sanitizedMessage.trimmed();
|
||||||
|
|
||||||
|
if (!sanitizedMessage.isEmpty()) {
|
||||||
|
if (sanitizedMessage.startsWith('_')) {
|
||||||
|
sanitizedMessage.remove(0, 1);
|
||||||
|
}
|
||||||
|
if (sanitizedMessage.endsWith('_')) {
|
||||||
|
sanitizedMessage.chop(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString targetDir = getChatsHistoryDir();
|
||||||
|
QString fullPath = QDir(targetDir).filePath(sanitizedMessage);
|
||||||
|
|
||||||
|
QFileInfo fileInfo(fullPath);
|
||||||
|
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
|
||||||
|
parts << sanitizedMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
|
||||||
|
|
||||||
|
QString fileName = parts.join("_");
|
||||||
|
|
||||||
|
QString fullPath = QDir(getChatsHistoryDir()).filePath(fileName);
|
||||||
|
QFileInfo finalCheck(fullPath);
|
||||||
|
|
||||||
|
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
||||||
|
fileName = QString("chat_%1").arg(
|
||||||
|
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::autosave()
|
||||||
|
{
|
||||||
|
if (m_chatModel->rowCount() == 0 || !Settings::chatAssistantSettings().autosave()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString filePath = getAutosaveFilePath();
|
||||||
|
if (!filePath.isEmpty()) {
|
||||||
|
ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||||
|
setRecentFilePath(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::getAutosaveFilePath() const
|
||||||
|
{
|
||||||
|
if (!m_recentFilePath.isEmpty()) {
|
||||||
|
return m_recentFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString dir = getChatsHistoryDir();
|
||||||
|
if (dir.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QDir(dir).filePath(getSuggestedFileName() + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatRootView::attachmentFiles() const
|
||||||
|
{
|
||||||
|
return m_attachmentFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatRootView::linkedFiles() const
|
||||||
|
{
|
||||||
|
return m_linkedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::showAttachFilesDialog()
|
||||||
|
{
|
||||||
|
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
|
||||||
|
dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||||
|
|
||||||
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
dialog.setDirectory(project->projectDirectory().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
QStringList newFilePaths = dialog.selectedFiles();
|
||||||
|
if (!newFilePaths.isEmpty()) {
|
||||||
|
bool filesAdded = false;
|
||||||
|
for (const QString &filePath : std::as_const(newFilePaths)) {
|
||||||
|
if (!m_attachmentFiles.contains(filePath)) {
|
||||||
|
m_attachmentFiles.append(filePath);
|
||||||
|
filesAdded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filesAdded) {
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::removeFileFromAttachList(int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < m_attachmentFiles.size()) {
|
||||||
|
m_attachmentFiles.removeAt(index);
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::showLinkFilesDialog()
|
||||||
|
{
|
||||||
|
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
|
||||||
|
dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||||
|
|
||||||
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
dialog.setDirectory(project->projectDirectory().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
QStringList newFilePaths = dialog.selectedFiles();
|
||||||
|
if (!newFilePaths.isEmpty()) {
|
||||||
|
bool filesAdded = false;
|
||||||
|
for (const QString &filePath : std::as_const(newFilePaths)) {
|
||||||
|
if (!m_linkedFiles.contains(filePath)) {
|
||||||
|
m_linkedFiles.append(filePath);
|
||||||
|
filesAdded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filesAdded) {
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::removeFileFromLinkList(int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < m_linkedFiles.size()) {
|
||||||
|
m_linkedFiles.removeAt(index);
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::calculateMessageTokensCount(const QString &message)
|
||||||
|
{
|
||||||
|
m_messageTokensCount = Context::TokenUtils::estimateTokens(message);
|
||||||
|
updateInputTokensCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::setIsSyncOpenFiles(bool state)
|
||||||
|
{
|
||||||
|
if (m_isSyncOpenFiles != state) {
|
||||||
|
m_isSyncOpenFiles = state;
|
||||||
|
emit isSyncOpenFilesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_isSyncOpenFiles) {
|
||||||
|
for (auto editor : std::as_const(m_currentEditors)) {
|
||||||
|
onAppendLinkFileFromEditor(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::openChatHistoryFolder()
|
||||||
|
{
|
||||||
|
QString path;
|
||||||
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
|
path = projectSettings.chatHistoryPath().toString();
|
||||||
|
} else {
|
||||||
|
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkpath(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||||
|
QDesktopServices::openUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatRootView.cpp
|
||||||
|
|
||||||
|
void ChatRootView::testRAG(const QString &message)
|
||||||
|
{
|
||||||
|
auto project = ProjectExplorer::ProjectTree::currentProject();
|
||||||
|
if (!project) {
|
||||||
|
qDebug() << "No active project found";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString TEST_QUERY = message;
|
||||||
|
|
||||||
|
qDebug() << "Starting RAG test with query:";
|
||||||
|
qDebug() << TEST_QUERY;
|
||||||
|
qDebug() << "\nFirst, processing project files...";
|
||||||
|
|
||||||
|
auto files = Context::ContextManager::instance().getProjectSourceFiles(project);
|
||||||
|
// Было: auto future = Context::RAGManager::instance().processFiles(project, files);
|
||||||
|
// Стало:
|
||||||
|
auto future = Context::RAGManager::instance().processProjectFiles(project, files);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
&Context::RAGManager::instance(),
|
||||||
|
&Context::RAGManager::vectorizationProgress,
|
||||||
|
this,
|
||||||
|
[](int processed, int total) {
|
||||||
|
qDebug() << QString("Vectorization progress: %1 of %2 files").arg(processed).arg(total);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(
|
||||||
|
&Context::RAGManager::instance(),
|
||||||
|
&Context::RAGManager::vectorizationFinished,
|
||||||
|
this,
|
||||||
|
[this, project, TEST_QUERY]() {
|
||||||
|
qDebug() << "\nVectorization completed. Starting similarity search...\n";
|
||||||
|
// Было: Context::RAGManager::instance().searchSimilarDocuments(TEST_QUERY, project, 5);
|
||||||
|
// Стало:
|
||||||
|
auto future = Context::RAGManager::instance().findRelevantChunks(TEST_QUERY, project, 5);
|
||||||
|
future.then([](const QList<Context::RAGManager::ChunkSearchResult> &results) {
|
||||||
|
qDebug() << "Found" << results.size() << "relevant chunks:";
|
||||||
|
for (const auto &result : results) {
|
||||||
|
qDebug() << "File:" << result.filePath;
|
||||||
|
qDebug() << "Lines:" << result.startLine << "-" << result.endLine;
|
||||||
|
qDebug() << "Score:" << result.combinedScore;
|
||||||
|
qDebug() << "Content:" << result.content;
|
||||||
|
qDebug() << "---";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::testChunking()
|
||||||
|
{
|
||||||
|
auto project = ProjectExplorer::ProjectTree::currentProject();
|
||||||
|
if (!project) {
|
||||||
|
qDebug() << "No active project found";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Context::FileChunker::ChunkingConfig config;
|
||||||
|
Context::ContextManager::instance().testProjectChunks(project, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::updateInputTokensCount()
|
||||||
|
{
|
||||||
|
int inputTokens = m_messageTokensCount;
|
||||||
|
auto& settings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
|
if (settings.useSystemPrompt()) {
|
||||||
|
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_attachmentFiles.isEmpty()) {
|
||||||
|
auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
|
||||||
|
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_linkedFiles.isEmpty()) {
|
||||||
|
auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
|
||||||
|
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& history = m_chatModel->getChatHistory();
|
||||||
|
for (const auto& message : history) {
|
||||||
|
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
||||||
|
inputTokens += 4; // + role
|
||||||
|
}
|
||||||
|
|
||||||
|
m_inputTokensCount = inputTokens;
|
||||||
|
emit inputTokensCountChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::inputTokensCount() const
|
||||||
|
{
|
||||||
|
return m_inputTokensCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatRootView::isSyncOpenFiles() const
|
||||||
|
{
|
||||||
|
return m_isSyncOpenFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
||||||
|
{
|
||||||
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||||
|
QString filePath = document->filePath().toString();
|
||||||
|
m_linkedFiles.removeOne(filePath);
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
m_currentEditors.removeOne(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
||||||
|
{
|
||||||
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||||
|
QString filePath = document->filePath().toString();
|
||||||
|
if (!m_linkedFiles.contains(filePath)) {
|
||||||
|
m_linkedFiles.append(filePath);
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath)
|
||||||
|
{
|
||||||
|
if (editor && editor->document()) {
|
||||||
|
m_currentEditors.append(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::chatFileName() const
|
||||||
|
{
|
||||||
|
return QFileInfo(m_recentFilePath).baseName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::setRecentFilePath(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (m_recentFilePath != filePath) {
|
||||||
|
m_recentFilePath = filePath;
|
||||||
|
emit chatFileNameChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -23,24 +23,21 @@
|
|||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
class ChatRootView : public QQuickItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
// Possibly Qt bug: QTBUG-131004
|
|
||||||
// The class type name must be fully qualified
|
|
||||||
// including the namespace.
|
|
||||||
// Otherwise qmlls can't find it.
|
|
||||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||||
Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
|
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||||
Q_PROPERTY(QColor primaryColor READ primaryColor CONSTANT FINAL)
|
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||||
Q_PROPERTY(QColor secondaryColor READ secondaryColor CONSTANT FINAL)
|
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||||
Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
|
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||||
Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
|
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
|
||||||
isSharingCurrentFileChanged FINAL)
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -49,38 +46,70 @@ public:
|
|||||||
ChatModel *chatModel() const;
|
ChatModel *chatModel() const;
|
||||||
QString currentTemplate() const;
|
QString currentTemplate() const;
|
||||||
|
|
||||||
QColor backgroundColor() const;
|
void saveHistory(const QString &filePath);
|
||||||
QColor primaryColor() const;
|
void loadHistory(const QString &filePath);
|
||||||
QColor secondaryColor() const;
|
|
||||||
|
|
||||||
QColor codeColor() const;
|
Q_INVOKABLE void showSaveDialog();
|
||||||
|
Q_INVOKABLE void showLoadDialog();
|
||||||
|
|
||||||
bool isSharingCurrentFile() const;
|
void autosave();
|
||||||
|
QString getAutosaveFilePath() const;
|
||||||
|
|
||||||
|
QStringList attachmentFiles() const;
|
||||||
|
QStringList linkedFiles() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void showAttachFilesDialog();
|
||||||
|
Q_INVOKABLE void removeFileFromAttachList(int index);
|
||||||
|
Q_INVOKABLE void showLinkFilesDialog();
|
||||||
|
Q_INVOKABLE void removeFileFromLinkList(int index);
|
||||||
|
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||||
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
|
Q_INVOKABLE void openChatHistoryFolder();
|
||||||
|
Q_INVOKABLE void testRAG(const QString &message);
|
||||||
|
Q_INVOKABLE void testChunking();
|
||||||
|
|
||||||
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
|
int inputTokensCount() const;
|
||||||
|
|
||||||
|
bool isSyncOpenFiles() const;
|
||||||
|
|
||||||
|
void onEditorAboutToClose(Core::IEditor *editor);
|
||||||
|
void onAppendLinkFileFromEditor(Core::IEditor *editor);
|
||||||
|
void onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath);
|
||||||
|
|
||||||
|
QString chatFileName() const;
|
||||||
|
void setRecentFilePath(const QString &filePath);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message, bool sharingCurrentFile = false) const;
|
void sendMessage(const QString &message);
|
||||||
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 isSharingCurrentFileChanged();
|
void linkedFilesChanged();
|
||||||
|
void inputTokensCountChanged();
|
||||||
|
void isSyncOpenFilesChanged();
|
||||||
|
void chatFileNameChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void generateColors();
|
QString getChatsHistoryDir() const;
|
||||||
QColor generateColor(const QColor &baseColor,
|
QString getSuggestedFileName() const;
|
||||||
float hueShift,
|
|
||||||
float saturationMod,
|
|
||||||
float lightnessMod);
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
QString m_currentTemplate;
|
QString m_currentTemplate;
|
||||||
QColor m_primaryColor;
|
QString m_recentFilePath;
|
||||||
QColor m_secondaryColor;
|
QStringList m_attachmentFiles;
|
||||||
QColor m_codeColor;
|
QStringList m_linkedFiles;
|
||||||
|
int m_messageTokensCount{0};
|
||||||
|
int m_inputTokensCount{0};
|
||||||
|
bool m_isSyncOpenFiles;
|
||||||
|
QList<Core::IEditor *> m_currentEditors;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
142
ChatView/ChatSerializer.cpp
Normal file
142
ChatView/ChatSerializer.cpp
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
56
ChatView/ChatSerializer.hpp
Normal file
56
ChatView/ChatSerializer.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
// Q_NAMESPACE
|
|
||||||
|
|
||||||
class ChatUtils : public QObject
|
class ChatUtils : public QObject
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "ContextManager.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "PromptTemplateManager.hpp"
|
#include "PromptTemplateManager.hpp"
|
||||||
@@ -64,11 +65,13 @@ ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
|||||||
|
|
||||||
ClientInterface::~ClientInterface() = default;
|
ClientInterface::~ClientInterface() = default;
|
||||||
|
|
||||||
void ClientInterface::sendMessage(const QString &message, bool includeCurrentFile)
|
void ClientInterface::sendMessage(
|
||||||
|
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "");
|
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
|
||||||
|
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
||||||
|
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
@@ -97,16 +100,13 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil
|
|||||||
if (chatAssistantSettings.useSystemPrompt())
|
if (chatAssistantSettings.useSystemPrompt())
|
||||||
systemPrompt = chatAssistantSettings.systemPrompt();
|
systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
|
|
||||||
if (includeCurrentFile) {
|
if (!linkedFiles.isEmpty()) {
|
||||||
QString fileContext = getCurrentFileContext();
|
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||||
if (!fileContext.isEmpty()) {
|
|
||||||
systemPrompt = systemPrompt.append(fileContext);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject providerRequest;
|
QJsonObject providerRequest;
|
||||||
providerRequest["model"] = Settings::generalSettings().caModel();
|
providerRequest["model"] = Settings::generalSettings().caModel();
|
||||||
providerRequest["stream"] = true;
|
providerRequest["stream"] = chatAssistantSettings.stream();
|
||||||
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
|
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
|
||||||
|
|
||||||
if (promptTemplate)
|
if (promptTemplate)
|
||||||
@@ -126,11 +126,18 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil
|
|||||||
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
||||||
config.providerRequest = providerRequest;
|
config.providerRequest = providerRequest;
|
||||||
config.multiLineCompletion = false;
|
config.multiLineCompletion = false;
|
||||||
config.apiKey = Settings::chatAssistantSettings().apiKey();
|
config.apiKey = provider->apiKey();
|
||||||
|
|
||||||
QJsonObject request;
|
QJsonObject request;
|
||||||
request["id"] = QUuid::createUuid().toString();
|
request["id"] = QUuid::createUuid().toString();
|
||||||
|
|
||||||
|
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
LOG_MESSAGE("Validate errors for chat request:");
|
||||||
|
LOG_MESSAGES(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_requestHandler->sendLLMRequest(config, request);
|
m_requestHandler->sendLLMRequest(config, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +166,7 @@ void ClientInterface::handleLLMResponse(const QString &response,
|
|||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
"Message completed. Final response for message " + messageId + ": " + response);
|
"Message completed. Final response for message " + messageId + ": " + response);
|
||||||
|
emit messageReceivedCompletely();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,4 +195,21 @@ QString ClientInterface::getCurrentFileContext() const
|
|||||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
|
||||||
|
{
|
||||||
|
QString updatedPrompt = basePrompt;
|
||||||
|
|
||||||
|
if (!linkedFiles.isEmpty()) {
|
||||||
|
updatedPrompt += "\n\nLinked files for reference:\n";
|
||||||
|
|
||||||
|
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
|
||||||
|
for (const auto &file : contentFiles) {
|
||||||
|
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
|
||||||
|
.arg(file.filename, file.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -36,16 +36,23 @@ public:
|
|||||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||||
~ClientInterface();
|
~ClientInterface();
|
||||||
|
|
||||||
void sendMessage(const QString &message, bool includeCurrentFile = false);
|
void sendMessage(
|
||||||
|
const QString &message,
|
||||||
|
const QList<QString> &attachments = {},
|
||||||
|
const QList<QString> &linkedFiles = {});
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
|
void messageReceivedCompletely();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||||
QString getCurrentFileContext() const;
|
QString getCurrentFileContext() const;
|
||||||
|
QString getSystemPromptWithLinkedFiles(
|
||||||
|
const QString &basePrompt,
|
||||||
|
const QList<QString> &linkedFiles) const;
|
||||||
|
|
||||||
LLMCore::RequestHandler *m_requestHandler;
|
LLMCore::RequestHandler *m_requestHandler;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
|
|||||||
11
ChatView/icons/attach-file-dark.svg
Normal file
11
ChatView/icons/attach-file-dark.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 869 B |
11
ChatView/icons/attach-file-light.svg
Normal file
11
ChatView/icons/attach-file-light.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 869 B |
10
ChatView/icons/close-dark.svg
Normal file
10
ChatView/icons/close-dark.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 353 B |
10
ChatView/icons/close-light.svg
Normal file
10
ChatView/icons/close-light.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 353 B |
12
ChatView/icons/link-file-dark.svg
Normal file
12
ChatView/icons/link-file-dark.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 513 B |
12
ChatView/icons/link-file-light.svg
Normal file
12
ChatView/icons/link-file-light.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 507 B |
@@ -23,18 +23,18 @@ Rectangle {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias text: badgeText.text
|
property alias text: badgeText.text
|
||||||
property alias fontColor: badgeText.color
|
|
||||||
|
|
||||||
width: badgeText.implicitWidth + radius
|
implicitWidth: badgeText.implicitWidth + root.radius
|
||||||
height: badgeText.implicitHeight + 6
|
implicitHeight: badgeText.implicitHeight + 6
|
||||||
color: "lightgreen"
|
color: palette.button
|
||||||
radius: height / 2
|
radius: root.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,28 +17,25 @@
|
|||||||
* 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 color fontColor
|
property alias messageAttachments: attachmentsModel.model
|
||||||
property color codeBgColor
|
|
||||||
property color selectionColor
|
|
||||||
|
|
||||||
height: msgColumn.height
|
height: msgColumn.implicitHeight + 10
|
||||||
radius: 8
|
radius: 8
|
||||||
|
|
||||||
Column {
|
ColumnLayout {
|
||||||
id: msgColumn
|
id: msgColumn
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
@@ -49,7 +46,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
|
||||||
|
|
||||||
width: parent.width
|
Layout.preferredWidth: root.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
|
||||||
@@ -80,6 +77,40 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: attachmentsFlow
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: attachmentsModel.model && attachmentsModel.model.length > 0
|
||||||
|
leftPadding: 10
|
||||||
|
rightPadding: 10
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: attachmentsModel
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
height: attachText.implicitHeight + 8
|
||||||
|
width: attachText.implicitWidth + 16
|
||||||
|
radius: 4
|
||||||
|
color: palette.button
|
||||||
|
border.width: 1
|
||||||
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: attachText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
color: palette.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component TextComponent : TextBlock {
|
component TextComponent : TextBlock {
|
||||||
@@ -88,8 +119,6 @@ Rectangle {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
leftPadding: 10
|
leftPadding: 10
|
||||||
text: itemData.text
|
text: itemData.text
|
||||||
color: root.fontColor
|
|
||||||
selectionColor: root.selectionColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -104,8 +133,5 @@ Rectangle {
|
|||||||
|
|
||||||
code: itemData.text
|
code: itemData.text
|
||||||
language: itemData.language
|
language: itemData.language
|
||||||
color: root.codeBgColor
|
|
||||||
selectionColor: root.selectionColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,28 +17,66 @@
|
|||||||
* 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: root.backgroundColor
|
color: palette.window
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors {
|
anchors.fill: parent
|
||||||
fill: parent
|
spacing: 0
|
||||||
|
|
||||||
|
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
|
||||||
@@ -54,14 +92,12 @@ 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)
|
||||||
color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
|
messageAttachments: model.attachments
|
||||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
color: model.roleType === ChatModel.User ? palette.alternateBase
|
||||||
codeBgColor: root.codeColor
|
: palette.base
|
||||||
selectionColor: root.primaryColor.hslLightness > 0.5 ? Qt.darker(root.primaryColor, 1.5)
|
|
||||||
: Qt.lighter(root.primaryColor, 1.5)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header: Item {
|
header: Item {
|
||||||
@@ -69,7 +105,7 @@ ChatRootView {
|
|||||||
height: 30
|
height: 30
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: QQC.ScrollBar {
|
||||||
id: scroll
|
id: scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,15 +131,28 @@ ChatRootView {
|
|||||||
id: messageInput
|
id: messageInput
|
||||||
|
|
||||||
placeholderText: qsTr("Type your message here...")
|
placeholderText: qsTr("Type your message here...")
|
||||||
placeholderTextColor: "#888"
|
placeholderTextColor: palette.mid
|
||||||
color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
color: palette.text
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: 2
|
radius: 2
|
||||||
color: root.primaryColor
|
color: palette.base
|
||||||
border.color: root.primaryColor.hslLightness > 0.5 ? Qt.lighter(root.primaryColor, 1.5)
|
border.color: messageInput.activeFocus ? palette.highlight : palette.button
|
||||||
: 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()
|
||||||
@@ -113,65 +162,51 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
AttachedFilesPlace {
|
||||||
|
id: attachedFilesPlace
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 5
|
attachedFilesModel: root.attachmentFiles
|
||||||
|
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||||
Button {
|
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
|
||||||
id: sendButton
|
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
|
||||||
|
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
text: qsTr("Send")
|
|
||||||
onClicked: root.sendChatMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
id: stopButton
|
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
text: qsTr("Stop")
|
|
||||||
onClicked: root.cancelRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
id: clearButton
|
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
text: qsTr("Clear Chat")
|
|
||||||
onClicked: root.clearChat()
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
id: sharingCurrentFile
|
|
||||||
|
|
||||||
text: "Share current file with models"
|
|
||||||
checked: root.isSharingCurrentFile
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
AttachedFilesPlace {
|
||||||
id: bar
|
id: linkedFilesPlace
|
||||||
|
|
||||||
layoutDirection: Qt.RightToLeft
|
Layout.fillWidth: true
|
||||||
|
attachedFilesModel: root.linkedFiles
|
||||||
anchors {
|
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||||
left: parent.left
|
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
|
||||||
leftMargin: 5
|
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
|
||||||
right: parent.right
|
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||||
rightMargin: scroll.width
|
|
||||||
}
|
}
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Badge {
|
BottomBar {
|
||||||
text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
|
id: bottomBar
|
||||||
color: root.codeColor
|
|
||||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
Layout.preferredWidth: parent.width
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
|
||||||
|
sendButton.onClicked: root.sendChatMessage()
|
||||||
|
stopButton.onClicked: root.cancelRequest()
|
||||||
|
syncOpenFiles {
|
||||||
|
checked: root.isSyncOpenFiles
|
||||||
|
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||||
|
}
|
||||||
|
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||||
|
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||||
|
testRag.onClicked: root.testRAG(messageInput.text)
|
||||||
|
testChunks.onClicked: root.testChunking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.chatModel.clear()
|
root.chatModel.clear()
|
||||||
|
root.clearAttachmentFiles()
|
||||||
|
root.updateInputTokensCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
@@ -179,7 +214,7 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.sendMessage(messageInput.text, sharingCurrentFile.checked)
|
root.sendMessage(messageInput.text)
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|||||||
54
ChatView/qml/controls/QoAButton.qml
Normal file
54
ChatView/qml/controls/QoAButton.qml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ 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) {
|
||||||
@@ -41,6 +40,7 @@ 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: 12
|
font.pointSize: Qt.application.font.pointSize
|
||||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
selectionColor: root.selectionColor
|
selectionColor: palette.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
@@ -80,7 +80,7 @@ Rectangle {
|
|||||||
font.pointSize: 8
|
font.pointSize: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
QoAButton {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.margins: 5
|
anchors.margins: 5
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ TextEdit {
|
|||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
textFormat: Text.StyledText
|
textFormat: Text.StyledText
|
||||||
|
selectionColor: palette.highlight
|
||||||
|
color: palette.text
|
||||||
}
|
}
|
||||||
|
|||||||
109
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
109
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
ChatView/qml/parts/BottomBar.qml
Normal file
112
ChatView/qml/parts/BottomBar.qml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ChatView
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias sendButton: sendButtonId
|
||||||
|
property alias stopButton: stopButtonId
|
||||||
|
property alias syncOpenFiles: syncOpenFilesId
|
||||||
|
property alias attachFiles: attachFilesId
|
||||||
|
property alias linkFiles: linkFilesId
|
||||||
|
property alias testRag: testRagId
|
||||||
|
property alias testChunks: testChunksId
|
||||||
|
|
||||||
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
|
Qt.darker(palette.window, 1.1) :
|
||||||
|
Qt.lighter(palette.window, 1.1)
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: bottomBar
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 5
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: 5
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: sendButtonId
|
||||||
|
|
||||||
|
text: qsTr("Send")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: stopButtonId
|
||||||
|
|
||||||
|
text: qsTr("Stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: attachFilesId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
text: qsTr("Attach files")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: linkFilesId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
text: qsTr("Link files")
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: syncOpenFilesId
|
||||||
|
|
||||||
|
text: qsTr("Sync open files")
|
||||||
|
|
||||||
|
ToolTip.visible: syncOpenFilesId.hovered
|
||||||
|
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: testRagId
|
||||||
|
|
||||||
|
text: qsTr("Test RAG")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: testChunksId
|
||||||
|
|
||||||
|
text: qsTr("Test Chunks")
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
ChatView/qml/parts/TopBar.qml
Normal file
88
ChatView/qml/parts/TopBar.qml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
CodeHandler.cpp
Normal file
128
CodeHandler.cpp
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "CodeHandler.hpp"
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
QString CodeHandler::processText(QString text)
|
||||||
|
{
|
||||||
|
QString result;
|
||||||
|
QStringList lines = text.split('\n');
|
||||||
|
bool inCodeBlock = false;
|
||||||
|
QString pendingComments;
|
||||||
|
QString currentLanguage;
|
||||||
|
|
||||||
|
for (const QString &line : lines) {
|
||||||
|
if (line.trimmed().startsWith("```")) {
|
||||||
|
if (!inCodeBlock) {
|
||||||
|
currentLanguage = detectLanguage(line);
|
||||||
|
}
|
||||||
|
inCodeBlock = !inCodeBlock;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inCodeBlock) {
|
||||||
|
if (!pendingComments.isEmpty()) {
|
||||||
|
QStringList commentLines = pendingComments.split('\n');
|
||||||
|
QString commentPrefix = getCommentPrefix(currentLanguage);
|
||||||
|
|
||||||
|
for (const QString &commentLine : commentLines) {
|
||||||
|
if (!commentLine.trimmed().isEmpty()) {
|
||||||
|
result += commentPrefix + " " + commentLine.trimmed() + "\n";
|
||||||
|
} else {
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingComments.clear();
|
||||||
|
}
|
||||||
|
result += line + "\n";
|
||||||
|
} else {
|
||||||
|
QString trimmed = line.trimmed();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
pendingComments += trimmed + "\n";
|
||||||
|
} else {
|
||||||
|
pendingComments += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingComments.isEmpty()) {
|
||||||
|
QStringList commentLines = pendingComments.split('\n');
|
||||||
|
QString commentPrefix = getCommentPrefix(currentLanguage);
|
||||||
|
|
||||||
|
for (const QString &commentLine : commentLines) {
|
||||||
|
if (!commentLine.trimmed().isEmpty()) {
|
||||||
|
result += commentPrefix + " " + commentLine.trimmed() + "\n";
|
||||||
|
} else {
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CodeHandler::getCommentPrefix(const QString &language)
|
||||||
|
{
|
||||||
|
static const QHash<QString, QString> commentPrefixes
|
||||||
|
= {{"python", "#"}, {"py", "#"}, {"lua", "--"}, {"javascript", "//"},
|
||||||
|
{"js", "//"}, {"typescript", "//"}, {"ts", "//"}, {"cpp", "//"},
|
||||||
|
{"c++", "//"}, {"c", "//"}, {"java", "//"}, {"csharp", "//"},
|
||||||
|
{"cs", "//"}, {"php", "//"}, {"ruby", "#"}, {"rb", "#"},
|
||||||
|
{"rust", "//"}, {"rs", "//"}, {"go", "//"}, {"swift", "//"},
|
||||||
|
{"kotlin", "//"}, {"kt", "//"}, {"scala", "//"}, {"r", "#"},
|
||||||
|
{"shell", "#"}, {"bash", "#"}, {"sh", "#"}, {"perl", "#"},
|
||||||
|
{"pl", "#"}, {"haskell", "--"}, {"hs", "--"}};
|
||||||
|
|
||||||
|
return commentPrefixes.value(language.toLower(), "//");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CodeHandler::detectLanguage(const QString &line)
|
||||||
|
{
|
||||||
|
QString trimmed = line.trimmed();
|
||||||
|
if (trimmed.length() <= 3) { // Если только ```
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.mid(3).trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression
|
||||||
|
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression
|
||||||
|
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
@@ -17,32 +17,26 @@
|
|||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "CounterTooltip.hpp"
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
CounterTooltip::CounterTooltip(int count)
|
class CodeHandler
|
||||||
: m_count(count)
|
|
||||||
{
|
{
|
||||||
m_label = new QLabel(this);
|
public:
|
||||||
addWidget(m_label);
|
static QString processText(QString text);
|
||||||
updateLabel();
|
|
||||||
|
|
||||||
m_timer = new QTimer(this);
|
private:
|
||||||
m_timer->setSingleShot(true);
|
static QString getCommentPrefix(const QString &language);
|
||||||
m_timer->setInterval(2000);
|
static QString detectLanguage(const QString &line);
|
||||||
|
|
||||||
connect(m_timer, &QTimer::timeout, this, [this] { emit finished(m_count); });
|
static const QRegularExpression &getFullCodeBlockRegex();
|
||||||
|
static const QRegularExpression &getPartialStartBlockRegex();
|
||||||
m_timer->start();
|
static const QRegularExpression &getPartialEndBlockRegex();
|
||||||
}
|
};
|
||||||
|
|
||||||
CounterTooltip::~CounterTooltip() {}
|
|
||||||
|
|
||||||
void CounterTooltip::updateLabel()
|
|
||||||
{
|
|
||||||
const auto hotkey = QKeySequence(QKeySequence::MoveToNextWord).toString();
|
|
||||||
m_label->setText(QString("Insert Next %1 line(s) (%2)").arg(m_count).arg(hotkey));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
@@ -57,6 +57,13 @@ void ConfigurationManager::setupConnections()
|
|||||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||||
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||||
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||||
|
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||||
|
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
|
||||||
|
connect(
|
||||||
|
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigurationManager::selectProvider()
|
void ConfigurationManager::selectProvider()
|
||||||
@@ -69,6 +76,8 @@ 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] {
|
||||||
@@ -86,14 +95,19 @@ 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()
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
: isPreset1 ? m_generalSettings.ccPreset1Provider.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 : m_generalSettings.caModel;
|
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
||||||
|
: 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()) {
|
||||||
@@ -122,11 +136,13 @@ void ConfigurationManager::selectTemplate()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
||||||
|
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
||||||
|
|
||||||
const auto templateList = isCodeCompletion ? m_templateManger.fimTemplatesNames()
|
const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames()
|
||||||
: m_templateManger.chatTemplatesNames();
|
: m_templateManger.chatTemplatesNames();
|
||||||
|
|
||||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
||||||
|
: isPreset1 ? m_generalSettings.ccPreset1Template
|
||||||
: m_generalSettings.caTemplate;
|
: m_generalSettings.caTemplate;
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
||||||
@@ -150,8 +166,9 @@ void ConfigurationManager::selectUrl()
|
|||||||
urls.append(url);
|
urls.append(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
|
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
||||||
? m_generalSettings.ccUrl
|
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
||||||
|
? 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]() {
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
#include <llmcore/RequestConfig.hpp>
|
#include <llmcore/RequestConfig.hpp>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
|
|
||||||
#include "DocumentContextReader.hpp"
|
#include "CodeHandler.hpp"
|
||||||
|
#include "context/DocumentContextReader.hpp"
|
||||||
|
#include "llmcore/MessageBuilder.hpp"
|
||||||
#include "llmcore/PromptTemplateManager.hpp"
|
#include "llmcore/PromptTemplateManager.hpp"
|
||||||
#include "llmcore/ProvidersManager.hpp"
|
#include "llmcore/ProvidersManager.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
@@ -144,20 +146,41 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
|
|||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool QodeAssist::LLMClientInterface::isSpecifyCompletion(const QJsonObject &request)
|
||||||
|
{
|
||||||
|
auto &generalSettings = Settings::generalSettings();
|
||||||
|
|
||||||
|
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(request);
|
||||||
|
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
|
||||||
|
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
|
||||||
|
|
||||||
|
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
|
||||||
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||||
{
|
{
|
||||||
auto updatedContext = prepareContext(request);
|
const auto updatedContext = prepareContext(request);
|
||||||
auto &completeSettings = Settings::codeCompletionSettings();
|
auto &completeSettings = Settings::codeCompletionSettings();
|
||||||
|
auto &generalSettings = Settings::generalSettings();
|
||||||
|
|
||||||
auto providerName = Settings::generalSettings().ccProvider();
|
bool isPreset1Active = isSpecifyCompletion(request);
|
||||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
|
const auto providerName = !isPreset1Active ? generalSettings.ccProvider()
|
||||||
|
: generalSettings.ccPreset1Provider();
|
||||||
|
const auto modelName = !isPreset1Active ? generalSettings.ccModel()
|
||||||
|
: generalSettings.ccPreset1Model();
|
||||||
|
const auto url = !isPreset1Active ? generalSettings.ccUrl() : generalSettings.ccPreset1Url();
|
||||||
|
|
||||||
|
const auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().ccTemplate();
|
auto templateName = !isPreset1Active ? generalSettings.ccTemplate()
|
||||||
|
: generalSettings.ccPreset1Template();
|
||||||
|
|
||||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
||||||
templateName);
|
templateName);
|
||||||
|
|
||||||
@@ -166,33 +189,56 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO refactor to dynamic presets system
|
||||||
LLMCore::LLMConfig config;
|
LLMCore::LLMConfig config;
|
||||||
config.requestType = LLMCore::RequestType::Fim;
|
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||||
config.provider = provider;
|
config.provider = provider;
|
||||||
config.promptTemplate = promptTemplate;
|
config.promptTemplate = promptTemplate;
|
||||||
config.url = QUrl(
|
config.url = QUrl(QString("%1%2").arg(
|
||||||
QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
|
url,
|
||||||
config.apiKey = Settings::codeCompletionSettings().apiKey();
|
promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint()
|
||||||
|
: provider->chatEndpoint()));
|
||||||
|
config.apiKey = provider->apiKey();
|
||||||
|
|
||||||
config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
|
config.providerRequest = {{"model", modelName}, {"stream", completeSettings.stream()}};
|
||||||
|
|
||||||
config.multiLineCompletion = completeSettings.multiLineCompletion();
|
config.multiLineCompletion = completeSettings.multiLineCompletion();
|
||||||
|
|
||||||
|
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
||||||
|
if (!stopWords.isEmpty())
|
||||||
|
config.providerRequest["stop"] = stopWords;
|
||||||
|
|
||||||
QString systemPrompt;
|
QString systemPrompt;
|
||||||
if (completeSettings.useSystemPrompt())
|
if (completeSettings.useSystemPrompt())
|
||||||
systemPrompt.append(completeSettings.systemPrompt());
|
systemPrompt.append(completeSettings.systemPrompt());
|
||||||
if (!updatedContext.fileContext.isEmpty())
|
if (!updatedContext.fileContext.isEmpty())
|
||||||
systemPrompt.append(updatedContext.fileContext);
|
systemPrompt.append(updatedContext.fileContext);
|
||||||
if (!systemPrompt.isEmpty())
|
|
||||||
config.providerRequest["system"] = systemPrompt;
|
|
||||||
|
|
||||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
QString userMessage;
|
||||||
if (!stopWords.isEmpty())
|
if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) {
|
||||||
config.providerRequest["stop"] = stopWords;
|
userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix);
|
||||||
|
} else {
|
||||||
|
userMessage = updatedContext.prefix;
|
||||||
|
}
|
||||||
|
|
||||||
config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
|
auto message = LLMCore::MessageBuilder()
|
||||||
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
|
.addSystemMessage(systemPrompt)
|
||||||
|
.addUserMessage(userMessage)
|
||||||
|
.addSuffix(updatedContext.suffix)
|
||||||
|
.addTokenizer(promptTemplate);
|
||||||
|
|
||||||
|
message.saveTo(
|
||||||
|
config.providerRequest,
|
||||||
|
providerName == "Ollama" ? LLMCore::ProvidersApi::Ollama : LLMCore::ProvidersApi::OpenAI);
|
||||||
|
|
||||||
|
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::CodeCompletion);
|
||||||
|
|
||||||
|
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
LOG_MESSAGE("Validate errors for fim request:");
|
||||||
|
LOG_MESSAGES(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_requestHandler.sendLLMRequest(config, request);
|
m_requestHandler.sendLLMRequest(config, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,27 +262,61 @@ LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &reque
|
|||||||
int cursorPosition = position["character"].toInt();
|
int cursorPosition = position["character"].toInt();
|
||||||
int lineNumber = position["line"].toInt();
|
int lineNumber = position["line"].toInt();
|
||||||
|
|
||||||
DocumentContextReader reader(textDocument);
|
Context::DocumentContextReader reader(textDocument);
|
||||||
return reader.prepareContext(lineNumber, cursorPosition);
|
return reader.prepareContext(lineNumber, cursorPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Context::ProgrammingLanguage LLMClientInterface::getDocumentLanguage(const QJsonObject &request) const
|
||||||
|
{
|
||||||
|
QJsonObject params = request["params"].toObject();
|
||||||
|
QJsonObject doc = params["doc"].toObject();
|
||||||
|
QString uri = doc["uri"].toString();
|
||||||
|
|
||||||
|
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
|
||||||
|
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
|
||||||
|
filePath);
|
||||||
|
|
||||||
|
if (!textDocument) {
|
||||||
|
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
|
||||||
|
return Context::ProgrammingLanguage::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Context::ProgrammingLanguageUtils::fromMimeType(textDocument->mimeType());
|
||||||
|
}
|
||||||
|
|
||||||
void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||||
const QJsonObject &request,
|
const QJsonObject &request,
|
||||||
bool isComplete)
|
bool isComplete)
|
||||||
{
|
{
|
||||||
|
bool isPreset1Active = isSpecifyCompletion(request);
|
||||||
|
|
||||||
|
auto templateName = !isPreset1Active ? Settings::generalSettings().ccTemplate()
|
||||||
|
: Settings::generalSettings().ccPreset1Template();
|
||||||
|
|
||||||
|
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
||||||
|
templateName);
|
||||||
|
|
||||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||||
|
|
||||||
QJsonObject response;
|
QJsonObject response;
|
||||||
response["jsonrpc"] = "2.0";
|
response["jsonrpc"] = "2.0";
|
||||||
response[LanguageServerProtocol::idKey] = request["id"];
|
response[LanguageServerProtocol::idKey] = request["id"];
|
||||||
|
|
||||||
QJsonObject result;
|
QJsonObject result;
|
||||||
QJsonArray completions;
|
QJsonArray completions;
|
||||||
QJsonObject completionItem;
|
QJsonObject completionItem;
|
||||||
completionItem[LanguageServerProtocol::textKey] = completion;
|
|
||||||
|
QString processedCompletion
|
||||||
|
= promptTemplate->type() == LLMCore::TemplateType::Chat
|
||||||
|
&& Settings::codeCompletionSettings().smartProcessInstuctText()
|
||||||
|
? CodeHandler::processText(completion)
|
||||||
|
: completion;
|
||||||
|
|
||||||
|
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
||||||
QJsonObject range;
|
QJsonObject range;
|
||||||
range["start"] = position;
|
range["start"] = position;
|
||||||
QJsonObject end = position;
|
QJsonObject end = position;
|
||||||
end["character"] = position["character"].toInt() + completion.length();
|
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||||
range["end"] = end;
|
range["end"] = end;
|
||||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <languageclient/languageclientinterface.h>
|
#include <languageclient/languageclientinterface.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include <context/ProgrammingLanguage.hpp>
|
||||||
#include <llmcore/ContextData.hpp>
|
#include <llmcore/ContextData.hpp>
|
||||||
#include <llmcore/RequestHandler.hpp>
|
#include <llmcore/RequestHandler.hpp>
|
||||||
|
|
||||||
@@ -58,8 +59,10 @@ private:
|
|||||||
void handleExit(const QJsonObject &request);
|
void handleExit(const QJsonObject &request);
|
||||||
void handleCancelRequest(const QJsonObject &request);
|
void handleCancelRequest(const QJsonObject &request);
|
||||||
|
|
||||||
LLMCore::ContextData prepareContext(const QJsonObject &request,
|
LLMCore::ContextData prepareContext(
|
||||||
const QStringView &accumulatedCompletion = QString{});
|
const QJsonObject &request, const QStringView &accumulatedCompletion = QString{});
|
||||||
|
Context::ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
|
||||||
|
bool isSpecifyCompletion(const QJsonObject &request);
|
||||||
|
|
||||||
LLMCore::RequestHandler m_requestHandler;
|
LLMCore::RequestHandler m_requestHandler;
|
||||||
QElapsedTimer m_completionTimer;
|
QElapsedTimer m_completionTimer;
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
|
* 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
|
||||||
@@ -18,30 +23,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#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(const Completion &completion, QTextDocument *origin)
|
LLMSuggestion::LLMSuggestion(
|
||||||
: m_completion(completion)
|
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
|
||||||
, m_linesCount(0)
|
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
|
||||||
{
|
{
|
||||||
int startPos = completion.range().start().toPositionInDocument(origin);
|
const auto &data = suggestions[currentCompletion];
|
||||||
int endPos = completion.range().end().toPositionInDocument(origin);
|
|
||||||
|
|
||||||
startPos = qBound(0, startPos, origin->characterCount() - 1);
|
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
|
||||||
endPos = qBound(startPos, endPos, origin->characterCount() - 1);
|
int endPos = data.range.end.toPositionInDocument(sourceDocument);
|
||||||
|
|
||||||
m_start = QTextCursor(origin);
|
startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
|
||||||
m_start.setPosition(startPos);
|
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
|
||||||
m_start.setKeepPositionOnInsert(true);
|
|
||||||
|
|
||||||
QTextCursor cursor(origin);
|
QTextCursor cursor(sourceDocument);
|
||||||
cursor.setPosition(startPos);
|
cursor.setPosition(startPos);
|
||||||
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||||
|
|
||||||
@@ -51,74 +51,57 @@ LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin
|
|||||||
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, completion.text());
|
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.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 applyNextLine(widget);
|
return applyPart(Word, widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMSuggestion::applyNextLine(TextEditor::TextEditorWidget *widget)
|
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
|
||||||
{
|
{
|
||||||
const QString text = m_completion.text();
|
return applyPart(Line, widget);
|
||||||
QStringList lines = text.split('\n');
|
|
||||||
|
|
||||||
if (m_linesCount < lines.size())
|
|
||||||
m_linesCount++;
|
|
||||||
|
|
||||||
showTooltip(widget, m_linesCount);
|
|
||||||
|
|
||||||
return m_linesCount == lines.size() && !Utils::ToolTip::isVisible();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMSuggestion::onCounterFinished(int count)
|
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||||
{
|
{
|
||||||
Utils::ToolTip::hide();
|
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||||
m_linesCount = 0;
|
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||||
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
|
QTextCursor currentCursor = widget->textCursor();
|
||||||
cursor.beginEditBlock();
|
const QString text = suggestions()[currentSuggestion()].text;
|
||||||
cursor.removeSelectedText();
|
|
||||||
|
|
||||||
QStringList lines = m_completion.text().split('\n');
|
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
||||||
QString textToInsert = lines.mid(0, count).join('\n');
|
+ (cursor.selectionEnd() - cursor.selectionStart());
|
||||||
|
|
||||||
cursor.insertText(textToInsert);
|
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
||||||
cursor.endEditBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMSuggestion::reset()
|
if (next == -1)
|
||||||
{
|
return apply();
|
||||||
m_start.removeSelectedText();
|
|
||||||
}
|
|
||||||
|
|
||||||
int LLMSuggestion::position()
|
if (part == Line)
|
||||||
{
|
++next;
|
||||||
return m_start.position();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
|
QString subText = text.mid(startPos, next - startPos);
|
||||||
{
|
if (subText.isEmpty())
|
||||||
Utils::ToolTip::hide();
|
return false;
|
||||||
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
|
|
||||||
pos += QPoint(-10, -50);
|
currentCursor.insertText(subText);
|
||||||
m_counterTooltip = new CounterTooltip(count);
|
|
||||||
Utils::ToolTip::show(pos, m_counterTooltip, widget);
|
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
|
||||||
connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
|
* 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
|
||||||
@@ -19,37 +24,21 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <texteditor/texteditor.h>
|
||||||
#include "LSPCompletion.hpp"
|
#include <texteditor/textsuggestion.h>
|
||||||
#include <texteditor/textdocumentlayout.h>
|
|
||||||
|
|
||||||
#include "utils/CounterTooltip.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion
|
class LLMSuggestion : public TextEditor::CyclicSuggestion
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
LLMSuggestion(const Completion &completion, QTextDocument *origin);
|
enum Part { Word, Line };
|
||||||
|
|
||||||
bool apply() final;
|
LLMSuggestion(
|
||||||
bool applyWord(TextEditor::TextEditorWidget *widget) final;
|
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
|
||||||
bool applyNextLine(TextEditor::TextEditorWidget *widget);
|
|
||||||
void reset() final;
|
|
||||||
int position() final;
|
|
||||||
|
|
||||||
const Completion &completion() const { return m_completion; }
|
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
||||||
|
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||||
void showTooltip(TextEditor::TextEditorWidget *widget, int count);
|
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||||
void onCounterFinished(int count);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Completion m_completion;
|
|
||||||
QTextCursor m_start;
|
|
||||||
int m_linesCount;
|
|
||||||
|
|
||||||
CounterTooltip *m_counterTooltip = nullptr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.3.10",
|
"Version" : "0.4.13",
|
||||||
"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" : "GNU General Public License Usage
|
"License" : "GPLv3",
|
||||||
|
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
||||||
Alternatively, this file may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this file. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.",
|
|
||||||
"Description" : ["QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code",
|
|
||||||
"Prerequisites:",
|
|
||||||
"- One of the supported LLM providers installed (e.g., Ollama or LM Studio)",
|
|
||||||
"- A compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2)"],
|
|
||||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||||
|
"DocumentationUrl" : "",
|
||||||
${IDE_PLUGIN_DEPENDENCIES}
|
${IDE_PLUGIN_DEPENDENCIES}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,10 @@
|
|||||||
|
|
||||||
#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;
|
||||||
@@ -70,48 +71,63 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Client::openDocument(document);
|
Client::openDocument(document);
|
||||||
connect(document,
|
connect(
|
||||||
&TextDocument::contentsChangedWithPosition,
|
document,
|
||||||
this,
|
&TextDocument::contentsChangedWithPosition,
|
||||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
this,
|
||||||
Q_UNUSED(charsRemoved)
|
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto project = ProjectManager::projectForFile(document->filePath());
|
auto project = ProjectManager::projectForFile(document->filePath());
|
||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto textEditor = BaseTextEditor::currentTextEditor();
|
auto textEditor = BaseTextEditor::currentTextEditor();
|
||||||
if (!textEditor || textEditor->document() != document)
|
if (!textEditor || textEditor->document() != document)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
||||||
ChangesManager::instance().addChange(document,
|
Context::ChangesManager::instance()
|
||||||
position,
|
.addChange(document, position, charsRemoved, charsAdded);
|
||||||
charsRemoved,
|
|
||||||
charsAdded);
|
|
||||||
|
|
||||||
TextEditorWidget *widget = textEditor->editorWidget();
|
TextEditorWidget *widget = textEditor->editorWidget();
|
||||||
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
||||||
return;
|
return;
|
||||||
const int cursorPosition = widget->textCursor().position();
|
|
||||||
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_recentCharCount += charsAdded;
|
const int cursorPosition = widget->textCursor().position();
|
||||||
|
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||||
|
return;
|
||||||
|
|
||||||
if (m_typingTimer.elapsed()
|
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
m_recentCharCount = 0;
|
||||||
m_recentCharCount = charsAdded;
|
m_typingTimer.restart();
|
||||||
m_typingTimer.restart();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_recentCharCount
|
QTextCursor cursor = widget->textCursor();
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||||
scheduleRequest(widget);
|
QString lastChar = cursor.selectedText();
|
||||||
}
|
|
||||||
});
|
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
|
||||||
|
m_recentCharCount = 0;
|
||||||
|
m_typingTimer.restart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_recentCharCount += charsAdded;
|
||||||
|
|
||||||
|
if (m_typingTimer.elapsed()
|
||||||
|
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
||||||
|
m_recentCharCount = charsAdded;
|
||||||
|
m_typingTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_recentCharCount
|
||||||
|
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
||||||
|
scheduleRequest(widget);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
|
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
|
||||||
@@ -193,8 +209,8 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
|||||||
auto isValidCompletion = [](const Completion &completion) {
|
auto isValidCompletion = [](const Completion &completion) {
|
||||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||||
};
|
};
|
||||||
QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(),
|
QList<Completion> completions
|
||||||
isValidCompletion);
|
= Utils::filtered(result->completions().toListOrEmpty(), 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) {
|
||||||
@@ -211,10 +227,18 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
|||||||
if (delta > 0)
|
if (delta > 0)
|
||||||
completion.setText(completionText.chopped(delta));
|
completion.setText(completionText.chopped(delta));
|
||||||
}
|
}
|
||||||
|
auto suggestions = Utils::transform(completions, [](const Completion &c) {
|
||||||
|
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
||||||
|
return Text::Position{pos.line() + 1, pos.character()};
|
||||||
|
};
|
||||||
|
|
||||||
|
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
||||||
|
Text::Position pos{toTextPos(c.position())};
|
||||||
|
return TextSuggestion::Data{range, pos, c.text()};
|
||||||
|
});
|
||||||
if (completions.isEmpty())
|
if (completions.isEmpty())
|
||||||
return;
|
return;
|
||||||
editor->insertSuggestion(
|
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||||
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +253,11 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
|||||||
|
|
||||||
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
|
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
|
||||||
{
|
{
|
||||||
return Settings::generalSettings().enableQodeAssist();
|
if (!project)
|
||||||
|
return Settings::generalSettings().enableQodeAssist();
|
||||||
|
|
||||||
|
Settings::ProjectSettings settings(project);
|
||||||
|
return settings.isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::setupConnections()
|
void QodeAssistClient::setupConnections()
|
||||||
|
|||||||
208
README.md
208
README.md
@@ -1,34 +1,57 @@
|
|||||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
||||||
[](https://discord.gg/DGgMtTteAD)
|
|
||||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
[](https://discord.gg/BGMkUsXUgf)
|
||||||
|
|
||||||
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. [Installation](#installation)
|
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
|
||||||
3. [Configure Plugin](#configure-plugin)
|
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
|
||||||
4. [Supported LLM Providers](#supported-llm-providers)
|
4. [Configure for OpenAI](#configure-for-openai)
|
||||||
5. [Recommended Models](#recommended-models)
|
5. [Configure for using Ollama](#configure-for-using-ollama)
|
||||||
- [Ollama](#ollama)
|
6. [System Prompt Configuration](#system-prompt-configuration)
|
||||||
- [LM Studio](#lm-studio)
|
7. [File Context Features](#file-context-features)
|
||||||
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
8. [Template-Model Compatibility](#template-model-compatibility)
|
||||||
7. [Development Progress](#development-progress)
|
9. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||||
8. [Hotkeys](#hotkeys)
|
10. [Development Progress](#development-progress)
|
||||||
9. [Troubleshooting](#troubleshooting)
|
11. [Hotkeys](#hotkeys)
|
||||||
10. [Support the Development](#support-the-development-of-qodeassist)
|
12. [Troubleshooting](#troubleshooting)
|
||||||
11. [How to Build](#how-to-build)
|
13. [Support the Development](#support-the-development-of-qodeassist)
|
||||||
|
14. [How to Build](#how-to-build)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
- AI-powered code completion
|
- AI-powered code completion
|
||||||
- Chat functionality:
|
- Chat functionality:
|
||||||
- Side and Bottom panels
|
- Side and Bottom panels
|
||||||
|
- Chat history autosave and restore
|
||||||
|
- Token usage monitoring and management
|
||||||
|
- Attach files for one-time code analysis
|
||||||
|
- Link files for persistent context with auto update in conversations
|
||||||
|
- Automatic syncing with open editor files (optional)
|
||||||
- Support for multiple LLM providers:
|
- Support for multiple LLM providers:
|
||||||
- Ollama
|
- Ollama
|
||||||
|
- OpenAI
|
||||||
|
- Anthropic Claude
|
||||||
- LM Studio
|
- LM Studio
|
||||||
- OpenAI-compatible providers(eg. https://openrouter.ai)
|
- OpenAI-compatible providers(eg. https://openrouter.ai)
|
||||||
- Extensive library of model-specific templates
|
- Extensive library of model-specific templates
|
||||||
@@ -40,6 +63,11 @@ QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides
|
|||||||
<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">
|
||||||
@@ -50,11 +78,52 @@ QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides
|
|||||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Installation
|
<details>
|
||||||
|
<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>
|
||||||
|
|
||||||
1. Install Latest QtCreator
|
## Install plugin to QtCreator
|
||||||
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
1. Install Latest Qt Creator
|
||||||
3. Install a language models in Ollama via terminal. For example, you can run:
|
2. Download the QodeAssist plugin for your Qt Creator
|
||||||
|
- Remove old version plugin if already was installed
|
||||||
|
3. Launch Qt Creator and install the plugin:
|
||||||
|
- Go to:
|
||||||
|
- MacOS: Qt Creator -> About Plugins...
|
||||||
|
- Windows\Linux: Help -> About Plugins...
|
||||||
|
- Click on "Install Plugin..."
|
||||||
|
- Select the downloaded QodeAssist plugin archive file
|
||||||
|
|
||||||
|
## Configure for Anthropic Claude
|
||||||
|
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||||
|
2. Go to Provider Settings tab and configure Claude api key
|
||||||
|
3. Return to General tab and configure:
|
||||||
|
- Set "Claude" as the provider for code completion or/and chat assistant
|
||||||
|
- Set the Claude URL (https://api.anthropic.com)
|
||||||
|
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
|
||||||
|
- Choose the Claude template for code completion or/and chat
|
||||||
|
<details>
|
||||||
|
<summary>Example of Claude settings: (click to expand)</summary>
|
||||||
|
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Configure for OpenAI
|
||||||
|
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||||
|
2. Go to Provider Settings tab and configure OpenAI api key
|
||||||
|
3. Return to General tab and configure:
|
||||||
|
- Set "OpenAI" as the provider for code completion or/and chat assistant
|
||||||
|
- Set the OpenAI URL (https://api.openai.com)
|
||||||
|
- Select your preferred model (e.g., gpt-4o)
|
||||||
|
- Choose the OpenAI template for code completion or/and chat
|
||||||
|
<details>
|
||||||
|
<summary>Example of OpenAI settings: (click to expand)</summary>
|
||||||
|
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Configure for using Ollama
|
||||||
|
|
||||||
|
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
||||||
|
2. Install a language models in Ollama via terminal. For example, you can run:
|
||||||
|
|
||||||
For standard computers (minimum 8GB RAM):
|
For standard computers (minimum 8GB RAM):
|
||||||
```
|
```
|
||||||
@@ -68,16 +137,6 @@ 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
|
||||||
@@ -85,65 +144,72 @@ QodeAssist comes with default settings that should work immediately after instal
|
|||||||
- 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
|
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
|
||||||
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>
|
||||||
|
|
||||||
## Supported LLM Providers
|
## System Prompt Configuration
|
||||||
QodeAssist currently supports the following LLM (Large Language Model) providers:
|
|
||||||
- [Ollama](https://ollama.com)
|
|
||||||
- [LM Studio](https://lmstudio.ai) (experimental)
|
|
||||||
- OpenAI compatible providers (experimental)
|
|
||||||
|
|
||||||
## Recommended 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.
|
||||||
QodeAssist has been thoroughly tested and optimized for use with the following language models:
|
|
||||||
|
|
||||||
- Qwen2.5-coder
|
## File Context Features
|
||||||
- CodeLlama
|
|
||||||
- StarCoder2
|
|
||||||
- DeepSeek-Coder-V2
|
|
||||||
|
|
||||||
### Ollama:
|
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.
|
||||||
### 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Template-Model Compatibility
|
### Attached Files
|
||||||
|
|
||||||
|
Attachments are designed for one-time code analysis and specific queries:
|
||||||
|
- Files are included only in the current message
|
||||||
|
- Content is discarded after the message is processed
|
||||||
|
- Ideal for:
|
||||||
|
- Getting specific feedback on code changes
|
||||||
|
- Code review requests
|
||||||
|
- Analyzing isolated code segments
|
||||||
|
- Quick implementation questions
|
||||||
|
- Files can be attached using the paperclip icon in the chat interface
|
||||||
|
- Multiple files can be attached to a single message
|
||||||
|
|
||||||
|
### Linked Files
|
||||||
|
|
||||||
|
Linked files provide persistent context throughout the conversation:
|
||||||
|
|
||||||
|
- Files remain accessible for the entire chat session
|
||||||
|
- Content is included in every message exchange
|
||||||
|
- Files are automatically refreshed - always using latest content from disk
|
||||||
|
- Perfect for:
|
||||||
|
- Long-term refactoring discussions
|
||||||
|
- Complex architectural changes
|
||||||
|
- Multi-file implementations
|
||||||
|
- Maintaining context across related questions
|
||||||
|
- Can be managed using the link icon in the chat interface
|
||||||
|
- Supports automatic syncing with open editor files (can be enabled in settings)
|
||||||
|
- Files can be added/removed at any time during the conversation
|
||||||
|
|
||||||
|
## Template-Model Compatibility
|
||||||
|
|
||||||
| Template | Compatible Models | Purpose |
|
| Template | Compatible Models | Purpose |
|
||||||
|----------|------------------|----------|
|
|----------|------------------|----------|
|
||||||
| CodeLlama FIM | `codellama:code` | Code completion |
|
| CodeLlama FIM | `codellama:code` | Code completion |
|
||||||
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
|
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
|
||||||
| Ollama Auto FIM | `Any Ollama base model` | Code completion |
|
| Ollama Auto FIM | `Any Ollama base/fim models` | Code completion |
|
||||||
| Qwen FIM | `Qwen 2.5 models` | Code completion |
|
| Qwen FIM | `Qwen 2.5 models(exclude instruct)` | Code completion |
|
||||||
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
|
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
|
||||||
| Alpaca | `starcoder2:instruct` | Chat assistance |
|
| Alpaca | `starcoder2:instruct` | Chat assistance |
|
||||||
| Basic Chat| `Messages without tokens` | Chat assistance |
|
| Basic Chat| `Messages without tokens` | Chat assistance |
|
||||||
| ChatML | `Qwen 2.5 models` | Chat assistance |
|
| ChatML | `Qwen 2.5 models(exclude base models)` | Chat assistance |
|
||||||
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
|
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
|
||||||
| Llama3 | `llama3 model family` | Chat assistance |
|
| Llama3 | `llama3 model family` | Chat assistance |
|
||||||
| Ollama Auto Chat | `Any Ollama chat model` | Chat assistance |
|
| Ollama Auto Chat | `Any Ollama chat/instruct models` | Chat assistance |
|
||||||
|
|
||||||
> Note:
|
|
||||||
> - FIM (Fill-in-Middle) templates are optimized for code completion
|
|
||||||
> - Chat templates are designed for interactive dialogue
|
|
||||||
> - The Ollama Auto templates automatically adapt to most Ollama models
|
|
||||||
> - Custom Template allows you to define your own prompt format
|
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
|
- QtCreator 15.0.1 - 0.4.8 - 0.4.x
|
||||||
|
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
|
||||||
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
|
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
|
||||||
- QtCreator 14.0.1 - 0.2.2 plugin version and below
|
- QtCreator 14.0.1 - 0.2.2 plugin version and below
|
||||||
|
|
||||||
@@ -162,9 +228,7 @@ ollama run deepseek-coder-v2
|
|||||||
- on Mac: Option + Command + Q
|
- on Mac: Option + Command + Q
|
||||||
- on Windows: Ctrl + Alt + Q
|
- on Windows: Ctrl + Alt + Q
|
||||||
- To insert the full suggestion, you can use the TAB key
|
- To insert the full suggestion, you can use the TAB key
|
||||||
- To insert line by line, you can use the "Move cursor word right" shortcut:
|
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
|
||||||
- On Mac: Option + Right Arrow
|
|
||||||
- On Windows: Alt + Right Arrow
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -227,3 +291,7 @@ 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
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|||||||
72
UpdateStatusWidget.cpp
Normal file
72
UpdateStatusWidget.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
47
UpdateStatusWidget.hpp
Normal file
47
UpdateStatusWidget.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QFrame>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class UpdateStatusWidget : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit UpdateStatusWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setDefaultAction(QAction *action);
|
||||||
|
void showUpdateAvailable(const QString &version);
|
||||||
|
void hideUpdateInfo();
|
||||||
|
|
||||||
|
QPushButton *updateButton() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QToolButton *m_actionButton;
|
||||||
|
QLabel *m_versionLabel;
|
||||||
|
QPushButton *m_updateButton;
|
||||||
|
};
|
||||||
|
} // namespace QodeAssist
|
||||||
31
context/CMakeLists.txt
Normal file
31
context/CMakeLists.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
add_library(Context STATIC
|
||||||
|
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||||
|
ChangesManager.h ChangesManager.cpp
|
||||||
|
ContextManager.hpp ContextManager.cpp
|
||||||
|
ContentFile.hpp
|
||||||
|
TokenUtils.hpp TokenUtils.cpp
|
||||||
|
ProgrammingLanguage.hpp ProgrammingLanguage.cpp
|
||||||
|
RAGManager.hpp RAGManager.cpp
|
||||||
|
RAGStorage.hpp RAGStorage.cpp
|
||||||
|
RAGData.hpp
|
||||||
|
RAGVectorizer.hpp RAGVectorizer.cpp
|
||||||
|
RAGSimilaritySearch.hpp RAGSimilaritySearch.cpp
|
||||||
|
RAGPreprocessor.hpp RAGPreprocessor.cpp
|
||||||
|
EnhancedRAGSimilaritySearch.hpp EnhancedRAGSimilaritySearch.cpp
|
||||||
|
FileChunker.hpp FileChunker.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(Context
|
||||||
|
PUBLIC
|
||||||
|
Qt::Core
|
||||||
|
Qt::Sql
|
||||||
|
QtCreator::Core
|
||||||
|
QtCreator::TextEditor
|
||||||
|
QtCreator::Utils
|
||||||
|
QtCreator::ProjectExplorer
|
||||||
|
PRIVATE
|
||||||
|
LLMCore
|
||||||
|
QodeAssistSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(Context PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR})
|
||||||
@@ -18,9 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ChangesManager.h"
|
#include "ChangesManager.h"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "CodeCompletionSettings.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
ChangesManager &ChangesManager::instance()
|
ChangesManager &ChangesManager::instance()
|
||||||
{
|
{
|
||||||
@@ -79,4 +79,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist::Context
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class ChangesManager : public QObject
|
class ChangesManager : public QObject
|
||||||
{
|
{
|
||||||
@@ -58,4 +58,4 @@ private:
|
|||||||
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist::Context
|
||||||
32
context/ContentFile.hpp
Normal file
32
context/ContentFile.hpp
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
176
context/ContextManager.cpp
Normal file
176
context/ContextManager.cpp
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ContextManager.hpp"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectnodes.h>
|
||||||
|
|
||||||
|
#include "FileChunker.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
ContextManager &ContextManager::instance()
|
||||||
|
{
|
||||||
|
static ContextManager manager;
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextManager::ContextManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QString ContextManager::readFile(const QString &filePath) const
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
QTextStream in(&file);
|
||||||
|
return in.readAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
|
||||||
|
{
|
||||||
|
QList<ContentFile> files;
|
||||||
|
for (const QString &path : filePaths) {
|
||||||
|
ContentFile contentFile = createContentFile(path);
|
||||||
|
files.append(contentFile);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
||||||
|
{
|
||||||
|
ContentFile contentFile;
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
contentFile.filename = fileInfo.fileName();
|
||||||
|
contentFile.content = readFile(filePath);
|
||||||
|
return contentFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContextManager::isInBuildDirectory(const QString &filePath) const
|
||||||
|
{
|
||||||
|
static const QStringList buildDirPatterns
|
||||||
|
= {QString(QDir::separator()) + QLatin1String("build") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("Build") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("BUILD") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("debug") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("Debug") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("DEBUG") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("release") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("Release") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("RELEASE") + QDir::separator(),
|
||||||
|
QString(QDir::separator()) + QLatin1String("builds") + QDir::separator()};
|
||||||
|
|
||||||
|
// Нормализуем путь
|
||||||
|
QString normalizedPath = QDir::fromNativeSeparators(filePath);
|
||||||
|
|
||||||
|
// Проверяем, содержит ли путь паттерны build-директории
|
||||||
|
for (const QString &pattern : buildDirPatterns) {
|
||||||
|
// Сравниваем с нормализованным паттерном
|
||||||
|
QString normalizedPattern = QDir::fromNativeSeparators(pattern);
|
||||||
|
if (normalizedPath.contains(normalizedPattern)) {
|
||||||
|
qDebug() << "Skipping build file:" << filePath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
|
||||||
|
{
|
||||||
|
QStringList sourceFiles;
|
||||||
|
if (!project)
|
||||||
|
return sourceFiles;
|
||||||
|
|
||||||
|
auto projectNode = project->rootProjectNode();
|
||||||
|
if (!projectNode)
|
||||||
|
return sourceFiles;
|
||||||
|
|
||||||
|
projectNode->forEachNode(
|
||||||
|
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
|
||||||
|
if (fileNode) {
|
||||||
|
QString filePath = fileNode->filePath().toString();
|
||||||
|
if (shouldProcessFile(filePath) && !isInBuildDirectory(filePath)) {
|
||||||
|
sourceFiles.append(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
return sourceFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContextManager::shouldProcessFile(const QString &filePath) const
|
||||||
|
{
|
||||||
|
static const QStringList supportedExtensions
|
||||||
|
= {"cpp", "hpp", "c", "h", "cc", "hh", "cxx", "hxx", "qml", "js", "py"};
|
||||||
|
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
return supportedExtensions.contains(fileInfo.suffix().toLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContextManager::testProjectChunks(
|
||||||
|
ProjectExplorer::Project *project, const FileChunker::ChunkingConfig &config)
|
||||||
|
{
|
||||||
|
if (!project) {
|
||||||
|
qDebug() << "No project provided";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "\nStarting test chunking for project:" << project->displayName();
|
||||||
|
|
||||||
|
// Get source files
|
||||||
|
QStringList sourceFiles = getProjectSourceFiles(project);
|
||||||
|
qDebug() << "Found" << sourceFiles.size() << "source files";
|
||||||
|
|
||||||
|
// Create chunker
|
||||||
|
auto chunker = new FileChunker(config, this);
|
||||||
|
|
||||||
|
// Connect progress and error signals
|
||||||
|
connect(chunker, &FileChunker::progressUpdated, this, [](int processed, int total) {
|
||||||
|
qDebug() << "Progress:" << processed << "/" << total << "files";
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(chunker, &FileChunker::error, this, [](const QString &error) {
|
||||||
|
qDebug() << "Error:" << error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start chunking and handle results
|
||||||
|
auto future = chunker->chunkFiles(sourceFiles);
|
||||||
|
|
||||||
|
// Используем QFutureWatcher для обработки результатов
|
||||||
|
auto watcher = new QFutureWatcher<QList<FileChunk>>(this);
|
||||||
|
|
||||||
|
connect(watcher, &QFutureWatcher<QList<FileChunk>>::finished, this, [watcher, chunker]() {
|
||||||
|
// Очистка
|
||||||
|
watcher->deleteLater();
|
||||||
|
chunker->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
60
context/ContextManager.hpp
Normal file
60
context/ContextManager.hpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ContentFile.hpp"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "FileChunker.hpp"
|
||||||
|
|
||||||
|
namespace ProjectExplorer {
|
||||||
|
class Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
class ContextManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static ContextManager &instance();
|
||||||
|
|
||||||
|
QString readFile(const QString &filePath) const;
|
||||||
|
QList<ContentFile> getContentFiles(const QStringList &filePaths) const;
|
||||||
|
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const;
|
||||||
|
|
||||||
|
void testProjectChunks(
|
||||||
|
ProjectExplorer::Project *project, const FileChunker::ChunkingConfig &config);
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit ContextManager(QObject *parent = nullptr);
|
||||||
|
~ContextManager() = default;
|
||||||
|
ContextManager(const ContextManager &) = delete;
|
||||||
|
ContextManager &operator=(const ContextManager &) = delete;
|
||||||
|
|
||||||
|
ContentFile createContentFile(const QString &filePath) const;
|
||||||
|
bool shouldProcessFile(const QString &filePath) const;
|
||||||
|
bool isInBuildDirectory(const QString &filePath) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
@@ -23,8 +23,9 @@
|
|||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
#include <languageserverprotocol/lsptypes.h>
|
#include <languageserverprotocol/lsptypes.h>
|
||||||
|
|
||||||
#include "core/ChangesManager.h"
|
#include "CodeCompletionSettings.hpp"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
|
||||||
|
#include "ChangesManager.h"
|
||||||
|
|
||||||
const QRegularExpression &getYearRegex()
|
const QRegularExpression &getYearRegex()
|
||||||
{
|
{
|
||||||
@@ -46,7 +47,7 @@ const QRegularExpression &getCommentRegex()
|
|||||||
return commentRegex;
|
return commentRegex;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
|
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
|
||||||
: m_textDocument(textDocument)
|
: m_textDocument(textDocument)
|
||||||
@@ -246,4 +247,4 @@ QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPositio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist::Context
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QTextDocument>
|
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
|
#include <QTextDocument>
|
||||||
|
|
||||||
#include <llmcore/ContextData.hpp>
|
#include <llmcore/ContextData.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
struct CopyrightInfo
|
struct CopyrightInfo
|
||||||
{
|
{
|
||||||
@@ -61,4 +61,4 @@ private:
|
|||||||
CopyrightInfo m_copyrightInfo;
|
CopyrightInfo m_copyrightInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist::Context
|
||||||
265
context/EnhancedRAGSimilaritySearch.cpp
Normal file
265
context/EnhancedRAGSimilaritySearch.cpp
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
#include "EnhancedRAGSimilaritySearch.hpp"
|
||||||
|
|
||||||
|
#include <QSet>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
// Static regex getters
|
||||||
|
const QRegularExpression &EnhancedRAGSimilaritySearch::getNamespaceRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression regex(R"(namespace\s+(?:\w+\s*::\s*)*\w+\s*\{)");
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRegularExpression &EnhancedRAGSimilaritySearch::getClassRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression regex(
|
||||||
|
R"((?:template\s*<[^>]*>\s*)?(?:class|struct)\s+(\w+)\s*(?:final\s*)?(?::\s*(?:public|protected|private)\s+\w+(?:\s*,\s*(?:public|protected|private)\s+\w+)*\s*)?{)");
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRegularExpression &EnhancedRAGSimilaritySearch::getFunctionRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression regex(
|
||||||
|
R"((?:virtual\s+)?(?:static\s+)?(?:inline\s+)?(?:explicit\s+)?(?:constexpr\s+)?(?:[\w:]+\s+)?(?:\w+\s*::\s*)*\w+\s*\([^)]*\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:final\s*)?(?:=\s*0\s*)?(?:=\s*default\s*)?(?:=\s*delete\s*)?(?:\s*->.*?)?\s*{)");
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRegularExpression &EnhancedRAGSimilaritySearch::getTemplateRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression regex(R"(template\s*<[^>]*>\s*(?:class|struct|typename)\s+\w+)");
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache getters
|
||||||
|
QCache<QString, EnhancedRAGSimilaritySearch::SimilarityScore> &
|
||||||
|
EnhancedRAGSimilaritySearch::getScoreCache()
|
||||||
|
{
|
||||||
|
static QCache<QString, SimilarityScore> cache(1000); // Cache size of 1000 entries
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCache<QString, QStringList> &EnhancedRAGSimilaritySearch::getStructureCache()
|
||||||
|
{
|
||||||
|
static QCache<QString, QStringList> cache(500); // Cache size of 500 entries
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main public interface
|
||||||
|
EnhancedRAGSimilaritySearch::SimilarityScore EnhancedRAGSimilaritySearch::calculateSimilarity(
|
||||||
|
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2)
|
||||||
|
{
|
||||||
|
// Generate cache key based on content hashes
|
||||||
|
QString cacheKey = QString("%1_%2").arg(qHash(code1)).arg(qHash(code2));
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
auto &scoreCache = getScoreCache();
|
||||||
|
if (auto *cached = scoreCache.object(cacheKey)) {
|
||||||
|
return *cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new similarity score
|
||||||
|
SimilarityScore score = calculateSimilarityInternal(v1, v2, code1, code2);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
scoreCache.insert(cacheKey, new SimilarityScore(score));
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal implementation
|
||||||
|
EnhancedRAGSimilaritySearch::SimilarityScore EnhancedRAGSimilaritySearch::calculateSimilarityInternal(
|
||||||
|
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2)
|
||||||
|
{
|
||||||
|
if (v1.empty() || v2.empty()) {
|
||||||
|
LOG_MESSAGE("Warning: Empty vectors in similarity calculation");
|
||||||
|
return SimilarityScore(0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v1.size() != v2.size()) {
|
||||||
|
LOG_MESSAGE(QString("Vector size mismatch: %1 vs %2").arg(v1.size()).arg(v2.size()));
|
||||||
|
return SimilarityScore(0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate semantic similarity using vector embeddings
|
||||||
|
float semantic_similarity = 0.0f;
|
||||||
|
|
||||||
|
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
|
||||||
|
if (v1.size() >= 4) { // Use SSE for vectors of 4 or more elements
|
||||||
|
semantic_similarity = calculateCosineSimilaritySSE(v1, v2);
|
||||||
|
} else {
|
||||||
|
semantic_similarity = calculateCosineSimilarity(v1, v2);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
semantic_similarity = calculateCosineSimilarity(v1, v2);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// If semantic similarity is very low, skip structural analysis
|
||||||
|
if (semantic_similarity < 0.0001f) {
|
||||||
|
return SimilarityScore(0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate structural similarity
|
||||||
|
float structural_similarity = calculateStructuralSimilarity(code1, code2);
|
||||||
|
|
||||||
|
// Calculate combined score with dynamic weights
|
||||||
|
float semantic_weight = 0.7f;
|
||||||
|
const int large_file_threshold = 10000;
|
||||||
|
|
||||||
|
if (code1.size() > large_file_threshold || code2.size() > large_file_threshold) {
|
||||||
|
semantic_weight = 0.8f; // Increase semantic weight for large files
|
||||||
|
}
|
||||||
|
|
||||||
|
float combined_score = semantic_weight * semantic_similarity
|
||||||
|
+ (1.0f - semantic_weight) * structural_similarity;
|
||||||
|
|
||||||
|
return SimilarityScore(semantic_similarity, structural_similarity, combined_score);
|
||||||
|
}
|
||||||
|
|
||||||
|
float EnhancedRAGSimilaritySearch::calculateCosineSimilarity(const RAGVector &v1, const RAGVector &v2)
|
||||||
|
{
|
||||||
|
float dotProduct = 0.0f;
|
||||||
|
float norm1 = 0.0f;
|
||||||
|
float norm2 = 0.0f;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < v1.size(); ++i) {
|
||||||
|
dotProduct += v1[i] * v2[i];
|
||||||
|
norm1 += v1[i] * v1[i];
|
||||||
|
norm2 += v2[i] * v2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
norm1 = std::sqrt(norm1);
|
||||||
|
norm2 = std::sqrt(norm2);
|
||||||
|
|
||||||
|
if (norm1 == 0.0f || norm2 == 0.0f) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dotProduct / (norm1 * norm2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
|
||||||
|
float EnhancedRAGSimilaritySearch::calculateCosineSimilaritySSE(
|
||||||
|
const RAGVector &v1, const RAGVector &v2)
|
||||||
|
{
|
||||||
|
const float *p1 = v1.data();
|
||||||
|
const float *p2 = v2.data();
|
||||||
|
const size_t size = v1.size();
|
||||||
|
const size_t alignedSize = size & ~3ULL; // Round down to multiple of 4
|
||||||
|
|
||||||
|
__m128 sum = _mm_setzero_ps();
|
||||||
|
__m128 norm1 = _mm_setzero_ps();
|
||||||
|
__m128 norm2 = _mm_setzero_ps();
|
||||||
|
|
||||||
|
// Process 4 elements at a time using SSE
|
||||||
|
for (size_t i = 0; i < alignedSize; i += 4) {
|
||||||
|
__m128 v1_vec = _mm_loadu_ps(p1 + i); // Use unaligned load for safety
|
||||||
|
__m128 v2_vec = _mm_loadu_ps(p2 + i);
|
||||||
|
|
||||||
|
sum = _mm_add_ps(sum, _mm_mul_ps(v1_vec, v2_vec));
|
||||||
|
norm1 = _mm_add_ps(norm1, _mm_mul_ps(v1_vec, v1_vec));
|
||||||
|
norm2 = _mm_add_ps(norm2, _mm_mul_ps(v2_vec, v2_vec));
|
||||||
|
}
|
||||||
|
|
||||||
|
float dotProduct = horizontalSum(sum);
|
||||||
|
float n1 = std::sqrt(horizontalSum(norm1));
|
||||||
|
float n2 = std::sqrt(horizontalSum(norm2));
|
||||||
|
|
||||||
|
// Process remaining elements
|
||||||
|
for (size_t i = alignedSize; i < size; ++i) {
|
||||||
|
dotProduct += v1[i] * v2[i];
|
||||||
|
n1 += v1[i] * v1[i];
|
||||||
|
n2 += v2[i] * v2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n1 == 0.0f || n2 == 0.0f) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dotProduct / (std::sqrt(n1) * std::sqrt(n2));
|
||||||
|
}
|
||||||
|
|
||||||
|
float EnhancedRAGSimilaritySearch::horizontalSum(__m128 x)
|
||||||
|
{
|
||||||
|
__m128 shuf = _mm_shuffle_ps(x, x, _MM_SHUFFLE(2, 3, 0, 1));
|
||||||
|
__m128 sums = _mm_add_ps(x, shuf);
|
||||||
|
shuf = _mm_movehl_ps(shuf, sums);
|
||||||
|
sums = _mm_add_ss(sums, shuf);
|
||||||
|
return _mm_cvtss_f32(sums);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
float EnhancedRAGSimilaritySearch::calculateStructuralSimilarity(
|
||||||
|
const QString &code1, const QString &code2)
|
||||||
|
{
|
||||||
|
QStringList structures1 = extractStructures(code1);
|
||||||
|
QStringList structures2 = extractStructures(code2);
|
||||||
|
|
||||||
|
return calculateJaccardSimilarity(structures1, structures2);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList EnhancedRAGSimilaritySearch::extractStructures(const QString &code)
|
||||||
|
{
|
||||||
|
// Check cache first
|
||||||
|
auto &structureCache = getStructureCache();
|
||||||
|
QString cacheKey = QString::number(qHash(code));
|
||||||
|
|
||||||
|
if (auto *cached = structureCache.object(cacheKey)) {
|
||||||
|
return *cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList structures;
|
||||||
|
structures.reserve(100); // Reserve space for typical file
|
||||||
|
|
||||||
|
// Extract namespaces
|
||||||
|
auto namespaceMatches = getNamespaceRegex().globalMatch(code);
|
||||||
|
while (namespaceMatches.hasNext()) {
|
||||||
|
structures.append(namespaceMatches.next().captured().trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract classes
|
||||||
|
auto classMatches = getClassRegex().globalMatch(code);
|
||||||
|
while (classMatches.hasNext()) {
|
||||||
|
structures.append(classMatches.next().captured().trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract functions
|
||||||
|
auto functionMatches = getFunctionRegex().globalMatch(code);
|
||||||
|
while (functionMatches.hasNext()) {
|
||||||
|
structures.append(functionMatches.next().captured().trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract templates
|
||||||
|
auto templateMatches = getTemplateRegex().globalMatch(code);
|
||||||
|
while (templateMatches.hasNext()) {
|
||||||
|
structures.append(templateMatches.next().captured().trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
structureCache.insert(cacheKey, new QStringList(structures));
|
||||||
|
|
||||||
|
return structures;
|
||||||
|
}
|
||||||
|
|
||||||
|
float EnhancedRAGSimilaritySearch::calculateJaccardSimilarity(
|
||||||
|
const QStringList &set1, const QStringList &set2)
|
||||||
|
{
|
||||||
|
if (set1.isEmpty() && set2.isEmpty()) {
|
||||||
|
return 1.0f; // Пустые множества считаем идентичными
|
||||||
|
}
|
||||||
|
if (set1.isEmpty() || set2.isEmpty()) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSet<QString> set1Unique = QSet<QString>(set1.begin(), set1.end());
|
||||||
|
QSet<QString> set2Unique = QSet<QString>(set2.begin(), set2.end());
|
||||||
|
|
||||||
|
QSet<QString> intersection = set1Unique;
|
||||||
|
intersection.intersect(set2Unique);
|
||||||
|
|
||||||
|
QSet<QString> union_set = set1Unique;
|
||||||
|
union_set.unite(set2Unique);
|
||||||
|
|
||||||
|
return static_cast<float>(intersection.size()) / union_set.size();
|
||||||
|
}
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
74
context/EnhancedRAGSimilaritySearch.hpp
Normal file
74
context/EnhancedRAGSimilaritySearch.hpp
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QCache>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
|
||||||
|
#include <emmintrin.h>
|
||||||
|
#include <xmmintrin.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "RAGData.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
class EnhancedRAGSimilaritySearch
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct SimilarityScore
|
||||||
|
{
|
||||||
|
float semantic_similarity{0.0f};
|
||||||
|
float structural_similarity{0.0f};
|
||||||
|
float combined_score{0.0f};
|
||||||
|
|
||||||
|
SimilarityScore() = default;
|
||||||
|
SimilarityScore(float sem, float str, float comb)
|
||||||
|
: semantic_similarity(sem)
|
||||||
|
, structural_similarity(str)
|
||||||
|
, combined_score(comb)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
static SimilarityScore calculateSimilarity(
|
||||||
|
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static SimilarityScore calculateSimilarityInternal(
|
||||||
|
const RAGVector &v1, const RAGVector &v2, const QString &code1, const QString &code2);
|
||||||
|
|
||||||
|
static float calculateCosineSimilarity(const RAGVector &v1, const RAGVector &v2);
|
||||||
|
|
||||||
|
#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64)
|
||||||
|
static float calculateCosineSimilaritySSE(const RAGVector &v1, const RAGVector &v2);
|
||||||
|
static float horizontalSum(__m128 x);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static float calculateStructuralSimilarity(const QString &code1, const QString &code2);
|
||||||
|
static QStringList extractStructures(const QString &code);
|
||||||
|
static float calculateJaccardSimilarity(const QStringList &set1, const QStringList &set2);
|
||||||
|
|
||||||
|
static const QRegularExpression &getNamespaceRegex();
|
||||||
|
static const QRegularExpression &getClassRegex();
|
||||||
|
static const QRegularExpression &getFunctionRegex();
|
||||||
|
static const QRegularExpression &getTemplateRegex();
|
||||||
|
|
||||||
|
// Cache for similarity scores
|
||||||
|
static QCache<QString, SimilarityScore> &getScoreCache();
|
||||||
|
|
||||||
|
// Cache for extracted structures
|
||||||
|
static QCache<QString, QStringList> &getStructureCache();
|
||||||
|
|
||||||
|
EnhancedRAGSimilaritySearch() = delete; // Prevent instantiation
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
198
context/FileChunker.cpp
Normal file
198
context/FileChunker.cpp
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
// FileChunker.cpp
|
||||||
|
#include "FileChunker.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/idocument.h>
|
||||||
|
#include <texteditor/syntaxhighlighter.h>
|
||||||
|
#include <texteditor/textdocument.h>
|
||||||
|
#include <texteditor/texteditorconstants.h>
|
||||||
|
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
FileChunker::FileChunker(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
FileChunker::FileChunker(const ChunkingConfig &config, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_config(config)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QFuture<QList<FileChunk>> FileChunker::chunkFiles(const QStringList &filePaths)
|
||||||
|
{
|
||||||
|
qDebug() << "\nStarting chunking process for" << filePaths.size() << "files";
|
||||||
|
qDebug() << "Configuration:"
|
||||||
|
<< "\n Max lines per chunk:" << m_config.maxLinesPerChunk
|
||||||
|
<< "\n Overlap lines:" << m_config.overlapLines
|
||||||
|
<< "\n Skip empty lines:" << m_config.skipEmptyLines
|
||||||
|
<< "\n Preserve functions:" << m_config.preserveFunctions
|
||||||
|
<< "\n Preserve classes:" << m_config.preserveClasses
|
||||||
|
<< "\n Batch size:" << m_config.batchSize;
|
||||||
|
|
||||||
|
auto promise = std::make_shared<QPromise<QList<FileChunk>>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
if (filePaths.isEmpty()) {
|
||||||
|
qDebug() << "No files to process";
|
||||||
|
promise->addResult({});
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
processNextBatch(promise, filePaths, 0);
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileChunker::processNextBatch(
|
||||||
|
std::shared_ptr<QPromise<QList<FileChunk>>> promise, const QStringList &files, int startIndex)
|
||||||
|
{
|
||||||
|
if (startIndex >= files.size()) {
|
||||||
|
emit chunkingComplete();
|
||||||
|
promise->finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endIndex = qMin(startIndex + m_config.batchSize, files.size());
|
||||||
|
QList<FileChunk> batchChunks;
|
||||||
|
|
||||||
|
for (int i = startIndex; i < endIndex; ++i) {
|
||||||
|
try {
|
||||||
|
auto chunks = processFile(files[i]);
|
||||||
|
batchChunks.append(chunks);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
emit error(QString("Error processing file %1: %2").arg(files[i], e.what()));
|
||||||
|
}
|
||||||
|
emit progressUpdated(i + 1, files.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
promise->addResult(batchChunks);
|
||||||
|
|
||||||
|
// Планируем обработку следующего батча
|
||||||
|
QTimer::singleShot(0, this, [this, promise, files, endIndex]() {
|
||||||
|
processNextBatch(promise, files, endIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<FileChunk> FileChunker::processFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
qDebug() << "\nProcessing file:" << filePath;
|
||||||
|
|
||||||
|
auto document = new TextEditor::TextDocument;
|
||||||
|
auto filePathObj = Utils::FilePath::fromString(filePath);
|
||||||
|
auto result = document->open(&m_error, filePathObj, filePathObj);
|
||||||
|
if (result != Core::IDocument::OpenResult::Success) {
|
||||||
|
qDebug() << "Failed to open document:" << filePath << "-" << m_error;
|
||||||
|
emit error(QString("Failed to open document: %1 - %2").arg(filePath, m_error));
|
||||||
|
delete document;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Document opened successfully. Line count:" << document->document()->blockCount();
|
||||||
|
|
||||||
|
auto chunks = createChunksForDocument(document);
|
||||||
|
qDebug() << "Created" << chunks.size() << "chunks for file";
|
||||||
|
|
||||||
|
delete document;
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<FileChunk> FileChunker::createChunksForDocument(TextEditor::TextDocument *document)
|
||||||
|
{
|
||||||
|
QList<FileChunk> chunks;
|
||||||
|
QString filePath = document->filePath().toString();
|
||||||
|
qDebug() << "\nCreating chunks for document:" << filePath << "\nConfiguration:"
|
||||||
|
<< "\n Max lines per chunk:" << m_config.maxLinesPerChunk
|
||||||
|
<< "\n Min lines per chunk:" << m_config.minLinesPerChunk
|
||||||
|
<< "\n Overlap lines:" << m_config.overlapLines;
|
||||||
|
// Если файл меньше минимального размера чанка, создаем один чанк
|
||||||
|
if (document->document()->blockCount() <= m_config.minLinesPerChunk) {
|
||||||
|
FileChunk chunk;
|
||||||
|
chunk.filePath = filePath;
|
||||||
|
chunk.startLine = 0;
|
||||||
|
chunk.endLine = document->document()->blockCount() - 1;
|
||||||
|
chunk.createdAt = QDateTime::currentDateTime();
|
||||||
|
chunk.updatedAt = chunk.createdAt;
|
||||||
|
|
||||||
|
QString content;
|
||||||
|
QTextBlock block = document->document()->firstBlock();
|
||||||
|
while (block.isValid()) {
|
||||||
|
content += block.text() + "\n";
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
chunk.content = content;
|
||||||
|
|
||||||
|
qDebug() << "File is smaller than minimum chunk size. Creating single chunk:"
|
||||||
|
<< "\n Lines:" << chunk.lineCount() << "\n Content size:" << chunk.content.size()
|
||||||
|
<< "bytes";
|
||||||
|
|
||||||
|
chunks.append(chunk);
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для больших файлов создаем чанки фиксированного размера с перекрытием
|
||||||
|
int currentStartLine = 0;
|
||||||
|
int lineCount = 0;
|
||||||
|
QString content;
|
||||||
|
QTextBlock block = document->document()->firstBlock();
|
||||||
|
|
||||||
|
while (block.isValid()) {
|
||||||
|
content += block.text() + "\n";
|
||||||
|
lineCount++;
|
||||||
|
|
||||||
|
// Если достигли размера чанка или это последний блок
|
||||||
|
if (lineCount >= m_config.maxLinesPerChunk || !block.next().isValid()) {
|
||||||
|
FileChunk chunk;
|
||||||
|
chunk.filePath = filePath;
|
||||||
|
chunk.startLine = currentStartLine;
|
||||||
|
chunk.endLine = currentStartLine + lineCount - 1;
|
||||||
|
chunk.content = content;
|
||||||
|
chunk.createdAt = QDateTime::currentDateTime();
|
||||||
|
chunk.updatedAt = chunk.createdAt;
|
||||||
|
|
||||||
|
qDebug() << "Creating chunk:"
|
||||||
|
<< "\n Start line:" << chunk.startLine << "\n End line:" << chunk.endLine
|
||||||
|
<< "\n Lines:" << chunk.lineCount()
|
||||||
|
<< "\n Content size:" << chunk.content.size() << "bytes";
|
||||||
|
|
||||||
|
chunks.append(chunk);
|
||||||
|
|
||||||
|
// Начинаем новый чанк с учетом перекрытия
|
||||||
|
if (block.next().isValid()) {
|
||||||
|
// Отступаем назад на размер перекрытия
|
||||||
|
int overlapLines = qMin(m_config.overlapLines, lineCount);
|
||||||
|
currentStartLine = chunk.endLine - overlapLines + 1;
|
||||||
|
|
||||||
|
// Сбрасываем контент, но добавляем перекрывающиеся строки
|
||||||
|
content.clear();
|
||||||
|
QTextBlock overlapBlock = document->document()->findBlockByLineNumber(
|
||||||
|
currentStartLine);
|
||||||
|
while (overlapBlock.isValid() && overlapBlock.blockNumber() <= chunk.endLine) {
|
||||||
|
content += overlapBlock.text() + "\n";
|
||||||
|
overlapBlock = overlapBlock.next();
|
||||||
|
}
|
||||||
|
lineCount = overlapLines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Finished creating chunks for file:" << filePath
|
||||||
|
<< "\nTotal chunks:" << chunks.size();
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileChunker::setConfig(const ChunkingConfig &config)
|
||||||
|
{
|
||||||
|
m_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileChunker::ChunkingConfig FileChunker::config() const
|
||||||
|
{
|
||||||
|
return m_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
68
context/FileChunker.hpp
Normal file
68
context/FileChunker.hpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// FileChunker.hpp
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <texteditor/textdocument.h>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
struct FileChunk
|
||||||
|
{
|
||||||
|
QString filePath; // Path to the source file
|
||||||
|
int startLine; // Starting line of the chunk
|
||||||
|
int endLine; // Ending line of the chunk
|
||||||
|
QDateTime createdAt; // When the chunk was created
|
||||||
|
QDateTime updatedAt; // When the chunk was last updated
|
||||||
|
QString content; // Content of the chunk
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
int lineCount() const { return endLine - startLine + 1; }
|
||||||
|
bool isValid() const { return !filePath.isEmpty() && startLine >= 0 && endLine >= startLine; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileChunker : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct ChunkingConfig
|
||||||
|
{
|
||||||
|
int maxLinesPerChunk = 80;
|
||||||
|
int minLinesPerChunk = 40;
|
||||||
|
int overlapLines = 20;
|
||||||
|
bool skipEmptyLines = true;
|
||||||
|
bool preserveFunctions = true;
|
||||||
|
bool preserveClasses = true;
|
||||||
|
int batchSize = 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit FileChunker(QObject *parent = nullptr);
|
||||||
|
explicit FileChunker(const ChunkingConfig &config, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
// Main chunking method
|
||||||
|
QFuture<QList<FileChunk>> chunkFiles(const QStringList &filePaths);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
void setConfig(const ChunkingConfig &config);
|
||||||
|
ChunkingConfig config() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void progressUpdated(int processedFiles, int totalFiles);
|
||||||
|
void chunkingComplete();
|
||||||
|
void error(const QString &errorMessage);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<FileChunk> processFile(const QString &filePath);
|
||||||
|
QList<FileChunk> createChunksForDocument(TextEditor::TextDocument *document);
|
||||||
|
void processNextBatch(
|
||||||
|
std::shared_ptr<QPromise<QList<FileChunk>>> promise,
|
||||||
|
const QStringList &files,
|
||||||
|
int startIndex);
|
||||||
|
|
||||||
|
ChunkingConfig m_config;
|
||||||
|
QString m_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
70
context/ProgrammingLanguage.cpp
Normal file
70
context/ProgrammingLanguage.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
43
context/ProgrammingLanguage.hpp
Normal file
43
context/ProgrammingLanguage.hpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
enum class ProgrammingLanguage {
|
||||||
|
QML,
|
||||||
|
Cpp,
|
||||||
|
Python,
|
||||||
|
Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace ProgrammingLanguageUtils {
|
||||||
|
|
||||||
|
ProgrammingLanguage fromMimeType(const QString &mimeType);
|
||||||
|
|
||||||
|
QString toString(ProgrammingLanguage language);
|
||||||
|
|
||||||
|
ProgrammingLanguage fromString(const QString &str);
|
||||||
|
|
||||||
|
} // namespace ProgrammingLanguageUtils
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
7
context/RAGData.hpp
Normal file
7
context/RAGData.hpp
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
using RAGVector = std::vector<float>;
|
||||||
|
}
|
||||||
443
context/RAGManager.cpp
Normal file
443
context/RAGManager.cpp
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "RAGManager.hpp"
|
||||||
|
#include "EnhancedRAGSimilaritySearch.hpp"
|
||||||
|
#include "RAGPreprocessor.hpp"
|
||||||
|
#include "RAGSimilaritySearch.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
RAGManager &RAGManager::instance()
|
||||||
|
{
|
||||||
|
static RAGManager manager;
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
RAGManager::RAGManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_vectorizer(std::make_unique<RAGVectorizer>())
|
||||||
|
{}
|
||||||
|
|
||||||
|
RAGManager::~RAGManager() {}
|
||||||
|
|
||||||
|
QString RAGManager::getStoragePath(ProjectExplorer::Project *project) const
|
||||||
|
{
|
||||||
|
return QString("%1/qodeassist/%2/rag/vectors.db")
|
||||||
|
.arg(Core::ICore::userResourcePath().toString(), project->displayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QString> RAGManager::loadFileContent(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
qDebug() << "ERROR: Failed to open file for reading:" << filePath
|
||||||
|
<< "Error:" << file.errorString();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
qDebug() << "Loading content from file:" << fileInfo.fileName() << "Size:" << fileInfo.size()
|
||||||
|
<< "bytes";
|
||||||
|
|
||||||
|
QString content = QString::fromUtf8(file.readAll());
|
||||||
|
if (content.isEmpty()) {
|
||||||
|
qDebug() << "WARNING: Empty content read from file:" << filePath;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RAGManager::ensureStorageForProject(ProjectExplorer::Project *project) const
|
||||||
|
{
|
||||||
|
qDebug() << "Ensuring storage for project:" << project->displayName();
|
||||||
|
|
||||||
|
if (m_currentProject == project && m_currentStorage) {
|
||||||
|
qDebug() << "Using existing storage";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Creating new storage";
|
||||||
|
m_currentStorage.reset();
|
||||||
|
m_currentProject = project;
|
||||||
|
|
||||||
|
if (project) {
|
||||||
|
QString storagePath = getStoragePath(project);
|
||||||
|
qDebug() << "Storage path:" << storagePath;
|
||||||
|
|
||||||
|
StorageOptions options;
|
||||||
|
m_currentStorage = std::make_unique<RAGStorage>(storagePath, options);
|
||||||
|
|
||||||
|
qDebug() << "Initializing storage...";
|
||||||
|
if (!m_currentStorage->init()) {
|
||||||
|
qDebug() << "Failed to initialize storage";
|
||||||
|
m_currentStorage.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qDebug() << "Storage initialized successfully";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<void> RAGManager::processProjectFiles(
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
const QStringList &filePaths,
|
||||||
|
const FileChunker::ChunkingConfig &config)
|
||||||
|
{
|
||||||
|
qDebug() << "\nStarting batch processing of" << filePaths.size()
|
||||||
|
<< "files for project:" << project->displayName();
|
||||||
|
|
||||||
|
auto promise = std::make_shared<QPromise<void>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
qDebug() << "Initializing storage...";
|
||||||
|
ensureStorageForProject(project);
|
||||||
|
|
||||||
|
if (!m_currentStorage) {
|
||||||
|
qDebug() << "Failed to initialize storage for project:" << project->displayName();
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
qDebug() << "Storage initialized successfully";
|
||||||
|
|
||||||
|
qDebug() << "Checking files for processing...";
|
||||||
|
QSet<QString> uniqueFiles;
|
||||||
|
for (const QString &filePath : filePaths) {
|
||||||
|
qDebug() << "Checking file:" << filePath;
|
||||||
|
if (isFileStorageOutdated(project, filePath)) {
|
||||||
|
qDebug() << "File needs processing:" << filePath;
|
||||||
|
uniqueFiles.insert(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList filesToProcess = uniqueFiles.values();
|
||||||
|
|
||||||
|
if (filesToProcess.isEmpty()) {
|
||||||
|
qDebug() << "No files need processing";
|
||||||
|
emit vectorizationFinished();
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Starting to process" << filesToProcess.size() << "files";
|
||||||
|
const int batchSize = 10;
|
||||||
|
processNextFileBatch(promise, project, filesToProcess, config, 0, batchSize);
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RAGManager::processNextFileBatch(
|
||||||
|
std::shared_ptr<QPromise<void>> promise,
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
const QStringList &files,
|
||||||
|
const FileChunker::ChunkingConfig &config,
|
||||||
|
int startIndex,
|
||||||
|
int batchSize)
|
||||||
|
{
|
||||||
|
if (startIndex >= files.size()) {
|
||||||
|
qDebug() << "All batches processed successfully";
|
||||||
|
emit vectorizationFinished();
|
||||||
|
promise->finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endIndex = qMin(startIndex + batchSize, files.size());
|
||||||
|
auto currentBatch = files.mid(startIndex, endIndex - startIndex);
|
||||||
|
|
||||||
|
qDebug() << "\nProcessing batch" << (startIndex / batchSize + 1) << "(" << currentBatch.size()
|
||||||
|
<< "files)"
|
||||||
|
<< "\nProgress:" << startIndex << "to" << endIndex << "of" << files.size();
|
||||||
|
|
||||||
|
for (const QString &filePath : currentBatch) {
|
||||||
|
qDebug() << "Starting processing file:" << filePath;
|
||||||
|
auto future = processFileWithChunks(project, filePath, config);
|
||||||
|
auto watcher = new QFutureWatcher<bool>;
|
||||||
|
watcher->setFuture(future);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
watcher,
|
||||||
|
&QFutureWatcher<bool>::finished,
|
||||||
|
this,
|
||||||
|
[this,
|
||||||
|
watcher,
|
||||||
|
promise,
|
||||||
|
project,
|
||||||
|
files,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
batchSize,
|
||||||
|
config,
|
||||||
|
filePath]() {
|
||||||
|
bool success = watcher->result();
|
||||||
|
qDebug() << "File processed:" << filePath << "success:" << success;
|
||||||
|
|
||||||
|
bool isLastFileInBatch = (filePath == files[endIndex - 1]);
|
||||||
|
if (isLastFileInBatch) {
|
||||||
|
qDebug() << "Batch completed, moving to next batch";
|
||||||
|
emit vectorizationProgress(endIndex, files.size());
|
||||||
|
processNextFileBatch(promise, project, files, config, endIndex, batchSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<bool> RAGManager::processFileWithChunks(
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
const QString &filePath,
|
||||||
|
const FileChunker::ChunkingConfig &config)
|
||||||
|
{
|
||||||
|
auto promise = std::make_shared<QPromise<bool>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
ensureStorageForProject(project);
|
||||||
|
if (!m_currentStorage) {
|
||||||
|
qDebug() << "Storage not initialized for file:" << filePath;
|
||||||
|
promise->addResult(false);
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fileContent = loadFileContent(filePath);
|
||||||
|
if (!fileContent) {
|
||||||
|
qDebug() << "Failed to load content for file:" << filePath;
|
||||||
|
promise->addResult(false);
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Creating chunks for file:" << filePath;
|
||||||
|
auto chunksFuture = m_chunker.chunkFiles({filePath});
|
||||||
|
auto chunks = chunksFuture.result();
|
||||||
|
|
||||||
|
if (chunks.isEmpty()) {
|
||||||
|
qDebug() << "No chunks created for file:" << filePath;
|
||||||
|
promise->addResult(false);
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Created" << chunks.size() << "chunks for file:" << filePath;
|
||||||
|
|
||||||
|
// Преобразуем FileChunk в FileChunkData
|
||||||
|
QList<FileChunkData> chunkData;
|
||||||
|
for (const auto &chunk : chunks) {
|
||||||
|
FileChunkData data;
|
||||||
|
data.filePath = chunk.filePath;
|
||||||
|
data.startLine = chunk.startLine;
|
||||||
|
data.endLine = chunk.endLine;
|
||||||
|
data.content = chunk.content;
|
||||||
|
chunkData.append(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Deleting old chunks for file:" << filePath;
|
||||||
|
m_currentStorage->deleteChunksForFile(filePath);
|
||||||
|
|
||||||
|
auto vectorizeFuture = vectorizeAndStoreChunks(filePath, chunkData);
|
||||||
|
auto watcher = new QFutureWatcher<void>;
|
||||||
|
watcher->setFuture(vectorizeFuture);
|
||||||
|
|
||||||
|
connect(watcher, &QFutureWatcher<void>::finished, this, [promise, watcher, filePath]() {
|
||||||
|
qDebug() << "Completed processing file:" << filePath;
|
||||||
|
promise->addResult(true);
|
||||||
|
promise->finish();
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<void> RAGManager::vectorizeAndStoreChunks(
|
||||||
|
const QString &filePath, const QList<FileChunkData> &chunks)
|
||||||
|
{
|
||||||
|
qDebug() << "Vectorizing and storing" << chunks.size() << "chunks for file:" << filePath;
|
||||||
|
|
||||||
|
auto promise = std::make_shared<QPromise<void>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
// Обрабатываем чанки последовательно
|
||||||
|
processNextChunk(promise, chunks, 0);
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RAGManager::processNextChunk(
|
||||||
|
std::shared_ptr<QPromise<void>> promise, const QList<FileChunkData> &chunks, int currentIndex)
|
||||||
|
{
|
||||||
|
if (currentIndex >= chunks.size()) {
|
||||||
|
promise->finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &chunk = chunks[currentIndex];
|
||||||
|
QString processedContent = RAGPreprocessor::preprocessCode(chunk.content);
|
||||||
|
qDebug() << "Processing chunk" << currentIndex + 1 << "of" << chunks.size();
|
||||||
|
|
||||||
|
auto vectorFuture = m_vectorizer->vectorizeText(processedContent);
|
||||||
|
auto watcher = new QFutureWatcher<RAGVector>;
|
||||||
|
watcher->setFuture(vectorFuture);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
watcher,
|
||||||
|
&QFutureWatcher<RAGVector>::finished,
|
||||||
|
this,
|
||||||
|
[this, watcher, promise, chunks, currentIndex, chunk]() {
|
||||||
|
auto vector = watcher->result();
|
||||||
|
|
||||||
|
if (!vector.empty()) {
|
||||||
|
qDebug() << "Storing vector and chunk for file:" << chunk.filePath;
|
||||||
|
bool vectorStored = m_currentStorage->storeVector(chunk.filePath, vector);
|
||||||
|
bool chunkStored = m_currentStorage->storeChunk(chunk);
|
||||||
|
qDebug() << "Storage results - Vector:" << vectorStored << "Chunk:" << chunkStored;
|
||||||
|
} else {
|
||||||
|
qDebug() << "Failed to vectorize chunk content";
|
||||||
|
}
|
||||||
|
|
||||||
|
processNextChunk(promise, chunks, currentIndex + 1);
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<QList<RAGManager::ChunkSearchResult>> RAGManager::findRelevantChunks(
|
||||||
|
const QString &query, ProjectExplorer::Project *project, int topK)
|
||||||
|
{
|
||||||
|
auto promise = std::make_shared<QPromise<QList<ChunkSearchResult>>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
ensureStorageForProject(project);
|
||||||
|
if (!m_currentStorage) {
|
||||||
|
qDebug() << "Storage not initialized for project:" << project->displayName();
|
||||||
|
promise->addResult({});
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString processedQuery = RAGPreprocessor::preprocessCode(query);
|
||||||
|
|
||||||
|
auto vectorFuture = m_vectorizer->vectorizeText(processedQuery);
|
||||||
|
vectorFuture.then([this, promise, project, processedQuery, topK](const RAGVector &queryVector) {
|
||||||
|
if (queryVector.empty()) {
|
||||||
|
qDebug() << "Failed to vectorize query";
|
||||||
|
promise->addResult({});
|
||||||
|
promise->finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto files = m_currentStorage->getFilesWithChunks();
|
||||||
|
QList<FileChunkData> allChunks;
|
||||||
|
|
||||||
|
for (const auto &filePath : files) {
|
||||||
|
auto fileChunks = m_currentStorage->getChunksForFile(filePath);
|
||||||
|
allChunks.append(fileChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto results = rankChunks(queryVector, processedQuery, allChunks);
|
||||||
|
|
||||||
|
if (results.size() > topK) {
|
||||||
|
results = results.mid(0, topK);
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Found" << results.size() << "relevant chunks";
|
||||||
|
promise->addResult(results);
|
||||||
|
promise->finish();
|
||||||
|
|
||||||
|
closeStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<RAGManager::ChunkSearchResult> RAGManager::rankChunks(
|
||||||
|
const RAGVector &queryVector, const QString &queryText, const QList<FileChunkData> &chunks)
|
||||||
|
{
|
||||||
|
QList<ChunkSearchResult> results;
|
||||||
|
results.reserve(chunks.size());
|
||||||
|
|
||||||
|
for (const auto &chunk : chunks) {
|
||||||
|
auto chunkVector = m_currentStorage->getVector(chunk.filePath);
|
||||||
|
if (!chunkVector.has_value()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString processedChunk = RAGPreprocessor::preprocessCode(chunk.content);
|
||||||
|
|
||||||
|
auto similarity = EnhancedRAGSimilaritySearch::calculateSimilarity(
|
||||||
|
queryVector, chunkVector.value(), queryText, processedChunk);
|
||||||
|
|
||||||
|
results.append(ChunkSearchResult{
|
||||||
|
chunk.filePath,
|
||||||
|
chunk.startLine,
|
||||||
|
chunk.endLine,
|
||||||
|
chunk.content,
|
||||||
|
similarity.semantic_similarity,
|
||||||
|
similarity.structural_similarity,
|
||||||
|
similarity.combined_score});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(results.begin(), results.end());
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList RAGManager::getStoredFiles(ProjectExplorer::Project *project) const
|
||||||
|
{
|
||||||
|
ensureStorageForProject(project);
|
||||||
|
if (!m_currentStorage) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return m_currentStorage->getAllFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RAGManager::isFileStorageOutdated(
|
||||||
|
ProjectExplorer::Project *project, const QString &filePath) const
|
||||||
|
{
|
||||||
|
ensureStorageForProject(project);
|
||||||
|
if (!m_currentStorage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return m_currentStorage->needsUpdate(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<RAGVector> RAGManager::loadVectorFromStorage(
|
||||||
|
ProjectExplorer::Project *project, const QString &filePath)
|
||||||
|
{
|
||||||
|
ensureStorageForProject(project);
|
||||||
|
if (!m_currentStorage) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return m_currentStorage->getVector(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RAGManager::closeStorage()
|
||||||
|
{
|
||||||
|
qDebug() << "Closing storage...";
|
||||||
|
if (m_currentStorage) {
|
||||||
|
m_currentStorage.reset();
|
||||||
|
m_currentProject = nullptr;
|
||||||
|
qDebug() << "Storage closed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
119
context/RAGManager.hpp
Normal file
119
context/RAGManager.hpp
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "FileChunker.hpp"
|
||||||
|
#include "RAGData.hpp"
|
||||||
|
#include "RAGStorage.hpp"
|
||||||
|
#include "RAGVectorizer.hpp"
|
||||||
|
|
||||||
|
namespace ProjectExplorer {
|
||||||
|
class Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
class RAGManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct ChunkSearchResult
|
||||||
|
{
|
||||||
|
QString filePath;
|
||||||
|
int startLine;
|
||||||
|
int endLine;
|
||||||
|
QString content;
|
||||||
|
float semanticScore;
|
||||||
|
float structuralScore;
|
||||||
|
float combinedScore;
|
||||||
|
|
||||||
|
bool operator<(const ChunkSearchResult &other) const
|
||||||
|
{
|
||||||
|
return combinedScore > other.combinedScore;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static RAGManager &instance();
|
||||||
|
|
||||||
|
QFuture<void> processProjectFiles(
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
const QStringList &filePaths,
|
||||||
|
const FileChunker::ChunkingConfig &config = FileChunker::ChunkingConfig());
|
||||||
|
|
||||||
|
QFuture<QList<ChunkSearchResult>> findRelevantChunks(
|
||||||
|
const QString &query, ProjectExplorer::Project *project, int topK = 5);
|
||||||
|
|
||||||
|
QStringList getStoredFiles(ProjectExplorer::Project *project) const;
|
||||||
|
bool isFileStorageOutdated(ProjectExplorer::Project *project, const QString &filePath) const;
|
||||||
|
|
||||||
|
void processNextChunk(
|
||||||
|
std::shared_ptr<QPromise<void>> promise,
|
||||||
|
const QList<FileChunkData> &chunks,
|
||||||
|
int currentIndex);
|
||||||
|
void closeStorage();
|
||||||
|
signals:
|
||||||
|
void vectorizationProgress(int processed, int total);
|
||||||
|
void vectorizationFinished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit RAGManager(QObject *parent = nullptr);
|
||||||
|
~RAGManager();
|
||||||
|
RAGManager(const RAGManager &) = delete;
|
||||||
|
RAGManager &operator=(const RAGManager &) = delete;
|
||||||
|
|
||||||
|
QString getStoragePath(ProjectExplorer::Project *project) const;
|
||||||
|
void ensureStorageForProject(ProjectExplorer::Project *project) const;
|
||||||
|
std::optional<QString> loadFileContent(const QString &filePath);
|
||||||
|
std::optional<RAGVector> loadVectorFromStorage(
|
||||||
|
ProjectExplorer::Project *project, const QString &filePath);
|
||||||
|
|
||||||
|
void processNextFileBatch(
|
||||||
|
std::shared_ptr<QPromise<void>> promise,
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
const QStringList &files,
|
||||||
|
const FileChunker::ChunkingConfig &config,
|
||||||
|
int startIndex,
|
||||||
|
int batchSize);
|
||||||
|
|
||||||
|
QFuture<bool> processFileWithChunks(
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
const QString &filePath,
|
||||||
|
const FileChunker::ChunkingConfig &config);
|
||||||
|
|
||||||
|
QFuture<void> vectorizeAndStoreChunks(
|
||||||
|
const QString &filePath, const QList<FileChunkData> &chunks);
|
||||||
|
|
||||||
|
QList<ChunkSearchResult> rankChunks(
|
||||||
|
const RAGVector &queryVector, const QString &queryText, const QList<FileChunkData> &chunks);
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::unique_ptr<RAGVectorizer> m_vectorizer;
|
||||||
|
mutable std::unique_ptr<RAGStorage> m_currentStorage;
|
||||||
|
mutable ProjectExplorer::Project *m_currentProject{nullptr};
|
||||||
|
FileChunker m_chunker;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
2
context/RAGPreprocessor.cpp
Normal file
2
context/RAGPreprocessor.cpp
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include "RAGPreprocessor.hpp"
|
||||||
|
|
||||||
64
context/RAGPreprocessor.hpp
Normal file
64
context/RAGPreprocessor.hpp
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "Logger.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
class RAGPreprocessor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static const QRegularExpression &getLicenseRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression regex(
|
||||||
|
R"((/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)|//[^\n]*(?:\n|$))",
|
||||||
|
QRegularExpression::MultilineOption);
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const QRegularExpression &getClassRegex()
|
||||||
|
{
|
||||||
|
static const QRegularExpression regex(
|
||||||
|
R"((?:template\s*<[^>]*>\s*)?(?:class|struct)\s+(\w+)\s*(?:final\s*)?(?::\s*(?:public|protected|private)\s+\w+(?:\s*,\s*(?:public|protected|private)\s+\w+)*\s*)?{)");
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString preprocessCode(const QString &code)
|
||||||
|
{
|
||||||
|
if (code.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
QStringList lines = code.split('\n', Qt::SkipEmptyParts);
|
||||||
|
return processLines(lines);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
LOG_MESSAGE(QString("Error preprocessing code: %1").arg(e.what()));
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString processLines(const QStringList &lines)
|
||||||
|
{
|
||||||
|
const int estimatedAvgLength = 80;
|
||||||
|
QString result;
|
||||||
|
result.reserve(lines.size() * estimatedAvgLength);
|
||||||
|
|
||||||
|
for (const QString &line : lines) {
|
||||||
|
const QString trimmed = line.trimmed();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
result += trimmed;
|
||||||
|
result += QLatin1Char('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.endsWith('\n')) {
|
||||||
|
result.chop(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
67
context/RAGSimilaritySearch.cpp
Normal file
67
context/RAGSimilaritySearch.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "RAGSimilaritySearch.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
float RAGSimilaritySearch::l2Distance(const RAGVector &v1, const RAGVector &v2)
|
||||||
|
{
|
||||||
|
if (v1.size() != v2.size()) {
|
||||||
|
LOG_MESSAGE(QString("Vector size mismatch: %1 vs %2").arg(v1.size()).arg(v2.size()));
|
||||||
|
return std::numeric_limits<float>::max();
|
||||||
|
}
|
||||||
|
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (size_t i = 0; i < v1.size(); ++i) {
|
||||||
|
float diff = v1[i] - v2[i];
|
||||||
|
sum += diff * diff;
|
||||||
|
}
|
||||||
|
return std::sqrt(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
float RAGSimilaritySearch::cosineSimilarity(const RAGVector &v1, const RAGVector &v2)
|
||||||
|
{
|
||||||
|
if (v1.size() != v2.size()) {
|
||||||
|
LOG_MESSAGE(QString("Vector size mismatch: %1 vs %2").arg(v1.size()).arg(v2.size()));
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dotProduct = 0.0f;
|
||||||
|
float norm1 = 0.0f;
|
||||||
|
float norm2 = 0.0f;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < v1.size(); ++i) {
|
||||||
|
dotProduct += v1[i] * v2[i];
|
||||||
|
norm1 += v1[i] * v1[i];
|
||||||
|
norm2 += v2[i] * v2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
norm1 = std::sqrt(norm1);
|
||||||
|
norm2 = std::sqrt(norm2);
|
||||||
|
|
||||||
|
if (norm1 == 0.0f || norm2 == 0.0f)
|
||||||
|
return 0.0f;
|
||||||
|
return dotProduct / (norm1 * norm2);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
@@ -19,30 +19,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QLabel>
|
#include "RAGData.hpp"
|
||||||
#include <QTimer>
|
|
||||||
#include <QToolBar>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class CounterTooltip : public QToolBar
|
class RAGSimilaritySearch
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
CounterTooltip(int count);
|
static float l2Distance(const RAGVector &v1, const RAGVector &v2);
|
||||||
~CounterTooltip();
|
|
||||||
|
|
||||||
signals:
|
static float cosineSimilarity(const RAGVector &v1, const RAGVector &v2);
|
||||||
void finished(int count);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateLabel();
|
RAGSimilaritySearch() = delete;
|
||||||
|
|
||||||
QLabel *m_label;
|
|
||||||
QTimer *m_timer;
|
|
||||||
int m_count;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist::Context
|
||||||
1047
context/RAGStorage.cpp
Normal file
1047
context/RAGStorage.cpp
Normal file
File diff suppressed because it is too large
Load Diff
174
context/RAGStorage.hpp
Normal file
174
context/RAGStorage.hpp
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// RAGStorage.hpp
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QSqlDatabase>
|
||||||
|
#include <QString>
|
||||||
|
#include <qsqlquery.h>
|
||||||
|
|
||||||
|
#include <RAGData.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
struct FileChunkData
|
||||||
|
{
|
||||||
|
QString filePath;
|
||||||
|
int startLine;
|
||||||
|
int endLine;
|
||||||
|
QString content;
|
||||||
|
QDateTime createdAt;
|
||||||
|
QDateTime updatedAt;
|
||||||
|
|
||||||
|
bool isValid() const
|
||||||
|
{
|
||||||
|
return !filePath.isEmpty() && startLine >= 0 && endLine >= startLine && !content.isEmpty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StorageOptions
|
||||||
|
{
|
||||||
|
int maxChunkSize = 1024 * 1024;
|
||||||
|
int maxVectorSize = 1024;
|
||||||
|
bool useCompression = false;
|
||||||
|
bool enableLogging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StorageStatistics
|
||||||
|
{
|
||||||
|
int totalChunks;
|
||||||
|
int totalVectors;
|
||||||
|
int totalFiles;
|
||||||
|
qint64 totalSize;
|
||||||
|
QDateTime lastUpdate;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RAGStorage : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr int CURRENT_VERSION = 1;
|
||||||
|
|
||||||
|
enum class Status { Ok, DatabaseError, ValidationError, VersionError, ConnectionError };
|
||||||
|
|
||||||
|
struct ValidationResult
|
||||||
|
{
|
||||||
|
bool isValid;
|
||||||
|
QString errorMessage;
|
||||||
|
Status errorStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Error
|
||||||
|
{
|
||||||
|
QString message;
|
||||||
|
QString sqlError;
|
||||||
|
QString query;
|
||||||
|
Status status;
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit RAGStorage(
|
||||||
|
const QString &dbPath,
|
||||||
|
const StorageOptions &options = StorageOptions(),
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
~RAGStorage();
|
||||||
|
|
||||||
|
bool init();
|
||||||
|
Status status() const;
|
||||||
|
Error lastError() const;
|
||||||
|
bool isReady() const;
|
||||||
|
QString dbPath() const;
|
||||||
|
|
||||||
|
bool beginTransaction();
|
||||||
|
bool commitTransaction();
|
||||||
|
bool rollbackTransaction();
|
||||||
|
|
||||||
|
bool storeVector(const QString &filePath, const RAGVector &vector);
|
||||||
|
bool updateVector(const QString &filePath, const RAGVector &vector);
|
||||||
|
std::optional<RAGVector> getVector(const QString &filePath);
|
||||||
|
bool needsUpdate(const QString &filePath);
|
||||||
|
QStringList getAllFiles();
|
||||||
|
|
||||||
|
bool storeChunk(const FileChunkData &chunk);
|
||||||
|
bool storeChunks(const QList<FileChunkData> &chunks);
|
||||||
|
bool updateChunk(const FileChunkData &chunk);
|
||||||
|
bool updateChunks(const QList<FileChunkData> &chunks);
|
||||||
|
bool deleteChunksForFile(const QString &filePath);
|
||||||
|
std::optional<FileChunkData> getChunk(const QString &filePath, int startLine, int endLine);
|
||||||
|
QList<FileChunkData> getChunksForFile(const QString &filePath);
|
||||||
|
bool chunkExists(const QString &filePath, int startLine, int endLine);
|
||||||
|
|
||||||
|
int getChunkCount(const QString &filePath);
|
||||||
|
bool deleteOldChunks(const QString &filePath, const QDateTime &olderThan);
|
||||||
|
bool deleteAllChunks();
|
||||||
|
QStringList getFilesWithChunks();
|
||||||
|
bool vacuum();
|
||||||
|
bool backup(const QString &backupPath);
|
||||||
|
bool restore(const QString &backupPath);
|
||||||
|
StorageStatistics getStatistics() const;
|
||||||
|
|
||||||
|
int getStorageVersion() const;
|
||||||
|
bool isVersionCompatible() const;
|
||||||
|
|
||||||
|
bool applyMigration(int version);
|
||||||
|
signals:
|
||||||
|
void errorOccurred(const Error &error);
|
||||||
|
void operationCompleted(const QString &operation);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool createTables();
|
||||||
|
bool createIndices();
|
||||||
|
bool createVersionTable();
|
||||||
|
bool createChunksTable();
|
||||||
|
bool createVectorsTable();
|
||||||
|
bool openDatabase();
|
||||||
|
bool initializeNewStorage();
|
||||||
|
bool upgradeStorage(int fromVersion);
|
||||||
|
bool validateSchema() const;
|
||||||
|
|
||||||
|
QDateTime getFileLastModified(const QString &filePath);
|
||||||
|
RAGVector blobToVector(const QByteArray &blob);
|
||||||
|
QByteArray vectorToBlob(const RAGVector &vector);
|
||||||
|
|
||||||
|
void setError(const QString &message, Status status = Status::DatabaseError);
|
||||||
|
void clearError();
|
||||||
|
bool prepareStatements();
|
||||||
|
ValidationResult validateChunk(const FileChunkData &chunk) const;
|
||||||
|
ValidationResult validateVector(const RAGVector &vector) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QSqlDatabase m_db;
|
||||||
|
QString m_dbPath;
|
||||||
|
StorageOptions m_options;
|
||||||
|
mutable QMutex m_mutex;
|
||||||
|
Error m_lastError;
|
||||||
|
Status m_status;
|
||||||
|
|
||||||
|
QSqlQuery m_insertChunkQuery;
|
||||||
|
QSqlQuery m_updateChunkQuery;
|
||||||
|
QSqlQuery m_insertVectorQuery;
|
||||||
|
QSqlQuery m_updateVectorQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
116
context/RAGVectorizer.cpp
Normal file
116
context/RAGVectorizer.cpp
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "RAGVectorizer.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
RAGVectorizer::RAGVectorizer(const QString &providerUrl,
|
||||||
|
const QString &modelName,
|
||||||
|
QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_network(new QNetworkAccessManager(this))
|
||||||
|
, m_embedProviderUrl(providerUrl)
|
||||||
|
, m_model(modelName)
|
||||||
|
{}
|
||||||
|
|
||||||
|
RAGVectorizer::~RAGVectorizer() {}
|
||||||
|
|
||||||
|
QJsonObject RAGVectorizer::prepareEmbeddingRequest(const QString &text) const
|
||||||
|
{
|
||||||
|
return QJsonObject{{"model", m_model}, {"prompt", text}};
|
||||||
|
}
|
||||||
|
|
||||||
|
RAGVector RAGVectorizer::parseEmbeddingResponse(const QByteArray &response) const
|
||||||
|
{
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(response);
|
||||||
|
if (doc.isNull()) {
|
||||||
|
qDebug() << "Failed to parse JSON response";
|
||||||
|
return RAGVector();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject obj = doc.object();
|
||||||
|
if (!obj.contains("embedding")) {
|
||||||
|
qDebug() << "Response does not contain 'embedding' field";
|
||||||
|
// qDebug() << "Response content:" << response;
|
||||||
|
return RAGVector();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray array = obj["embedding"].toArray();
|
||||||
|
if (array.isEmpty()) {
|
||||||
|
qDebug() << "Embedding array is empty";
|
||||||
|
return RAGVector();
|
||||||
|
}
|
||||||
|
|
||||||
|
RAGVector result;
|
||||||
|
result.reserve(array.size());
|
||||||
|
for (const auto &value : array) {
|
||||||
|
result.push_back(value.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Successfully parsed vector with size:" << result.size();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<RAGVector> RAGVectorizer::vectorizeText(const QString &text)
|
||||||
|
{
|
||||||
|
qDebug() << "Vectorizing text, length:" << text.length();
|
||||||
|
qDebug() << "Using embedding provider:" << m_embedProviderUrl;
|
||||||
|
|
||||||
|
auto promise = std::make_shared<QPromise<RAGVector>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
QNetworkRequest request(QUrl(m_embedProviderUrl + "/api/embeddings"));
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
QJsonObject requestData = prepareEmbeddingRequest(text);
|
||||||
|
QByteArray jsonData = QJsonDocument(requestData).toJson();
|
||||||
|
qDebug() << "Sending request to embeddings API:" << jsonData;
|
||||||
|
|
||||||
|
auto reply = m_network->post(request, jsonData);
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [promise, reply, this]() {
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
QByteArray response = reply->readAll();
|
||||||
|
// qDebug() << "Received response from embeddings API:" << response;
|
||||||
|
|
||||||
|
auto vector = parseEmbeddingResponse(response);
|
||||||
|
qDebug() << "Parsed vector size:" << vector.size();
|
||||||
|
promise->addResult(vector);
|
||||||
|
} else {
|
||||||
|
qDebug() << "Network error:" << reply->errorString();
|
||||||
|
qDebug() << "HTTP status code:"
|
||||||
|
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
qDebug() << "Response:" << reply->readAll();
|
||||||
|
|
||||||
|
promise->addResult(RAGVector());
|
||||||
|
}
|
||||||
|
promise->finish();
|
||||||
|
reply->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
51
context/RAGVectorizer.hpp
Normal file
51
context/RAGVectorizer.hpp
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <RAGData.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
class RAGVectorizer : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit RAGVectorizer(
|
||||||
|
const QString &providerUrl = "http://localhost:11434",
|
||||||
|
const QString &modelName = "all-minilm:33m-l12-v2-fp16",
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
~RAGVectorizer();
|
||||||
|
|
||||||
|
QFuture<RAGVector> vectorizeText(const QString &text);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QJsonObject prepareEmbeddingRequest(const QString &text) const;
|
||||||
|
RAGVector parseEmbeddingResponse(const QByteArray &response) const;
|
||||||
|
|
||||||
|
QNetworkAccessManager *m_network;
|
||||||
|
QString m_embedProviderUrl;
|
||||||
|
QString m_model;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
54
context/TokenUtils.cpp
Normal file
54
context/TokenUtils.cpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
36
context/TokenUtils.hpp
Normal file
36
context/TokenUtils.hpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include "ContentFile.hpp"
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
class TokenUtils
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static int estimateTokens(const QString& text);
|
||||||
|
static int estimateFileTokens(const Context::ContentFile& file);
|
||||||
|
static int estimateFilesTokens(const QList<Context::ContentFile>& files);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ add_library(LLMCore STATIC
|
|||||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||||
RequestConfig.hpp
|
RequestConfig.hpp
|
||||||
RequestHandler.hpp RequestHandler.cpp
|
RequestHandler.hpp RequestHandler.cpp
|
||||||
|
OllamaMessage.hpp OllamaMessage.cpp
|
||||||
|
OpenAIMessage.hpp OpenAIMessage.cpp
|
||||||
|
ValidationUtils.hpp ValidationUtils.cpp
|
||||||
|
MessageBuilder.hpp MessageBuilder.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(LLMCore
|
target_link_libraries(LLMCore
|
||||||
|
|||||||
93
llmcore/MessageBuilder.cpp
Normal file
93
llmcore/MessageBuilder.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "MessageBuilder.hpp"
|
||||||
|
|
||||||
|
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addSystemMessage(
|
||||||
|
const QString &content)
|
||||||
|
{
|
||||||
|
m_systemMessage = content;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addUserMessage(
|
||||||
|
const QString &content)
|
||||||
|
{
|
||||||
|
m_messages.append({MessageRole::User, content});
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addSuffix(
|
||||||
|
const QString &content)
|
||||||
|
{
|
||||||
|
m_suffix = content;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addTokenizer(
|
||||||
|
PromptTemplate *promptTemplate)
|
||||||
|
{
|
||||||
|
m_promptTemplate = promptTemplate;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QodeAssist::LLMCore::MessageBuilder::roleToString(MessageRole role) const
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case MessageRole::System:
|
||||||
|
return ROLE_SYSTEM;
|
||||||
|
case MessageRole::User:
|
||||||
|
return ROLE_USER;
|
||||||
|
case MessageRole::Assistant:
|
||||||
|
return ROLE_ASSISTANT;
|
||||||
|
default:
|
||||||
|
return ROLE_USER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssist::LLMCore::MessageBuilder::saveTo(QJsonObject &request, ProvidersApi api)
|
||||||
|
{
|
||||||
|
if (!m_promptTemplate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextData context{
|
||||||
|
m_messages.isEmpty() ? QString() : m_messages.last().content, m_suffix, m_systemMessage};
|
||||||
|
|
||||||
|
if (api == ProvidersApi::Ollama) {
|
||||||
|
if (m_promptTemplate->type() == TemplateType::Fim) {
|
||||||
|
request["system"] = m_systemMessage;
|
||||||
|
m_promptTemplate->prepareRequest(request, context);
|
||||||
|
} else {
|
||||||
|
QJsonArray messages;
|
||||||
|
|
||||||
|
messages.append(QJsonObject{{"role", "system"}, {"content", m_systemMessage}});
|
||||||
|
messages.append(QJsonObject{{"role", "user"}, {"content", m_messages.last().content}});
|
||||||
|
request["messages"] = messages;
|
||||||
|
m_promptTemplate->prepareRequest(request, context);
|
||||||
|
}
|
||||||
|
} else if (api == ProvidersApi::OpenAI) {
|
||||||
|
QJsonArray messages;
|
||||||
|
|
||||||
|
messages.append(QJsonObject{{"role", "system"}, {"content", m_systemMessage}});
|
||||||
|
messages.append(QJsonObject{{"role", "user"}, {"content", m_messages.last().content}});
|
||||||
|
request["messages"] = messages;
|
||||||
|
m_promptTemplate->prepareRequest(request, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
llmcore/MessageBuilder.hpp
Normal file
68
llmcore/MessageBuilder.hpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include "PromptTemplate.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::LLMCore {
|
||||||
|
|
||||||
|
enum class MessageRole { System, User, Assistant };
|
||||||
|
|
||||||
|
enum class OllamaFormat { Messages, Completions };
|
||||||
|
|
||||||
|
enum class ProvidersApi { Ollama, OpenAI, Claude };
|
||||||
|
|
||||||
|
static const QString ROLE_SYSTEM = "system";
|
||||||
|
static const QString ROLE_USER = "user";
|
||||||
|
static const QString ROLE_ASSISTANT = "assistant";
|
||||||
|
|
||||||
|
struct Message
|
||||||
|
{
|
||||||
|
MessageRole role;
|
||||||
|
QString content;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MessageBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MessageBuilder &addSystemMessage(const QString &content);
|
||||||
|
|
||||||
|
MessageBuilder &addUserMessage(const QString &content);
|
||||||
|
|
||||||
|
MessageBuilder &addSuffix(const QString &content);
|
||||||
|
|
||||||
|
MessageBuilder &addTokenizer(PromptTemplate *promptTemplate);
|
||||||
|
|
||||||
|
QString roleToString(MessageRole role) const;
|
||||||
|
|
||||||
|
void saveTo(QJsonObject &request, ProvidersApi api);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_systemMessage;
|
||||||
|
QString m_suffix;
|
||||||
|
QVector<Message> m_messages;
|
||||||
|
PromptTemplate *m_promptTemplate;
|
||||||
|
};
|
||||||
|
} // namespace QodeAssist::LLMCore
|
||||||
102
llmcore/OllamaMessage.cpp
Normal file
102
llmcore/OllamaMessage.cpp
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
71
llmcore/OllamaMessage.hpp
Normal file
71
llmcore/OllamaMessage.hpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
82
llmcore/OpenAIMessage.cpp
Normal file
82
llmcore/OpenAIMessage.cpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
56
llmcore/OpenAIMessage.hpp
Normal file
56
llmcore/OpenAIMessage.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::LLMCore {
|
||||||
|
|
||||||
|
class OpenAIMessage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Choice
|
||||||
|
{
|
||||||
|
QString content;
|
||||||
|
QString finishReason;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Usage
|
||||||
|
{
|
||||||
|
int promptTokens{0};
|
||||||
|
int completionTokens{0};
|
||||||
|
int totalTokens{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
Choice choice;
|
||||||
|
QString error;
|
||||||
|
bool done{false};
|
||||||
|
Usage usage;
|
||||||
|
|
||||||
|
QString getContent() const;
|
||||||
|
bool hasError() const;
|
||||||
|
bool isDone() const;
|
||||||
|
|
||||||
|
static OpenAIMessage fromJson(const QJsonObject &obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::LLMCore
|
||||||
@@ -40,7 +40,6 @@ QStringList PromptTemplateManager::chatTemplatesNames() const
|
|||||||
PromptTemplateManager::~PromptTemplateManager()
|
PromptTemplateManager::~PromptTemplateManager()
|
||||||
{
|
{
|
||||||
qDeleteAll(m_fimTemplates);
|
qDeleteAll(m_fimTemplates);
|
||||||
qDeleteAll(m_chatTemplates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
|
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
|
||||||
|
|||||||
@@ -39,9 +39,8 @@ public:
|
|||||||
"T must inherit from PromptTemplate");
|
"T must inherit from PromptTemplate");
|
||||||
T *template_ptr = new T();
|
T *template_ptr = new T();
|
||||||
QString name = template_ptr->name();
|
QString name = template_ptr->name();
|
||||||
if (template_ptr->type() == TemplateType::Fim) {
|
m_fimTemplates[name] = template_ptr;
|
||||||
m_fimTemplates[name] = template_ptr;
|
if (template_ptr->type() == TemplateType::Chat) {
|
||||||
} else if (template_ptr->type() == TemplateType::Chat) {
|
|
||||||
m_chatTemplates[name] = template_ptr;
|
m_chatTemplates[name] = template_ptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
#include "RequestType.hpp"
|
|
||||||
#include <utils/environment.h>
|
#include <utils/environment.h>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "PromptTemplate.hpp"
|
||||||
|
#include "RequestType.hpp"
|
||||||
|
|
||||||
class QNetworkReply;
|
class QNetworkReply;
|
||||||
class QJsonObject;
|
class QJsonObject;
|
||||||
@@ -42,6 +45,9 @@ public:
|
|||||||
virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
|
virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
|
||||||
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0;
|
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0;
|
||||||
virtual QList<QString> getInstalledModels(const QString &url) = 0;
|
virtual QList<QString> getInstalledModels(const QString &url) = 0;
|
||||||
|
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
|
||||||
|
virtual QString apiKey() const = 0;
|
||||||
|
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::LLMCore
|
} // namespace QodeAssist::LLMCore
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
|
|||||||
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
||||||
|
|
||||||
QNetworkRequest networkRequest(config.url);
|
QNetworkRequest networkRequest(config.url);
|
||||||
prepareNetworkRequest(networkRequest, config.apiKey);
|
config.provider->prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->post(networkRequest,
|
QNetworkReply *reply = m_manager->post(networkRequest,
|
||||||
QJsonDocument(config.providerRequest).toJson());
|
QJsonDocument(config.providerRequest).toJson());
|
||||||
@@ -75,7 +75,7 @@ void RequestHandler::handleLLMResponse(QNetworkReply *reply,
|
|||||||
|
|
||||||
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
|
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
|
||||||
|
|
||||||
if (config.requestType == RequestType::Fim) {
|
if (config.requestType == RequestType::CodeCompletion) {
|
||||||
if (!config.multiLineCompletion
|
if (!config.multiLineCompletion
|
||||||
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
|
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
|
||||||
return;
|
return;
|
||||||
@@ -84,7 +84,6 @@ void RequestHandler::handleLLMResponse(QNetworkReply *reply,
|
|||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
auto cleanedCompletion = removeStopWords(accumulatedResponse,
|
auto cleanedCompletion = removeStopWords(accumulatedResponse,
|
||||||
config.promptTemplate->stopWords());
|
config.promptTemplate->stopWords());
|
||||||
removeCodeBlockWrappers(cleanedCompletion);
|
|
||||||
|
|
||||||
emit completionReceived(cleanedCompletion, request, true);
|
emit completionReceived(cleanedCompletion, request, true);
|
||||||
}
|
}
|
||||||
@@ -109,16 +108,6 @@ 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,
|
||||||
@@ -126,7 +115,6 @@ 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) {
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ private:
|
|||||||
QMap<QString, QNetworkReply *> m_activeRequests;
|
QMap<QString, QNetworkReply *> m_activeRequests;
|
||||||
QMap<QNetworkReply *, QString> m_accumulatedResponses;
|
QMap<QNetworkReply *, QString> m_accumulatedResponses;
|
||||||
|
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest, const QString &apiKey) const;
|
|
||||||
bool processSingleLineCompletion(QNetworkReply *reply,
|
bool processSingleLineCompletion(QNetworkReply *reply,
|
||||||
const QJsonObject &request,
|
const QJsonObject &request,
|
||||||
const QString &accumulatedResponse,
|
const QString &accumulatedResponse,
|
||||||
|
|||||||
@@ -21,5 +21,5 @@
|
|||||||
|
|
||||||
namespace QodeAssist::LLMCore {
|
namespace QodeAssist::LLMCore {
|
||||||
|
|
||||||
enum RequestType { Fim, Chat };
|
enum RequestType { CodeCompletion, Chat };
|
||||||
}
|
}
|
||||||
|
|||||||
57
llmcore/ValidationUtils.cpp
Normal file
57
llmcore/ValidationUtils.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
41
llmcore/ValidationUtils.hpp
Normal file
41
llmcore/ValidationUtils.hpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
238
providers/ClaudeProvider.cpp
Normal file
238
providers/ClaudeProvider.cpp
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ClaudeProvider.hpp"
|
||||||
|
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
|
#include "settings/GeneralSettings.hpp"
|
||||||
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
ClaudeProvider::ClaudeProvider() {}
|
||||||
|
|
||||||
|
QString ClaudeProvider::name() const
|
||||||
|
{
|
||||||
|
return "Claude";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClaudeProvider::url() const
|
||||||
|
{
|
||||||
|
return "https://api.anthropic.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClaudeProvider::completionEndpoint() const
|
||||||
|
{
|
||||||
|
return "/v1/messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClaudeProvider::chatEndpoint() const
|
||||||
|
{
|
||||||
|
return "/v1/messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ClaudeProvider::supportsModelListing() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClaudeProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
|
||||||
|
{
|
||||||
|
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
|
||||||
|
QJsonArray messages;
|
||||||
|
if (req.contains("messages")) {
|
||||||
|
QJsonArray origMessages = req["messages"].toArray();
|
||||||
|
for (const auto &msg : origMessages) {
|
||||||
|
QJsonObject message = msg.toObject();
|
||||||
|
if (message["role"].toString() == "system") {
|
||||||
|
req["system"] = message["content"];
|
||||||
|
} else {
|
||||||
|
messages.append(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (req.contains("system")) {
|
||||||
|
req["system"] = req["system"].toString();
|
||||||
|
}
|
||||||
|
if (req.contains("prompt")) {
|
||||||
|
messages.append(
|
||||||
|
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto applyModelParams = [&request](const auto &settings) {
|
||||||
|
request["max_tokens"] = settings.maxTokens();
|
||||||
|
request["temperature"] = settings.temperature();
|
||||||
|
if (settings.useTopP())
|
||||||
|
request["top_p"] = settings.topP();
|
||||||
|
if (settings.useTopK())
|
||||||
|
request["top_k"] = settings.topK();
|
||||||
|
request["stream"] = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
QJsonArray messages = prepareMessages(request);
|
||||||
|
if (!messages.isEmpty()) {
|
||||||
|
request["messages"] = std::move(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
|
applyModelParams(Settings::codeCompletionSettings());
|
||||||
|
} else {
|
||||||
|
applyModelParams(Settings::chatAssistantSettings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ClaudeProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||||
|
{
|
||||||
|
bool isComplete = false;
|
||||||
|
QString tempResponse;
|
||||||
|
|
||||||
|
while (reply->canReadLine()) {
|
||||||
|
QByteArray line = reply->readLine().trimmed();
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.startsWith("data:")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
line = line.mid(6);
|
||||||
|
|
||||||
|
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
|
||||||
|
if (jsonResponse.isNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject responseObj = jsonResponse.object();
|
||||||
|
QString eventType = responseObj["type"].toString();
|
||||||
|
|
||||||
|
if (eventType == "message_delta") {
|
||||||
|
if (responseObj.contains("delta")) {
|
||||||
|
QJsonObject delta = responseObj["delta"].toObject();
|
||||||
|
if (delta.contains("stop_reason")) {
|
||||||
|
isComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (eventType == "content_block_delta") {
|
||||||
|
QJsonObject delta = responseObj["delta"].toObject();
|
||||||
|
if (delta["type"].toString() == "text_delta") {
|
||||||
|
tempResponse += delta["text"].toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tempResponse.isEmpty()) {
|
||||||
|
accumulatedResponse += tempResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
|
||||||
|
{
|
||||||
|
QList<QString> models;
|
||||||
|
QNetworkAccessManager manager;
|
||||||
|
|
||||||
|
QUrl url(baseUrl + "/v1/models");
|
||||||
|
QUrlQuery query;
|
||||||
|
query.addQueryItem("limit", "1000");
|
||||||
|
url.setQuery(query);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setRawHeader("anthropic-version", "2023-06-01");
|
||||||
|
|
||||||
|
if (!apiKey().isEmpty()) {
|
||||||
|
request.setRawHeader("x-api-key", apiKey().toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = manager.get(request);
|
||||||
|
QEventLoop loop;
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
QByteArray responseData = reply->readAll();
|
||||||
|
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||||
|
QJsonObject jsonObject = jsonResponse.object();
|
||||||
|
|
||||||
|
if (jsonObject.contains("data")) {
|
||||||
|
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||||
|
for (const QJsonValue &value : modelArray) {
|
||||||
|
QJsonObject modelObject = value.toObject();
|
||||||
|
if (modelObject.contains("id")) {
|
||||||
|
QString modelId = modelObject["id"].toString();
|
||||||
|
models.append(modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(reply->errorString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
|
{
|
||||||
|
const auto templateReq = QJsonObject{
|
||||||
|
{"model", {}},
|
||||||
|
{"system", {}},
|
||||||
|
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
|
||||||
|
{"temperature", {}},
|
||||||
|
{"max_tokens", {}},
|
||||||
|
{"anthropic-version", {}},
|
||||||
|
{"top_p", {}},
|
||||||
|
{"top_k", {}},
|
||||||
|
{"stop", QJsonArray{}},
|
||||||
|
{"stream", {}}};
|
||||||
|
|
||||||
|
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClaudeProvider::apiKey() const
|
||||||
|
{
|
||||||
|
return Settings::providerSettings().claudeApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClaudeProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||||
|
{
|
||||||
|
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
if (!apiKey().isEmpty()) {
|
||||||
|
networkRequest.setRawHeader("x-api-key", apiKey().toUtf8());
|
||||||
|
networkRequest.setRawHeader("anthropic-version", "2023-06-01");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
44
providers/ClaudeProvider.hpp
Normal file
44
providers/ClaudeProvider.hpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "llmcore/Provider.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
class ClaudeProvider : public LLMCore::Provider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ClaudeProvider();
|
||||||
|
|
||||||
|
QString name() const override;
|
||||||
|
QString url() const override;
|
||||||
|
QString completionEndpoint() const override;
|
||||||
|
QString chatEndpoint() const override;
|
||||||
|
bool supportsModelListing() const override;
|
||||||
|
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
||||||
|
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||||
|
QList<QString> getInstalledModels(const QString &url) override;
|
||||||
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
|
QString apiKey() const override;
|
||||||
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "llmcore/OpenAIMessage.hpp"
|
||||||
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
#include "settings/ChatAssistantSettings.hpp"
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
@@ -92,7 +94,7 @@ void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType
|
|||||||
request["messages"] = std::move(messages);
|
request["messages"] = std::move(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == LLMCore::RequestType::Fim) {
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
applyModelParams(Settings::codeCompletionSettings());
|
applyModelParams(Settings::codeCompletionSettings());
|
||||||
} else {
|
} else {
|
||||||
applyModelParams(Settings::chatAssistantSettings());
|
applyModelParams(Settings::chatAssistantSettings());
|
||||||
@@ -101,43 +103,53 @@ void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType
|
|||||||
|
|
||||||
bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||||
{
|
{
|
||||||
bool isComplete = false;
|
QByteArray data = reply->readAll();
|
||||||
while (reply->canReadLine()) {
|
if (data.isEmpty()) {
|
||||||
QByteArray line = reply->readLine().trimmed();
|
return false;
|
||||||
if (line.isEmpty()) {
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line == "data: [DONE]") {
|
|
||||||
isComplete = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (line.startsWith("data: ")) {
|
|
||||||
line = line.mid(6); // Remove "data: " prefix
|
|
||||||
}
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
|
|
||||||
if (jsonResponse.isNull()) {
|
|
||||||
qWarning() << "Invalid JSON response from LM Studio:" << line;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
QJsonObject responseObj = jsonResponse.object();
|
|
||||||
if (responseObj.contains("choices")) {
|
|
||||||
QJsonArray choices = responseObj["choices"].toArray();
|
|
||||||
if (!choices.isEmpty()) {
|
|
||||||
QJsonObject choice = choices.first().toObject();
|
|
||||||
QJsonObject delta = choice["delta"].toObject();
|
|
||||||
if (delta.contains("content")) {
|
|
||||||
QString completion = delta["content"].toString();
|
|
||||||
|
|
||||||
accumulatedResponse += completion;
|
bool isDone = false;
|
||||||
}
|
QByteArrayList lines = data.split('\n');
|
||||||
if (choice["finish_reason"].toString() == "stop") {
|
|
||||||
isComplete = true;
|
for (const QByteArray &line : lines) {
|
||||||
break;
|
if (line.trimmed().isEmpty()) {
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line == "data: [DONE]") {
|
||||||
|
isDone = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray jsonData = line;
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
jsonData = line.mid(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||||
|
|
||||||
|
if (doc.isNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||||
|
if (message.hasError()) {
|
||||||
|
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString content = message.getContent();
|
||||||
|
if (!content.isEmpty()) {
|
||||||
|
accumulatedResponse += content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isDone()) {
|
||||||
|
isDone = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isComplete;
|
|
||||||
|
return isDone;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
||||||
@@ -171,4 +183,32 @@ QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<QString> LMStudioProvider::validateRequest(
|
||||||
|
const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
|
{
|
||||||
|
const auto templateReq = QJsonObject{
|
||||||
|
{"model", {}},
|
||||||
|
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
|
||||||
|
{"temperature", {}},
|
||||||
|
{"max_tokens", {}},
|
||||||
|
{"top_p", {}},
|
||||||
|
{"top_k", {}},
|
||||||
|
{"frequency_penalty", {}},
|
||||||
|
{"presence_penalty", {}},
|
||||||
|
{"stop", QJsonArray{}},
|
||||||
|
{"stream", {}}};
|
||||||
|
|
||||||
|
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LMStudioProvider::apiKey() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void LMStudioProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||||
|
{
|
||||||
|
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public:
|
|||||||
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
||||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QList<QString> getInstalledModels(const QString &url) override;
|
||||||
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
|
QString apiKey() const override;
|
||||||
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QtCore/qeventloop.h>
|
#include <QtCore/qeventloop.h>
|
||||||
|
|
||||||
|
#include "llmcore/OllamaMessage.hpp"
|
||||||
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
#include "settings/ChatAssistantSettings.hpp"
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
@@ -64,6 +66,7 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
|
|||||||
QJsonObject options;
|
QJsonObject options;
|
||||||
options["num_predict"] = settings.maxTokens();
|
options["num_predict"] = settings.maxTokens();
|
||||||
options["temperature"] = settings.temperature();
|
options["temperature"] = settings.temperature();
|
||||||
|
options["stop"] = request.take("stop");
|
||||||
|
|
||||||
if (settings.useTopP())
|
if (settings.useTopP())
|
||||||
options["top_p"] = settings.topP();
|
options["top_p"] = settings.topP();
|
||||||
@@ -78,7 +81,7 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
|
|||||||
request["keep_alive"] = settings.ollamaLivetime();
|
request["keep_alive"] = settings.ollamaLivetime();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type == LLMCore::RequestType::Fim) {
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
applySettings(Settings::codeCompletionSettings());
|
applySettings(Settings::codeCompletionSettings());
|
||||||
} else {
|
} else {
|
||||||
applySettings(Settings::chatAssistantSettings());
|
applySettings(Settings::chatAssistantSettings());
|
||||||
@@ -87,53 +90,41 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
|
|||||||
|
|
||||||
bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||||
{
|
{
|
||||||
QString endpoint = reply->url().path();
|
QByteArray data = reply->readAll();
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isComplete = false;
|
QByteArrayList lines = data.split('\n');
|
||||||
while (reply->canReadLine()) {
|
bool isDone = false;
|
||||||
QByteArray line = reply->readLine().trimmed();
|
|
||||||
if (line.isEmpty()) {
|
for (const QByteArray &line : lines) {
|
||||||
|
if (line.trimmed().isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(line);
|
const QString endpoint = reply->url().path();
|
||||||
if (doc.isNull()) {
|
auto messageType = endpoint == completionEndpoint()
|
||||||
LOG_MESSAGE("Invalid JSON response from Ollama: " + QString::fromUtf8(line));
|
? LLMCore::OllamaMessage::Type::Generate
|
||||||
|
: LLMCore::OllamaMessage::Type::Chat;
|
||||||
|
|
||||||
|
auto message = LLMCore::OllamaMessage::fromJson(line, messageType);
|
||||||
|
if (message.hasError()) {
|
||||||
|
LOG_MESSAGE("Error in Ollama response: " + message.error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject responseObj = doc.object();
|
QString content = message.getContent();
|
||||||
|
if (!content.isEmpty()) {
|
||||||
if (responseObj.contains("error")) {
|
accumulatedResponse += content;
|
||||||
QString errorMessage = responseObj["error"].toString();
|
|
||||||
LOG_MESSAGE("Error in Ollama response: " + errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpoint == completionEndpoint()) {
|
if (message.done) {
|
||||||
if (responseObj.contains("response")) {
|
isDone = true;
|
||||||
QString completion = responseObj["response"].toString();
|
|
||||||
accumulatedResponse += completion;
|
|
||||||
}
|
|
||||||
} else if (endpoint == chatEndpoint()) {
|
|
||||||
if (responseObj.contains("message")) {
|
|
||||||
QJsonObject message = responseObj["message"].toObject();
|
|
||||||
if (message.contains("content")) {
|
|
||||||
QString content = message["content"].toString();
|
|
||||||
accumulatedResponse += content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_MESSAGE("Unknown endpoint: " + endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseObj.contains("done") && responseObj["done"].toBool()) {
|
|
||||||
isComplete = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isComplete;
|
return isDone;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OllamaProvider::getInstalledModels(const QString &url)
|
QList<QString> OllamaProvider::getInstalledModels(const QString &url)
|
||||||
@@ -166,4 +157,52 @@ QList<QString> OllamaProvider::getInstalledModels(const QString &url)
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
|
{
|
||||||
|
const auto fimReq = QJsonObject{
|
||||||
|
{"keep_alive", {}},
|
||||||
|
{"model", {}},
|
||||||
|
{"stream", {}},
|
||||||
|
{"prompt", {}},
|
||||||
|
{"suffix", {}},
|
||||||
|
{"system", {}},
|
||||||
|
{"options",
|
||||||
|
QJsonObject{
|
||||||
|
{"temperature", {}},
|
||||||
|
{"stop", {}},
|
||||||
|
{"top_p", {}},
|
||||||
|
{"top_k", {}},
|
||||||
|
{"num_predict", {}},
|
||||||
|
{"frequency_penalty", {}},
|
||||||
|
{"presence_penalty", {}}}}};
|
||||||
|
|
||||||
|
const auto messageReq = QJsonObject{
|
||||||
|
{"keep_alive", {}},
|
||||||
|
{"model", {}},
|
||||||
|
{"stream", {}},
|
||||||
|
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
|
||||||
|
{"options",
|
||||||
|
QJsonObject{
|
||||||
|
{"temperature", {}},
|
||||||
|
{"stop", {}},
|
||||||
|
{"top_p", {}},
|
||||||
|
{"top_k", {}},
|
||||||
|
{"num_predict", {}},
|
||||||
|
{"frequency_penalty", {}},
|
||||||
|
{"presence_penalty", {}}}}};
|
||||||
|
|
||||||
|
return LLMCore::ValidationUtils::validateRequestFields(
|
||||||
|
request, type == LLMCore::TemplateType::Fim ? fimReq : messageReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OllamaProvider::apiKey() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void OllamaProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||||
|
{
|
||||||
|
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public:
|
|||||||
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
||||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QList<QString> getInstalledModels(const QString &url) override;
|
||||||
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
|
QString apiKey() const override;
|
||||||
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -18,14 +18,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "OpenAICompatProvider.hpp"
|
#include "OpenAICompatProvider.hpp"
|
||||||
|
|
||||||
#include "settings/ChatAssistantSettings.hpp"
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "llmcore/OpenAIMessage.hpp"
|
||||||
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
namespace QodeAssist::Providers {
|
||||||
@@ -91,7 +95,7 @@ void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::Request
|
|||||||
request["messages"] = std::move(messages);
|
request["messages"] = std::move(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == LLMCore::RequestType::Fim) {
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
applyModelParams(Settings::codeCompletionSettings());
|
applyModelParams(Settings::codeCompletionSettings());
|
||||||
} else {
|
} else {
|
||||||
applyModelParams(Settings::chatAssistantSettings());
|
applyModelParams(Settings::chatAssistantSettings());
|
||||||
@@ -100,74 +104,53 @@ void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::Request
|
|||||||
|
|
||||||
bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||||
{
|
{
|
||||||
bool isComplete = false;
|
QByteArray data = reply->readAll();
|
||||||
QString tempResponse = accumulatedResponse;
|
if (data.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
while (reply->canReadLine()) {
|
bool isDone = false;
|
||||||
QByteArray line = reply->readLine().trimmed();
|
QByteArrayList lines = data.split('\n');
|
||||||
if (line.isEmpty()) {
|
|
||||||
|
for (const QByteArray &line : lines) {
|
||||||
|
if (line.trimmed().isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!line.startsWith("data:")) {
|
if (line == "data: [DONE]") {
|
||||||
|
isDone = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
line = line.mid(6);
|
QByteArray jsonData = line;
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
if (line == "[DONE]") {
|
jsonData = line.mid(6);
|
||||||
isComplete = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
|
QJsonParseError error;
|
||||||
if (jsonResponse.isNull()) {
|
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||||
LOG_MESSAGE(
|
|
||||||
"Invalid JSON response from OpenAI compatible provider: " + QString::fromUtf8(line));
|
if (doc.isNull()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject responseObj = jsonResponse.object();
|
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||||
|
if (message.hasError()) {
|
||||||
if (responseObj.contains("error")) {
|
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||||
LOG_MESSAGE(
|
continue;
|
||||||
"OpenAI compatible provider error: "
|
|
||||||
+ QString::fromUtf8(QJsonDocument(responseObj).toJson(QJsonDocument::Indented)));
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseObj.contains("choices")) {
|
QString content = message.getContent();
|
||||||
QJsonArray choices = responseObj["choices"].toArray();
|
if (!content.isEmpty()) {
|
||||||
if (!choices.isEmpty()) {
|
accumulatedResponse += content;
|
||||||
QJsonObject choice = choices.first().toObject();
|
|
||||||
QJsonObject delta = choice["delta"].toObject();
|
|
||||||
if (delta.contains("content")) {
|
|
||||||
QString completion = delta["content"].toString();
|
|
||||||
if (!completion.isEmpty()) {
|
|
||||||
tempResponse += completion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QString finishReason = choice["finish_reason"].toString();
|
|
||||||
if (!finishReason.isNull() && finishReason == "stop") {
|
|
||||||
isComplete = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseObj.contains("usage")) {
|
if (message.isDone()) {
|
||||||
QJsonObject usage = responseObj["usage"].toObject();
|
isDone = true;
|
||||||
LOG_MESSAGE(QString("Token usage - Prompt: %1, Completion: %2, Total: %3")
|
|
||||||
.arg(usage["prompt_tokens"].toInt())
|
|
||||||
.arg(usage["completion_tokens"].toInt())
|
|
||||||
.arg(usage["total_tokens"].toInt()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tempResponse.isEmpty()) {
|
return isDone;
|
||||||
accumulatedResponse = tempResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isComplete;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
|
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
|
||||||
@@ -175,4 +158,36 @@ QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
|
|||||||
return QStringList();
|
return QStringList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<QString> OpenAICompatProvider::validateRequest(
|
||||||
|
const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
|
{
|
||||||
|
const auto templateReq = QJsonObject{
|
||||||
|
{"model", {}},
|
||||||
|
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
|
||||||
|
{"temperature", {}},
|
||||||
|
{"max_tokens", {}},
|
||||||
|
{"top_p", {}},
|
||||||
|
{"top_k", {}},
|
||||||
|
{"frequency_penalty", {}},
|
||||||
|
{"presence_penalty", {}},
|
||||||
|
{"stop", QJsonArray{}},
|
||||||
|
{"stream", {}}};
|
||||||
|
|
||||||
|
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAICompatProvider::apiKey() const
|
||||||
|
{
|
||||||
|
return Settings::providerSettings().openAiCompatApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAICompatProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||||
|
{
|
||||||
|
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
if (!apiKey().isEmpty()) {
|
||||||
|
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public:
|
|||||||
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
||||||
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QList<QString> getInstalledModels(const QString &url) override;
|
||||||
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
|
QString apiKey() const override;
|
||||||
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
229
providers/OpenAIProvider.cpp
Normal file
229
providers/OpenAIProvider.cpp
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "OpenAIProvider.hpp"
|
||||||
|
|
||||||
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "llmcore/OpenAIMessage.hpp"
|
||||||
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
OpenAIProvider::OpenAIProvider() {}
|
||||||
|
|
||||||
|
QString OpenAIProvider::name() const
|
||||||
|
{
|
||||||
|
return "OpenAI";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIProvider::url() const
|
||||||
|
{
|
||||||
|
return "https://api.openai.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIProvider::completionEndpoint() const
|
||||||
|
{
|
||||||
|
return "/v1/chat/completions";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIProvider::chatEndpoint() const
|
||||||
|
{
|
||||||
|
return "/v1/chat/completions";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenAIProvider::supportsModelListing() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
|
||||||
|
{
|
||||||
|
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
|
||||||
|
QJsonArray messages;
|
||||||
|
if (req.contains("system")) {
|
||||||
|
messages.append(
|
||||||
|
QJsonObject{{"role", "system"}, {"content", req.take("system").toString()}});
|
||||||
|
}
|
||||||
|
if (req.contains("prompt")) {
|
||||||
|
messages.append(
|
||||||
|
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto applyModelParams = [&request](const auto &settings) {
|
||||||
|
request["max_tokens"] = settings.maxTokens();
|
||||||
|
request["temperature"] = settings.temperature();
|
||||||
|
|
||||||
|
if (settings.useTopP())
|
||||||
|
request["top_p"] = settings.topP();
|
||||||
|
if (settings.useTopK())
|
||||||
|
request["top_k"] = settings.topK();
|
||||||
|
if (settings.useFrequencyPenalty())
|
||||||
|
request["frequency_penalty"] = settings.frequencyPenalty();
|
||||||
|
if (settings.usePresencePenalty())
|
||||||
|
request["presence_penalty"] = settings.presencePenalty();
|
||||||
|
};
|
||||||
|
|
||||||
|
QJsonArray messages = prepareMessages(request);
|
||||||
|
if (!messages.isEmpty()) {
|
||||||
|
request["messages"] = std::move(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
|
applyModelParams(Settings::codeCompletionSettings());
|
||||||
|
} else {
|
||||||
|
applyModelParams(Settings::chatAssistantSettings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||||
|
{
|
||||||
|
QByteArray data = reply->readAll();
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDone = false;
|
||||||
|
QByteArrayList lines = data.split('\n');
|
||||||
|
|
||||||
|
for (const QByteArray &line : lines) {
|
||||||
|
if (line.trimmed().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line == "data: [DONE]") {
|
||||||
|
isDone = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray jsonData = line;
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
jsonData = line.mid(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||||
|
|
||||||
|
if (doc.isNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||||
|
if (message.hasError()) {
|
||||||
|
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString content = message.getContent();
|
||||||
|
if (!content.isEmpty()) {
|
||||||
|
accumulatedResponse += content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isDone()) {
|
||||||
|
isDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
|
||||||
|
{
|
||||||
|
QList<QString> models;
|
||||||
|
QNetworkAccessManager manager;
|
||||||
|
QNetworkRequest request(QString("%1/v1/models").arg(url));
|
||||||
|
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
if (!apiKey().isEmpty()) {
|
||||||
|
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = manager.get(request);
|
||||||
|
QEventLoop loop;
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
QByteArray responseData = reply->readAll();
|
||||||
|
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||||
|
QJsonObject jsonObject = jsonResponse.object();
|
||||||
|
|
||||||
|
if (jsonObject.contains("data")) {
|
||||||
|
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||||
|
for (const QJsonValue &value : modelArray) {
|
||||||
|
QJsonObject modelObject = value.toObject();
|
||||||
|
if (modelObject.contains("id")) {
|
||||||
|
QString modelId = modelObject["id"].toString();
|
||||||
|
if (modelId.startsWith("gpt")) {
|
||||||
|
models.append(modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("Error fetching ChatGPT models: %1").arg(reply->errorString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
|
{
|
||||||
|
const auto templateReq = QJsonObject{
|
||||||
|
{"model", {}},
|
||||||
|
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
|
||||||
|
{"temperature", {}},
|
||||||
|
{"max_tokens", {}},
|
||||||
|
{"top_p", {}},
|
||||||
|
{"top_k", {}},
|
||||||
|
{"frequency_penalty", {}},
|
||||||
|
{"presence_penalty", {}},
|
||||||
|
{"stop", QJsonArray{}},
|
||||||
|
{"stream", {}}};
|
||||||
|
|
||||||
|
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIProvider::apiKey() const
|
||||||
|
{
|
||||||
|
return Settings::providerSettings().openAiApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||||
|
{
|
||||||
|
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
if (!apiKey().isEmpty()) {
|
||||||
|
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
44
providers/OpenAIProvider.hpp
Normal file
44
providers/OpenAIProvider.hpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "llmcore/Provider.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
class OpenAIProvider : public LLMCore::Provider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OpenAIProvider();
|
||||||
|
|
||||||
|
QString name() const override;
|
||||||
|
QString url() const override;
|
||||||
|
QString completionEndpoint() const override;
|
||||||
|
QString chatEndpoint() const override;
|
||||||
|
bool supportsModelListing() const override;
|
||||||
|
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
||||||
|
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||||
|
QList<QString> getInstalledModels(const QString &url) override;
|
||||||
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
|
QString apiKey() const override;
|
||||||
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
145
providers/OpenRouterAIProvider.cpp
Normal file
145
providers/OpenRouterAIProvider.cpp
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "OpenRouterAIProvider.hpp"
|
||||||
|
|
||||||
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "llmcore/OpenAIMessage.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
OpenRouterProvider::OpenRouterProvider() {}
|
||||||
|
|
||||||
|
QString OpenRouterProvider::name() const
|
||||||
|
{
|
||||||
|
return "OpenRouter";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenRouterProvider::url() const
|
||||||
|
{
|
||||||
|
return "https://openrouter.ai/api";
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenRouterProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
|
||||||
|
{
|
||||||
|
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
|
||||||
|
QJsonArray messages;
|
||||||
|
if (req.contains("system")) {
|
||||||
|
messages.append(
|
||||||
|
QJsonObject{{"role", "system"}, {"content", req.take("system").toString()}});
|
||||||
|
}
|
||||||
|
if (req.contains("prompt")) {
|
||||||
|
messages.append(
|
||||||
|
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto applyModelParams = [&request](const auto &settings) {
|
||||||
|
request["max_tokens"] = settings.maxTokens();
|
||||||
|
request["temperature"] = settings.temperature();
|
||||||
|
|
||||||
|
if (settings.useTopP())
|
||||||
|
request["top_p"] = settings.topP();
|
||||||
|
if (settings.useTopK())
|
||||||
|
request["top_k"] = settings.topK();
|
||||||
|
if (settings.useFrequencyPenalty())
|
||||||
|
request["frequency_penalty"] = settings.frequencyPenalty();
|
||||||
|
if (settings.usePresencePenalty())
|
||||||
|
request["presence_penalty"] = settings.presencePenalty();
|
||||||
|
};
|
||||||
|
|
||||||
|
QJsonArray messages = prepareMessages(request);
|
||||||
|
if (!messages.isEmpty()) {
|
||||||
|
request["messages"] = std::move(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
|
applyModelParams(Settings::codeCompletionSettings());
|
||||||
|
} else {
|
||||||
|
applyModelParams(Settings::chatAssistantSettings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||||
|
{
|
||||||
|
QByteArray data = reply->readAll();
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDone = false;
|
||||||
|
QByteArrayList lines = data.split('\n');
|
||||||
|
|
||||||
|
for (const QByteArray &line : lines) {
|
||||||
|
if (line.trimmed().isEmpty() || line.contains("OPENROUTER PROCESSING")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line == "data: [DONE]") {
|
||||||
|
isDone = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray jsonData = line;
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
jsonData = line.mid(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
|
||||||
|
|
||||||
|
if (doc.isNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
|
||||||
|
if (message.hasError()) {
|
||||||
|
LOG_MESSAGE("Error in OpenAI response: " + message.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString content = message.getContent();
|
||||||
|
if (!content.isEmpty()) {
|
||||||
|
accumulatedResponse += content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isDone()) {
|
||||||
|
isDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenRouterProvider::apiKey() const
|
||||||
|
{
|
||||||
|
return Settings::providerSettings().openRouterApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
39
providers/OpenRouterAIProvider.hpp
Normal file
39
providers/OpenRouterAIProvider.hpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "llmcore/Provider.hpp"
|
||||||
|
#include "providers/OpenAICompatProvider.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
class OpenRouterProvider : public OpenAICompatProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OpenRouterProvider();
|
||||||
|
|
||||||
|
QString name() const override;
|
||||||
|
QString url() const override;
|
||||||
|
void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override;
|
||||||
|
bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override;
|
||||||
|
QString apiKey() const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
@@ -20,9 +20,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "llmcore/ProvidersManager.hpp"
|
#include "llmcore/ProvidersManager.hpp"
|
||||||
|
#include "providers/ClaudeProvider.hpp"
|
||||||
#include "providers/LMStudioProvider.hpp"
|
#include "providers/LMStudioProvider.hpp"
|
||||||
#include "providers/OllamaProvider.hpp"
|
#include "providers/OllamaProvider.hpp"
|
||||||
#include "providers/OpenAICompatProvider.hpp"
|
#include "providers/OpenAICompatProvider.hpp"
|
||||||
|
#include "providers/OpenAIProvider.hpp"
|
||||||
|
#include "providers/OpenRouterAIProvider.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
@@ -32,6 +35,9 @@ inline void registerProviders()
|
|||||||
providerManager.registerProvider<OllamaProvider>();
|
providerManager.registerProvider<OllamaProvider>();
|
||||||
providerManager.registerProvider<LMStudioProvider>();
|
providerManager.registerProvider<LMStudioProvider>();
|
||||||
providerManager.registerProvider<OpenAICompatProvider>();
|
providerManager.registerProvider<OpenAICompatProvider>();
|
||||||
|
providerManager.registerProvider<OpenRouterProvider>();
|
||||||
|
providerManager.registerProvider<ClaudeProvider>();
|
||||||
|
providerManager.registerProvider<OpenAIProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "QodeAssistConstants.hpp"
|
#include "QodeAssistConstants.hpp"
|
||||||
#include "QodeAssisttr.h"
|
#include "QodeAssisttr.h"
|
||||||
|
#include "settings/PluginUpdater.hpp"
|
||||||
|
#include "settings/UpdateDialog.hpp"
|
||||||
|
|
||||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||||
#include <coreplugin/actionmanager/actionmanager.h>
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
@@ -32,18 +34,21 @@
|
|||||||
#include <extensionsystem/iplugin.h>
|
#include <extensionsystem/iplugin.h>
|
||||||
#include <languageclient/languageclientmanager.h>
|
#include <languageclient/languageclientmanager.h>
|
||||||
|
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
#include <utils/icon.h>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <texteditor/texteditor.h>
|
|
||||||
#include <utils/icon.h>
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
#include "ConfigurationManager.hpp"
|
||||||
#include "QodeAssistClient.hpp"
|
#include "QodeAssistClient.hpp"
|
||||||
#include "chat/ChatOutputPane.h"
|
#include "chat/ChatOutputPane.h"
|
||||||
#include "chat/NavigationPanel.hpp"
|
#include "chat/NavigationPanel.hpp"
|
||||||
|
#include "settings/GeneralSettings.hpp"
|
||||||
|
#include "settings/ProjectSettingsPanel.hpp"
|
||||||
|
|
||||||
|
#include "UpdateStatusWidget.hpp"
|
||||||
#include "providers/Providers.hpp"
|
#include "providers/Providers.hpp"
|
||||||
#include "templates/Templates.hpp"
|
#include "templates/Templates.hpp"
|
||||||
|
|
||||||
@@ -60,8 +65,8 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
QodeAssistPlugin()
|
QodeAssistPlugin()
|
||||||
{
|
: m_updater(new PluginUpdater(this))
|
||||||
}
|
{}
|
||||||
|
|
||||||
~QodeAssistPlugin() final
|
~QodeAssistPlugin() final
|
||||||
{
|
{
|
||||||
@@ -95,31 +100,36 @@ public:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
auto toggleButton = new QToolButton;
|
m_statusWidget = new UpdateStatusWidget;
|
||||||
toggleButton->setDefaultAction(requestAction.contextAction());
|
m_statusWidget->setDefaultAction(requestAction.contextAction());
|
||||||
StatusBarManager::addStatusBarWidget(toggleButton, StatusBarManager::RightCorner);
|
StatusBarManager::addStatusBarWidget(m_statusWidget, StatusBarManager::RightCorner);
|
||||||
|
|
||||||
|
connect(m_statusWidget->updateButton(), &QPushButton::clicked, this, [this]() {
|
||||||
|
UpdateDialog::checkForUpdatesAndShow(Core::ICore::mainWindow());
|
||||||
|
});
|
||||||
|
|
||||||
m_chatOutputPane = new Chat::ChatOutputPane(this);
|
m_chatOutputPane = new Chat::ChatOutputPane(this);
|
||||||
m_navigationPanel = new Chat::NavigationPanel();
|
m_navigationPanel = new Chat::NavigationPanel();
|
||||||
|
|
||||||
|
Settings::setupProjectPanel();
|
||||||
ConfigurationManager::instance().init();
|
ConfigurationManager::instance().init();
|
||||||
|
|
||||||
|
if (Settings::generalSettings().enableCheckUpdate()) {
|
||||||
|
QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void extensionsInitialized() final
|
void extensionsInitialized() final {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void restartClient()
|
void restartClient()
|
||||||
{
|
{
|
||||||
LanguageClient::LanguageClientManager::shutdownClient(m_qodeAssistClient);
|
LanguageClient::LanguageClientManager::shutdownClient(m_qodeAssistClient);
|
||||||
|
|
||||||
m_qodeAssistClient = new QodeAssistClient();
|
m_qodeAssistClient = new QodeAssistClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool delayedInitialize() final
|
bool delayedInitialize() final
|
||||||
{
|
{
|
||||||
restartClient();
|
restartClient();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,17 +137,38 @@ public:
|
|||||||
{
|
{
|
||||||
if (!m_qodeAssistClient)
|
if (!m_qodeAssistClient)
|
||||||
return SynchronousShutdown;
|
return SynchronousShutdown;
|
||||||
connect(m_qodeAssistClient,
|
connect(m_qodeAssistClient, &QObject::destroyed, this, &IPlugin::asynchronousShutdownFinished);
|
||||||
&QObject::destroyed,
|
|
||||||
this,
|
|
||||||
&IPlugin::asynchronousShutdownFinished);
|
|
||||||
return AsynchronousShutdown;
|
return AsynchronousShutdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void checkForUpdates()
|
||||||
|
{
|
||||||
|
connect(
|
||||||
|
m_updater,
|
||||||
|
&PluginUpdater::updateCheckFinished,
|
||||||
|
this,
|
||||||
|
&QodeAssistPlugin::handleUpdateCheckResult,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
m_updater->checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleUpdateCheckResult(const PluginUpdater::UpdateInfo &info)
|
||||||
|
{
|
||||||
|
if (!info.isUpdateAvailable
|
||||||
|
|| QVersionNumber::fromString(info.currentIdeVersion)
|
||||||
|
> QVersionNumber::fromString(info.targetIdeVersion))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (m_statusWidget)
|
||||||
|
m_statusWidget->showUpdateAvailable(info.version);
|
||||||
|
}
|
||||||
|
|
||||||
QPointer<QodeAssistClient> m_qodeAssistClient;
|
QPointer<QodeAssistClient> m_qodeAssistClient;
|
||||||
QPointer<Chat::ChatOutputPane> m_chatOutputPane;
|
QPointer<Chat::ChatOutputPane> m_chatOutputPane;
|
||||||
QPointer<Chat::NavigationPanel> m_navigationPanel;
|
QPointer<Chat::NavigationPanel> m_navigationPanel;
|
||||||
|
QPointer<PluginUpdater> m_updater;
|
||||||
|
UpdateStatusWidget *m_statusWidget{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Internal
|
} // namespace QodeAssist::Internal
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"prompt": "{{QODE_INSTRUCTIONS}}<fim_prefix>{{QODE_PREFIX}}<fim_suffix>{{QODE_SUFFIX}}<fim_middle>",
|
|
||||||
"options": {
|
|
||||||
"temperature": 0.7,
|
|
||||||
"top_p": 0.95,
|
|
||||||
"top_k": 40,
|
|
||||||
"num_predict": 175,
|
|
||||||
"stop": [
|
|
||||||
"<|endoftext|>",
|
|
||||||
"<file_sep>",
|
|
||||||
"<fim_prefix>",
|
|
||||||
"<fim_suffix>",
|
|
||||||
"<fim_middle>"
|
|
||||||
],
|
|
||||||
"frequency_penalty": 0,
|
|
||||||
"presence_penalty": 0
|
|
||||||
},
|
|
||||||
"stream": true
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"max_tokens": 150,
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"content": "{{QODE_INSTRUCTIONS}}\n### Instruction:{{QODE_PREFIX}}{{QODE_SUFFIX}} ### Response:\n",
|
|
||||||
"role": "user"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stop": [
|
|
||||||
"### Instruction:",
|
|
||||||
"### Response:",
|
|
||||||
"\n\n### "
|
|
||||||
],
|
|
||||||
"stream": true,
|
|
||||||
"temperature": 0.2
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user