mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-12 10:10:44 -05:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 76c17f03dd | |||
| 19c25043fb | |||
| 56b5ea8e68 | |||
| b475f15e3d | |||
| 31f4516e7b | |||
| bfdbc755e3 | |||
| 30964d90d5 | |||
| 1261f913bb | |||
| 36d5242a1f | |||
| 6503887091 | |||
| 50087aa744 | |||
| 4f2dc0c450 | |||
| 80fe388bdd | |||
| 8375d85f7d | |||
| 54b2cc7011 | |||
| f209cb75a2 | |||
| 5e813ba402 | |||
| 7af8fc2ddc | |||
| 46300f7635 | |||
| 0a1c941d8b | |||
| f86182408d | |||
| 252db4c5f7 | |||
| e5af3a2884 | |||
| bb543d1f40 | |||
| a184916d7b | |||
| 7ad8ddfee4 | |||
| 0ed6fb4a6b | |||
| 00fce5db99 | |||
| 251a9bae03 | |||
| 3dba9d7abe | |||
| 45b0f3f18e | |||
| 6d3bc362b3 |
108
.clang-format
Normal file
108
.clang-format
Normal file
@ -0,0 +1,108 @@
|
||||
# .clang-format from Qt Creator
|
||||
# https://github.com/qt-creator/qt-creator/blob/master/.clang-format
|
||||
#
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/clang-format.json
|
||||
#
|
||||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignConsecutiveAssignments: None
|
||||
AlignConsecutiveDeclarations: None
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterFunction: true
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: true
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: false
|
||||
BreakBeforeBinaryOperators: All
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeComma
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 100
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- forever # avoids { wrapped to next line
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeCategories:
|
||||
- Regex: '^<Q.*'
|
||||
Priority: 200
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
# Do not add QT_BEGIN_NAMESPACE/QT_END_NAMESPACE as this will indent lines in between.
|
||||
MacroBlockBegin: ""
|
||||
MacroBlockEnd: ""
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 88
|
||||
PenaltyBreakBeforeFirstCallParameter: 300
|
||||
PenaltyBreakComment: 500
|
||||
PenaltyBreakFirstLessLess: 400
|
||||
PenaltyBreakString: 600
|
||||
PenaltyExcessCharacter: 50
|
||||
PenaltyReturnTypeOnItsOwnLine: 300
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SortIncludes: CaseSensitive
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: true
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++17
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -3,7 +3,7 @@
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: palm1r
|
||||
ko_fi: qodeassist
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
|
||||
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Log**
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request 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'));
|
||||
182
.github/workflows/build_cmake.yml
vendored
182
.github/workflows/build_cmake.yml
vendored
@ -1,12 +1,20 @@
|
||||
name: Build plugin
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: QodeAssist
|
||||
QT_VERSION: 6.7.3
|
||||
QT_CREATOR_VERSION: 14.0.2
|
||||
QT_CREATOR_SNAPSHOT: NO
|
||||
QT_VERSION: 6.8.1
|
||||
QT_CREATOR_VERSION: 15.0.0
|
||||
QT_CREATOR_VERSION_INTERNAL: 15.0.0
|
||||
MACOS_DEPLOYMENT_TARGET: "11.0"
|
||||
CMAKE_VERSION: "3.29.6"
|
||||
NINJA_VERSION: "1.12.1"
|
||||
@ -23,74 +31,44 @@ jobs:
|
||||
- {
|
||||
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
||||
os: windows-latest,
|
||||
platform: windows_x64,
|
||||
cc: "cl", cxx: "cl",
|
||||
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
||||
}
|
||||
- {
|
||||
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
|
||||
os: ubuntu-latest,
|
||||
platform: linux_x64,
|
||||
cc: "gcc", cxx: "g++"
|
||||
}
|
||||
- {
|
||||
name: "macOS Latest Clang", artifact: "macOS-universal",
|
||||
os: macos-latest,
|
||||
platform: mac_x64,
|
||||
cc: "clang", cxx: "clang++"
|
||||
}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Checkout submodules
|
||||
id: git
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
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()
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\n")
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
|
||||
endif()
|
||||
|
||||
- name: Download Ninja and CMake
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(cmake_version "$ENV{CMAKE_VERSION}")
|
||||
set(ninja_version "$ENV{NINJA_VERSION}")
|
||||
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(ninja_suffix "win.zip")
|
||||
set(cmake_suffix "windows-x86_64.zip")
|
||||
set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin")
|
||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||
set(ninja_suffix "linux.zip")
|
||||
set(cmake_suffix "linux-x86_64.tar.gz")
|
||||
set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin")
|
||||
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
||||
set(ninja_suffix "mac.zip")
|
||||
set(cmake_suffix "macos-universal.tar.gz")
|
||||
set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin")
|
||||
endif()
|
||||
|
||||
set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
|
||||
file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)
|
||||
|
||||
set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
|
||||
file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)
|
||||
|
||||
# Add to PATH environment variable
|
||||
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
|
||||
set(path_separator ":")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(path_separator ";")
|
||||
endif()
|
||||
file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}")
|
||||
|
||||
if (NOT "${{ runner.os }}" STREQUAL "Windows")
|
||||
execute_process(
|
||||
COMMAND chmod +x ninja
|
||||
COMMAND chmod +x ${cmake_dir}/cmake
|
||||
)
|
||||
endif()
|
||||
uses: lukka/get-cmake@latest
|
||||
with:
|
||||
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||
|
||||
- name: Install system libs
|
||||
shell: cmake -P {0}
|
||||
@ -100,7 +78,7 @@ jobs:
|
||||
COMMAND sudo apt update
|
||||
)
|
||||
execute_process(
|
||||
COMMAND sudo apt install libgl1-mesa-dev libcups2-dev
|
||||
COMMAND sudo apt install libgl1-mesa-dev
|
||||
RESULT_VARIABLE result
|
||||
)
|
||||
if (NOT result EQUAL 0)
|
||||
@ -117,9 +95,9 @@ jobs:
|
||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(url_os "windows_x86")
|
||||
set(qt_package_arch_suffix "win64_msvc2019_64")
|
||||
set(qt_dir_prefix "${qt_version}/msvc2019_64")
|
||||
set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
|
||||
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
|
||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||
set(url_os "linux_x64")
|
||||
if (qt_version VERSION_LESS "6.7.0")
|
||||
@ -128,15 +106,15 @@ jobs:
|
||||
set(qt_package_arch_suffix "linux_gcc_64")
|
||||
endif()
|
||||
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")
|
||||
set(url_os "mac_x64")
|
||||
set(qt_package_arch_suffix "clang_64")
|
||||
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()
|
||||
|
||||
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(READ ./Updates.xml updates_xml)
|
||||
@ -146,7 +124,7 @@ jobs:
|
||||
file(MAKE_DIRECTORY qt6)
|
||||
|
||||
# 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}")
|
||||
|
||||
message("Downloading Qt to ${qt_dir}")
|
||||
@ -165,11 +143,17 @@ jobs:
|
||||
|
||||
foreach(package qt5compat qtshadertools)
|
||||
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
|
||||
)
|
||||
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
|
||||
if ("${{ runner.os }}" STREQUAL "Linux")
|
||||
if (qt_version VERSION_LESS "6.7.0")
|
||||
@ -177,47 +161,25 @@ jobs:
|
||||
else()
|
||||
set(uic_suffix "Rhel8.6-x86_64")
|
||||
endif()
|
||||
downloadAndExtract(
|
||||
downloadAndExtractLibicu(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z"
|
||||
icu.7z
|
||||
)
|
||||
endif()
|
||||
|
||||
- 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
|
||||
shell: cmake -P {0}
|
||||
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)
|
||||
# Save the path for other steps
|
||||
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
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
@ -276,11 +238,59 @@ jobs:
|
||||
path: ./${{ 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')
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Download the JSON file
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||
path: ./${{ env.PLUGIN_NAME }}-origin
|
||||
|
||||
- name: Store Release upload_url
|
||||
run: |
|
||||
RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}")
|
||||
echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV
|
||||
|
||||
- name: Run the Node.js script to update JSON
|
||||
env:
|
||||
QT_TOKEN: ${{ secrets.TOKEN }}
|
||||
API_URL: ${{ secrets.API_URL }}
|
||||
run: |
|
||||
node .github/scripts/registerPlugin.js ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} ${{ env.QT_TOKEN }} ${{ env.API_URL }}
|
||||
|
||||
- name: Delete previous json artifacts
|
||||
uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}*-json
|
||||
|
||||
- name: Upload the modified JSON file as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugin-json
|
||||
path: .github/scripts/${{ env.PLUGIN_NAME }}.json
|
||||
|
||||
release:
|
||||
if: contains(github.ref, 'tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, update_json]
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -34,6 +34,7 @@ Thumbs.db
|
||||
*.rc
|
||||
/.qmake.cache
|
||||
/.qmake.stash
|
||||
.qmlls.ini
|
||||
|
||||
# qtcreator generated files
|
||||
*.pro.user*
|
||||
|
||||
3
.qmlformat.ini
Normal file
3
.qmlformat.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[General]
|
||||
IndentWidth=4
|
||||
NewlineType=native
|
||||
@ -15,7 +15,8 @@ find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
|
||||
add_subdirectory(llmcore)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(logger)
|
||||
add_subdirectory(chatview)
|
||||
add_subdirectory(ChatView)
|
||||
add_subdirectory(context)
|
||||
|
||||
add_qtc_plugin(QodeAssist
|
||||
PLUGIN_DEPENDS
|
||||
@ -40,27 +41,34 @@ add_qtc_plugin(QodeAssist
|
||||
QodeAssistConstants.hpp
|
||||
QodeAssisttr.h
|
||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||
templates/Templates.hpp
|
||||
templates/CodeLlamaFim.hpp
|
||||
templates/StarCoder2Fim.hpp
|
||||
templates/DeepSeekCoderFim.hpp
|
||||
templates/CustomFimTemplate.hpp
|
||||
templates/DeepSeekCoderChat.hpp
|
||||
templates/CodeLlamaChat.hpp
|
||||
templates/Qwen.hpp
|
||||
templates/StarCoderChat.hpp
|
||||
templates/Ollama.hpp
|
||||
templates/BasicChat.hpp
|
||||
templates/Llama3.hpp
|
||||
templates/ChatML.hpp
|
||||
templates/Alpaca.hpp
|
||||
templates/Llama2.hpp
|
||||
templates/Claude.hpp
|
||||
templates/OpenAI.hpp
|
||||
providers/Providers.hpp
|
||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.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
|
||||
LSPCompletion.hpp
|
||||
LLMSuggestion.hpp LLMSuggestion.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/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
CodeHandler.hpp CodeHandler.cpp
|
||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssist PRIVATE )
|
||||
|
||||
@ -1,14 +1,26 @@
|
||||
qt_add_library(QodeAssistChatView STATIC)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
|
||||
qt_add_qml_module(QodeAssistChatView
|
||||
URI ChatView
|
||||
VERSION 1.0
|
||||
DEPENDENCIES QtQuick
|
||||
QML_FILES
|
||||
qml/RootItem.qml
|
||||
qml/ChatItem.qml
|
||||
qml/Badge.qml
|
||||
qml/dialog/CodeBlock.qml
|
||||
qml/dialog/TextBlock.qml
|
||||
qml/controls/QoAButton.qml
|
||||
qml/parts/TopBar.qml
|
||||
qml/parts/BottomBar.qml
|
||||
qml/parts/AttachedFilesPlace.qml
|
||||
RESOURCES
|
||||
icons/attach-file.svg
|
||||
icons/close-dark.svg
|
||||
icons/close-light.svg
|
||||
SOURCES
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
ChatModel.hpp ChatModel.cpp
|
||||
@ -16,6 +28,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
ClientInterface.hpp ClientInterface.cpp
|
||||
MessagePart.hpp
|
||||
ChatUtils.h ChatUtils.cpp
|
||||
ChatSerializer.hpp ChatSerializer.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
@ -28,8 +41,9 @@ target_link_libraries(QodeAssistChatView
|
||||
QtCreator::Utils
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
@ -55,6 +55,13 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
case Roles::Content: {
|
||||
return message.content;
|
||||
}
|
||||
case Roles::Attachments: {
|
||||
QStringList filenames;
|
||||
for (const auto &attachment : message.attachments) {
|
||||
filenames << attachment.filename;
|
||||
}
|
||||
return filenames;
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@ -65,28 +72,43 @@ QHash<int, QByteArray> ChatModel::roleNames() const
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[Roles::RoleType] = "roleType";
|
||||
roles[Roles::Content] = "content";
|
||||
roles[Roles::Attachments] = "attachments";
|
||||
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);
|
||||
}
|
||||
}
|
||||
int tokenCount = estimateTokenCount(fullContent);
|
||||
|
||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
|
||||
Message &lastMessage = m_messages.last();
|
||||
int oldTokenCount = lastMessage.tokenCount;
|
||||
lastMessage.content = content;
|
||||
lastMessage.attachments = attachments;
|
||||
lastMessage.tokenCount = tokenCount;
|
||||
m_totalTokens += (tokenCount - oldTokenCount);
|
||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
m_messages.append({role, content, tokenCount, id});
|
||||
Message newMessage{role, content, tokenCount, id};
|
||||
newMessage.attachments = attachments;
|
||||
m_messages.append(newMessage);
|
||||
m_totalTokens += tokenCount;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
trim();
|
||||
emit totalTokensChanged();
|
||||
}
|
||||
|
||||
@ -95,20 +117,6 @@ QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
||||
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;
|
||||
@ -121,6 +129,7 @@ void ChatModel::clear()
|
||||
m_totalTokens = 0;
|
||||
endResetModel();
|
||||
emit totalTokensChanged();
|
||||
emit modelReseted();
|
||||
}
|
||||
|
||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||
@ -152,11 +161,10 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
||||
return parts;
|
||||
}
|
||||
|
||||
QJsonArray ChatModel::prepareMessagesForRequest(LLMCore::ContextData context) const
|
||||
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
|
||||
{
|
||||
QJsonArray messages;
|
||||
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", context.systemPrompt}});
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
|
||||
|
||||
for (const auto &message : m_messages) {
|
||||
QString role;
|
||||
@ -170,7 +178,22 @@ QJsonArray ChatModel::prepareMessagesForRequest(LLMCore::ContextData context) co
|
||||
default:
|
||||
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;
|
||||
@ -24,7 +24,9 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <qqmlintegration.h>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "context/ContentFile.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -36,17 +38,19 @@ class ChatModel : public QAbstractListModel
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
enum Roles { RoleType = Qt::UserRole, Content };
|
||||
|
||||
enum ChatRole { System, User, Assistant };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
|
||||
|
||||
struct Message
|
||||
{
|
||||
ChatRole role;
|
||||
QString content;
|
||||
int tokenCount;
|
||||
QString id;
|
||||
|
||||
QList<Context::ContentFile> attachments;
|
||||
};
|
||||
|
||||
explicit ChatModel(QObject *parent = nullptr);
|
||||
@ -55,12 +59,16 @@ public:
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) 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 QList<MessagePart> processMessageContent(const QString &content) const;
|
||||
|
||||
QVector<Message> getChatHistory() const;
|
||||
QJsonArray prepareMessagesForRequest(LLMCore::ContextData context) const;
|
||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
||||
|
||||
int totalTokens() const;
|
||||
int tokensThreshold() const;
|
||||
@ -71,9 +79,9 @@ public:
|
||||
signals:
|
||||
void totalTokensChanged();
|
||||
void tokensThresholdChanged();
|
||||
void modelReseted();
|
||||
|
||||
private:
|
||||
void trim();
|
||||
int estimateTokenCount(const QString &text) const;
|
||||
|
||||
QVector<Message> m_messages;
|
||||
275
ChatView/ChatRootView.cpp
Normal file
275
ChatView/ChatRootView.cpp
Normal file
@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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 "ChatRootView.hpp"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(&settings.caModel,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::currentTemplateChanged);
|
||||
|
||||
connect(&Settings::chatAssistantSettings().sharingCurrentFile,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::isSharingCurrentFileChanged);
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageReceivedCompletely,
|
||||
this,
|
||||
&ChatRootView::autosave);
|
||||
|
||||
connect(m_chatModel, &ChatModel::modelReseted, [this]() { m_recentFilePath = QString(); });
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
{
|
||||
return m_chatModel;
|
||||
}
|
||||
|
||||
void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile)
|
||||
{
|
||||
if (m_chatModel->totalTokens() > 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);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
autosave();
|
||||
m_chatModel->clear();
|
||||
m_recentFilePath = QString{};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_clientInterface->sendMessage(message, m_attachmentFiles, sharingCurrentFile);
|
||||
clearAttachmentFiles();
|
||||
}
|
||||
|
||||
void ChatRootView::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
void ChatRootView::cancelRequest()
|
||||
{
|
||||
m_clientInterface->cancelRequest();
|
||||
}
|
||||
|
||||
void ChatRootView::clearAttachmentFiles()
|
||||
{
|
||||
if (!m_attachmentFiles.isEmpty()) {
|
||||
m_attachmentFiles.clear();
|
||||
emit attachmentFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString ChatRootView::getChatsHistoryDir() const
|
||||
{
|
||||
QString path;
|
||||
|
||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
path = projectSettings.chatHistoryPath().toString();
|
||||
} else {
|
||||
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
|
||||
}
|
||||
|
||||
QDir dir(path);
|
||||
if (!dir.exists() && !dir.mkpath(".")) {
|
||||
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
||||
return QString();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
QString ChatRootView::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.caModel();
|
||||
}
|
||||
|
||||
bool ChatRootView::isSharingCurrentFile() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().sharingCurrentFile();
|
||||
}
|
||||
|
||||
void ChatRootView::saveHistory(const QString &filePath)
|
||||
{
|
||||
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||
if (!result.success) {
|
||||
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::loadHistory(const QString &filePath)
|
||||
{
|
||||
auto result = ChatSerializer::loadFromFile(m_chatModel, filePath);
|
||||
if (!result.success) {
|
||||
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
||||
} else {
|
||||
m_recentFilePath = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::showSaveDialog()
|
||||
{
|
||||
QString initialDir = getChatsHistoryDir();
|
||||
|
||||
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
|
||||
dialog->setAcceptMode(QFileDialog::AcceptSave);
|
||||
dialog->setFileMode(QFileDialog::AnyFile);
|
||||
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||
dialog->setDefaultSuffix("json");
|
||||
if (!initialDir.isEmpty()) {
|
||||
dialog->setDirectory(initialDir);
|
||||
dialog->selectFile(getSuggestedFileName() + ".json");
|
||||
}
|
||||
|
||||
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||
if (result == QFileDialog::Accepted) {
|
||||
QStringList files = dialog->selectedFiles();
|
||||
if (!files.isEmpty()) {
|
||||
saveHistory(files.first());
|
||||
}
|
||||
}
|
||||
dialog->deleteLater();
|
||||
});
|
||||
|
||||
dialog->open();
|
||||
}
|
||||
|
||||
void ChatRootView::showLoadDialog()
|
||||
{
|
||||
QString initialDir = getChatsHistoryDir();
|
||||
|
||||
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
|
||||
dialog->setAcceptMode(QFileDialog::AcceptOpen);
|
||||
dialog->setFileMode(QFileDialog::ExistingFile);
|
||||
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||
if (!initialDir.isEmpty()) {
|
||||
dialog->setDirectory(initialDir);
|
||||
}
|
||||
|
||||
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||
if (result == QFileDialog::Accepted) {
|
||||
QStringList files = dialog->selectedFiles();
|
||||
if (!files.isEmpty()) {
|
||||
loadHistory(files.first());
|
||||
}
|
||||
}
|
||||
dialog->deleteLater();
|
||||
});
|
||||
|
||||
dialog->open();
|
||||
}
|
||||
|
||||
QString ChatRootView::getSuggestedFileName() const
|
||||
{
|
||||
QStringList parts;
|
||||
|
||||
if (m_chatModel->rowCount() > 0) {
|
||||
QString firstMessage
|
||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
||||
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||
shortMessage.replace(QRegularExpression("[^a-zA-Z0-9_-]"), "_");
|
||||
parts << shortMessage;
|
||||
}
|
||||
|
||||
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
|
||||
|
||||
return parts.join("_");
|
||||
}
|
||||
|
||||
void ChatRootView::autosave()
|
||||
{
|
||||
if (m_chatModel->rowCount() == 0 || !Settings::chatAssistantSettings().autosave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString filePath = getAutosaveFilePath();
|
||||
if (!filePath.isEmpty()) {
|
||||
ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||
m_recentFilePath = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
QString ChatRootView::getAutosaveFilePath() const
|
||||
{
|
||||
if (!m_recentFilePath.isEmpty()) {
|
||||
return m_recentFilePath;
|
||||
}
|
||||
|
||||
QString dir = getChatsHistoryDir();
|
||||
if (dir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QDir(dir).filePath(getSuggestedFileName() + ".json");
|
||||
}
|
||||
|
||||
void ChatRootView::showAttachFilesDialog()
|
||||
{
|
||||
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
|
||||
dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
|
||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||
dialog.setDirectory(project->projectDirectory().toString());
|
||||
}
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList filePaths = dialog.selectedFiles();
|
||||
if (!filePaths.isEmpty()) {
|
||||
m_attachmentFiles = filePaths;
|
||||
emit attachmentFilesChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@ -28,15 +28,13 @@ namespace QodeAssist::Chat {
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
|
||||
Q_PROPERTY(QColor primaryColor READ primaryColor CONSTANT FINAL)
|
||||
Q_PROPERTY(QColor secondaryColor READ secondaryColor CONSTANT FINAL)
|
||||
Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
|
||||
Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
|
||||
isSharingCurrentFileChanged FINAL)
|
||||
Q_PROPERTY(QStringList attachmentFiles MEMBER m_attachmentFiles NOTIFY attachmentFilesChanged)
|
||||
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
@ -45,38 +43,40 @@ public:
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
QColor backgroundColor() const;
|
||||
QColor primaryColor() const;
|
||||
QColor secondaryColor() const;
|
||||
|
||||
QColor codeColor() const;
|
||||
|
||||
bool isSharingCurrentFile() const;
|
||||
|
||||
void saveHistory(const QString &filePath);
|
||||
void loadHistory(const QString &filePath);
|
||||
|
||||
Q_INVOKABLE void showSaveDialog();
|
||||
Q_INVOKABLE void showLoadDialog();
|
||||
|
||||
void autosave();
|
||||
QString getAutosaveFilePath() const;
|
||||
|
||||
Q_INVOKABLE void showAttachFilesDialog();
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message, bool sharingCurrentFile = false) const;
|
||||
void sendMessage(const QString &message, bool sharingCurrentFile = false);
|
||||
void copyToClipboard(const QString &text);
|
||||
void cancelRequest();
|
||||
void clearAttachmentFiles();
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
|
||||
void isSharingCurrentFileChanged();
|
||||
void attachmentFilesChanged();
|
||||
|
||||
private:
|
||||
void generateColors();
|
||||
QColor generateColor(const QColor &baseColor,
|
||||
float hueShift,
|
||||
float saturationMod,
|
||||
float lightnessMod);
|
||||
QString getChatsHistoryDir() const;
|
||||
QString getSuggestedFileName() const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
ClientInterface *m_clientInterface;
|
||||
QString m_currentTemplate;
|
||||
QColor m_primaryColor;
|
||||
QColor m_secondaryColor;
|
||||
QColor m_codeColor;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
145
ChatView/ChatSerializer.cpp
Normal file
145
ChatView/ChatSerializer.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 "ChatSerializer.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
const QString ChatSerializer::VERSION = "0.1";
|
||||
|
||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
||||
{
|
||||
if (!ensureDirectoryExists(filePath)) {
|
||||
return {false, "Failed to create directory structure"};
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||
}
|
||||
|
||||
QJsonObject root = serializeChat(model);
|
||||
QJsonDocument doc(root);
|
||||
|
||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||
}
|
||||
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QString version = root["version"].toString();
|
||||
|
||||
if (!validateVersion(version)) {
|
||||
return {false, QString("Unsupported version: %1").arg(version)};
|
||||
}
|
||||
|
||||
if (!deserializeChat(model, root)) {
|
||||
return {false, "Failed to deserialize chat data"};
|
||||
}
|
||||
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
|
||||
{
|
||||
QJsonObject messageObj;
|
||||
messageObj["role"] = static_cast<int>(message.role);
|
||||
messageObj["content"] = message.content;
|
||||
messageObj["tokenCount"] = message.tokenCount;
|
||||
messageObj["id"] = message.id;
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
|
||||
{
|
||||
ChatModel::Message message;
|
||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||
message.content = json["content"].toString();
|
||||
message.tokenCount = json["tokenCount"].toInt();
|
||||
message.id = json["id"].toString();
|
||||
return message;
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
|
||||
{
|
||||
QJsonArray messagesArray;
|
||||
for (const auto &message : model->getChatHistory()) {
|
||||
messagesArray.append(serializeMessage(message));
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["version"] = VERSION;
|
||||
root["messages"] = messagesArray;
|
||||
root["totalTokens"] = model->totalTokens();
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
|
||||
{
|
||||
QJsonArray messagesArray = json["messages"].toArray();
|
||||
QVector<ChatModel::Message> messages;
|
||||
messages.reserve(messagesArray.size());
|
||||
|
||||
for (const auto &messageValue : messagesArray) {
|
||||
messages.append(deserializeMessage(messageValue.toObject()));
|
||||
}
|
||||
|
||||
model->clear();
|
||||
for (const auto &message : messages) {
|
||||
model->addMessage(message.content, message.role, message.id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QDir dir = fileInfo.dir();
|
||||
return dir.exists() || dir.mkpath(".");
|
||||
}
|
||||
|
||||
bool ChatSerializer::validateVersion(const QString &version)
|
||||
{
|
||||
return version == VERSION;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
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>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
// Q_NAMESPACE
|
||||
|
||||
class ChatUtils : public QObject
|
||||
{
|
||||
@ -27,7 +27,7 @@ namespace QodeAssist::Chat {
|
||||
ChatWidget::ChatWidget(QWidget *parent)
|
||||
: QQuickWidget(parent)
|
||||
{
|
||||
setSource(QUrl("qrc:/ChatView/qml/RootItem.qml"));
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ContextManager.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
@ -64,41 +65,52 @@ ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||
|
||||
ClientInterface::~ClientInterface() = default;
|
||||
|
||||
void ClientInterface::sendMessage(const QString &message, bool includeCurrentFile)
|
||||
void ClientInterface::sendMessage(
|
||||
const QString &message, const QList<QString> &attachments, bool includeCurrentFile)
|
||||
{
|
||||
cancelRequest();
|
||||
|
||||
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
||||
|
||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||
templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
return;
|
||||
}
|
||||
|
||||
LLMCore::ContextData context;
|
||||
context.prefix = message;
|
||||
context.suffix = "";
|
||||
|
||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
QString systemPrompt;
|
||||
if (chatAssistantSettings.useSystemPrompt())
|
||||
systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
if (includeCurrentFile) {
|
||||
QString fileContext = getCurrentFileContext();
|
||||
if (!fileContext.isEmpty()) {
|
||||
context.systemPrompt = QString("%1\n\n%2").arg(systemPrompt, fileContext);
|
||||
LOG_MESSAGE("Using system prompt with file context");
|
||||
} else {
|
||||
context.systemPrompt = systemPrompt;
|
||||
LOG_MESSAGE("Failed to get file context, using default system prompt");
|
||||
systemPrompt = systemPrompt.append(fileContext);
|
||||
}
|
||||
} else {
|
||||
context.systemPrompt = systemPrompt;
|
||||
}
|
||||
|
||||
QJsonObject providerRequest;
|
||||
providerRequest["model"] = Settings::generalSettings().caModel();
|
||||
providerRequest["stream"] = true;
|
||||
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(context);
|
||||
providerRequest["stream"] = chatAssistantSettings.stream();
|
||||
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
|
||||
|
||||
if (promptTemplate)
|
||||
promptTemplate->prepareRequest(providerRequest, context);
|
||||
@ -117,11 +129,18 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil
|
||||
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
||||
config.providerRequest = providerRequest;
|
||||
config.multiLineCompletion = false;
|
||||
config.apiKey = provider->apiKey();
|
||||
|
||||
QJsonObject request;
|
||||
request["id"] = QUuid::createUuid().toString();
|
||||
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "");
|
||||
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);
|
||||
}
|
||||
|
||||
@ -141,11 +160,17 @@ void ClientInterface::handleLLMResponse(const QString &response,
|
||||
const QJsonObject &request,
|
||||
bool isComplete)
|
||||
{
|
||||
QString messageId = request["id"].toString();
|
||||
m_chatModel->addMessage(response.trimmed(), ChatModel::ChatRole::Assistant, messageId);
|
||||
const auto message = response.trimmed();
|
||||
|
||||
if (isComplete) {
|
||||
LOG_MESSAGE("Message completed. Final response for message " + messageId + ": " + response);
|
||||
if (!message.isEmpty()) {
|
||||
QString messageId = request["id"].toString();
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
||||
|
||||
if (isComplete) {
|
||||
LOG_MESSAGE(
|
||||
"Message completed. Final response for message " + messageId + ": " + response);
|
||||
emit messageReceivedCompletely();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,12 +36,16 @@ public:
|
||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void sendMessage(const QString &message, bool includeCurrentFile = false);
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
bool includeCurrentFile = false);
|
||||
void clearMessages();
|
||||
void cancelRequest();
|
||||
|
||||
signals:
|
||||
void errorOccurred(const QString &error);
|
||||
void messageReceivedCompletely();
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||
10
ChatView/icons/attach-file.svg
Normal file
10
ChatView/icons/attach-file.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_37_14)">
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_37_14">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 555 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 |
@ -23,18 +23,18 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property alias text: badgeText.text
|
||||
property alias fontColor: badgeText.color
|
||||
|
||||
width: badgeText.implicitWidth + radius
|
||||
height: badgeText.implicitHeight + 6
|
||||
color: "lightgreen"
|
||||
radius: height / 2
|
||||
implicitWidth: badgeText.implicitWidth + root.radius
|
||||
implicitHeight: badgeText.implicitHeight + 6
|
||||
color: palette.button
|
||||
radius: root.height / 2
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
border.color: "gray"
|
||||
|
||||
Text {
|
||||
id: badgeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: palette.buttonText
|
||||
}
|
||||
}
|
||||
137
ChatView/qml/ChatItem.qml
Normal file
137
ChatView/qml/ChatItem.qml
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 ChatView
|
||||
import QtQuick.Layouts
|
||||
import "./dialog"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property alias messageAttachments: attachmentsModel.model
|
||||
|
||||
height: msgColumn.implicitHeight + 10
|
||||
radius: 8
|
||||
|
||||
ColumnLayout {
|
||||
id: msgColumn
|
||||
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: msgCreator
|
||||
delegate: Loader {
|
||||
id: msgCreatorDelegate
|
||||
// Fix me:
|
||||
// why does `required property MessagePart modelData` not work?
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: root.width
|
||||
sourceComponent: {
|
||||
// If `required property MessagePart modelData` is used
|
||||
// and conversion to MessagePart fails, you're left
|
||||
// with a nullptr. This tests that to prevent crashing.
|
||||
if(!modelData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch(modelData.type) {
|
||||
case MessagePart.Text: return textComponent;
|
||||
case MessagePart.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
TextComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
CodeBlockComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
required property var itemData
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: itemData.text
|
||||
}
|
||||
|
||||
|
||||
component CodeBlockComponent : CodeBlock {
|
||||
required property var itemData
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
}
|
||||
}
|
||||
@ -22,21 +22,57 @@ import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic as QQC
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import "./controls"
|
||||
import "./parts"
|
||||
|
||||
ChatRootView {
|
||||
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 {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
color: palette.window
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
anchors.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.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
|
||||
}
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
ListView {
|
||||
id: chatListView
|
||||
@ -51,14 +87,13 @@ ChatRootView {
|
||||
cacheBuffer: 2000
|
||||
|
||||
delegate: ChatItem {
|
||||
required property var model
|
||||
|
||||
width: ListView.view.width - scroll.width
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
|
||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
codeBgColor: root.codeColor
|
||||
selectionColor: root.primaryColor.hslLightness > 0.5 ? Qt.darker(root.primaryColor, 1.5)
|
||||
: Qt.lighter(root.primaryColor, 1.5)
|
||||
|
||||
messageAttachments: model.attachments
|
||||
color: model.roleType === ChatModel.User ? palette.alternateBase
|
||||
: palette.base
|
||||
}
|
||||
|
||||
header: Item {
|
||||
@ -71,12 +106,12 @@ ChatRootView {
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
scrollToBottom()
|
||||
root.scrollToBottom()
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (atYEnd) {
|
||||
scrollToBottom()
|
||||
root.scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,78 +127,52 @@ ChatRootView {
|
||||
id: messageInput
|
||||
|
||||
placeholderText: qsTr("Type your message here...")
|
||||
placeholderTextColor: "#888"
|
||||
color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
placeholderTextColor: palette.mid
|
||||
color: palette.text
|
||||
background: Rectangle {
|
||||
radius: 2
|
||||
color: root.primaryColor
|
||||
border.color: root.primaryColor.hslLightness > 0.5 ? Qt.lighter(root.primaryColor, 1.5)
|
||||
: Qt.darker(root.primaryColor, 1.5)
|
||||
color: palette.base
|
||||
border.color: messageInput.activeFocus ? palette.highlight : palette.button
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
|
||||
sendChatMessage()
|
||||
root.sendChatMessage()
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
AttachedFilesPlace {
|
||||
id: attachedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Button {
|
||||
id: sendButton
|
||||
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
text: qsTr("Send")
|
||||
onClicked: 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: clearChat()
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: sharingCurrentFile
|
||||
|
||||
text: "Share current file with models"
|
||||
checked: root.isSharingCurrentFile
|
||||
}
|
||||
attachedFilesModel: root.attachmentFiles
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: bar
|
||||
BottomBar {
|
||||
id: bottomBar
|
||||
|
||||
layoutDirection: Qt.RightToLeft
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: scroll.width
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
Badge {
|
||||
text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
|
||||
color: root.codeColor
|
||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
sendButton.onClicked: root.sendChatMessage()
|
||||
stopButton.onClicked: root.cancelRequest()
|
||||
sharingCurrentFile.checked: root.isSharingCurrentFile
|
||||
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,7 +185,7 @@ ChatRootView {
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
root.sendMessage(messageInput.text, sharingCurrentFile.checked)
|
||||
root.sendMessage(messageInput.text, bottomBar.sharingCurrentFile.checked)
|
||||
messageInput.text = ""
|
||||
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 language: ""
|
||||
property color selectionColor
|
||||
|
||||
readonly property string monospaceFont: {
|
||||
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)
|
||||
: Qt.lighter(root.color, 1.3)
|
||||
border.width: 2
|
||||
@ -61,11 +61,11 @@ Rectangle {
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
font.family: monospaceFont
|
||||
font.pointSize: 12
|
||||
font.family: root.monospaceFont
|
||||
font.pointSize: Qt.application.font.pointSize
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: root.selectionColor
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
@ -80,7 +80,7 @@ Rectangle {
|
||||
font.pointSize: 8
|
||||
}
|
||||
|
||||
Button {
|
||||
QoAButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
@ -26,4 +26,6 @@ TextEdit {
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.StyledText
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
}
|
||||
94
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
94
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
Flow {
|
||||
id: attachFilesPlace
|
||||
|
||||
property alias attachedFilesModel: attachRepeater.model
|
||||
|
||||
spacing: 5
|
||||
leftPadding: 5
|
||||
rightPadding: 5
|
||||
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: fileNameText.width + closeButton.width + 20
|
||||
radius: 4
|
||||
color: palette.button
|
||||
border.width: 1
|
||||
border.color: palette.mid
|
||||
|
||||
Row {
|
||||
spacing: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
|
||||
Text {
|
||||
id: fileNameText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: palette.buttonText
|
||||
|
||||
text: {
|
||||
const parts = modelData.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeButton
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: closeIcon.width
|
||||
height: closeButton.width
|
||||
|
||||
onClicked: {
|
||||
const newList = [...root.attachmentFiles];
|
||||
newList.splice(index, 1);
|
||||
root.attachmentFiles = newList;
|
||||
}
|
||||
|
||||
Image {
|
||||
id: closeIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
|
||||
width: 6
|
||||
height: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
ChatView/qml/parts/BottomBar.qml
Normal file
83
ChatView/qml/parts/BottomBar.qml
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias sendButton: sendButtonId
|
||||
property alias stopButton: stopButtonId
|
||||
property alias sharingCurrentFile: sharingCurrentFileId
|
||||
property alias attachFiles: attachFilesId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
|
||||
RowLayout {
|
||||
id: bottomBar
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: sendButtonId
|
||||
|
||||
text: qsTr("Send")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: stopButtonId
|
||||
|
||||
text: qsTr("Stop")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: sharingCurrentFileId
|
||||
|
||||
text: qsTr("Share current file with models")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: attachFilesId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/attach-file.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
text: qsTr("Attach files")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
73
ChatView/qml/parts/TopBar.qml
Normal file
73
ChatView/qml/parts/TopBar.qml
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias saveButton: saveButtonId
|
||||
property alias loadButton: loadButtonId
|
||||
property alias clearButton: clearButtonId
|
||||
property alias tokensBadge: tokensBadgeId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: saveButtonId
|
||||
|
||||
text: qsTr("Save")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: loadButtonId
|
||||
|
||||
text: qsTr("Load")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: clearButtonId
|
||||
|
||||
text: qsTr("Clear")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Badge {
|
||||
id: tokensBadgeId
|
||||
}
|
||||
}
|
||||
}
|
||||
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/>.
|
||||
*/
|
||||
|
||||
#include "CounterTooltip.hpp"
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
CounterTooltip::CounterTooltip(int count)
|
||||
: m_count(count)
|
||||
class CodeHandler
|
||||
{
|
||||
m_label = new QLabel(this);
|
||||
addWidget(m_label);
|
||||
updateLabel();
|
||||
public:
|
||||
static QString processText(QString text);
|
||||
|
||||
m_timer = new QTimer(this);
|
||||
m_timer->setSingleShot(true);
|
||||
m_timer->setInterval(2000);
|
||||
private:
|
||||
static QString getCommentPrefix(const QString &language);
|
||||
static QString detectLanguage(const QString &line);
|
||||
|
||||
connect(m_timer, &QTimer::timeout, this, [this] { emit finished(m_count); });
|
||||
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
CounterTooltip::~CounterTooltip() {}
|
||||
|
||||
void CounterTooltip::updateLabel()
|
||||
{
|
||||
const auto hotkey = QKeySequence(QKeySequence::MoveToNextWord).toString();
|
||||
m_label->setText(QString("Insert Next %1 line(s) (%2)").arg(m_count).arg(hotkey));
|
||||
}
|
||||
static const QRegularExpression &getFullCodeBlockRegex();
|
||||
static const QRegularExpression &getPartialStartBlockRegex();
|
||||
static const QRegularExpression &getPartialEndBlockRegex();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -81,28 +81,38 @@ void ConfigurationManager::selectProvider()
|
||||
|
||||
void ConfigurationManager::selectModel()
|
||||
{
|
||||
const QString providerName = m_generalSettings.ccProvider.volatileValue();
|
||||
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const auto providerUrl = (settingsButton == &m_generalSettings.ccSelectModel)
|
||||
? m_generalSettings.ccUrl.volatileValue()
|
||||
: m_generalSettings.caUrl.volatileValue();
|
||||
const auto modelList = m_providersManager.getProviderByName(providerName)
|
||||
->getInstalledModels(providerUrl);
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectModel)
|
||||
? m_generalSettings.ccModel
|
||||
: m_generalSettings.caModel;
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(modelList,
|
||||
targetSettings,
|
||||
Tr::tr("Select LLM Model"),
|
||||
Tr::tr("Models:"));
|
||||
});
|
||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
||||
: m_generalSettings.caUrl.volatileValue();
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : m_generalSettings.caModel;
|
||||
|
||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||
if (!provider->supportsModelListing()) {
|
||||
m_generalSettings.showModelsNotSupportedDialog(targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto modelList = provider->getInstalledModels(providerUrl);
|
||||
|
||||
if (modelList.isEmpty()) {
|
||||
m_generalSettings.showModelsNotFoundDialog(targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectTemplate()
|
||||
@ -111,13 +121,13 @@ void ConfigurationManager::selectTemplate()
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const auto templateList = (settingsButton == &m_generalSettings.ccSelectTemplate)
|
||||
? m_templateManger.fimTemplatesNames()
|
||||
: m_templateManger.chatTemplatesNames();
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectTemplate)
|
||||
? m_generalSettings.ccTemplate
|
||||
: m_generalSettings.caTemplate;
|
||||
const auto templateList = isCodeCompletion ? m_templateManger.fimTemplatesNames()
|
||||
: m_templateManger.chatTemplatesNames();
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
||||
: m_generalSettings.caTemplate;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(templateList,
|
||||
@ -129,6 +139,10 @@ void ConfigurationManager::selectTemplate()
|
||||
|
||||
void ConfigurationManager::selectUrl()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
QStringList urls;
|
||||
for (const auto &name : m_providersManager.providersNames()) {
|
||||
const auto url = m_providersManager.getProviderByName(name)->url();
|
||||
@ -136,19 +150,12 @@ void ConfigurationManager::selectUrl()
|
||||
urls.append(url);
|
||||
}
|
||||
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
|
||||
? m_generalSettings.ccUrl
|
||||
: m_generalSettings.caUrl;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(urls,
|
||||
targetSettings,
|
||||
Tr::tr("Select URL"),
|
||||
Tr::tr("URLs:"));
|
||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,9 @@
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
#include "DocumentContextReader.hpp"
|
||||
#include "CodeHandler.hpp"
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "llmcore/MessageBuilder.hpp"
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
@ -152,29 +154,71 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
auto providerName = Settings::generalSettings().ccProvider();
|
||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().ccTemplate();
|
||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
||||
templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
return;
|
||||
}
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Fim;
|
||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
config.url = QUrl(
|
||||
QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
|
||||
config.url = QUrl(QString("%1%2").arg(
|
||||
Settings::generalSettings().ccUrl(),
|
||||
promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint()
|
||||
: provider->chatEndpoint()));
|
||||
config.apiKey = provider->apiKey();
|
||||
|
||||
config.providerRequest
|
||||
= {{"model", Settings::generalSettings().ccModel()},
|
||||
{"stream", Settings::codeCompletionSettings().stream()}};
|
||||
|
||||
config.providerRequest = {{"model", Settings::generalSettings().ccModel()},
|
||||
{"stream", true},
|
||||
{"stop",
|
||||
QJsonArray::fromStringList(config.promptTemplate->stopWords())}};
|
||||
config.multiLineCompletion = completeSettings.multiLineCompletion();
|
||||
|
||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
||||
if (!stopWords.isEmpty())
|
||||
config.providerRequest["stop"] = stopWords;
|
||||
|
||||
QString systemPrompt;
|
||||
if (completeSettings.useSystemPrompt())
|
||||
config.providerRequest["system"] = completeSettings.systemPrompt();
|
||||
systemPrompt.append(completeSettings.systemPrompt());
|
||||
if (!updatedContext.fileContext.isEmpty())
|
||||
systemPrompt.append(updatedContext.fileContext);
|
||||
|
||||
config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
|
||||
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
|
||||
QString userMessage;
|
||||
if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) {
|
||||
userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix);
|
||||
} else {
|
||||
userMessage = updatedContext.prefix;
|
||||
}
|
||||
|
||||
auto message = LLMCore::MessageBuilder()
|
||||
.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);
|
||||
}
|
||||
|
||||
@ -198,7 +242,7 @@ LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &reque
|
||||
int cursorPosition = position["character"].toInt();
|
||||
int lineNumber = position["line"].toInt();
|
||||
|
||||
DocumentContextReader reader(textDocument);
|
||||
Context::DocumentContextReader reader(textDocument);
|
||||
return reader.prepareContext(lineNumber, cursorPosition);
|
||||
}
|
||||
|
||||
@ -206,19 +250,31 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||
const QJsonObject &request,
|
||||
bool isComplete)
|
||||
{
|
||||
auto templateName = Settings::generalSettings().ccTemplate();
|
||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
||||
templateName);
|
||||
|
||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||
|
||||
QJsonObject response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response[LanguageServerProtocol::idKey] = request["id"];
|
||||
|
||||
QJsonObject result;
|
||||
QJsonArray completions;
|
||||
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;
|
||||
range["start"] = position;
|
||||
QJsonObject end = position;
|
||||
end["character"] = position["character"].toInt() + completion.length();
|
||||
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||
range["end"] = end;
|
||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@ -18,30 +23,25 @@
|
||||
*/
|
||||
|
||||
#include "LLMSuggestion.hpp"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QtWidgets/qtoolbar.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/stringutils.h>
|
||||
#include <utils/tooltip/tooltip.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin)
|
||||
: m_completion(completion)
|
||||
, m_linesCount(0)
|
||||
LLMSuggestion::LLMSuggestion(
|
||||
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
|
||||
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
|
||||
{
|
||||
int startPos = completion.range().start().toPositionInDocument(origin);
|
||||
int endPos = completion.range().end().toPositionInDocument(origin);
|
||||
const auto &data = suggestions[currentCompletion];
|
||||
|
||||
startPos = qBound(0, startPos, origin->characterCount() - 1);
|
||||
endPos = qBound(startPos, endPos, origin->characterCount() - 1);
|
||||
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
|
||||
int endPos = data.range.end.toPositionInDocument(sourceDocument);
|
||||
|
||||
m_start = QTextCursor(origin);
|
||||
m_start.setPosition(startPos);
|
||||
m_start.setKeepPositionOnInsert(true);
|
||||
startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
|
||||
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
|
||||
|
||||
QTextCursor cursor(origin);
|
||||
QTextCursor cursor(sourceDocument);
|
||||
cursor.setPosition(startPos);
|
||||
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||
|
||||
@ -51,74 +51,57 @@ LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin
|
||||
int startPosInBlock = startPos - block.position();
|
||||
int endPosInBlock = endPos - block.position();
|
||||
|
||||
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, completion.text());
|
||||
|
||||
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;
|
||||
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
|
||||
replacementDocument()->setPlainText(blockText);
|
||||
}
|
||||
|
||||
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();
|
||||
QStringList lines = text.split('\n');
|
||||
|
||||
if (m_linesCount < lines.size())
|
||||
m_linesCount++;
|
||||
|
||||
showTooltip(widget, m_linesCount);
|
||||
|
||||
return m_linesCount == lines.size() && !Utils::ToolTip::isVisible();
|
||||
return applyPart(Line, widget);
|
||||
}
|
||||
|
||||
void LLMSuggestion::onCounterFinished(int count)
|
||||
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
Utils::ToolTip::hide();
|
||||
m_linesCount = 0;
|
||||
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
|
||||
cursor.beginEditBlock();
|
||||
cursor.removeSelectedText();
|
||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||
QTextCursor currentCursor = widget->textCursor();
|
||||
const QString text = suggestions()[currentSuggestion()].text;
|
||||
|
||||
QStringList lines = m_completion.text().split('\n');
|
||||
QString textToInsert = lines.mid(0, count).join('\n');
|
||||
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
||||
+ (cursor.selectionEnd() - cursor.selectionStart());
|
||||
|
||||
cursor.insertText(textToInsert);
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
||||
|
||||
void LLMSuggestion::reset()
|
||||
{
|
||||
m_start.removeSelectedText();
|
||||
}
|
||||
if (next == -1)
|
||||
return apply();
|
||||
|
||||
int LLMSuggestion::position()
|
||||
{
|
||||
return m_start.position();
|
||||
}
|
||||
if (part == Line)
|
||||
++next;
|
||||
|
||||
void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
|
||||
{
|
||||
Utils::ToolTip::hide();
|
||||
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
|
||||
pos += QPoint(-10, -50);
|
||||
m_counterTooltip = new CounterTooltip(count);
|
||||
Utils::ToolTip::show(pos, m_counterTooltip, widget);
|
||||
connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
|
||||
QString subText = text.mid(startPos, next - startPos);
|
||||
if (subText.isEmpty())
|
||||
return false;
|
||||
|
||||
currentCursor.insertText(subText);
|
||||
|
||||
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
|
||||
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
|
||||
if (!newCompletionText.isEmpty()) {
|
||||
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
|
||||
const Utils::Text::Position
|
||||
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
|
||||
const Utils::Text::Range newRange{newStart, newEnd};
|
||||
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
|
||||
widget->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@ -19,37 +24,21 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include "LSPCompletion.hpp"
|
||||
#include <texteditor/textdocumentlayout.h>
|
||||
|
||||
#include "utils/CounterTooltip.hpp"
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/textsuggestion.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion
|
||||
class LLMSuggestion : public TextEditor::CyclicSuggestion
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LLMSuggestion(const Completion &completion, QTextDocument *origin);
|
||||
enum Part { Word, Line };
|
||||
|
||||
bool apply() final;
|
||||
bool applyWord(TextEditor::TextEditorWidget *widget) final;
|
||||
bool applyNextLine(TextEditor::TextEditorWidget *widget);
|
||||
void reset() final;
|
||||
int position() final;
|
||||
LLMSuggestion(
|
||||
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
|
||||
|
||||
const Completion &completion() const { return m_completion; }
|
||||
|
||||
void showTooltip(TextEditor::TextEditorWidget *widget, int count);
|
||||
void onCounterFinished(int count);
|
||||
|
||||
private:
|
||||
Completion m_completion;
|
||||
QTextCursor m_start;
|
||||
int m_linesCount;
|
||||
|
||||
CounterTooltip *m_counterTooltip = nullptr;
|
||||
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.3.6",
|
||||
"CompatVersion" : "${IDE_VERSION_COMPAT}",
|
||||
"Version" : "0.4.5",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||
"License" : "GNU General Public License Usage
|
||||
|
||||
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)"],
|
||||
"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).",
|
||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||
"DocumentationUrl" : "",
|
||||
${IDE_PLUGIN_DEPENDENCIES}
|
||||
}
|
||||
|
||||
@ -31,9 +31,10 @@
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include "core/ChangesManager.h"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProjectSettings.hpp"
|
||||
#include <context/ChangesManager.h>
|
||||
|
||||
using namespace LanguageServerProtocol;
|
||||
using namespace TextEditor;
|
||||
@ -70,48 +71,63 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||
return;
|
||||
|
||||
Client::openDocument(document);
|
||||
connect(document,
|
||||
&TextDocument::contentsChangedWithPosition,
|
||||
this,
|
||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||
Q_UNUSED(charsRemoved)
|
||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||
return;
|
||||
connect(
|
||||
document,
|
||||
&TextDocument::contentsChangedWithPosition,
|
||||
this,
|
||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||
return;
|
||||
|
||||
auto project = ProjectManager::projectForFile(document->filePath());
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
auto project = ProjectManager::projectForFile(document->filePath());
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
|
||||
auto textEditor = BaseTextEditor::currentTextEditor();
|
||||
if (!textEditor || textEditor->document() != document)
|
||||
return;
|
||||
auto textEditor = BaseTextEditor::currentTextEditor();
|
||||
if (!textEditor || textEditor->document() != document)
|
||||
return;
|
||||
|
||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
||||
ChangesManager::instance().addChange(document,
|
||||
position,
|
||||
charsRemoved,
|
||||
charsAdded);
|
||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
||||
Context::ChangesManager::instance()
|
||||
.addChange(document, position, charsRemoved, charsAdded);
|
||||
|
||||
TextEditorWidget *widget = textEditor->editorWidget();
|
||||
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
||||
return;
|
||||
const int cursorPosition = widget->textCursor().position();
|
||||
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||
return;
|
||||
TextEditorWidget *widget = textEditor->editorWidget();
|
||||
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
||||
return;
|
||||
|
||||
m_recentCharCount += charsAdded;
|
||||
const int cursorPosition = widget->textCursor().position();
|
||||
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||
return;
|
||||
|
||||
if (m_typingTimer.elapsed()
|
||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
||||
m_recentCharCount = charsAdded;
|
||||
m_typingTimer.restart();
|
||||
}
|
||||
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_recentCharCount
|
||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
||||
scheduleRequest(widget);
|
||||
}
|
||||
});
|
||||
QTextCursor cursor = widget->textCursor();
|
||||
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||
QString lastChar = cursor.selectedText();
|
||||
|
||||
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
m_recentCharCount += charsAdded;
|
||||
|
||||
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)
|
||||
@ -193,8 +209,8 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
||||
auto isValidCompletion = [](const Completion &completion) {
|
||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||
};
|
||||
QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(),
|
||||
isValidCompletion);
|
||||
QList<Completion> completions
|
||||
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
||||
|
||||
// remove trailing whitespaces from the end of the completions
|
||||
for (Completion &completion : completions) {
|
||||
@ -211,10 +227,18 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
||||
if (delta > 0)
|
||||
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())
|
||||
return;
|
||||
editor->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
|
||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,7 +253,11 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
||||
|
||||
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()
|
||||
|
||||
125
README.md
125
README.md
@ -2,18 +2,25 @@
|
||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
|
||||
 QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
|
||||
|
||||
⚠️ **Important Notice About Paid Providers**
|
||||
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
||||
> - These services will consume API tokens which may result in charges to your account
|
||||
> - The QodeAssist developer bears no responsibility for any charges incurred
|
||||
> - Please carefully review the provider's pricing and your account settings before use
|
||||
|
||||
## Table of Contents
|
||||
1. [Overview](#overview)
|
||||
2. [Installation](#installation)
|
||||
2. [Installation for using Ollama](#installation-for-using-Ollama)
|
||||
3. [Installation for using Claude](#installation-for-using-Claude)
|
||||
3. [Configure Plugin](#configure-plugin)
|
||||
4. [Supported LLM Providers](#supported-llm-providers)
|
||||
5. [Recommended Models](#recommended-models)
|
||||
- [Ollama](#ollama)
|
||||
- [LM Studio](#lm-studio)
|
||||
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
7. [Development Progress](#development-progress)
|
||||
8. [Hotkeys](#hotkeys)
|
||||
@ -28,8 +35,9 @@ QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides
|
||||
- Side and Bottom panels
|
||||
- Support for multiple LLM providers:
|
||||
- Ollama
|
||||
- Claude
|
||||
- LM Studio
|
||||
- OpenAI-compatible local providers
|
||||
- OpenAI-compatible providers(eg. https://openrouter.ai)
|
||||
- Extensive library of model-specific templates
|
||||
- Custom template support
|
||||
- Easy configuration and model selection
|
||||
@ -39,6 +47,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">
|
||||
</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>
|
||||
<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">
|
||||
@ -49,19 +62,23 @@ 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">
|
||||
</details>
|
||||
|
||||
## Installation
|
||||
## Installation for using Ollama
|
||||
|
||||
1. Install Latest QtCreator
|
||||
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
||||
3. Install a language models in Ollama via terminal. For example, you can run:
|
||||
|
||||
For suggestions:
|
||||
For standard computers (minimum 8GB RAM):
|
||||
```
|
||||
ollama run codellama:7b-code
|
||||
ollama run qwen2.5-coder:7b
|
||||
```
|
||||
For chat:
|
||||
For better performance (16GB+ RAM):
|
||||
```
|
||||
ollama run codellama:7b-instruct
|
||||
ollama run qwen2.5-coder:14b
|
||||
```
|
||||
For high-end systems (32GB+ RAM):
|
||||
```
|
||||
ollama run qwen2.5-coder:32b
|
||||
```
|
||||
4. Download the QodeAssist plugin for your QtCreator.
|
||||
5. Launch Qt Creator and install the plugin:
|
||||
@ -70,40 +87,58 @@ ollama run codellama:7b-instruct
|
||||
- Click on "Install Plugin..."
|
||||
- Select the downloaded QodeAssist plugin archive file
|
||||
|
||||
## Installation for using Claude
|
||||
|
||||
1. Install Latest QtCreator
|
||||
2. Download the QodeAssist plugin for your QtCreator.
|
||||
3. Launch Qt Creator and install the plugin:
|
||||
- Go to MacOS: Qt Creator -> About Plugins...
|
||||
Windows\Linux: Help -> About Plugins...
|
||||
- Click on "Install Plugin..."
|
||||
- Select the downloaded QodeAssist plugin archive file
|
||||
4. Select Claude provider
|
||||
5. Select Claude api
|
||||
6. Fill in api key for Claude
|
||||
5. Select Claude templates for code completion and chat
|
||||
6. Enjoy!
|
||||
|
||||
## Configure Plugin
|
||||
|
||||
<details>
|
||||
<summary>Configure plugins: (click to expand)</summary>
|
||||
<img src="https://github.com/user-attachments/assets/00ad980f-b470-48eb-9aaa-077783d38798" width="600" alt="Configuere QodeAssist">
|
||||
</details>
|
||||
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
|
||||
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
|
||||
2. Navigate to the "Qode Assist" tab
|
||||
3. Select "General" page
|
||||
4. Choose your LLM provider (e.g., Ollama)
|
||||
5. Select the installed model by the "Select Model" button
|
||||
- For LM Studio you will see current loaded model
|
||||
6. Choose the prompt template that corresponds to your model
|
||||
7. Apply the settings
|
||||
3. On the "General" page, verify:
|
||||
- Ollama is selected as your LLM provider
|
||||
- The URL is set to http://localhost:11434
|
||||
- Your installed model appears in the model selection
|
||||
- The prompt template is Ollama Auto FIM
|
||||
4. Click Apply if you made any changes
|
||||
|
||||
You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
|
||||
[](https://ko-fi.com/P5P412V96G)
|
||||
|
||||
## Supported LLM Providers
|
||||
QodeAssist currently supports the following LLM (Large Language Model) providers:
|
||||
- [Ollama](https://ollama.com)
|
||||
- [LM Studio](https://lmstudio.ai)
|
||||
- [OpenRouter](https://openrouter.ai)
|
||||
- OpenAI compatible providers
|
||||
|
||||
## Recommended Models:
|
||||
QodeAssist has been thoroughly tested and optimized for use with the following language models:
|
||||
|
||||
- Llama
|
||||
- Qwen2.5-coder
|
||||
- CodeLlama
|
||||
- StarCoder2
|
||||
- DeepSeek-Coder-V2
|
||||
- Qwen-2.5
|
||||
|
||||
### Model Types
|
||||
|
||||
FIM models (codellama:7b-code, starcoder2:7b, etc.) - Optimized for code completion and suggestions
|
||||
|
||||
Instruct models (codellama:7b-instruct, starcoder2:instruct, etc.) - Better for chat assistance, explanations, and code review
|
||||
|
||||
For best results, use FIM models with code completion and Instruct models with chat features.
|
||||
|
||||
### Ollama:
|
||||
### For autocomplete(FIM)
|
||||
@ -113,7 +148,7 @@ ollama run starcoder2:7b
|
||||
ollama run qwen2.5-coder:7b-base
|
||||
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
|
||||
```
|
||||
### For chat
|
||||
### For chat and instruct
|
||||
```
|
||||
ollama run codellama:7b-instruct
|
||||
ollama run starcoder2:instruct
|
||||
@ -121,15 +156,31 @@ ollama run qwen2.5-coder:7b-instruct
|
||||
ollama run deepseek-coder-v2
|
||||
```
|
||||
|
||||
### LM Studio:
|
||||
similar models, like for ollama
|
||||
### Template-Model Compatibility
|
||||
|
||||
Please note that while these models have been specifically tested and confirmed to work well with QodeAssist, other models compatible with the supported providers may also work. We encourage users to experiment with different models and report their experiences.
|
||||
| Template | Compatible Models | Purpose |
|
||||
|----------|------------------|----------|
|
||||
| CodeLlama FIM | `codellama:code` | Code completion |
|
||||
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
|
||||
| Ollama Auto FIM | `Any Ollama base model` | Code completion |
|
||||
| Qwen FIM | `Qwen 2.5 models` | Code completion |
|
||||
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
|
||||
| Alpaca | `starcoder2:instruct` | Chat assistance |
|
||||
| Basic Chat| `Messages without tokens` | Chat assistance |
|
||||
| ChatML | `Qwen 2.5 models` | Chat assistance |
|
||||
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
|
||||
| Llama3 | `llama3 model family` | Chat assistance |
|
||||
| Ollama Auto Chat | `Any Ollama chat model` | Chat assistance |
|
||||
|
||||
If you've successfully used a model that's not listed here, please let us know by opening an issue or submitting a pull request to update this list.
|
||||
> 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 15.0.0 - 0.4.x
|
||||
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
|
||||
- QtCreator 14.0.1 - 0.2.2 plugin version and below
|
||||
|
||||
@ -148,9 +199,7 @@ If you've successfully used a model that's not listed here, please let us know b
|
||||
- on Mac: Option + Command + Q
|
||||
- on Windows: Ctrl + Alt + Q
|
||||
- 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:
|
||||
- On Mac: Option + Right Arrow
|
||||
- On Windows: Alt + Right Arrow
|
||||
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@ -190,7 +239,6 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||
|
||||
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
||||
- [](https://ko-fi.com/P5P412V96G)
|
||||
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
||||
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
||||
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
||||
@ -209,3 +257,12 @@ where `<path_to_qtcreator>` is the relative or absolute path to a Qt Creator bui
|
||||
combined binary and development package (Windows / Linux), or to the `Qt Creator.app/Contents/Resources/`
|
||||
directory of a combined binary and development package (macOS), and `<path_to_plugin_source>` is the
|
||||
relative or absolute path to this plugin directory.
|
||||
|
||||
## For Contributors
|
||||
|
||||
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
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
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
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "chatview/ChatWidget.hpp"
|
||||
#include "ChatView/ChatWidget.hpp"
|
||||
#include <coreplugin/ioutputpane.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
#include "NavigationPanel.hpp"
|
||||
|
||||
#include "chatview/ChatWidget.hpp"
|
||||
#include "ChatView/ChatWidget.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
|
||||
@ -1,144 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatRootView.hpp"
|
||||
#include <QtGui/qclipboard.h>
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(&settings.caModel,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::currentTemplateChanged);
|
||||
|
||||
connect(&Settings::chatAssistantSettings().sharingCurrentFile,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::isSharingCurrentFileChanged);
|
||||
|
||||
generateColors();
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
{
|
||||
return m_chatModel;
|
||||
}
|
||||
|
||||
QColor ChatRootView::backgroundColor() const
|
||||
{
|
||||
return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||
}
|
||||
|
||||
void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const
|
||||
{
|
||||
m_clientInterface->sendMessage(message, sharingCurrentFile);
|
||||
}
|
||||
|
||||
void ChatRootView::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
void ChatRootView::cancelRequest()
|
||||
{
|
||||
m_clientInterface->cancelRequest();
|
||||
}
|
||||
|
||||
void ChatRootView::generateColors()
|
||||
{
|
||||
QColor baseColor = backgroundColor();
|
||||
bool isDarkTheme = baseColor.lightness() < 128;
|
||||
|
||||
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,
|
||||
float hueShift,
|
||||
float saturationMod,
|
||||
float lightnessMod)
|
||||
{
|
||||
float h, s, l, a;
|
||||
baseColor.getHslF(&h, &s, &l, &a);
|
||||
bool isDarkTheme = l < 0.5;
|
||||
|
||||
h = fmod(h + hueShift + 1.0, 1.0);
|
||||
|
||||
s = qBound(0.0f, s * saturationMod, 1.0f);
|
||||
|
||||
if (isDarkTheme) {
|
||||
l = qBound(0.0f, l * lightnessMod, 1.0f);
|
||||
} else {
|
||||
l = qBound(0.0f, l / lightnessMod, 1.0f);
|
||||
}
|
||||
|
||||
h = qBound(0.0f, h, 1.0f);
|
||||
s = qBound(0.0f, s, 1.0f);
|
||||
l = qBound(0.0f, l, 1.0f);
|
||||
a = qBound(0.0f, a, 1.0f);
|
||||
|
||||
return QColor::fromHslF(h, s, l, a);
|
||||
}
|
||||
|
||||
QString ChatRootView::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.caModel();
|
||||
}
|
||||
|
||||
QColor ChatRootView::primaryColor() const
|
||||
{
|
||||
return m_primaryColor;
|
||||
}
|
||||
|
||||
QColor ChatRootView::secondaryColor() const
|
||||
{
|
||||
return m_secondaryColor;
|
||||
}
|
||||
|
||||
QColor ChatRootView::codeColor() const
|
||||
{
|
||||
return m_codeColor;
|
||||
}
|
||||
|
||||
bool ChatRootView::isSharingCurrentFile() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().sharingCurrentFile();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
import "./dialog"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property color fontColor
|
||||
property color codeBgColor
|
||||
property color selectionColor
|
||||
|
||||
height: msgColumn.height
|
||||
radius: 8
|
||||
|
||||
Column {
|
||||
id: msgColumn
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: msgCreator
|
||||
delegate: Loader {
|
||||
property var itemData: modelData
|
||||
|
||||
width: parent.width
|
||||
sourceComponent: {
|
||||
switch(modelData.type) {
|
||||
case MessagePart.Text: return textComponent;
|
||||
case MessagePart.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
|
||||
TextBlock {
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: itemData.text
|
||||
color: fontColor
|
||||
selectionColor: root.selectionColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
|
||||
CodeBlock {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
color: root.codeBgColor
|
||||
selectionColor: root.selectionColor
|
||||
}
|
||||
}
|
||||
}
|
||||
20
context/CMakeLists.txt
Normal file
20
context/CMakeLists.txt
Normal file
@ -0,0 +1,20 @@
|
||||
add_library(Context STATIC
|
||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||
ChangesManager.h ChangesManager.cpp
|
||||
ContextManager.hpp ContextManager.cpp
|
||||
ContentFile.hpp
|
||||
)
|
||||
|
||||
target_link_libraries(Context
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
QtCreator::Core
|
||||
QtCreator::TextEditor
|
||||
QtCreator::Utils
|
||||
QtCreator::ProjectExplorer
|
||||
PRIVATE
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
)
|
||||
|
||||
target_include_directories(Context PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR})
|
||||
@ -18,9 +18,9 @@
|
||||
*/
|
||||
|
||||
#include "ChangesManager.h"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "CodeCompletionSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
ChangesManager &ChangesManager::instance()
|
||||
{
|
||||
@ -79,4 +79,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
|
||||
return context;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::Context
|
||||
@ -25,7 +25,7 @@
|
||||
#include <QTimer>
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
class ChangesManager : public QObject
|
||||
{
|
||||
@ -58,4 +58,4 @@ private:
|
||||
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::Context
|
||||
@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
@ -19,30 +19,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
#include <QToolBar>
|
||||
#include <QWidget>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
class CounterTooltip : public QToolBar
|
||||
struct ContentFile
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CounterTooltip(int count);
|
||||
~CounterTooltip();
|
||||
|
||||
signals:
|
||||
void finished(int count);
|
||||
|
||||
private:
|
||||
void updateLabel();
|
||||
|
||||
QLabel *m_label;
|
||||
QTimer *m_timer;
|
||||
int m_count;
|
||||
QString filename;
|
||||
QString content;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::Context
|
||||
67
context/ContextManager.cpp
Normal file
67
context/ContextManager.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 "ContextManager.hpp"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTextStream>
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
ContextManager &ContextManager::instance()
|
||||
{
|
||||
static ContextManager manager;
|
||||
return manager;
|
||||
}
|
||||
|
||||
ContextManager::ContextManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
QString ContextManager::readFile(const QString &filePath) const
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return QString();
|
||||
|
||||
QTextStream in(&file);
|
||||
return in.readAll();
|
||||
}
|
||||
|
||||
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
|
||||
{
|
||||
QList<ContentFile> files;
|
||||
for (const QString &path : filePaths) {
|
||||
ContentFile contentFile = createContentFile(path);
|
||||
files.append(contentFile);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
||||
{
|
||||
ContentFile contentFile;
|
||||
QFileInfo fileInfo(filePath);
|
||||
contentFile.filename = fileInfo.fileName();
|
||||
contentFile.content = readFile(filePath);
|
||||
return contentFile;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
46
context/ContextManager.hpp
Normal file
46
context/ContextManager.hpp
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "ContentFile.hpp"
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
class ContextManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ContextManager &instance();
|
||||
QString readFile(const QString &filePath) const;
|
||||
QList<ContentFile> getContentFiles(const QStringList &filePaths) const;
|
||||
|
||||
private:
|
||||
explicit ContextManager(QObject *parent = nullptr);
|
||||
~ContextManager() = default;
|
||||
ContextManager(const ContextManager &) = delete;
|
||||
ContextManager &operator=(const ContextManager &) = delete;
|
||||
ContentFile createContentFile(const QString &filePath) const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
@ -23,8 +23,9 @@
|
||||
#include <QTextBlock>
|
||||
#include <languageserverprotocol/lsptypes.h>
|
||||
|
||||
#include "core/ChangesManager.h"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "CodeCompletionSettings.hpp"
|
||||
|
||||
#include "ChangesManager.h"
|
||||
|
||||
const QRegularExpression &getYearRegex()
|
||||
{
|
||||
@ -46,7 +47,7 @@ const QRegularExpression &getCommentRegex()
|
||||
return commentRegex;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
|
||||
: m_textDocument(textDocument)
|
||||
@ -207,9 +208,16 @@ LLMCore::ContextData DocumentContextReader::prepareContext(int lineNumber, int c
|
||||
{
|
||||
QString contextBefore = getContextBefore(lineNumber, cursorPosition);
|
||||
QString contextAfter = getContextAfter(lineNumber, cursorPosition);
|
||||
QString instructions = getInstructions();
|
||||
|
||||
return {contextBefore, contextAfter, instructions};
|
||||
QString fileContext;
|
||||
if (Settings::codeCompletionSettings().useFilePathInContext())
|
||||
fileContext.append("\n ").append(getLanguageAndFileInfo());
|
||||
|
||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
||||
fileContext.append("\n ").append(
|
||||
ChangesManager::instance().getRecentChangesContext(m_textDocument));
|
||||
|
||||
return {contextBefore, contextAfter, fileContext};
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
|
||||
@ -239,17 +247,4 @@ QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPositio
|
||||
}
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getInstructions() const
|
||||
{
|
||||
QString instructions;
|
||||
|
||||
if (Settings::codeCompletionSettings().useFilePathInContext())
|
||||
instructions += getLanguageAndFileInfo();
|
||||
|
||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
||||
instructions += ChangesManager::instance().getRecentChangesContext(m_textDocument);
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::Context
|
||||
@ -19,12 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QTextDocument>
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <QTextDocument>
|
||||
|
||||
#include <llmcore/ContextData.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
struct CopyrightInfo
|
||||
{
|
||||
@ -54,7 +54,6 @@ public:
|
||||
private:
|
||||
QString getContextBefore(int lineNumber, int cursorPosition) const;
|
||||
QString getContextAfter(int lineNumber, int cursorPosition) const;
|
||||
QString getInstructions() const;
|
||||
|
||||
private:
|
||||
TextEditor::TextDocument *m_textDocument;
|
||||
@ -62,4 +61,4 @@ private:
|
||||
CopyrightInfo m_copyrightInfo;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::Context
|
||||
@ -7,6 +7,10 @@ add_library(LLMCore STATIC
|
||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||
RequestConfig.hpp
|
||||
RequestHandler.hpp RequestHandler.cpp
|
||||
OllamaMessage.hpp OllamaMessage.cpp
|
||||
OpenAIMessage.hpp OpenAIMessage.cpp
|
||||
ValidationUtils.hpp ValidationUtils.cpp
|
||||
MessageBuilder.hpp MessageBuilder.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(LLMCore
|
||||
|
||||
@ -27,7 +27,7 @@ struct ContextData
|
||||
{
|
||||
QString prefix;
|
||||
QString suffix;
|
||||
QString systemPrompt;
|
||||
QString fileContext;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::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
|
||||
@ -38,5 +38,6 @@ public:
|
||||
virtual QString promptTemplate() const = 0;
|
||||
virtual QStringList stopWords() const = 0;
|
||||
virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0;
|
||||
virtual QString description() const = 0;
|
||||
};
|
||||
} // namespace QodeAssist::LLMCore
|
||||
|
||||
@ -40,7 +40,6 @@ QStringList PromptTemplateManager::chatTemplatesNames() const
|
||||
PromptTemplateManager::~PromptTemplateManager()
|
||||
{
|
||||
qDeleteAll(m_fimTemplates);
|
||||
qDeleteAll(m_chatTemplates);
|
||||
}
|
||||
|
||||
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
|
||||
|
||||
@ -39,9 +39,8 @@ public:
|
||||
"T must inherit from PromptTemplate");
|
||||
T *template_ptr = new T();
|
||||
QString name = template_ptr->name();
|
||||
if (template_ptr->type() == TemplateType::Fim) {
|
||||
m_fimTemplates[name] = template_ptr;
|
||||
} else if (template_ptr->type() == TemplateType::Chat) {
|
||||
m_fimTemplates[name] = template_ptr;
|
||||
if (template_ptr->type() == TemplateType::Chat) {
|
||||
m_chatTemplates[name] = template_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,9 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include "RequestType.hpp"
|
||||
#include <utils/environment.h>
|
||||
#include <QNetworkRequest>
|
||||
#include <QString>
|
||||
|
||||
#include "PromptTemplate.hpp"
|
||||
#include "RequestType.hpp"
|
||||
|
||||
class QNetworkReply;
|
||||
class QJsonObject;
|
||||
@ -37,10 +40,14 @@ public:
|
||||
virtual QString url() const = 0;
|
||||
virtual QString completionEndpoint() const = 0;
|
||||
virtual QString chatEndpoint() const = 0;
|
||||
virtual bool supportsModelListing() const = 0;
|
||||
|
||||
virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
|
||||
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 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
|
||||
|
||||
@ -35,6 +35,7 @@ struct LLMConfig
|
||||
QJsonObject providerRequest;
|
||||
RequestType requestType;
|
||||
bool multiLineCompletion;
|
||||
QString apiKey;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
|
||||
@ -38,7 +38,7 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
|
||||
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QNetworkRequest networkRequest(config.url);
|
||||
prepareNetworkRequest(networkRequest, config.providerRequest);
|
||||
config.provider->prepareNetworkRequest(networkRequest);
|
||||
|
||||
QNetworkReply *reply = m_manager->post(networkRequest,
|
||||
QJsonDocument(config.providerRequest).toJson());
|
||||
@ -75,7 +75,7 @@ void RequestHandler::handleLLMResponse(QNetworkReply *reply,
|
||||
|
||||
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
|
||||
|
||||
if (config.requestType == RequestType::Fim) {
|
||||
if (config.requestType == RequestType::CodeCompletion) {
|
||||
if (!config.multiLineCompletion
|
||||
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
|
||||
return;
|
||||
@ -84,6 +84,7 @@ void RequestHandler::handleLLMResponse(QNetworkReply *reply,
|
||||
if (isComplete) {
|
||||
auto cleanedCompletion = removeStopWords(accumulatedResponse,
|
||||
config.promptTemplate->stopWords());
|
||||
|
||||
emit completionReceived(cleanedCompletion, request, true);
|
||||
}
|
||||
} else if (config.requestType == RequestType::Chat) {
|
||||
@ -107,33 +108,22 @@ bool RequestHandler::cancelRequest(const QString &id)
|
||||
return false;
|
||||
}
|
||||
|
||||
void RequestHandler::prepareNetworkRequest(QNetworkRequest &networkRequest,
|
||||
const QJsonObject &providerRequest)
|
||||
bool RequestHandler::processSingleLineCompletion(
|
||||
QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedResponse,
|
||||
const LLMConfig &config)
|
||||
{
|
||||
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
if (providerRequest.contains("api_key")) {
|
||||
QString apiKey = providerRequest["api_key"].toString();
|
||||
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
bool RequestHandler::processSingleLineCompletion(QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedResponse,
|
||||
const LLMConfig &config)
|
||||
{
|
||||
int newlinePos = accumulatedResponse.indexOf('\n');
|
||||
QString cleanedResponse = accumulatedResponse;
|
||||
|
||||
int newlinePos = cleanedResponse.indexOf('\n');
|
||||
if (newlinePos != -1) {
|
||||
QString singleLineCompletion = accumulatedResponse.left(newlinePos).trimmed();
|
||||
singleLineCompletion = removeStopWords(singleLineCompletion,
|
||||
config.promptTemplate->stopWords());
|
||||
|
||||
QString singleLineCompletion = cleanedResponse.left(newlinePos).trimmed();
|
||||
singleLineCompletion
|
||||
= removeStopWords(singleLineCompletion, config.promptTemplate->stopWords());
|
||||
emit completionReceived(singleLineCompletion, request, true);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
reply->abort();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -150,4 +140,36 @@ QString RequestHandler::removeStopWords(const QStringView &completion, const QSt
|
||||
return filteredCompletion;
|
||||
}
|
||||
|
||||
void RequestHandler::removeCodeBlockWrappers(QString &response)
|
||||
{
|
||||
static const QRegularExpression
|
||||
fullCodeBlockRegex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||
static const QRegularExpression
|
||||
partialStartBlockRegex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
|
||||
static const QRegularExpression
|
||||
partialEndBlockRegex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||
|
||||
QRegularExpressionMatchIterator matchIterator = fullCodeBlockRegex.globalMatch(response);
|
||||
while (matchIterator.hasNext()) {
|
||||
QRegularExpressionMatch match = matchIterator.next();
|
||||
QString codeBlock = match.captured(0);
|
||||
QString codeContent = match.captured(1).trimmed();
|
||||
response.replace(codeBlock, codeContent);
|
||||
}
|
||||
|
||||
QRegularExpressionMatch startMatch = partialStartBlockRegex.match(response);
|
||||
if (startMatch.hasMatch()) {
|
||||
QString partialBlock = startMatch.captured(0);
|
||||
QString codeContent = startMatch.captured(1).trimmed();
|
||||
response.replace(partialBlock, codeContent);
|
||||
}
|
||||
|
||||
QRegularExpressionMatch endMatch = partialEndBlockRegex.match(response);
|
||||
if (endMatch.hasMatch()) {
|
||||
QString partialBlock = endMatch.captured(0);
|
||||
QString codeContent = endMatch.captured(1).trimmed();
|
||||
response.replace(partialBlock, codeContent);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
|
||||
@ -52,12 +52,12 @@ private:
|
||||
QMap<QString, QNetworkReply *> m_activeRequests;
|
||||
QMap<QNetworkReply *, QString> m_accumulatedResponses;
|
||||
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest, const QJsonObject &providerRequest);
|
||||
bool processSingleLineCompletion(QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedResponse,
|
||||
const LLMConfig &config);
|
||||
QString removeStopWords(const QStringView &completion, const QStringList &stopWords);
|
||||
void removeCodeBlockWrappers(QString &response);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
|
||||
@ -21,5 +21,5 @@
|
||||
|
||||
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 <QNetworkReply>
|
||||
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
@ -53,6 +55,11 @@ QString LMStudioProvider::chatEndpoint() const
|
||||
return "/v1/chat/completions";
|
||||
}
|
||||
|
||||
bool LMStudioProvider::supportsModelListing() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
|
||||
{
|
||||
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
|
||||
@ -87,7 +94,7 @@ void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType
|
||||
request["messages"] = std::move(messages);
|
||||
}
|
||||
|
||||
if (type == LLMCore::RequestType::Fim) {
|
||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
@ -96,43 +103,53 @@ void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType
|
||||
|
||||
bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
bool isComplete = false;
|
||||
while (reply->canReadLine()) {
|
||||
QByteArray line = reply->readLine().trimmed();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (line == "data: [DONE]") {
|
||||
isComplete = true;
|
||||
break;
|
||||
}
|
||||
if (line.startsWith("data: ")) {
|
||||
line = line.mid(6); // Remove "data: " prefix
|
||||
}
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
|
||||
if (jsonResponse.isNull()) {
|
||||
qWarning() << "Invalid JSON response from LM Studio:" << line;
|
||||
continue;
|
||||
}
|
||||
QJsonObject responseObj = jsonResponse.object();
|
||||
if (responseObj.contains("choices")) {
|
||||
QJsonArray choices = responseObj["choices"].toArray();
|
||||
if (!choices.isEmpty()) {
|
||||
QJsonObject choice = choices.first().toObject();
|
||||
QJsonObject delta = choice["delta"].toObject();
|
||||
if (delta.contains("content")) {
|
||||
QString completion = delta["content"].toString();
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
accumulatedResponse += completion;
|
||||
}
|
||||
if (choice["finish_reason"].toString() == "stop") {
|
||||
isComplete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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 isComplete;
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
||||
@ -166,4 +183,32 @@ QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
||||
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
|
||||
|
||||
@ -32,9 +32,13 @@ public:
|
||||
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 <QNetworkReply>
|
||||
#include <QtCore/qeventloop.h>
|
||||
|
||||
#include "llmcore/OllamaMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
@ -53,12 +55,18 @@ QString OllamaProvider::chatEndpoint() const
|
||||
return "/api/chat";
|
||||
}
|
||||
|
||||
bool OllamaProvider::supportsModelListing() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
|
||||
{
|
||||
auto applySettings = [&request](const auto &settings) {
|
||||
QJsonObject options;
|
||||
options["num_predict"] = settings.maxTokens();
|
||||
options["temperature"] = settings.temperature();
|
||||
options["stop"] = request.take("stop");
|
||||
|
||||
if (settings.useTopP())
|
||||
options["top_p"] = settings.topP();
|
||||
@ -73,7 +81,7 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
|
||||
request["keep_alive"] = settings.ollamaLivetime();
|
||||
};
|
||||
|
||||
if (type == LLMCore::RequestType::Fim) {
|
||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||
applySettings(Settings::codeCompletionSettings());
|
||||
} else {
|
||||
applySettings(Settings::chatAssistantSettings());
|
||||
@ -82,53 +90,41 @@ void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType t
|
||||
|
||||
bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
QString endpoint = reply->url().path();
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isComplete = false;
|
||||
while (reply->canReadLine()) {
|
||||
QByteArray line = reply->readLine().trimmed();
|
||||
if (line.isEmpty()) {
|
||||
QByteArrayList lines = data.split('\n');
|
||||
bool isDone = false;
|
||||
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(line);
|
||||
if (doc.isNull()) {
|
||||
LOG_MESSAGE("Invalid JSON response from Ollama: " + QString::fromUtf8(line));
|
||||
const QString endpoint = reply->url().path();
|
||||
auto messageType = endpoint == completionEndpoint()
|
||||
? 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;
|
||||
}
|
||||
|
||||
QJsonObject responseObj = doc.object();
|
||||
|
||||
if (responseObj.contains("error")) {
|
||||
QString errorMessage = responseObj["error"].toString();
|
||||
LOG_MESSAGE("Error in Ollama response: " + errorMessage);
|
||||
return false;
|
||||
QString content = message.getContent();
|
||||
if (!content.isEmpty()) {
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
|
||||
if (endpoint == completionEndpoint()) {
|
||||
if (responseObj.contains("response")) {
|
||||
QString completion = responseObj["response"].toString();
|
||||
accumulatedResponse += completion;
|
||||
}
|
||||
} else if (endpoint == chatEndpoint()) {
|
||||
if (responseObj.contains("message")) {
|
||||
QJsonObject message = responseObj["message"].toObject();
|
||||
if (message.contains("content")) {
|
||||
QString content = message["content"].toString();
|
||||
accumulatedResponse += content;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE("Unknown endpoint: " + endpoint);
|
||||
}
|
||||
|
||||
if (responseObj.contains("done") && responseObj["done"].toBool()) {
|
||||
isComplete = true;
|
||||
break;
|
||||
if (message.done) {
|
||||
isDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
return isComplete;
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> OllamaProvider::getInstalledModels(const QString &url)
|
||||
@ -161,4 +157,52 @@ QList<QString> OllamaProvider::getInstalledModels(const QString &url)
|
||||
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
|
||||
|
||||
@ -32,9 +32,13 @@ public:
|
||||
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
|
||||
|
||||
@ -18,21 +18,27 @@
|
||||
*/
|
||||
|
||||
#include "OpenAICompatProvider.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 "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
OpenAICompatProvider::OpenAICompatProvider() {}
|
||||
|
||||
QString OpenAICompatProvider::name() const
|
||||
{
|
||||
return "OpenAI Compatible (experimental)";
|
||||
return "OpenAI Compatible";
|
||||
}
|
||||
|
||||
QString OpenAICompatProvider::url() const
|
||||
@ -50,6 +56,11 @@ QString OpenAICompatProvider::chatEndpoint() const
|
||||
return "/v1/chat/completions";
|
||||
}
|
||||
|
||||
bool OpenAICompatProvider::supportsModelListing() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
|
||||
{
|
||||
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
|
||||
@ -77,10 +88,6 @@ void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::Request
|
||||
request["frequency_penalty"] = settings.frequencyPenalty();
|
||||
if (settings.usePresencePenalty())
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
const QString &apiKey = settings.apiKey();
|
||||
if (!apiKey.isEmpty()) {
|
||||
request["api_key"] = apiKey;
|
||||
}
|
||||
};
|
||||
|
||||
QJsonArray messages = prepareMessages(request);
|
||||
@ -88,7 +95,7 @@ void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::Request
|
||||
request["messages"] = std::move(messages);
|
||||
}
|
||||
|
||||
if (type == LLMCore::RequestType::Fim) {
|
||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
@ -97,43 +104,53 @@ void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::Request
|
||||
|
||||
bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
|
||||
{
|
||||
bool isComplete = false;
|
||||
while (reply->canReadLine()) {
|
||||
QByteArray line = reply->readLine().trimmed();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (line == "data: [DONE]") {
|
||||
isComplete = true;
|
||||
break;
|
||||
}
|
||||
if (line.startsWith("data: ")) {
|
||||
line = line.mid(6); // Remove "data: " prefix
|
||||
}
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(line);
|
||||
if (jsonResponse.isNull()) {
|
||||
qWarning() << "Invalid JSON response from LM Studio:" << line;
|
||||
continue;
|
||||
}
|
||||
QJsonObject responseObj = jsonResponse.object();
|
||||
if (responseObj.contains("choices")) {
|
||||
QJsonArray choices = responseObj["choices"].toArray();
|
||||
if (!choices.isEmpty()) {
|
||||
QJsonObject choice = choices.first().toObject();
|
||||
QJsonObject delta = choice["delta"].toObject();
|
||||
if (delta.contains("content")) {
|
||||
QString completion = delta["content"].toString();
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
accumulatedResponse += completion;
|
||||
}
|
||||
if (choice["finish_reason"].toString() == "stop") {
|
||||
isComplete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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 isComplete;
|
||||
|
||||
return isDone;
|
||||
}
|
||||
|
||||
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
|
||||
@ -141,4 +158,36 @@ QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
|
||||
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
|
||||
|
||||
@ -32,9 +32,13 @@ public:
|
||||
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
|
||||
|
||||
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
|
||||
43
providers/Providers.hpp
Normal file
43
providers/Providers.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 "llmcore/ProvidersManager.hpp"
|
||||
#include "providers/ClaudeProvider.hpp"
|
||||
#include "providers/LMStudioProvider.hpp"
|
||||
#include "providers/OllamaProvider.hpp"
|
||||
#include "providers/OpenAICompatProvider.hpp"
|
||||
#include "providers/OpenAIProvider.hpp"
|
||||
#include "providers/OpenRouterAIProvider.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
inline void registerProviders()
|
||||
{
|
||||
auto &providerManager = LLMCore::ProvidersManager::instance();
|
||||
providerManager.registerProvider<OllamaProvider>();
|
||||
providerManager.registerProvider<LMStudioProvider>();
|
||||
providerManager.registerProvider<OpenAICompatProvider>();
|
||||
providerManager.registerProvider<OpenRouterProvider>();
|
||||
providerManager.registerProvider<ClaudeProvider>();
|
||||
providerManager.registerProvider<OpenAIProvider>();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
@ -19,6 +19,8 @@
|
||||
|
||||
#include "QodeAssistConstants.hpp"
|
||||
#include "QodeAssisttr.h"
|
||||
#include "settings/PluginUpdater.hpp"
|
||||
#include "settings/UpdateDialog.hpp"
|
||||
|
||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
@ -32,31 +34,23 @@
|
||||
#include <extensionsystem/iplugin.h>
|
||||
#include <languageclient/languageclientmanager.h>
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/icon.h>
|
||||
#include <QAction>
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/icon.h>
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
#include "QodeAssistClient.hpp"
|
||||
#include "chat/ChatOutputPane.h"
|
||||
#include "chat/NavigationPanel.hpp"
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "providers/LMStudioProvider.hpp"
|
||||
#include "providers/OllamaProvider.hpp"
|
||||
#include "providers/OpenAICompatProvider.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProjectSettingsPanel.hpp"
|
||||
|
||||
#include "templates/CodeLlamaChat.hpp"
|
||||
#include "templates/CodeLlamaFim.hpp"
|
||||
#include "templates/CustomFimTemplate.hpp"
|
||||
#include "templates/DeepSeekCoderChat.hpp"
|
||||
#include "templates/DeepSeekCoderFim.hpp"
|
||||
#include "templates/Qwen.hpp"
|
||||
#include "templates/StarCoder2Fim.hpp"
|
||||
#include "templates/StarCoderChat.hpp"
|
||||
#include "UpdateStatusWidget.hpp"
|
||||
#include "providers/Providers.hpp"
|
||||
#include "templates/Templates.hpp"
|
||||
|
||||
using namespace Utils;
|
||||
using namespace Core;
|
||||
@ -71,8 +65,8 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin
|
||||
|
||||
public:
|
||||
QodeAssistPlugin()
|
||||
{
|
||||
}
|
||||
: m_updater(new PluginUpdater(this))
|
||||
{}
|
||||
|
||||
~QodeAssistPlugin() final
|
||||
{
|
||||
@ -83,22 +77,8 @@ public:
|
||||
|
||||
void initialize() final
|
||||
{
|
||||
auto &providerManager = LLMCore::ProvidersManager::instance();
|
||||
providerManager.registerProvider<Providers::OllamaProvider>();
|
||||
providerManager.registerProvider<Providers::LMStudioProvider>();
|
||||
providerManager.registerProvider<Providers::OpenAICompatProvider>();
|
||||
|
||||
auto &templateManager = LLMCore::PromptTemplateManager::instance();
|
||||
templateManager.registerTemplate<Templates::CodeLlamaFim>();
|
||||
templateManager.registerTemplate<Templates::StarCoder2Fim>();
|
||||
templateManager.registerTemplate<Templates::DeepSeekCoderFim>();
|
||||
templateManager.registerTemplate<Templates::CustomTemplate>();
|
||||
templateManager.registerTemplate<Templates::DeepSeekCoderChat>();
|
||||
templateManager.registerTemplate<Templates::CodeLlamaChat>();
|
||||
templateManager.registerTemplate<Templates::LlamaChat>();
|
||||
templateManager.registerTemplate<Templates::StarCoderChat>();
|
||||
templateManager.registerTemplate<Templates::QwenChat>();
|
||||
templateManager.registerTemplate<Templates::QwenFim>();
|
||||
Providers::registerProviders();
|
||||
Templates::registerTemplates();
|
||||
|
||||
Utils::Icon QCODEASSIST_ICON(
|
||||
{{":/resources/images/qoderassist-icon.png", Utils::Theme::IconsBaseColor}});
|
||||
@ -120,31 +100,36 @@ public:
|
||||
}
|
||||
});
|
||||
|
||||
auto toggleButton = new QToolButton;
|
||||
toggleButton->setDefaultAction(requestAction.contextAction());
|
||||
StatusBarManager::addStatusBarWidget(toggleButton, StatusBarManager::RightCorner);
|
||||
m_statusWidget = new UpdateStatusWidget;
|
||||
m_statusWidget->setDefaultAction(requestAction.contextAction());
|
||||
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_navigationPanel = new Chat::NavigationPanel();
|
||||
|
||||
Settings::setupProjectPanel();
|
||||
ConfigurationManager::instance().init();
|
||||
|
||||
if (Settings::generalSettings().enableCheckUpdate()) {
|
||||
QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
void extensionsInitialized() final
|
||||
{
|
||||
}
|
||||
void extensionsInitialized() final {}
|
||||
|
||||
void restartClient()
|
||||
{
|
||||
LanguageClient::LanguageClientManager::shutdownClient(m_qodeAssistClient);
|
||||
|
||||
m_qodeAssistClient = new QodeAssistClient();
|
||||
}
|
||||
|
||||
bool delayedInitialize() final
|
||||
{
|
||||
restartClient();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -152,17 +137,36 @@ public:
|
||||
{
|
||||
if (!m_qodeAssistClient)
|
||||
return SynchronousShutdown;
|
||||
connect(m_qodeAssistClient,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&IPlugin::asynchronousShutdownFinished);
|
||||
connect(m_qodeAssistClient, &QObject::destroyed, this, &IPlugin::asynchronousShutdownFinished);
|
||||
return AsynchronousShutdown;
|
||||
}
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
if (m_statusWidget)
|
||||
m_statusWidget->showUpdateAvailable(info.version);
|
||||
}
|
||||
|
||||
QPointer<QodeAssistClient> m_qodeAssistClient;
|
||||
QPointer<Chat::ChatOutputPane> m_chatOutputPane;
|
||||
QPointer<Chat::NavigationPanel> m_navigationPanel;
|
||||
QPointer<PluginUpdater> m_updater;
|
||||
UpdateStatusWidget *m_statusWidget{nullptr};
|
||||
};
|
||||
|
||||
} // 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
|
||||
}
|
||||
@ -32,7 +32,7 @@ public:
|
||||
: Utils::BaseAspect(container)
|
||||
{}
|
||||
|
||||
void addToLayout(Layouting::Layout &parent) override
|
||||
void addToLayoutImpl(Layouting::Layout &parent) override
|
||||
{
|
||||
auto button = new QPushButton(m_buttonText);
|
||||
connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked);
|
||||
|
||||
@ -7,6 +7,12 @@ add_library(QodeAssistSettings STATIC
|
||||
SettingsTr.hpp
|
||||
CodeCompletionSettings.hpp CodeCompletionSettings.cpp
|
||||
ChatAssistantSettings.hpp ChatAssistantSettings.cpp
|
||||
SettingsDialog.hpp SettingsDialog.cpp
|
||||
ProjectSettings.hpp ProjectSettings.cpp
|
||||
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
|
||||
ProviderSettings.hpp ProviderSettings.cpp
|
||||
PluginUpdater.hpp PluginUpdater.cpp
|
||||
UpdateDialog.hpp UpdateDialog.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistSettings
|
||||
|
||||
@ -47,13 +47,21 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
chatTokensThreshold.setLabelText(Tr::tr("Chat History Token Limit:"));
|
||||
chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When "
|
||||
"exceeded, oldest messages will be removed."));
|
||||
chatTokensThreshold.setRange(1000, 16000);
|
||||
chatTokensThreshold.setRange(1, 900000);
|
||||
chatTokensThreshold.setDefaultValue(8000);
|
||||
|
||||
sharingCurrentFile.setSettingsKey(Constants::CA_SHARING_CURRENT_FILE);
|
||||
sharingCurrentFile.setLabelText(Tr::tr("Share Current File With Assistant by Default"));
|
||||
sharingCurrentFile.setDefaultValue(true);
|
||||
|
||||
stream.setSettingsKey(Constants::CA_STREAM);
|
||||
stream.setDefaultValue(true);
|
||||
stream.setLabelText(Tr::tr("Enable stream option"));
|
||||
|
||||
autosave.setSettingsKey(Constants::CA_AUTOSAVE);
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::CA_TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
@ -131,7 +139,7 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
|
||||
// API Configuration Settings
|
||||
apiKey.setSettingsKey(Constants::CA_API_KEY);
|
||||
apiKey.setLabelText(Tr::tr("API Key:"));
|
||||
apiKey.setLabelText(Tr::tr("[Deprecated, see Provider Settings]API Key:"));
|
||||
apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
apiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
|
||||
@ -158,28 +166,30 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
return Column{Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Chat Settings")),
|
||||
Column{Row{chatTokensThreshold, Stretch{1}}, sharingCurrentFile}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Row{genGrid, Stretch{1}},
|
||||
},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")),
|
||||
Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")),
|
||||
Column{
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
systemPrompt,
|
||||
}},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("API Configuration")), Column{apiKey}},
|
||||
Stretch{1}};
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Chat Settings")),
|
||||
Column{Row{chatTokensThreshold, Stretch{1}}, sharingCurrentFile, stream, autosave}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Row{genGrid, Stretch{1}},
|
||||
},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Context Settings")),
|
||||
Column{
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
systemPrompt,
|
||||
}},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("API Configuration")), Column{apiKey}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
@ -201,6 +211,7 @@ void ChatAssistantSettings::resetSettingsToDefaults()
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(stream);
|
||||
resetAspect(chatTokensThreshold);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
|
||||
@ -35,6 +35,8 @@ public:
|
||||
// Chat settings
|
||||
Utils::IntegerAspect chatTokensThreshold{this};
|
||||
Utils::BoolAspect sharingCurrentFile{this};
|
||||
Utils::BoolAspect stream{this};
|
||||
Utils::BoolAspect autosave{this};
|
||||
|
||||
// General Parameters Settings
|
||||
Utils::DoubleAspect temperature{this};
|
||||
|
||||
@ -48,13 +48,21 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
autoCompletion.setDefaultValue(true);
|
||||
|
||||
multiLineCompletion.setSettingsKey(Constants::CC_MULTILINE_COMPLETION);
|
||||
multiLineCompletion.setDefaultValue(false);
|
||||
multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion(experimental)"));
|
||||
multiLineCompletion.setDefaultValue(true);
|
||||
multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion"));
|
||||
|
||||
stream.setSettingsKey(Constants::CC_STREAM);
|
||||
stream.setDefaultValue(true);
|
||||
stream.setLabelText(Tr::tr("Enable stream option"));
|
||||
|
||||
smartProcessInstuctText.setSettingsKey(Constants::CC_SMART_PROCESS_INSTRUCT_TEXT);
|
||||
smartProcessInstuctText.setDefaultValue(true);
|
||||
smartProcessInstuctText.setLabelText(Tr::tr("Enable smart process text from instruct model"));
|
||||
|
||||
startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER);
|
||||
startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)"));
|
||||
startSuggestionTimer.setRange(10, 10000);
|
||||
startSuggestionTimer.setDefaultValue(500);
|
||||
startSuggestionTimer.setDefaultValue(350);
|
||||
|
||||
autoCompletionCharThreshold.setSettingsKey(Constants::СС_AUTO_COMPLETION_CHAR_THRESHOLD);
|
||||
autoCompletionCharThreshold.setLabelText(Tr::tr("AI suggestion triggers after typing"));
|
||||
@ -62,7 +70,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
Tr::tr("The number of characters that need to be typed within the typing interval "
|
||||
"before an AI suggestion request is sent."));
|
||||
autoCompletionCharThreshold.setRange(0, 10);
|
||||
autoCompletionCharThreshold.setDefaultValue(0);
|
||||
autoCompletionCharThreshold.setDefaultValue(1);
|
||||
|
||||
autoCompletionTypingInterval.setSettingsKey(Constants::СС_AUTO_COMPLETION_TYPING_INTERVAL);
|
||||
autoCompletionTypingInterval.setLabelText(Tr::tr("character(s) within(ms)"));
|
||||
@ -70,7 +78,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
Tr::tr("The time window (in milliseconds) during which the character threshold "
|
||||
"must be met to trigger an AI suggestion request."));
|
||||
autoCompletionTypingInterval.setRange(500, 5000);
|
||||
autoCompletionTypingInterval.setDefaultValue(2000);
|
||||
autoCompletionTypingInterval.setDefaultValue(1200);
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::CC_TEMPERATURE);
|
||||
@ -81,7 +89,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
|
||||
maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS);
|
||||
maxTokens.setLabelText(Tr::tr("Max Tokens:"));
|
||||
maxTokens.setRange(-1, 10000);
|
||||
maxTokens.setRange(-1, 900000);
|
||||
maxTokens.setDefaultValue(50);
|
||||
|
||||
// Advanced Parameters
|
||||
@ -144,11 +152,29 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT);
|
||||
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
systemPrompt.setDefaultValue(
|
||||
"You are an expert C++, Qt, and QML code completion AI. Your task is to provide accurate "
|
||||
"and contextually appropriate code suggestions.");
|
||||
"You are an expert in C++, Qt, and QML programming. Your task is to provide code "
|
||||
"suggestions that seamlessly integrate with existing code. You will receive a code context "
|
||||
"with specified insertion points. Your goal is to complete only one logic expression "
|
||||
"within these points."
|
||||
"Here is the code context with insertion points:<code_context>Before: {{variable}}After: "
|
||||
"{{variable}}</code_context> Instructions: 1. Carefully analyze the provided code context. "
|
||||
"2. Consider the existing code and the specified insertion points.3. Generate a code "
|
||||
"suggestion that completes one logic expression between the 'Before' and 'After' points. "
|
||||
"4. Ensure your suggestion does not repeat any existing code. 5. Format your suggestion as "
|
||||
"a code block using triple backticks. 6. Do not include any comments or descriptions with "
|
||||
"your code suggestion. Remember to include only the new code to be inserted.");
|
||||
|
||||
useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE);
|
||||
useUserMessageTemplateForCC.setDefaultValue(true);
|
||||
useUserMessageTemplateForCC.setLabelText(Tr::tr("Use User Template for code completion message for non-FIM models"));
|
||||
|
||||
userMessageTemplateForCC.setSettingsKey(Constants::CC_USER_TEMPLATE);
|
||||
userMessageTemplateForCC.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
userMessageTemplateForCC.setDefaultValue("Here is the code context with insertion points: <code_context>"
|
||||
"\nBefore: %1After: %2\n </code_context>");
|
||||
|
||||
useFilePathInContext.setSettingsKey(Constants::CC_USE_FILE_PATH_IN_CONTEXT);
|
||||
useFilePathInContext.setDefaultValue(false);
|
||||
useFilePathInContext.setDefaultValue(true);
|
||||
useFilePathInContext.setLabelText(Tr::tr("Use File Path in Context"));
|
||||
|
||||
useProjectChangesCache.setSettingsKey(Constants::CC_USE_PROJECT_CHANGES_CACHE);
|
||||
@ -175,7 +201,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
|
||||
// API Configuration Settings
|
||||
apiKey.setSettingsKey(Constants::CC_API_KEY);
|
||||
apiKey.setLabelText(Tr::tr("API Key:"));
|
||||
apiKey.setLabelText(Tr::tr("[Deprecated, see Provider Settings]API Key:"));
|
||||
apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
apiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
|
||||
@ -211,34 +237,41 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
auto contextItem = Column{Row{contextGrid, Stretch{1}},
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
systemPrompt,
|
||||
Row{useUserMessageTemplateForCC, Stretch{1}},
|
||||
userMessageTemplateForCC,
|
||||
Row{useFilePathInContext, Stretch{1}},
|
||||
Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
|
||||
|
||||
return Column{Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Auto Completion Settings")),
|
||||
Column{autoCompletion,
|
||||
Space{8},
|
||||
multiLineCompletion,
|
||||
Row{autoCompletionCharThreshold,
|
||||
autoCompletionTypingInterval,
|
||||
startSuggestionTimer,
|
||||
Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("General Parameters")),
|
||||
Column{
|
||||
Row{genGrid, Stretch{1}},
|
||||
}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")),
|
||||
Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), contextItem},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("API Configuration")), Column{apiKey}},
|
||||
Stretch{1}};
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(TrConstants::AUTO_COMPLETION_SETTINGS),
|
||||
Column{
|
||||
autoCompletion,
|
||||
Space{8},
|
||||
multiLineCompletion,
|
||||
stream,
|
||||
smartProcessInstuctText,
|
||||
Row{autoCompletionCharThreshold,
|
||||
autoCompletionTypingInterval,
|
||||
startSuggestionTimer,
|
||||
Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Column{
|
||||
Row{genGrid, Stretch{1}},
|
||||
}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), contextItem},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("API Configuration")), Column{apiKey}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
@ -276,6 +309,7 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(autoCompletion);
|
||||
resetAspect(multiLineCompletion);
|
||||
resetAspect(stream);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(useTopP);
|
||||
|
||||
@ -35,6 +35,8 @@ public:
|
||||
// Auto Completion Settings
|
||||
Utils::BoolAspect autoCompletion{this};
|
||||
Utils::BoolAspect multiLineCompletion{this};
|
||||
Utils::BoolAspect stream{this};
|
||||
Utils::BoolAspect smartProcessInstuctText{this};
|
||||
|
||||
Utils::IntegerAspect startSuggestionTimer{this};
|
||||
Utils::IntegerAspect autoCompletionCharThreshold{this};
|
||||
@ -64,6 +66,8 @@ public:
|
||||
Utils::IntegerAspect readStringsAfterCursor{this};
|
||||
Utils::BoolAspect useSystemPrompt{this};
|
||||
Utils::StringAspect systemPrompt{this};
|
||||
Utils::BoolAspect useUserMessageTemplateForCC{this};
|
||||
Utils::StringAspect userMessageTemplateForCC{this};
|
||||
Utils::BoolAspect useFilePathInContext{this};
|
||||
Utils::BoolAspect useProjectChangesCache{this};
|
||||
Utils::IntegerAspect maxChangesCacheSize{this};
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ContextSettings.hpp"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
ContextSettings &contextSettings()
|
||||
{
|
||||
static ContextSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
ContextSettings::ContextSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("Context"));
|
||||
|
||||
readFullFile.setSettingsKey(Constants::CC_READ_FULL_FILE);
|
||||
readFullFile.setLabelText(Tr::tr("Read Full File"));
|
||||
readFullFile.setDefaultValue(false);
|
||||
|
||||
readStringsBeforeCursor.setSettingsKey(Constants::CC_READ_STRINGS_BEFORE_CURSOR);
|
||||
readStringsBeforeCursor.setLabelText(Tr::tr("Read Strings Before Cursor"));
|
||||
readStringsBeforeCursor.setRange(0, 10000);
|
||||
readStringsBeforeCursor.setDefaultValue(50);
|
||||
|
||||
readStringsAfterCursor.setSettingsKey(Constants::CC_READ_STRINGS_AFTER_CURSOR);
|
||||
readStringsAfterCursor.setLabelText(Tr::tr("Read Strings After Cursor"));
|
||||
readStringsAfterCursor.setRange(0, 10000);
|
||||
readStringsAfterCursor.setDefaultValue(30);
|
||||
|
||||
useFilePathInContext.setSettingsKey(Constants::CC_USE_FILE_PATH_IN_CONTEXT);
|
||||
useFilePathInContext.setDefaultValue(false);
|
||||
useFilePathInContext.setLabelText(Tr::tr("Use File Path in Context"));
|
||||
|
||||
useSystemPrompt.setSettingsKey(Constants::CC_USE_SYSTEM_PROMPT);
|
||||
useSystemPrompt.setDefaultValue(true);
|
||||
useSystemPrompt.setLabelText(Tr::tr("Use System Prompt"));
|
||||
|
||||
systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT);
|
||||
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
systemPrompt.setDefaultValue(
|
||||
"You are an expert C++, Qt, and QML code completion AI. Your task is to provide accurate "
|
||||
"and "
|
||||
"contextually appropriate code suggestions. Focus on completing the code in a way that "
|
||||
"follows best practices, is efficient, and matches the surrounding code style. Prioritize "
|
||||
"Qt and QML-specific completions when appropriate. Avoid adding comments or explanations "
|
||||
"in your completions.");
|
||||
|
||||
useChatSystemPrompt.setSettingsKey(Constants::CA_SYSTEM_PROMPT);
|
||||
useChatSystemPrompt.setDefaultValue(true);
|
||||
useChatSystemPrompt.setLabelText(Tr::tr("Use System Prompt for chat"));
|
||||
|
||||
chatSystemPrompt.setSettingsKey(Constants::CA_SYSTEM_PROMPT);
|
||||
chatSystemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
chatSystemPrompt.setDefaultValue(
|
||||
"You are an advanced AI assistant specializing in C++, Qt, and QML development. Your role "
|
||||
"is "
|
||||
"to provide helpful, accurate, and detailed responses to questions about coding, "
|
||||
"debugging, "
|
||||
"and best practices in these technologies. Offer clear explanations, code examples when "
|
||||
"appropriate, and guidance on Qt Creator usage. Always prioritize officially recommended "
|
||||
"Qt "
|
||||
"and C++ practices. If you're unsure about something, state it clearly and suggest where "
|
||||
"the "
|
||||
"user might find more information.");
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
useProjectChangesCache.setSettingsKey(Constants::CC_USE_PROJECT_CHANGES_CACHE);
|
||||
useProjectChangesCache.setDefaultValue(true);
|
||||
useProjectChangesCache.setLabelText(Tr::tr("Use Project Changes Cache"));
|
||||
|
||||
maxChangesCacheSize.setSettingsKey(Constants::CC_MAX_CHANGES_CACHE_SIZE);
|
||||
maxChangesCacheSize.setLabelText(Tr::tr("Max Changes Cache Size"));
|
||||
maxChangesCacheSize.setRange(2, 1000);
|
||||
maxChangesCacheSize.setDefaultValue(20);
|
||||
|
||||
readSettings();
|
||||
|
||||
readStringsAfterCursor.setEnabled(!readFullFile());
|
||||
readStringsBeforeCursor.setEnabled(!readFullFile());
|
||||
systemPrompt.setEnabled(useSystemPrompt());
|
||||
|
||||
setupConnection();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
return Column{Row{Stretch{1}, resetToDefaults},
|
||||
Group{title(Tr::tr("AI Suggestions Context")),
|
||||
Column{Row{readFullFile, Stretch{1}},
|
||||
Row{readStringsBeforeCursor, Stretch{1}},
|
||||
Row{readStringsAfterCursor, Stretch{1}},
|
||||
useFilePathInContext,
|
||||
useSystemPrompt,
|
||||
systemPrompt,
|
||||
useProjectChangesCache,
|
||||
Row{maxChangesCacheSize, Stretch{1}},
|
||||
Stretch{1}}},
|
||||
Space{16},
|
||||
Group{title(Tr::tr("AI Chat Context")),
|
||||
Column{useChatSystemPrompt, chatSystemPrompt}}};
|
||||
});
|
||||
}
|
||||
|
||||
void ContextSettings::setupConnection()
|
||||
{
|
||||
connect(&readFullFile, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
readStringsAfterCursor.setEnabled(!readFullFile.volatileValue());
|
||||
readStringsBeforeCursor.setEnabled(!readFullFile.volatileValue());
|
||||
});
|
||||
connect(&useSystemPrompt, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
systemPrompt.setEnabled(useSystemPrompt.volatileValue());
|
||||
});
|
||||
connect(&resetToDefaults, &ButtonAspect::clicked, this, &ContextSettings::resetPageToDefaults);
|
||||
}
|
||||
|
||||
void ContextSettings::resetPageToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(readFullFile);
|
||||
resetAspect(readStringsBeforeCursor);
|
||||
resetAspect(readStringsAfterCursor);
|
||||
resetAspect(useFilePathInContext);
|
||||
resetAspect(useSystemPrompt);
|
||||
resetAspect(systemPrompt);
|
||||
resetAspect(useChatSystemPrompt);
|
||||
resetAspect(chatSystemPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
ContextSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_CONTEXT_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("Context"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setDisplayCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY);
|
||||
setCategoryIconPath(":/resources/images/qoderassist-icon.png");
|
||||
setSettingsProvider([] { return &contextSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const ContextSettingsPage contextSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user