mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-10 01:00:23 -05:00
Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31145f191b | |||
| 9096adde6f | |||
| b8e578d2d7 | |||
| 4e45774bce | |||
| 928490d31f | |||
| 97163cf6c9 | |||
| f85c162692 | |||
| 258053d826 | |||
| bf63ae5714 | |||
| ae76850e78 | |||
| bf3c0b3aa0 | |||
| 9add61c805 | |||
| add86d2e67 | |||
| a6c909d34d | |||
| 2814dec3e5 | |||
| 1b86b60de8 | |||
| 4b7f638731 | |||
| de046f0529 | |||
| e975e143b1 | |||
| c97c0f62e8 | |||
| 61fded34ea | |||
| 289a19ac1a | |||
| 43ac662671 | |||
| 1d64d2afc9 | |||
| 9db61119aa | |||
| 70481b3116 | |||
| 511f5b36eb | |||
| 35012865c7 | |||
| f27429aa66 | |||
| 113d5adcf4 | |||
| 30ea89cdc2 | |||
| 13469edce6 | |||
| ee2c3950e8 | |||
| d04e5bc967 | |||
| d8ef9d0120 | |||
| e544e46d76 | |||
| 63f0900511 | |||
| 7dee6f62c0 | |||
| dc06ea2ed5 | |||
| fc5e1adc0d | |||
| 93e59fb2dc | |||
| cd2a56cde0 | |||
| 09cde8fd3d | |||
| ac8080542d | |||
| 7376a11a05 | |||
| 10e8b16caf | |||
| a38debb140 | |||
| 844ac35a59 | |||
| 16b77a5722 | |||
| c070fd5cfd | |||
| 882047d7b2 | |||
| b692402897 | |||
| 8102ba95f9 | |||
| f8bb9998ab | |||
| 6dab055ca2 | |||
| 7b31fff9f2 | |||
| be9156fd0e | |||
| 657413344d | |||
| 5f3deb44b9 | |||
| 55e2b24b8d | |||
| 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 | |||
| 4432d4019d | |||
| f679d76d43 | |||
| 29f94561ef | |||
| cd6c766ed2 | |||
| 6d3bc362b3 | |||
| 87393b681f | |||
| 5d496fee58 | |||
| 9902623ba0 | |||
| 61f1f0ae4f | |||
| bc93bce03b | |||
| 85d039cbd5 | |||
| 2acaef553d | |||
| b141e54e3e | |||
| 1ec6098210 | |||
| 9c945f066b | |||
| 4a82e9c046 | |||
| 838d69623c | |||
| 693e429bdd | |||
| 496d8feb66 | |||
| 40a568ebd9 | |||
| 5b43eb4fd2 | |||
| 9c2516cd4c | |||
| 2257e6e45f | |||
| 80eda8c167 | |||
| 3db2691114 | |||
| bf518b4a01 | |||
| 46829720d8 | |||
| 9158a3ac0d | |||
| d6e02d9d2a | |||
| 9c8cac4e3a | |||
| 965af4a945 | |||
| 95f29fefc7 | |||
| 1dd50b6c83 | |||
| 146e772514 | |||
| 4b851f1662 | |||
| 6fea300825 | |||
| 14bf0e6c94 | |||
| 0c045e65df | |||
| 15138b4644 | |||
| 5c98de7440 | |||
| b808d0ec10 | |||
| 30fcd7e019 | |||
| 7442256bab | |||
| 8be279a5fd | |||
| d77e13cddb | |||
| 162c068431 | |||
| 4e8ff55355 | |||
| 8df21e96bd |
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: petrmdev
|
||||
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'));
|
||||
188
.github/workflows/build_cmake.yml
vendored
188
.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.1
|
||||
QT_CREATOR_VERSION_INTERNAL: 15.0.1
|
||||
MACOS_DEPLOYMENT_TARGET: "11.0"
|
||||
CMAKE_VERSION: "3.29.6"
|
||||
NINJA_VERSION: "1.12.1"
|
||||
@ -23,74 +31,50 @@ 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: "Ubuntu 22.04 GCC", artifact: "Linux-x64(Ubuntu-22.04-experimental)",
|
||||
os: ubuntu-22.04,
|
||||
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 +84,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 +101,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 +112,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 +130,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 +149,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 +167,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 +244,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
|
||||
@ -10,18 +10,29 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||
find_package(Qt6 COMPONENTS Core Gui Widgets Network REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
|
||||
|
||||
add_subdirectory(llmcore)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(logger)
|
||||
add_subdirectory(ChatView)
|
||||
add_subdirectory(context)
|
||||
|
||||
add_qtc_plugin(QodeAssist
|
||||
PLUGIN_DEPENDS
|
||||
QtCreator::Core
|
||||
QtCreator::LanguageClient
|
||||
QtCreator::TextEditor
|
||||
QtCreator::ProjectExplorer
|
||||
DEPENDS
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Widgets
|
||||
Qt::Network
|
||||
QtCreator::ExtensionSystem
|
||||
QtCreator::Utils
|
||||
QtCreator::ProjectExplorer
|
||||
QodeAssistChatViewplugin
|
||||
SOURCES
|
||||
.github/workflows/build_cmake.yml
|
||||
.github/workflows/README.md
|
||||
@ -30,36 +41,34 @@ add_qtc_plugin(QodeAssist
|
||||
QodeAssistConstants.hpp
|
||||
QodeAssisttr.h
|
||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||
templates/PromptTemplate.hpp
|
||||
templates/CodeLlamaFimTemplate.hpp
|
||||
templates/StarCoder2Template.hpp
|
||||
templates/DeepSeekCoderV2.hpp
|
||||
templates/CustomTemplate.hpp
|
||||
templates/DeepSeekCoderChatTemplate.hpp
|
||||
templates/CodeLlamaInstruct.hpp
|
||||
providers/LLMProvider.hpp
|
||||
templates/Templates.hpp
|
||||
templates/CodeLlamaFim.hpp
|
||||
templates/StarCoder2Fim.hpp
|
||||
templates/DeepSeekCoderFim.hpp
|
||||
templates/CustomFimTemplate.hpp
|
||||
templates/Qwen.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
|
||||
LLMProvidersManager.hpp LLMProvidersManager.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
|
||||
QodeAssistUtils.hpp
|
||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||
QodeAssistData.hpp
|
||||
utils/CounterTooltip.hpp utils/CounterTooltip.cpp
|
||||
settings/GeneralSettings.hpp settings/GeneralSettings.cpp
|
||||
settings/ContextSettings.hpp settings/ContextSettings.cpp
|
||||
settings/CustomPromptSettings.hpp settings/CustomPromptSettings.cpp
|
||||
settings/PresetPromptsSettings.hpp settings/PresetPromptsSettings.cpp
|
||||
settings/SettingsUtils.hpp
|
||||
core/ChangesManager.h core/ChangesManager.cpp
|
||||
core/LLMRequestHandler.hpp core/LLMRequestHandler.cpp
|
||||
core/LLMRequestConfig.hpp
|
||||
chat/ChatWidget.h chat/ChatWidget.cpp
|
||||
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
||||
chat/ChatClientInterface.hpp chat/ChatClientInterface.cpp
|
||||
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
CodeHandler.hpp CodeHandler.cpp
|
||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||
)
|
||||
|
||||
52
ChatView/CMakeLists.txt
Normal file
52
ChatView/CMakeLists.txt
Normal file
@ -0,0 +1,52 @@
|
||||
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-light.svg
|
||||
icons/attach-file-dark.svg
|
||||
icons/close-dark.svg
|
||||
icons/close-light.svg
|
||||
icons/link-file-light.svg
|
||||
icons/link-file-dark.svg
|
||||
SOURCES
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
ChatModel.hpp ChatModel.cpp
|
||||
ChatRootView.hpp ChatRootView.cpp
|
||||
ClientInterface.hpp ClientInterface.cpp
|
||||
MessagePart.hpp
|
||||
ChatUtils.h ChatUtils.cpp
|
||||
ChatSerializer.hpp ChatSerializer.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
PUBLIC
|
||||
Qt::Widgets
|
||||
Qt::Quick
|
||||
Qt::QuickWidgets
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
198
ChatView/ChatModel.cpp
Normal file
198
ChatView/ChatModel.cpp
Normal file
@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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 "ChatModel.hpp"
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QtQml>
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatModel::ChatModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
connect(&settings.chatTokensThreshold,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatModel::tokensThresholdChanged);
|
||||
}
|
||||
|
||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_messages.size();
|
||||
}
|
||||
|
||||
QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_messages.size())
|
||||
return QVariant();
|
||||
|
||||
const Message &message = m_messages[index.row()];
|
||||
switch (static_cast<Roles>(role)) {
|
||||
case Roles::RoleType:
|
||||
return QVariant::fromValue(message.role);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
const QList<Context::ContentFile> &attachments)
|
||||
{
|
||||
QString fullContent = content;
|
||||
if (!attachments.isEmpty()) {
|
||||
fullContent += "\n\nAttached files list:";
|
||||
for (const auto &attachment : attachments) {
|
||||
fullContent += QString("\nname: %1\nfile content:\n%2")
|
||||
.arg(attachment.filename, attachment.content);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
|
||||
Message &lastMessage = m_messages.last();
|
||||
lastMessage.content = content;
|
||||
lastMessage.attachments = attachments;
|
||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
Message newMessage{role, content, id};
|
||||
newMessage.attachments = attachments;
|
||||
m_messages.append(newMessage);
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
||||
{
|
||||
return m_messages;
|
||||
}
|
||||
|
||||
void ChatModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_messages.clear();
|
||||
endResetModel();
|
||||
emit modelReseted();
|
||||
}
|
||||
|
||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||
{
|
||||
QList<MessagePart> parts;
|
||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||
int lastIndex = 0;
|
||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||
|
||||
while (blockMatches.hasNext()) {
|
||||
auto match = blockMatches.next();
|
||||
if (match.capturedStart() > lastIndex) {
|
||||
QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||
if (!textBetween.isEmpty()) {
|
||||
parts.append({MessagePart::Text, textBetween, ""});
|
||||
}
|
||||
}
|
||||
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
|
||||
lastIndex = match.capturedEnd();
|
||||
}
|
||||
|
||||
if (lastIndex < content.length()) {
|
||||
QString remainingText = content.mid(lastIndex).trimmed();
|
||||
if (!remainingText.isEmpty()) {
|
||||
parts.append({MessagePart::Text, remainingText, ""});
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
|
||||
{
|
||||
QJsonArray messages;
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
|
||||
|
||||
for (const auto &message : m_messages) {
|
||||
QString role;
|
||||
switch (message.role) {
|
||||
case ChatRole::User:
|
||||
role = "user";
|
||||
break;
|
||||
case ChatRole::Assistant:
|
||||
role = "assistant";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int ChatModel::tokensThreshold() const
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
return settings.chatTokensThreshold();
|
||||
}
|
||||
|
||||
QString ChatModel::lastMessageId() const
|
||||
{
|
||||
return !m_messages.isEmpty() ? m_messages.last().id : "";
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
86
ChatView/ChatModel.hpp
Normal file
86
ChatView/ChatModel.hpp
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 "ContextData.hpp"
|
||||
#include "MessagePart.hpp"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "context/ContentFile.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
enum ChatRole { System, User, Assistant };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
|
||||
|
||||
struct Message
|
||||
{
|
||||
ChatRole role;
|
||||
QString content;
|
||||
QString id;
|
||||
|
||||
QList<Context::ContentFile> attachments;
|
||||
};
|
||||
|
||||
explicit ChatModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
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,
|
||||
const QList<Context::ContentFile> &attachments = {});
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||
|
||||
QVector<Message> getChatHistory() const;
|
||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
||||
|
||||
int tokensThreshold() const;
|
||||
|
||||
QString currentModel() const;
|
||||
QString lastMessageId() const;
|
||||
|
||||
signals:
|
||||
void tokensThresholdChanged();
|
||||
void modelReseted();
|
||||
|
||||
private:
|
||||
QVector<Message> m_messages;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
||||
463
ChatView/ChatRootView.cpp
Normal file
463
ChatView/ChatRootView.cpp
Normal file
@ -0,0 +1,463 @@
|
||||
/*
|
||||
* 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 <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "context/TokenUtils.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
{
|
||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||
connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
|
||||
this,
|
||||
[this](){
|
||||
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
|
||||
});
|
||||
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(&settings.caModel,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::currentTemplateChanged);
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageReceivedCompletely,
|
||||
this,
|
||||
&ChatRootView::autosave);
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageReceivedCompletely,
|
||||
this,
|
||||
&ChatRootView::updateInputTokensCount);
|
||||
|
||||
connect(m_chatModel, &ChatModel::modelReseted, [this]() { setRecentFilePath(QString{}); });
|
||||
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||
connect(&Settings::chatAssistantSettings().useSystemPrompt, &Utils::BaseAspect::changed,
|
||||
this, &ChatRootView::updateInputTokensCount);
|
||||
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
|
||||
this, &ChatRootView::updateInputTokensCount);
|
||||
|
||||
auto editors = Core::EditorManager::instance();
|
||||
|
||||
connect(editors, &Core::EditorManager::editorOpened, this, &ChatRootView::onEditorOpened);
|
||||
connect(editors, &Core::EditorManager::editorAboutToClose, this, &ChatRootView::onEditorAboutToClose);
|
||||
connect(editors, &Core::EditorManager::editorsClosed, this, &ChatRootView::onEditorsClosed);
|
||||
|
||||
updateInputTokensCount();
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
{
|
||||
return m_chatModel;
|
||||
}
|
||||
|
||||
void ChatRootView::sendMessage(const QString &message)
|
||||
{
|
||||
if (m_inputTokensCount > m_chatModel->tokensThreshold()) {
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
tr("Token Limit Exceeded"),
|
||||
tr("The chat history has exceeded the token limit.\n"
|
||||
"Would you like to create new chat?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
autosave();
|
||||
m_chatModel->clear();
|
||||
setRecentFilePath(QString{});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::clearLinkedFiles()
|
||||
{
|
||||
if (!m_linkedFiles.isEmpty()) {
|
||||
m_linkedFiles.clear();
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
setRecentFilePath(filePath);
|
||||
}
|
||||
updateInputTokensCount();
|
||||
}
|
||||
|
||||
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);
|
||||
setRecentFilePath(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
QString ChatRootView::getAutosaveFilePath() const
|
||||
{
|
||||
if (!m_recentFilePath.isEmpty()) {
|
||||
return m_recentFilePath;
|
||||
}
|
||||
|
||||
QString dir = getChatsHistoryDir();
|
||||
if (dir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QDir(dir).filePath(getSuggestedFileName() + ".json");
|
||||
}
|
||||
|
||||
QStringList ChatRootView::attachmentFiles() const
|
||||
{
|
||||
return m_attachmentFiles;
|
||||
}
|
||||
|
||||
QStringList ChatRootView::linkedFiles() const
|
||||
{
|
||||
return m_linkedFiles;
|
||||
}
|
||||
|
||||
void ChatRootView::showAttachFilesDialog()
|
||||
{
|
||||
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
|
||||
dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
|
||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||
dialog.setDirectory(project->projectDirectory().toString());
|
||||
}
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList newFilePaths = dialog.selectedFiles();
|
||||
if (!newFilePaths.isEmpty()) {
|
||||
bool filesAdded = false;
|
||||
for (const QString &filePath : newFilePaths) {
|
||||
if (!m_attachmentFiles.contains(filePath)) {
|
||||
m_attachmentFiles.append(filePath);
|
||||
filesAdded = true;
|
||||
}
|
||||
}
|
||||
if (filesAdded) {
|
||||
emit attachmentFilesChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::removeFileFromAttachList(int index)
|
||||
{
|
||||
if (index >= 0 && index < m_attachmentFiles.size()) {
|
||||
m_attachmentFiles.removeAt(index);
|
||||
emit attachmentFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::showLinkFilesDialog()
|
||||
{
|
||||
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
|
||||
dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
|
||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||
dialog.setDirectory(project->projectDirectory().toString());
|
||||
}
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList newFilePaths = dialog.selectedFiles();
|
||||
if (!newFilePaths.isEmpty()) {
|
||||
bool filesAdded = false;
|
||||
for (const QString &filePath : newFilePaths) {
|
||||
if (!m_linkedFiles.contains(filePath)) {
|
||||
m_linkedFiles.append(filePath);
|
||||
filesAdded = true;
|
||||
}
|
||||
}
|
||||
if (filesAdded) {
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::removeFileFromLinkList(int index)
|
||||
{
|
||||
if (index >= 0 && index < m_linkedFiles.size()) {
|
||||
m_linkedFiles.removeAt(index);
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::calculateMessageTokensCount(const QString &message)
|
||||
{
|
||||
m_messageTokensCount = Context::TokenUtils::estimateTokens(message);
|
||||
updateInputTokensCount();
|
||||
}
|
||||
|
||||
void ChatRootView::setIsSyncOpenFiles(bool state)
|
||||
{
|
||||
if (m_isSyncOpenFiles != state) {
|
||||
m_isSyncOpenFiles = state;
|
||||
emit isSyncOpenFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::updateInputTokensCount()
|
||||
{
|
||||
int inputTokens = m_messageTokensCount;
|
||||
auto& settings = Settings::chatAssistantSettings();
|
||||
|
||||
if (settings.useSystemPrompt()) {
|
||||
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
||||
}
|
||||
|
||||
if (!m_attachmentFiles.isEmpty()) {
|
||||
auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
|
||||
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||
}
|
||||
|
||||
if (!m_linkedFiles.isEmpty()) {
|
||||
auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
|
||||
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
||||
}
|
||||
|
||||
const auto& history = m_chatModel->getChatHistory();
|
||||
for (const auto& message : history) {
|
||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
||||
inputTokens += 4; // + role
|
||||
}
|
||||
|
||||
m_inputTokensCount = inputTokens;
|
||||
emit inputTokensCountChanged();
|
||||
}
|
||||
|
||||
int ChatRootView::inputTokensCount() const
|
||||
{
|
||||
return m_inputTokensCount;
|
||||
}
|
||||
|
||||
bool ChatRootView::isSyncOpenFiles() const
|
||||
{
|
||||
return m_isSyncOpenFiles;
|
||||
}
|
||||
|
||||
void ChatRootView::onEditorOpened(Core::IEditor *editor)
|
||||
{
|
||||
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||
QString filePath = document->filePath().toString();
|
||||
if (!m_linkedFiles.contains(filePath)) {
|
||||
m_linkedFiles.append(filePath);
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
||||
{
|
||||
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||
QString filePath = document->filePath().toString();
|
||||
m_linkedFiles.removeOne(filePath);
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::onEditorsClosed(QList<Core::IEditor *> editors)
|
||||
{
|
||||
if (isSyncOpenFiles()) {
|
||||
for (Core::IEditor *editor : editors) {
|
||||
if (auto document = editor->document()) {
|
||||
QString filePath = document->filePath().toString();
|
||||
m_linkedFiles.removeOne(filePath);
|
||||
}
|
||||
}
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString ChatRootView::chatFileName() const
|
||||
{
|
||||
return QFileInfo(m_recentFilePath).baseName();
|
||||
}
|
||||
|
||||
void ChatRootView::setRecentFilePath(const QString &filePath)
|
||||
{
|
||||
if (m_recentFilePath != filePath) {
|
||||
m_recentFilePath = filePath;
|
||||
emit chatFileNameChanged();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
111
ChatView/ChatRootView.hpp
Normal file
111
ChatView/ChatRootView.hpp
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 <QQuickItem>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
|
||||
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
ChatRootView(QQuickItem *parent = nullptr);
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
void saveHistory(const QString &filePath);
|
||||
void loadHistory(const QString &filePath);
|
||||
|
||||
Q_INVOKABLE void showSaveDialog();
|
||||
Q_INVOKABLE void showLoadDialog();
|
||||
|
||||
void autosave();
|
||||
QString getAutosaveFilePath() const;
|
||||
|
||||
QStringList attachmentFiles() const;
|
||||
QStringList linkedFiles() const;
|
||||
|
||||
Q_INVOKABLE void showAttachFilesDialog();
|
||||
Q_INVOKABLE void removeFileFromAttachList(int index);
|
||||
Q_INVOKABLE void showLinkFilesDialog();
|
||||
Q_INVOKABLE void removeFileFromLinkList(int index);
|
||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||
|
||||
Q_INVOKABLE void updateInputTokensCount();
|
||||
int inputTokensCount() const;
|
||||
|
||||
bool isSyncOpenFiles() const;
|
||||
|
||||
void onEditorOpened(Core::IEditor *editor);
|
||||
void onEditorAboutToClose(Core::IEditor *editor);
|
||||
void onEditorsClosed(QList<Core::IEditor *> editors);
|
||||
|
||||
QString chatFileName() const;
|
||||
void setRecentFilePath(const QString &filePath);
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message);
|
||||
void copyToClipboard(const QString &text);
|
||||
void cancelRequest();
|
||||
void clearAttachmentFiles();
|
||||
void clearLinkedFiles();
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
void attachmentFilesChanged();
|
||||
void linkedFilesChanged();
|
||||
void inputTokensCountChanged();
|
||||
void isSyncOpenFilesChanged();
|
||||
void chatFileNameChanged();
|
||||
|
||||
private:
|
||||
QString getChatsHistoryDir() const;
|
||||
QString getSuggestedFileName() const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
ClientInterface *m_clientInterface;
|
||||
QString m_currentTemplate;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
QStringList m_linkedFiles;
|
||||
int m_messageTokensCount{0};
|
||||
int m_inputTokensCount{0};
|
||||
bool m_isSyncOpenFiles;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
142
ChatView/ChatSerializer.cpp
Normal file
142
ChatView/ChatSerializer.cpp
Normal file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
const QString ChatSerializer::VERSION = "0.1";
|
||||
|
||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
||||
{
|
||||
if (!ensureDirectoryExists(filePath)) {
|
||||
return {false, "Failed to create directory structure"};
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||
}
|
||||
|
||||
QJsonObject root = serializeChat(model);
|
||||
QJsonDocument doc(root);
|
||||
|
||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||
}
|
||||
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QString version = root["version"].toString();
|
||||
|
||||
if (!validateVersion(version)) {
|
||||
return {false, QString("Unsupported version: %1").arg(version)};
|
||||
}
|
||||
|
||||
if (!deserializeChat(model, root)) {
|
||||
return {false, "Failed to deserialize chat data"};
|
||||
}
|
||||
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
|
||||
{
|
||||
QJsonObject messageObj;
|
||||
messageObj["role"] = static_cast<int>(message.role);
|
||||
messageObj["content"] = message.content;
|
||||
messageObj["id"] = message.id;
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
|
||||
{
|
||||
ChatModel::Message message;
|
||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||
message.content = json["content"].toString();
|
||||
message.id = json["id"].toString();
|
||||
return message;
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
|
||||
{
|
||||
QJsonArray messagesArray;
|
||||
for (const auto &message : model->getChatHistory()) {
|
||||
messagesArray.append(serializeMessage(message));
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["version"] = VERSION;
|
||||
root["messages"] = messagesArray;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
|
||||
{
|
||||
QJsonArray messagesArray = json["messages"].toArray();
|
||||
QVector<ChatModel::Message> messages;
|
||||
messages.reserve(messagesArray.size());
|
||||
|
||||
for (const auto &messageValue : messagesArray) {
|
||||
messages.append(deserializeMessage(messageValue.toObject()));
|
||||
}
|
||||
|
||||
model->clear();
|
||||
for (const auto &message : messages) {
|
||||
model->addMessage(message.content, message.role, message.id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QDir dir = fileInfo.dir();
|
||||
return dir.exists() || dir.mkpath(".");
|
||||
}
|
||||
|
||||
bool ChatSerializer::validateVersion(const QString &version)
|
||||
{
|
||||
return version == VERSION;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
56
ChatView/ChatSerializer.hpp
Normal file
56
ChatView/ChatSerializer.hpp
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
struct SerializationResult
|
||||
{
|
||||
bool success{false};
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class ChatSerializer
|
||||
{
|
||||
public:
|
||||
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
|
||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
||||
|
||||
// Public for testing purposes
|
||||
static QJsonObject serializeMessage(const ChatModel::Message &message);
|
||||
static ChatModel::Message deserializeMessage(const QJsonObject &json);
|
||||
static QJsonObject serializeChat(const ChatModel *model);
|
||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json);
|
||||
|
||||
private:
|
||||
static const QString VERSION;
|
||||
static constexpr int CURRENT_VERSION = 1;
|
||||
|
||||
static bool ensureDirectoryExists(const QString &filePath);
|
||||
static bool validateVersion(const QString &version);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
32
ChatView/ChatUtils.cpp
Normal file
32
ChatView/ChatUtils.cpp
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatUtils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
void ChatUtils::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
39
ChatView/ChatUtils.h
Normal file
39
ChatView/ChatUtils.h
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 <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatUtils : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_NAMED_ELEMENT(ChatUtils)
|
||||
|
||||
public:
|
||||
explicit ChatUtils(QObject *parent = nullptr)
|
||||
: QObject(parent) {};
|
||||
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
43
ChatView/ChatWidget.cpp
Normal file
43
ChatView/ChatWidget.cpp
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/>.
|
||||
*/
|
||||
|
||||
#include "ChatWidget.hpp"
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatWidget::ChatWidget(QWidget *parent)
|
||||
: QQuickWidget(parent)
|
||||
{
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
}
|
||||
|
||||
void ChatWidget::clear()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
||||
}
|
||||
|
||||
void ChatWidget::scrollToBottom()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
||||
}
|
||||
}
|
||||
41
ChatView/ChatWidget.hpp
Normal file
41
ChatView/ChatWidget.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 <QtQuickWidgets/QtQuickWidgets>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatWidget : public QQuickWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatWidget(QWidget *parent = nullptr);
|
||||
~ChatWidget() = default;
|
||||
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE void scrollToBottom();
|
||||
|
||||
signals:
|
||||
void clearPressed();
|
||||
};
|
||||
|
||||
}
|
||||
215
ChatView/ClientInterface.cpp
Normal file
215
ChatView/ClientInterface.cpp
Normal file
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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 "ClientInterface.hpp"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ContextManager.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_requestHandler(new LLMCore::RequestHandler(this))
|
||||
, m_chatModel(chatModel)
|
||||
{
|
||||
connect(m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
|
||||
handleLLMResponse(completion, request, isComplete);
|
||||
});
|
||||
|
||||
connect(m_requestHandler,
|
||||
&LLMCore::RequestHandler::requestFinished,
|
||||
this,
|
||||
[this](const QString &, bool success, const QString &errorString) {
|
||||
if (!success) {
|
||||
emit errorOccurred(errorString);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface() = default;
|
||||
|
||||
void ClientInterface::sendMessage(
|
||||
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
|
||||
{
|
||||
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;
|
||||
if (chatAssistantSettings.useSystemPrompt())
|
||||
systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||
}
|
||||
|
||||
QJsonObject providerRequest;
|
||||
providerRequest["model"] = Settings::generalSettings().caModel();
|
||||
providerRequest["stream"] = chatAssistantSettings.stream();
|
||||
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
|
||||
|
||||
if (promptTemplate)
|
||||
promptTemplate->prepareRequest(providerRequest, context);
|
||||
else
|
||||
qWarning("No prompt template found");
|
||||
|
||||
if (provider)
|
||||
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
|
||||
else
|
||||
qWarning("No provider found");
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Chat;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
m_chatModel->clear();
|
||||
LOG_MESSAGE("Chat history cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
{
|
||||
auto id = m_chatModel->lastMessageId();
|
||||
m_requestHandler->cancelRequest(id);
|
||||
}
|
||||
|
||||
void ClientInterface::handleLLMResponse(const QString &response,
|
||||
const QJsonObject &request,
|
||||
bool isComplete)
|
||||
{
|
||||
const auto message = response.trimmed();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString ClientInterface::getCurrentFileContext() const
|
||||
{
|
||||
auto currentEditor = Core::EditorManager::currentEditor();
|
||||
if (!currentEditor) {
|
||||
LOG_MESSAGE("No active editor found");
|
||||
return QString();
|
||||
}
|
||||
|
||||
auto textDocument = qobject_cast<TextEditor::TextDocument *>(currentEditor->document());
|
||||
if (!textDocument) {
|
||||
LOG_MESSAGE("Current document is not a text document");
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
|
||||
.arg(textDocument->mimeType(), textDocument->filePath().toString());
|
||||
|
||||
QString content = textDocument->document()->toPlainText();
|
||||
|
||||
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
|
||||
|
||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||
}
|
||||
|
||||
QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
|
||||
{
|
||||
QString updatedPrompt = basePrompt;
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
||||
|
||||
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
|
||||
for (const auto &file : contentFiles) {
|
||||
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
|
||||
.arg(file.filename, file.content);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedPrompt;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
61
ChatView/ClientInterface.hpp
Normal file
61
ChatView/ClientInterface.hpp
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 <QVector>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "RequestHandler.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ClientInterface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
const QList<QString> &linkedFiles = {});
|
||||
void clearMessages();
|
||||
void cancelRequest();
|
||||
|
||||
signals:
|
||||
void errorOccurred(const QString &error);
|
||||
void messageReceivedCompletely();
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||
QString getCurrentFileContext() const;
|
||||
QString getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt,
|
||||
const QList<QString> &linkedFiles) const;
|
||||
|
||||
LLMCore::RequestHandler *m_requestHandler;
|
||||
ChatModel *m_chatModel;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
51
ChatView/MessagePart.hpp
Normal file
51
ChatView/MessagePart.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
Q_NAMESPACE
|
||||
|
||||
class MessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(PartType type MEMBER type CONSTANT FINAL)
|
||||
Q_PROPERTY(QString text MEMBER text CONSTANT FINAL)
|
||||
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
|
||||
QML_VALUE_TYPE(messagePart)
|
||||
public:
|
||||
enum PartType { Code, Text };
|
||||
Q_ENUM(PartType)
|
||||
|
||||
PartType type;
|
||||
QString text;
|
||||
QString language;
|
||||
};
|
||||
|
||||
class MessagePartType : public MessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
};
|
||||
|
||||
QML_NAMED_ELEMENT(MessagePart)
|
||||
QML_FOREIGN_NAMESPACE(QodeAssist::Chat::MessagePartType)
|
||||
} // namespace QodeAssist::Chat
|
||||
11
ChatView/icons/attach-file-dark.svg
Normal file
11
ChatView/icons/attach-file-dark.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_37_14)">
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_37_14">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 869 B |
11
ChatView/icons/attach-file-light.svg
Normal file
11
ChatView/icons/attach-file-light.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_51_20)">
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_51_20">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 869 B |
10
ChatView/icons/close-dark.svg
Normal file
10
ChatView/icons/close-dark.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_41_14)">
|
||||
<path d="M0 0L24 24M0 24L24 0" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_41_14">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
10
ChatView/icons/close-light.svg
Normal file
10
ChatView/icons/close-light.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_41_14)">
|
||||
<path d="M0 0L24 24M0 24L24 0" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_41_14">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
12
ChatView/icons/link-file-dark.svg
Normal file
12
ChatView/icons/link-file-dark.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_49_24)">
|
||||
<path d="M10 12L10 32L10 12Z" fill="black"/>
|
||||
<path d="M10 12L10 32" stroke="black" stroke-width="3"/>
|
||||
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="black" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_49_24">
|
||||
<rect width="20" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 513 B |
12
ChatView/icons/link-file-light.svg
Normal file
12
ChatView/icons/link-file-light.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_51_24)">
|
||||
<path d="M10 12L10 32Z" fill="white"/>
|
||||
<path d="M10 12L10 32" stroke="white" stroke-width="3"/>
|
||||
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_51_24">
|
||||
<rect width="20" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
40
ChatView/qml/Badge.qml
Normal file
40
ChatView/qml/Badge.qml
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias text: badgeText.text
|
||||
|
||||
implicitWidth: badgeText.implicitWidth + root.radius
|
||||
implicitHeight: badgeText.implicitHeight + 6
|
||||
color: palette.button
|
||||
radius: root.height / 2
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
218
ChatView/qml/RootItem.qml
Normal file
218
ChatView/qml/RootItem.qml
Normal file
@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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.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: palette.window
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
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.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||
}
|
||||
recentPath {
|
||||
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: chatListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
leftMargin: 5
|
||||
model: root.chatModel
|
||||
clip: true
|
||||
spacing: 10
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 2000
|
||||
|
||||
delegate: ChatItem {
|
||||
required property var model
|
||||
|
||||
width: ListView.view.width - scroll.width
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
messageAttachments: model.attachments
|
||||
color: model.roleType === ChatModel.User ? palette.alternateBase
|
||||
: palette.base
|
||||
}
|
||||
|
||||
header: Item {
|
||||
width: ListView.view.width - scroll.width
|
||||
height: 30
|
||||
}
|
||||
|
||||
ScrollBar.vertical: QQC.ScrollBar {
|
||||
id: scroll
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (atYEnd) {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 30
|
||||
Layout.maximumHeight: root.height / 2
|
||||
|
||||
QQC.TextArea {
|
||||
id: messageInput
|
||||
|
||||
placeholderText: qsTr("Type your message here...")
|
||||
placeholderTextColor: palette.mid
|
||||
color: palette.text
|
||||
background: Rectangle {
|
||||
radius: 2
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
|
||||
root.sendChatMessage()
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AttachedFilesPlace {
|
||||
id: attachedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
attachedFilesModel: root.attachmentFiles
|
||||
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
|
||||
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
|
||||
}
|
||||
|
||||
AttachedFilesPlace {
|
||||
id: linkedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
attachedFilesModel: root.linkedFiles
|
||||
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
|
||||
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||
}
|
||||
|
||||
BottomBar {
|
||||
id: bottomBar
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
sendButton.onClicked: root.sendChatMessage()
|
||||
stopButton.onClicked: root.cancelRequest()
|
||||
syncOpenFiles {
|
||||
checked: root.isSyncOpenFiles
|
||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||
}
|
||||
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.chatModel.clear()
|
||||
root.clearAttachmentFiles()
|
||||
root.updateInputTokensCount()
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
Qt.callLater(chatListView.positionViewAtEnd)
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
root.sendMessage(messageInput.text)
|
||||
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}}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
ChatView/qml/dialog/CodeBlock.qml
Normal file
100
ChatView/qml/dialog/CodeBlock.qml
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
|
||||
readonly property string monospaceFont: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows":
|
||||
return "Consolas";
|
||||
case "osx":
|
||||
return "Menlo";
|
||||
case "linux":
|
||||
return "DejaVu Sans Mono";
|
||||
default:
|
||||
return "monospace";
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
radius: 4
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeText.implicitHeight + 20
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: codeText
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
font.family: root.monospaceFont
|
||||
font.pointSize: Qt.application.font.pointSize
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
text: root.language
|
||||
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
||||
: Qt.lighter(root.color, 1.1)
|
||||
font.pointSize: 8
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
text: "Copy"
|
||||
onClicked: {
|
||||
utils.copyToClipboard(root.code)
|
||||
text = qsTr("Copied")
|
||||
copyTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
interval: 2000
|
||||
onTriggered: parent.text = qsTr("Copy")
|
||||
}
|
||||
}
|
||||
}
|
||||
31
ChatView/qml/dialog/TextBlock.qml
Normal file
31
ChatView/qml/dialog/TextBlock.qml
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
TextEdit {
|
||||
id: root
|
||||
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.StyledText
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
}
|
||||
109
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
109
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
|
||||
property alias attachedFilesModel: attachRepeater.model
|
||||
property color accentColor: palette.mid
|
||||
property string iconPath
|
||||
|
||||
signal removeFileFromListByIndex(index: int)
|
||||
|
||||
spacing: 5
|
||||
leftPadding: 5
|
||||
rightPadding: 5
|
||||
topPadding: attachRepeater.model.length > 0 ? 2 : 0
|
||||
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
|
||||
|
||||
Repeater {
|
||||
id: attachRepeater
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
height: 30
|
||||
width: contentRow.width + 10
|
||||
radius: 4
|
||||
color: palette.button
|
||||
border.width: 1
|
||||
border.color: mouse.hovered ? palette.highlight : root.accentColor
|
||||
|
||||
HoverHandler {
|
||||
id: mouse
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
|
||||
spacing: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
|
||||
Image {
|
||||
id: icon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: root.iconPath
|
||||
sourceSize.width: 8
|
||||
sourceSize.height: 15
|
||||
}
|
||||
|
||||
Text {
|
||||
id: fileNameText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: palette.buttonText
|
||||
|
||||
text: {
|
||||
const parts = modelData.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeButton
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: closeIcon.width + 5
|
||||
height: closeButton.width + 5
|
||||
|
||||
onClicked: root.removeFileFromListByIndex(index)
|
||||
|
||||
Image {
|
||||
id: closeIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
|
||||
width: 6
|
||||
height: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
ChatView/qml/parts/BottomBar.qml
Normal file
98
ChatView/qml/parts/BottomBar.qml
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias sendButton: sendButtonId
|
||||
property alias stopButton: stopButtonId
|
||||
property alias syncOpenFiles: syncOpenFilesId
|
||||
property alias attachFiles: attachFilesId
|
||||
property alias linkFiles: linkFilesId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
|
||||
RowLayout {
|
||||
id: bottomBar
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: sendButtonId
|
||||
|
||||
text: qsTr("Send")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: stopButtonId
|
||||
|
||||
text: qsTr("Stop")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: attachFilesId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
text: qsTr("Attach files")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: linkFilesId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
text: qsTr("Link files")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: syncOpenFilesId
|
||||
|
||||
text: qsTr("Sync open files")
|
||||
|
||||
ToolTip.visible: syncOpenFilesId.hovered
|
||||
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
82
ChatView/qml/parts/TopBar.qml
Normal file
82
ChatView/qml/parts/TopBar.qml
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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias saveButton: saveButtonId
|
||||
property alias loadButton: loadButtonId
|
||||
property alias clearButton: clearButtonId
|
||||
property alias tokensBadge: tokensBadgeId
|
||||
property alias recentPath: recentPathId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: saveButtonId
|
||||
|
||||
text: qsTr("Save")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: loadButtonId
|
||||
|
||||
text: qsTr("Load")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: clearButtonId
|
||||
|
||||
text: qsTr("Clear")
|
||||
}
|
||||
|
||||
Text {
|
||||
id: recentPathId
|
||||
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideMiddle
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
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
|
||||
162
ConfigurationManager.cpp
Normal file
162
ConfigurationManager.cpp
Normal file
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 "ConfigurationManager.hpp"
|
||||
|
||||
#include <QTimer>
|
||||
#include <settings/ButtonAspect.hpp>
|
||||
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ConfigurationManager &ConfigurationManager::instance()
|
||||
{
|
||||
static ConfigurationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void ConfigurationManager::init()
|
||||
{
|
||||
setupConnections();
|
||||
}
|
||||
|
||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_generalSettings(Settings::generalSettings())
|
||||
, m_providersManager(LLMCore::ProvidersManager::instance())
|
||||
, m_templateManger(LLMCore::PromptTemplateManager::instance())
|
||||
{}
|
||||
|
||||
void ConfigurationManager::setupConnections()
|
||||
{
|
||||
using Config = ConfigurationManager;
|
||||
using Button = ButtonAspect;
|
||||
|
||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectProvider()
|
||||
{
|
||||
const auto providersList = m_providersManager.providersNames();
|
||||
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
|
||||
? m_generalSettings.ccProvider
|
||||
: m_generalSettings.caProvider;
|
||||
|
||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
||||
m_generalSettings.showSelectionDialog(providersList,
|
||||
targetSettings,
|
||||
Tr::tr("Select LLM Provider"),
|
||||
Tr::tr("Providers:"));
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectModel()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
||||
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
|
||||
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()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
||||
|
||||
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,
|
||||
targetSettings,
|
||||
Tr::tr("Select Template"),
|
||||
Tr::tr("Templates:"));
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
if (!urls.contains(url))
|
||||
urls.append(url);
|
||||
}
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
|
||||
? m_generalSettings.ccUrl
|
||||
: m_generalSettings.caUrl;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
58
ConfigurationManager.hpp
Normal file
58
ConfigurationManager.hpp
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ConfigurationManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ConfigurationManager &instance();
|
||||
|
||||
void init();
|
||||
|
||||
public slots:
|
||||
void selectProvider();
|
||||
void selectModel();
|
||||
void selectTemplate();
|
||||
void selectUrl();
|
||||
|
||||
private:
|
||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
||||
~ConfigurationManager() = default;
|
||||
ConfigurationManager(const ConfigurationManager &) = delete;
|
||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
||||
|
||||
Settings::GeneralSettings &m_generalSettings;
|
||||
LLMCore::ProvidersManager &m_providersManager;
|
||||
LLMCore::PromptTemplateManager &m_templateManger;
|
||||
|
||||
void setupConnections();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -23,14 +23,16 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
#include "DocumentContextReader.hpp"
|
||||
#include "LLMProvidersManager.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "QodeAssistUtils.hpp"
|
||||
#include "core/LLMRequestConfig.hpp"
|
||||
#include "settings/ContextSettings.hpp"
|
||||
#include "CodeHandler.hpp"
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "llmcore/MessageBuilder.hpp"
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
@ -39,7 +41,7 @@ LLMClientInterface::LLMClientInterface()
|
||||
: m_requestHandler(this)
|
||||
{
|
||||
connect(&m_requestHandler,
|
||||
&LLMRequestHandler::completionReceived,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
&LLMClientInterface::sendCompletionToClient);
|
||||
}
|
||||
@ -80,7 +82,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
} else if (method == "exit") {
|
||||
// TODO make exit handler
|
||||
} else {
|
||||
logMessage(QString("Unknown method: %1").arg(method));
|
||||
LOG_MESSAGE(QString("Unknown method: %1").arg(method));
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,9 +90,9 @@ void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
|
||||
{
|
||||
QString id = request["params"].toObject()["id"].toString();
|
||||
if (m_requestHandler.cancelRequest(id)) {
|
||||
logMessage(QString("Request %1 cancelled successfully").arg(id));
|
||||
LOG_MESSAGE(QString("Request %1 cancelled successfully").arg(id));
|
||||
} else {
|
||||
logMessage(QString("Request %1 not found").arg(id));
|
||||
LOG_MESSAGE(QString("Request %1 not found").arg(id));
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,30 +149,81 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
|
||||
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
{
|
||||
auto updatedContext = prepareContext(request);
|
||||
auto &completeSettings = Settings::codeCompletionSettings();
|
||||
|
||||
LLMConfig config;
|
||||
config.requestType = RequestType::Fim;
|
||||
config.provider = LLMProvidersManager::instance().getCurrentFimProvider();
|
||||
config.promptTemplate = PromptTemplateManager::instance().getCurrentFimTemplate();
|
||||
config.url = QUrl(QString("%1%2").arg(Settings::generalSettings().url(),
|
||||
Settings::generalSettings().endPoint()));
|
||||
auto providerName = Settings::generalSettings().ccProvider();
|
||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
config.providerRequest = {{"model", Settings::generalSettings().modelName.value()},
|
||||
{"stream", true},
|
||||
{"stop",
|
||||
QJsonArray::fromStringList(config.promptTemplate->stopWords())}};
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings::contextSettings().useSpecificInstructions())
|
||||
config.providerRequest["system"] = Settings::contextSettings().specificInstractions();
|
||||
auto templateName = Settings::generalSettings().ccTemplate();
|
||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
||||
templateName);
|
||||
|
||||
config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
|
||||
config.provider->prepareRequest(config.providerRequest);
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
return;
|
||||
}
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
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.multiLineCompletion = completeSettings.multiLineCompletion();
|
||||
|
||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
||||
if (!stopWords.isEmpty())
|
||||
config.providerRequest["stop"] = stopWords;
|
||||
|
||||
QString systemPrompt;
|
||||
if (completeSettings.useSystemPrompt())
|
||||
systemPrompt.append(completeSettings.systemPrompt());
|
||||
if (!updatedContext.fileContext.isEmpty())
|
||||
systemPrompt.append(updatedContext.fileContext);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
|
||||
const QStringView &accumulatedCompletion)
|
||||
LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
|
||||
const QStringView &accumulatedCompletion)
|
||||
{
|
||||
QJsonObject params = request["params"].toObject();
|
||||
QJsonObject doc = params["doc"].toObject();
|
||||
@ -182,14 +235,14 @@ ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
|
||||
filePath);
|
||||
|
||||
if (!textDocument) {
|
||||
logMessage("Error: Document is not available for" + filePath.toString());
|
||||
return ContextData{};
|
||||
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
|
||||
return LLMCore::ContextData{};
|
||||
}
|
||||
|
||||
int cursorPosition = position["character"].toInt();
|
||||
int lineNumber = position["line"].toInt();
|
||||
|
||||
DocumentContextReader reader(textDocument);
|
||||
Context::DocumentContextReader reader(textDocument);
|
||||
return reader.prepareContext(lineNumber, cursorPosition);
|
||||
}
|
||||
|
||||
@ -197,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;
|
||||
@ -218,11 +283,11 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||
result[LanguageServerProtocol::isIncompleteKey] = !isComplete;
|
||||
response[LanguageServerProtocol::resultKey] = result;
|
||||
|
||||
logMessage(
|
||||
LOG_MESSAGE(
|
||||
QString("Completions: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
|
||||
|
||||
logMessage(QString("Full response: \n%1")
|
||||
LOG_MESSAGE(QString("Full response: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
@ -250,7 +315,7 @@ void LLMClientInterface::logPerformance(const QString &requestId,
|
||||
const QString &operation,
|
||||
qint64 elapsedMs)
|
||||
{
|
||||
logMessage(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
|
||||
LOG_MESSAGE(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
|
||||
}
|
||||
|
||||
void LLMClientInterface::parseCurrentMessage() {}
|
||||
|
||||
@ -22,8 +22,8 @@
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "QodeAssistData.hpp"
|
||||
#include "core/LLMRequestHandler.hpp"
|
||||
#include <llmcore/ContextData.hpp>
|
||||
#include <llmcore/RequestHandler.hpp>
|
||||
|
||||
class QNetworkReply;
|
||||
class QNetworkAccessManager;
|
||||
@ -58,10 +58,10 @@ private:
|
||||
void handleExit(const QJsonObject &request);
|
||||
void handleCancelRequest(const QJsonObject &request);
|
||||
|
||||
ContextData prepareContext(const QJsonObject &request,
|
||||
const QStringView &accumulatedCompletion = QString{});
|
||||
LLMCore::ContextData prepareContext(const QJsonObject &request,
|
||||
const QStringView &accumulatedCompletion = QString{});
|
||||
|
||||
LLMRequestHandler m_requestHandler;
|
||||
LLMCore::RequestHandler m_requestHandler;
|
||||
QElapsedTimer m_completionTimer;
|
||||
QMap<QString, qint64> m_requestStartTimes;
|
||||
|
||||
|
||||
@ -1,86 +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 "LLMProvidersManager.hpp"
|
||||
|
||||
#include "QodeAssistUtils.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMProvidersManager &LLMProvidersManager::instance()
|
||||
{
|
||||
static LLMProvidersManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Providers::LLMProvider *LLMProvidersManager::setCurrentFimProvider(const QString &name)
|
||||
{
|
||||
logMessage("Setting current FIM provider to: " + name);
|
||||
if (!m_providers.contains(name)) {
|
||||
logMessage("Can't find provider with name: " + name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_currentFimProvider = m_providers[name];
|
||||
return m_currentFimProvider;
|
||||
}
|
||||
|
||||
Providers::LLMProvider *LLMProvidersManager::setCurrentChatProvider(const QString &name)
|
||||
{
|
||||
logMessage("Setting current chat provider to: " + name);
|
||||
if (!m_providers.contains(name)) {
|
||||
logMessage("Can't find chat provider with name: " + name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_currentChatProvider = m_providers[name];
|
||||
return m_currentChatProvider;
|
||||
}
|
||||
|
||||
Providers::LLMProvider *LLMProvidersManager::getCurrentFimProvider()
|
||||
{
|
||||
if (m_currentFimProvider == nullptr) {
|
||||
logMessage("Current fim provider is null");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_currentFimProvider;
|
||||
}
|
||||
|
||||
Providers::LLMProvider *LLMProvidersManager::getCurrentChatProvider()
|
||||
{
|
||||
if (m_currentChatProvider == nullptr) {
|
||||
logMessage("Current chat provider is null");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_currentChatProvider;
|
||||
}
|
||||
|
||||
QStringList LLMProvidersManager::providersNames() const
|
||||
{
|
||||
return m_providers.keys();
|
||||
}
|
||||
|
||||
LLMProvidersManager::~LLMProvidersManager()
|
||||
{
|
||||
qDeleteAll(m_providers);
|
||||
}
|
||||
|
||||
} // 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
|
||||
@ -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,88 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PromptTemplateManager.hpp"
|
||||
|
||||
#include "QodeAssistUtils.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
PromptTemplateManager &PromptTemplateManager::instance()
|
||||
{
|
||||
static PromptTemplateManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void PromptTemplateManager::setCurrentFimTemplate(const QString &name)
|
||||
{
|
||||
logMessage("Setting current FIM provider to: " + name);
|
||||
if (!m_fimTemplates.contains(name) || m_fimTemplates[name] == nullptr) {
|
||||
logMessage("Error to set current FIM template" + name);
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentFimTemplate = m_fimTemplates[name];
|
||||
}
|
||||
|
||||
Templates::PromptTemplate *PromptTemplateManager::getCurrentFimTemplate()
|
||||
{
|
||||
if (m_currentFimTemplate == nullptr) {
|
||||
logMessage("Current fim provider is null");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_currentFimTemplate;
|
||||
}
|
||||
|
||||
void PromptTemplateManager::setCurrentChatTemplate(const QString &name)
|
||||
{
|
||||
logMessage("Setting current chat provider to: " + name);
|
||||
if (!m_chatTemplates.contains(name) || m_chatTemplates[name] == nullptr) {
|
||||
logMessage("Error to set current chat template" + name);
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentChatTemplate = m_chatTemplates[name];
|
||||
}
|
||||
|
||||
Templates::PromptTemplate *PromptTemplateManager::getCurrentChatTemplate()
|
||||
{
|
||||
if (m_currentChatTemplate == nullptr)
|
||||
logMessage("Current chat provider is null");
|
||||
|
||||
return m_currentChatTemplate;
|
||||
}
|
||||
|
||||
QStringList PromptTemplateManager::fimTemplatesNames() const
|
||||
{
|
||||
return m_fimTemplates.keys();
|
||||
}
|
||||
|
||||
QStringList PromptTemplateManager::chatTemplatesNames() const
|
||||
{
|
||||
return m_chatTemplates.keys();
|
||||
}
|
||||
|
||||
PromptTemplateManager::~PromptTemplateManager()
|
||||
{
|
||||
qDeleteAll(m_fimTemplates);
|
||||
qDeleteAll(m_chatTemplates);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,16 +1,13 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.2.3",
|
||||
"CompatVersion" : "${IDE_VERSION_COMPAT}",
|
||||
"Version" : "0.4.9",
|
||||
"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/ContextSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProjectSettings.hpp"
|
||||
#include <context/ChangesManager.h>
|
||||
|
||||
using namespace LanguageServerProtocol;
|
||||
using namespace TextEditor;
|
||||
@ -70,47 +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::generalSettings().enableAutoComplete())
|
||||
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::contextSettings().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::generalSettings().autoCompletionTypingInterval()) {
|
||||
m_recentCharCount = charsAdded;
|
||||
m_typingTimer.restart();
|
||||
}
|
||||
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_recentCharCount > Settings::generalSettings().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)
|
||||
@ -154,7 +171,8 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
if (editor
|
||||
&& editor->textCursor().position()
|
||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
|
||||
&& m_recentCharCount > Settings::generalSettings().autoCompletionCharThreshold())
|
||||
&& m_recentCharCount
|
||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
|
||||
requestCompletions(editor);
|
||||
});
|
||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||
@ -168,7 +186,7 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
}
|
||||
|
||||
it.value()->setProperty("cursorPosition", editor->textCursor().position());
|
||||
it.value()->start(Settings::generalSettings().startSuggestionTimer());
|
||||
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
|
||||
}
|
||||
void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
|
||||
TextEditor::TextEditorWidget *editor)
|
||||
@ -191,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) {
|
||||
@ -209,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()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,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()
|
||||
|
||||
@ -24,60 +24,6 @@ namespace QodeAssist::Constants {
|
||||
const char ACTION_ID[] = "QodeAssist.Action";
|
||||
const char MENU_ID[] = "QodeAssist.Menu";
|
||||
|
||||
// settings
|
||||
const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
|
||||
const char ENABLE_AUTO_COMPLETE[] = "QodeAssist.enableAutoComplete";
|
||||
const char ENABLE_LOGGING[] = "QodeAssist.enableLogging";
|
||||
const char LLM_PROVIDERS[] = "QodeAssist.llmProviders";
|
||||
const char URL[] = "QodeAssist.url";
|
||||
const char END_POINT[] = "QodeAssist.endPoint";
|
||||
const char MODEL_NAME[] = "QodeAssist.modelName";
|
||||
const char SELECT_MODELS[] = "QodeAssist.selectModels";
|
||||
const char FIM_PROMPTS[] = "QodeAssist.fimPrompts";
|
||||
const char TEMPERATURE[] = "QodeAssist.temperature";
|
||||
const char MAX_TOKENS[] = "QodeAssist.maxTokens";
|
||||
const char READ_FULL_FILE[] = "QodeAssist.readFullFile";
|
||||
const char READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.readStringsBeforeCursor";
|
||||
const char READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.readStringsAfterCursor";
|
||||
const char USE_TOP_P[] = "QodeAssist.useTopP";
|
||||
const char TOP_P[] = "QodeAssist.topP";
|
||||
const char USE_TOP_K[] = "QodeAssist.useTopK";
|
||||
const char TOP_K[] = "QodeAssist.topK";
|
||||
const char USE_PRESENCE_PENALTY[] = "QodeAssist.usePresencePenalty";
|
||||
const char PRESENCE_PENALTY[] = "QodeAssist.presencePenalty";
|
||||
const char USE_FREQUENCY_PENALTY[] = "QodeAssist.useFrequencyPenalty";
|
||||
const char FREQUENCY_PENALTY[] = "QodeAssist.frequencyPenalty";
|
||||
const char PROVIDER_PATHS[] = "QodeAssist.providerPaths";
|
||||
const char START_SUGGESTION_TIMER[] = "QodeAssist.startSuggestionTimer";
|
||||
const char AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCharThreshold";
|
||||
const char AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
|
||||
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";
|
||||
const char OLLAMA_LIVETIME[] = "QodeAssist.ollamaLivetime";
|
||||
const char SYSTEM_PROMPT[] = "QodeAssist.systemPrompt";
|
||||
const char MULTILINE_COMPLETION[] = "QodeAssist.multilineCompletion";
|
||||
const char API_KEY[] = "QodeAssist.apiKey";
|
||||
const char USE_SYSTEM_PROMPT[] = "QodeAssist.useSystemPrompt";
|
||||
const char USE_FILE_PATH_IN_CONTEXT[] = "QodeAssist.useFilePathInContext";
|
||||
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
|
||||
const char USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.useProjectChangesCache";
|
||||
const char MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.maxChangesCacheSize";
|
||||
const char CHAT_LLM_PROVIDERS[] = "QodeAssist.chatLlmProviders";
|
||||
const char CHAT_URL[] = "QodeAssist.chatUrl";
|
||||
const char CHAT_END_POINT[] = "QodeAssist.chatEndPoint";
|
||||
const char CHAT_MODEL_NAME[] = "QodeAssist.chatModelName";
|
||||
const char CHAT_SELECT_MODELS[] = "QodeAssist.chatSelectModels";
|
||||
const char CHAT_PROMPTS[] = "QodeAssist.chatPrompts";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
const char QODE_ASSIST_CONTEXT_SETTINGS_PAGE_ID[] = "QodeAssist.2ContextSettingsPageId";
|
||||
const char QODE_ASSIST_PRESET_PROMPTS_SETTINGS_PAGE_ID[]
|
||||
= "QodeAssist.3PresetPromptsSettingsPageId";
|
||||
const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.4CustomPromptSettingsPageId";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "Qode Assist";
|
||||
|
||||
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
|
||||
|
||||
} // namespace QodeAssist::Constants
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <coreplugin/messagemanager.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
inline bool &loggingEnabled()
|
||||
{
|
||||
static bool enabled = false;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
inline void setLoggingEnabled(bool enable)
|
||||
{
|
||||
loggingEnabled() = enable;
|
||||
}
|
||||
|
||||
inline void logMessage(const QString &message, bool silent = true)
|
||||
{
|
||||
if (!loggingEnabled())
|
||||
return;
|
||||
|
||||
const QString prefixedMessage = QLatin1String("[Qode Assist] ") + message;
|
||||
if (silent) {
|
||||
Core::MessageManager::writeSilently(prefixedMessage);
|
||||
} else {
|
||||
Core::MessageManager::writeFlashing(prefixedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
inline void logMessages(const QStringList &messages, bool silent = true)
|
||||
{
|
||||
if (!loggingEnabled())
|
||||
return;
|
||||
|
||||
QStringList prefixedMessages;
|
||||
qDebug() << prefixedMessages;
|
||||
|
||||
for (const QString &message : messages) {
|
||||
prefixedMessages << (QLatin1String("[Qode Assist] ") + message);
|
||||
}
|
||||
if (silent) {
|
||||
Core::MessageManager::writeSilently(prefixedMessages);
|
||||
} else {
|
||||
Core::MessageManager::writeFlashing(prefixedMessages);
|
||||
}
|
||||
}
|
||||
|
||||
inline bool pingUrl(const QUrl &url, int timeout = 5000)
|
||||
{
|
||||
if (!url.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true);
|
||||
|
||||
QScopedPointer<QNetworkReply> reply(manager.get(request));
|
||||
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
|
||||
timer.start(timeout);
|
||||
loop.exec();
|
||||
|
||||
if (timer.isActive()) {
|
||||
timer.stop();
|
||||
return (reply->error() == QNetworkReply::NoError);
|
||||
} else {
|
||||
QObject::disconnect(reply.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
reply->abort();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
226
README.md
226
README.md
@ -1,111 +1,179 @@
|
||||
# QodeAssist
|
||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
||||
[](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.
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
||||
⚠️ **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
|
||||
|
||||
## Supported LLM Providers
|
||||
QodeAssist currently supports the following LLM (Large Language Model) providers:
|
||||
- [Ollama](https://ollama.com)
|
||||
- [LM Studio](https://lmstudio.ai)
|
||||
- OpenAI compatible providers
|
||||
## Table of Contents
|
||||
1. [Overview](#overview)
|
||||
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
|
||||
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
|
||||
4. [Configure for OpenAI](#configure-for-openai)
|
||||
5. [Configure for using Ollama](#configure-for-using-ollama)
|
||||
6. [System Prompt Configuration](#system-prompt-configuration)
|
||||
7. [Template-Model Compatibility](#template-model-compatibility)
|
||||
8. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
9. [Development Progress](#development-progress)
|
||||
10. [Hotkeys](#hotkeys)
|
||||
11. [Troubleshooting](#troubleshooting)
|
||||
12. [Support the Development](#support-the-development-of-qodeassist)
|
||||
13. [How to Build](#how-to-build)
|
||||
|
||||
## Supported Models
|
||||
## Overview
|
||||
|
||||
QodeAssist has been thoroughly tested and optimized for use with the following language models, all of which are specifically trained for Fill-in-the-Middle (FIM) tasks:
|
||||
- AI-powered code completion
|
||||
- Chat functionality:
|
||||
- Side and Bottom panels
|
||||
- Support for multiple LLM providers:
|
||||
- Ollama
|
||||
- OpenAI
|
||||
- Anthropic Claude
|
||||
- LM Studio
|
||||
- OpenAI-compatible providers(eg. https://openrouter.ai)
|
||||
- Extensive library of model-specific templates
|
||||
- Custom template support
|
||||
- Easy configuration and model selection
|
||||
|
||||
- CodeLlama
|
||||
- StarCoder2
|
||||
- DeepSeek-Coder-V2-Lite-Base
|
||||
<details>
|
||||
<summary>Code completion: (click to expand)</summary>
|
||||
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
||||
</details>
|
||||
|
||||
These models have demonstrated excellent performance in code completion and assistance tasks within the QodeAssist environment.
|
||||
<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>
|
||||
|
||||
### Custom Prompts
|
||||
<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">
|
||||
</details>
|
||||
|
||||
For advanced users or those with specific requirements, QodeAssist offers the flexibility to create, save, and load custom prompts using JSON templates. This feature allows you to tailor the AI's behavior to your exact needs.
|
||||
<details>
|
||||
<summary>Chat with LLM models in bottom panel: (click to expand)</summary>
|
||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||
</details>
|
||||
|
||||
To get started with custom prompts:
|
||||
## Install plugin to QtCreator
|
||||
1. Install Latest Qt Creator
|
||||
2. Download the QodeAssist plugin for your Qt Creator
|
||||
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
|
||||
|
||||
1. Navigate to the "Custom Template" option in the FIM Prompt Settings.
|
||||
2. Create your custom JSON prompt template.
|
||||
3. Use the "Save Custom Template to JSON" button to store your template for future use.
|
||||
4. To use a previously saved template, click "Load Custom Template from JSON".
|
||||
5. Make sure to select "Custom Template" from the dropdown menu in the FIM Prompt Settings on the General page to activate your custom template.
|
||||
## Configure for Anthropic Claude
|
||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||
2. Go to Provider Settings tab and configure Claude api key
|
||||
3. Return to General tab and configure:
|
||||
- Set "Claude" as the provider for code completion or/and chat assistant
|
||||
- Set the Claude URL (https://api.anthropic.com)
|
||||
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
|
||||
- Choose the Claude template for code completion or/and chat
|
||||
<details>
|
||||
<summary>Example of Claude settings: (click to expand)</summary>
|
||||
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
|
||||
</details>
|
||||
|
||||
For inspiration and examples of effective custom prompts, please refer to the `rawPromptExamples` folder in our repository.
|
||||
## Configure for OpenAI
|
||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||
2. Go to Provider Settings tab and configure OpenAI api key
|
||||
3. Return to General tab and configure:
|
||||
- Set "OpenAI" as the provider for code completion or/and chat assistant
|
||||
- Set the OpenAI URL (https://api.openai.com)
|
||||
- Select your preferred model (e.g., gpt-4o)
|
||||
- Choose the OpenAI template for code completion or/and chat
|
||||
<details>
|
||||
<summary>Example of OpenAI settings: (click to expand)</summary>
|
||||
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
|
||||
</details>
|
||||
|
||||
<img width="600" alt="Custom template" src="https://github.com/user-attachments/assets/4a14c552-baba-4531-ab4f-cb1f9ac6620b">
|
||||
<img width="600" alt="Select custom template" src="https://github.com/user-attachments/assets/3651dafd-83f9-4df9-943f-69c28cd3d8a3">
|
||||
## Configure for using Ollama
|
||||
|
||||
### Tested Models
|
||||
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
||||
2. Install a language models in Ollama via terminal. For example, you can run:
|
||||
|
||||
#### Ollama:
|
||||
- [starcoder2](https://ollama.com/library/starcoder2)
|
||||
- [codellama](https://ollama.com/library/codellama)
|
||||
For standard computers (minimum 8GB RAM):
|
||||
```
|
||||
ollama run qwen2.5-coder:7b
|
||||
```
|
||||
For better performance (16GB+ RAM):
|
||||
```
|
||||
ollama run qwen2.5-coder:14b
|
||||
```
|
||||
For high-end systems (32GB+ RAM):
|
||||
```
|
||||
ollama run qwen2.5-coder:32b
|
||||
```
|
||||
|
||||
#### LM Studio:
|
||||
- [second-state/StarCoder2-7B-GGUF](https://huggingface.co/second-state/StarCoder2-7B-GGUF)
|
||||
- [TheBloke/CodeLlama-7B-GGUF](https://huggingface.co/TheBloke/CodeLlama-7B-GGUF)
|
||||
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
|
||||
2. Navigate to the "Qode Assist" tab
|
||||
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 or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
|
||||
4. Click Apply if you made any changes
|
||||
|
||||
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.
|
||||
You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
<details>
|
||||
<summary>Example of Ollama settings: (click to expand)</summary>
|
||||
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
|
||||
</details>
|
||||
|
||||
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.
|
||||
## System Prompt Configuration
|
||||
|
||||
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
|
||||
|
||||
## Template-Model Compatibility
|
||||
|
||||
| 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 |
|
||||
|
||||
## 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
|
||||
|
||||
## Development Progress
|
||||
|
||||
- [x] Basic plugin with code autocomplete functionality
|
||||
- [x] Improve and automate settings
|
||||
- [ ] Add chat functionality
|
||||
- [x] Add chat functionality
|
||||
- [x] Sharing diff with model
|
||||
- [ ] Sharing project source with model
|
||||
- [ ] Support for more providers and models
|
||||
|
||||
## Plugin installation using Ollama as an example
|
||||
|
||||
1. Install QtCreator 14.0
|
||||
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
||||
3. Install a language models in Ollama. For example, you can run:
|
||||
|
||||
For suggestions:
|
||||
```
|
||||
ollama run codellama:7b-code
|
||||
```
|
||||
For chat:
|
||||
```
|
||||
ollama run codellama:7b-instruct
|
||||
```
|
||||
4. Download the QodeAssist plugin.
|
||||
5. Launch Qt Creator and install the plugin:
|
||||
- Go to MacOS: Qt Creator -> About Plugins...
|
||||
Windows\Linux: Help -> About Plugins...
|
||||
- Click on "Install Plugin..."
|
||||
- Select the downloaded QodeAssist plugin archive file
|
||||
|
||||
## Configure Plugin
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/0743d09e-1f02-44ed-9a1a-85e2a0a0c01a" width="800" alt="QodeAssist в действии">
|
||||
|
||||
1. Open Qt Creator settings
|
||||
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 load model
|
||||
6. Choose the prompt template that corresponds to your model
|
||||
7. Apply the settings
|
||||
|
||||
You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
|
||||
## Hotkeys
|
||||
|
||||
- To call manual request to suggestion, you can use or change it in settings
|
||||
- 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
|
||||
|
||||
@ -145,7 +213,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`
|
||||
@ -164,3 +231,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
|
||||
@ -1,192 +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 "ChatClientInterface.hpp"
|
||||
#include "LLMProvidersManager.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "QodeAssistUtils.hpp"
|
||||
#include "settings/ContextSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/PresetPromptsSettings.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
int ChatHistory::estimateTokenCount(const QString &text) const
|
||||
{
|
||||
return text.length() / 4;
|
||||
}
|
||||
|
||||
void ChatHistory::addMessage(ChatMessage::Role role, const QString &content)
|
||||
{
|
||||
int tokenCount = estimateTokenCount(content);
|
||||
m_messages.append({role, content, tokenCount});
|
||||
m_totalTokens += tokenCount;
|
||||
trim();
|
||||
}
|
||||
void ChatHistory::clear()
|
||||
{
|
||||
m_messages.clear();
|
||||
m_totalTokens = 0;
|
||||
}
|
||||
|
||||
QVector<ChatMessage> ChatHistory::getMessages() const
|
||||
{
|
||||
return m_messages;
|
||||
}
|
||||
|
||||
QString ChatHistory::getSystemPrompt() const
|
||||
{
|
||||
return m_systemPrompt;
|
||||
}
|
||||
|
||||
void ChatHistory::setSystemPrompt(const QString &prompt)
|
||||
{
|
||||
m_systemPrompt = prompt;
|
||||
}
|
||||
|
||||
void ChatHistory::trim()
|
||||
{
|
||||
while (m_messages.size() > MAX_HISTORY_SIZE || m_totalTokens > MAX_TOKENS) {
|
||||
if (!m_messages.isEmpty()) {
|
||||
m_totalTokens -= m_messages.first().tokenCount;
|
||||
m_messages.removeFirst();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChatClientInterface::ChatClientInterface(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_requestHandler(new LLMRequestHandler(this))
|
||||
{
|
||||
connect(m_requestHandler,
|
||||
&LLMRequestHandler::completionReceived,
|
||||
this,
|
||||
[this](const QString &completion, const QJsonObject &, bool isComplete) {
|
||||
handleLLMResponse(completion, isComplete);
|
||||
});
|
||||
|
||||
connect(m_requestHandler,
|
||||
&LLMRequestHandler::requestFinished,
|
||||
this,
|
||||
[this](const QString &, bool success, const QString &errorString) {
|
||||
if (!success) {
|
||||
emit errorOccurred(errorString);
|
||||
}
|
||||
});
|
||||
|
||||
m_chatHistory.setSystemPrompt("You are a helpful C++ and QML programming assistant.");
|
||||
}
|
||||
|
||||
ChatClientInterface::~ChatClientInterface() = default;
|
||||
|
||||
void ChatClientInterface::sendMessage(const QString &message)
|
||||
{
|
||||
logMessage("Sending message: " + message);
|
||||
logMessage("chatProvider " + Settings::generalSettings().chatLlmProviders.stringValue());
|
||||
logMessage("chatTemplate " + Settings::generalSettings().chatPrompts.stringValue());
|
||||
|
||||
auto chatTemplate = PromptTemplateManager::instance().getCurrentChatTemplate();
|
||||
auto chatProvider = LLMProvidersManager::instance().getCurrentChatProvider();
|
||||
|
||||
ContextData context;
|
||||
context.prefix = message;
|
||||
context.suffix = "";
|
||||
if (Settings::contextSettings().useSpecificInstructions())
|
||||
context.instriuctions = Settings::contextSettings().specificInstractions();
|
||||
|
||||
QJsonObject providerRequest;
|
||||
providerRequest["model"] = Settings::generalSettings().chatModelName();
|
||||
providerRequest["stream"] = true;
|
||||
providerRequest["messages"] = prepareMessagesForRequest();
|
||||
|
||||
chatTemplate->prepareRequest(providerRequest, context);
|
||||
chatProvider->prepareRequest(providerRequest);
|
||||
|
||||
LLMConfig config;
|
||||
config.requestType = RequestType::Chat;
|
||||
config.provider = chatProvider;
|
||||
config.promptTemplate = chatTemplate;
|
||||
config.url = QString("%1%2").arg(Settings::generalSettings().chatUrl(),
|
||||
Settings::generalSettings().chatEndPoint());
|
||||
config.providerRequest = providerRequest;
|
||||
|
||||
QJsonObject request;
|
||||
request["id"] = QUuid::createUuid().toString();
|
||||
|
||||
m_accumulatedResponse.clear();
|
||||
m_chatHistory.addMessage(ChatMessage::Role::User, message);
|
||||
m_requestHandler->sendLLMRequest(config, request);
|
||||
}
|
||||
|
||||
void ChatClientInterface::clearMessages()
|
||||
{
|
||||
m_chatHistory.clear();
|
||||
m_accumulatedResponse.clear();
|
||||
logMessage("Chat history cleared");
|
||||
}
|
||||
|
||||
QVector<ChatMessage> ChatClientInterface::getChatHistory() const
|
||||
{
|
||||
return m_chatHistory.getMessages();
|
||||
}
|
||||
|
||||
void ChatClientInterface::handleLLMResponse(const QString &response, bool isComplete)
|
||||
{
|
||||
m_accumulatedResponse += response;
|
||||
|
||||
if (isComplete) {
|
||||
logMessage("Message completed. Final response: " + m_accumulatedResponse);
|
||||
emit messageReceived(m_accumulatedResponse.trimmed());
|
||||
|
||||
m_chatHistory.addMessage(ChatMessage::Role::Assistant, m_accumulatedResponse.trimmed());
|
||||
m_accumulatedResponse.clear();
|
||||
}
|
||||
}
|
||||
|
||||
QJsonArray ChatClientInterface::prepareMessagesForRequest() const
|
||||
{
|
||||
QJsonArray messages;
|
||||
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", m_chatHistory.getSystemPrompt()}});
|
||||
|
||||
for (const auto &message : m_chatHistory.getMessages()) {
|
||||
QString role;
|
||||
switch (message.role) {
|
||||
case ChatMessage::Role::User:
|
||||
role = "user";
|
||||
break;
|
||||
case ChatMessage::Role::Assistant:
|
||||
role = "assistant";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
messages.append(QJsonObject{{"role", role}, {"content", message.content}});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include "QodeAssistData.hpp"
|
||||
#include "core/LLMRequestHandler.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
struct ChatMessage
|
||||
{
|
||||
enum class Role { System, User, Assistant };
|
||||
Role role;
|
||||
QString content;
|
||||
int tokenCount;
|
||||
};
|
||||
|
||||
class ChatHistory
|
||||
{
|
||||
public:
|
||||
void addMessage(ChatMessage::Role role, const QString &content);
|
||||
void clear();
|
||||
QVector<ChatMessage> getMessages() const;
|
||||
QString getSystemPrompt() const;
|
||||
void setSystemPrompt(const QString &prompt);
|
||||
void trim();
|
||||
|
||||
private:
|
||||
QVector<ChatMessage> m_messages;
|
||||
QString m_systemPrompt;
|
||||
int m_totalTokens = 0;
|
||||
static const int MAX_HISTORY_SIZE = 50;
|
||||
static const int MAX_TOKENS = 4000;
|
||||
|
||||
int estimateTokenCount(const QString &text) const;
|
||||
};
|
||||
|
||||
class ChatClientInterface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatClientInterface(QObject *parent = nullptr);
|
||||
~ChatClientInterface();
|
||||
|
||||
void sendMessage(const QString &message);
|
||||
void clearMessages();
|
||||
QVector<ChatMessage> getChatHistory() const;
|
||||
|
||||
signals:
|
||||
void messageReceived(const QString &message);
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, bool isComplete);
|
||||
QJsonArray prepareMessagesForRequest() const;
|
||||
|
||||
LLMRequestHandler *m_requestHandler;
|
||||
QString m_accumulatedResponse;
|
||||
ChatHistory m_chatHistory;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ChatWidget.h"
|
||||
#include "ChatView/ChatWidget.hpp"
|
||||
#include <coreplugin/ioutputpane.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -1,147 +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 "ChatWidget.h"
|
||||
#include "QodeAssistUtils.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QScrollBar>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatWidget::ChatWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_showTimestamp(false)
|
||||
, m_chatClient(new ChatClientInterface(this))
|
||||
{
|
||||
setupUi();
|
||||
|
||||
connect(m_sendButton, &QPushButton::clicked, this, &ChatWidget::sendMessage);
|
||||
connect(m_messageInput, &QLineEdit::returnPressed, this, &ChatWidget::sendMessage);
|
||||
|
||||
connect(m_chatClient, &ChatClientInterface::messageReceived, this, &ChatWidget::receiveMessage);
|
||||
connect(m_chatClient, &ChatClientInterface::errorOccurred, this, &ChatWidget::handleError);
|
||||
|
||||
logMessage("ChatWidget initialized");
|
||||
}
|
||||
|
||||
void ChatWidget::setupUi()
|
||||
{
|
||||
m_chatDisplay = new QTextEdit(this);
|
||||
m_chatDisplay->setReadOnly(true);
|
||||
|
||||
m_messageInput = new QLineEdit(this);
|
||||
m_sendButton = new QPushButton("Send", this);
|
||||
|
||||
QHBoxLayout *inputLayout = new QHBoxLayout;
|
||||
inputLayout->addWidget(m_messageInput);
|
||||
inputLayout->addWidget(m_sendButton);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addWidget(m_chatDisplay);
|
||||
mainLayout->addLayout(inputLayout);
|
||||
|
||||
setLayout(mainLayout);
|
||||
}
|
||||
|
||||
void ChatWidget::sendMessage()
|
||||
{
|
||||
QString message = m_messageInput->text().trimmed();
|
||||
if (!message.isEmpty()) {
|
||||
logMessage("Sending message: " + message);
|
||||
addMessage(message, true);
|
||||
m_chatClient->sendMessage(message);
|
||||
m_messageInput->clear();
|
||||
addMessage("AI is typing...", false);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatWidget::receiveMessage(const QString &message)
|
||||
{
|
||||
updateLastAIMessage(message);
|
||||
}
|
||||
|
||||
void ChatWidget::receivePartialMessage(const QString &partialMessage)
|
||||
{
|
||||
logMessage("Received partial message: " + partialMessage);
|
||||
m_currentAIResponse += partialMessage;
|
||||
updateLastAIMessage(m_currentAIResponse);
|
||||
}
|
||||
|
||||
void ChatWidget::onMessageCompleted()
|
||||
{
|
||||
updateLastAIMessage(m_currentAIResponse);
|
||||
m_currentAIResponse.clear();
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
void ChatWidget::handleError(const QString &error)
|
||||
{
|
||||
logMessage("Error occurred: " + error);
|
||||
addMessage("Error: " + error, false);
|
||||
}
|
||||
|
||||
void ChatWidget::addMessage(const QString &message, bool fromUser)
|
||||
{
|
||||
auto prefix = fromUser ? "You: " : "AI: ";
|
||||
QString timestamp = m_showTimestamp ? QDateTime::currentDateTime().toString("[hh:mm:ss] ") : "";
|
||||
QString fullMessage = timestamp + prefix + message;
|
||||
m_chatDisplay->append(fullMessage);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
void ChatWidget::updateLastAIMessage(const QString &message)
|
||||
{
|
||||
QTextCursor cursor = m_chatDisplay->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
|
||||
cursor.removeSelectedText();
|
||||
|
||||
QString timestamp = m_showTimestamp ? QDateTime::currentDateTime().toString("[hh:mm:ss] ") : "";
|
||||
cursor.insertText(timestamp + "AI: " + message);
|
||||
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
m_chatDisplay->setTextCursor(cursor);
|
||||
|
||||
scrollToBottom();
|
||||
m_chatDisplay->repaint();
|
||||
}
|
||||
|
||||
void ChatWidget::clear()
|
||||
{
|
||||
m_chatDisplay->clear();
|
||||
m_currentAIResponse.clear();
|
||||
m_chatClient->clearMessages();
|
||||
}
|
||||
|
||||
void ChatWidget::scrollToBottom()
|
||||
{
|
||||
QScrollBar *scrollBar = m_chatDisplay->verticalScrollBar();
|
||||
scrollBar->setValue(scrollBar->maximum());
|
||||
}
|
||||
|
||||
void ChatWidget::setShowTimestamp(bool show)
|
||||
{
|
||||
m_showTimestamp = show;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QWidget>
|
||||
|
||||
#include "ChatClientInterface.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatWidget(QWidget *parent = nullptr);
|
||||
|
||||
void clear();
|
||||
void scrollToBottom();
|
||||
void setShowTimestamp(bool show);
|
||||
|
||||
void receiveMessage(const QString &message);
|
||||
|
||||
private slots:
|
||||
void sendMessage();
|
||||
void receivePartialMessage(const QString &partialMessage);
|
||||
void onMessageCompleted();
|
||||
void handleError(const QString &error);
|
||||
|
||||
private:
|
||||
QTextEdit *m_chatDisplay;
|
||||
QLineEdit *m_messageInput;
|
||||
QPushButton *m_sendButton;
|
||||
bool m_showTimestamp;
|
||||
ChatClientInterface *m_chatClient;
|
||||
QString m_currentAIResponse;
|
||||
|
||||
void setupUi();
|
||||
void addMessage(const QString &message, bool fromUser = true);
|
||||
void updateLastAIMessage(const QString &message);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
45
chat/NavigationPanel.cpp
Normal file
45
chat/NavigationPanel.cpp
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 "NavigationPanel.hpp"
|
||||
|
||||
#include "ChatView/ChatWidget.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
NavigationPanel::NavigationPanel() {
|
||||
setDisplayName(tr("QodeAssist Chat"));
|
||||
setPriority(500);
|
||||
setId("QodeAssistChat");
|
||||
setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C));
|
||||
}
|
||||
|
||||
NavigationPanel::~NavigationPanel()
|
||||
{
|
||||
}
|
||||
|
||||
Core::NavigationView NavigationPanel::createWidget()
|
||||
{
|
||||
Core::NavigationView view;
|
||||
view.widget = new ChatWidget;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
37
chat/NavigationPanel.hpp
Normal file
37
chat/NavigationPanel.hpp
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 <coreplugin/inavigationwidgetfactory.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class NavigationPanel : public Core::INavigationWidgetFactory
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit NavigationPanel();
|
||||
~NavigationPanel();
|
||||
|
||||
Core::NavigationView createWidget() override;
|
||||
};
|
||||
|
||||
}
|
||||
21
context/CMakeLists.txt
Normal file
21
context/CMakeLists.txt
Normal file
@ -0,0 +1,21 @@
|
||||
add_library(Context STATIC
|
||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||
ChangesManager.h ChangesManager.cpp
|
||||
ContextManager.hpp ContextManager.cpp
|
||||
ContentFile.hpp
|
||||
TokenUtils.hpp TokenUtils.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(Context
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
QtCreator::Core
|
||||
QtCreator::TextEditor
|
||||
QtCreator::Utils
|
||||
QtCreator::ProjectExplorer
|
||||
PRIVATE
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
)
|
||||
|
||||
target_include_directories(Context PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR})
|
||||
@ -18,10 +18,9 @@
|
||||
*/
|
||||
|
||||
#include "ChangesManager.h"
|
||||
#include "QodeAssistUtils.hpp"
|
||||
#include "settings/ContextSettings.hpp"
|
||||
#include "CodeCompletionSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
ChangesManager &ChangesManager::instance()
|
||||
{
|
||||
@ -61,7 +60,7 @@ void ChangesManager::addChange(TextEditor::TextDocument *document,
|
||||
} else {
|
||||
documentQueue.enqueue(change);
|
||||
|
||||
if (documentQueue.size() > Settings::contextSettings().maxChangesCacheSize()) {
|
||||
if (documentQueue.size() > Settings::codeCompletionSettings().maxChangesCacheSize()) {
|
||||
documentQueue.dequeue();
|
||||
}
|
||||
}
|
||||
@ -80,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
|
||||
32
context/ContentFile.hpp
Normal file
32
context/ContentFile.hpp
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
struct ContentFile
|
||||
{
|
||||
QString filename;
|
||||
QString content;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
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/ContextSettings.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)
|
||||
@ -135,13 +136,6 @@ QString DocumentContextReader::getLanguageAndFileInfo() const
|
||||
.arg(language, mimeType, filePath, fileExtension);
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getSpecificInstructions() const
|
||||
{
|
||||
QString specificInstruction = Settings::contextSettings().specificInstractions().arg(
|
||||
LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_textDocument->mimeType()));
|
||||
return QString("%1").arg(specificInstruction);
|
||||
}
|
||||
|
||||
CopyrightInfo DocumentContextReader::findCopyright()
|
||||
{
|
||||
CopyrightInfo result = {-1, -1, false};
|
||||
@ -210,22 +204,29 @@ CopyrightInfo DocumentContextReader::copyrightInfo() const
|
||||
return m_copyrightInfo;
|
||||
}
|
||||
|
||||
ContextData DocumentContextReader::prepareContext(int lineNumber, int cursorPosition) const
|
||||
LLMCore::ContextData DocumentContextReader::prepareContext(int lineNumber, int cursorPosition) const
|
||||
{
|
||||
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
|
||||
{
|
||||
if (Settings::contextSettings().readFullFile()) {
|
||||
if (Settings::codeCompletionSettings().readFullFile()) {
|
||||
return readWholeFileBefore(lineNumber, cursorPosition);
|
||||
} else {
|
||||
int effectiveStartLine;
|
||||
int beforeCursor = Settings::contextSettings().readStringsBeforeCursor();
|
||||
int beforeCursor = Settings::codeCompletionSettings().readStringsBeforeCursor();
|
||||
if (m_copyrightInfo.found) {
|
||||
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - beforeCursor);
|
||||
} else {
|
||||
@ -237,26 +238,13 @@ QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPositi
|
||||
|
||||
QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPosition) const
|
||||
{
|
||||
if (Settings::contextSettings().readFullFile()) {
|
||||
if (Settings::codeCompletionSettings().readFullFile()) {
|
||||
return readWholeFileAfter(lineNumber, cursorPosition);
|
||||
} else {
|
||||
int endLine = qMin(m_document->blockCount() - 1,
|
||||
lineNumber + Settings::contextSettings().readStringsAfterCursor());
|
||||
lineNumber + Settings::codeCompletionSettings().readStringsAfterCursor());
|
||||
return getContextBetween(lineNumber + 1, endLine, -1);
|
||||
}
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getInstructions() const
|
||||
{
|
||||
QString instructions;
|
||||
|
||||
if (Settings::contextSettings().useFilePathInContext())
|
||||
instructions += getLanguageAndFileInfo();
|
||||
|
||||
if (Settings::contextSettings().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 "QodeAssistData.hpp"
|
||||
#include <llmcore/ContextData.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
struct CopyrightInfo
|
||||
{
|
||||
@ -44,18 +44,16 @@ public:
|
||||
QString readWholeFileBefore(int lineNumber, int cursorPosition) const;
|
||||
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
|
||||
QString getLanguageAndFileInfo() const;
|
||||
QString getSpecificInstructions() const;
|
||||
CopyrightInfo findCopyright();
|
||||
QString getContextBetween(int startLine, int endLine, int cursorPosition) const;
|
||||
|
||||
CopyrightInfo copyrightInfo() const;
|
||||
|
||||
ContextData prepareContext(int lineNumber, int cursorPosition) const;
|
||||
LLMCore::ContextData prepareContext(int lineNumber, int cursorPosition) const;
|
||||
|
||||
private:
|
||||
QString getContextBefore(int lineNumber, int cursorPosition) const;
|
||||
QString getContextAfter(int lineNumber, int cursorPosition) const;
|
||||
QString getInstructions() const;
|
||||
|
||||
private:
|
||||
TextEditor::TextDocument *m_textDocument;
|
||||
@ -63,4 +61,4 @@ private:
|
||||
CopyrightInfo m_copyrightInfo;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::Context
|
||||
54
context/TokenUtils.cpp
Normal file
54
context/TokenUtils.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TokenUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
int TokenUtils::estimateTokens(const QString& text)
|
||||
{
|
||||
if (text.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: need to improve
|
||||
return text.length() / 4;
|
||||
}
|
||||
|
||||
int TokenUtils::estimateFileTokens(const Context::ContentFile& file)
|
||||
{
|
||||
int total = 0;
|
||||
|
||||
total += estimateTokens(file.filename);
|
||||
total += estimateTokens(file.content);
|
||||
total += 5;
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
int TokenUtils::estimateFilesTokens(const QList<Context::ContentFile>& files)
|
||||
{
|
||||
int total = 0;
|
||||
for (const auto& file : files) {
|
||||
total += estimateFileTokens(file);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
}
|
||||
36
context/TokenUtils.hpp
Normal file
36
context/TokenUtils.hpp
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include "ContentFile.hpp"
|
||||
#include <QList>
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
class TokenUtils
|
||||
{
|
||||
public:
|
||||
static int estimateTokens(const QString& text);
|
||||
static int estimateFileTokens(const Context::ContentFile& file);
|
||||
static int estimateFilesTokens(const QList<Context::ContentFile>& files);
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,160 +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 "LLMRequestHandler.hpp"
|
||||
#include "LLMProvidersManager.hpp"
|
||||
#include "QodeAssistUtils.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMRequestHandler::LLMRequestHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{}
|
||||
|
||||
void LLMRequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request)
|
||||
{
|
||||
logMessage(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
|
||||
.arg(config.url.toString(),
|
||||
QString::fromUtf8(
|
||||
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QNetworkRequest networkRequest(config.url);
|
||||
prepareNetworkRequest(networkRequest, config.providerRequest);
|
||||
|
||||
QNetworkReply *reply = m_manager->post(networkRequest,
|
||||
QJsonDocument(config.providerRequest).toJson());
|
||||
if (!reply) {
|
||||
logMessage("Error: Failed to create network reply");
|
||||
return;
|
||||
}
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
m_activeRequests[requestId] = reply;
|
||||
|
||||
connect(reply, &QNetworkReply::readyRead, this, [this, reply, request, config]() {
|
||||
handleLLMResponse(reply, request, config);
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId]() {
|
||||
reply->deleteLater();
|
||||
m_activeRequests.remove(requestId);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
logMessage(QString("Error in QodeAssist request: %1").arg(reply->errorString()));
|
||||
emit requestFinished(requestId, false, reply->errorString());
|
||||
} else {
|
||||
logMessage("Request finished successfully");
|
||||
emit requestFinished(requestId, true, QString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void LLMRequestHandler::handleLLMResponse(QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const LLMConfig &config)
|
||||
{
|
||||
QString &accumulatedResponse = m_accumulatedResponses[reply];
|
||||
|
||||
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
|
||||
|
||||
if (config.requestType == RequestType::Fim) {
|
||||
if (!Settings::generalSettings().multiLineCompletion()
|
||||
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isComplete || reply->isFinished()) {
|
||||
if (isComplete) {
|
||||
if (config.requestType == RequestType::Fim) {
|
||||
auto cleanedCompletion = removeStopWords(accumulatedResponse,
|
||||
config.promptTemplate->stopWords());
|
||||
emit completionReceived(cleanedCompletion, request, true);
|
||||
} else {
|
||||
emit completionReceived(accumulatedResponse, request, true);
|
||||
}
|
||||
} else {
|
||||
emit completionReceived(accumulatedResponse, request, false);
|
||||
}
|
||||
m_accumulatedResponses.remove(reply);
|
||||
}
|
||||
}
|
||||
|
||||
bool LLMRequestHandler::cancelRequest(const QString &id)
|
||||
{
|
||||
if (m_activeRequests.contains(id)) {
|
||||
QNetworkReply *reply = m_activeRequests[id];
|
||||
reply->abort();
|
||||
m_activeRequests.remove(id);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
emit requestCancelled(id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void LLMRequestHandler::prepareNetworkRequest(QNetworkRequest &networkRequest,
|
||||
const QJsonObject &providerRequest)
|
||||
{
|
||||
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 LLMRequestHandler::processSingleLineCompletion(QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedResponse,
|
||||
const LLMConfig &config)
|
||||
{
|
||||
int newlinePos = accumulatedResponse.indexOf('\n');
|
||||
|
||||
if (newlinePos != -1) {
|
||||
QString singleLineCompletion = accumulatedResponse.left(newlinePos).trimmed();
|
||||
singleLineCompletion = removeStopWords(singleLineCompletion,
|
||||
config.promptTemplate->stopWords());
|
||||
|
||||
emit completionReceived(singleLineCompletion, request, true);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
reply->abort();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString LLMRequestHandler::removeStopWords(const QStringView &completion,
|
||||
const QStringList &stopWords)
|
||||
{
|
||||
QString filteredCompletion = completion.toString();
|
||||
|
||||
for (const QString &stopWord : stopWords) {
|
||||
filteredCompletion = filteredCompletion.replace(stopWord, "");
|
||||
}
|
||||
|
||||
return filteredCompletion;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
27
llmcore/CMakeLists.txt
Normal file
27
llmcore/CMakeLists.txt
Normal file
@ -0,0 +1,27 @@
|
||||
add_library(LLMCore STATIC
|
||||
RequestType.hpp
|
||||
Provider.hpp
|
||||
ProvidersManager.hpp ProvidersManager.cpp
|
||||
ContextData.hpp
|
||||
PromptTemplate.hpp
|
||||
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
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
QtCreator::ExtensionSystem
|
||||
PRIVATE
|
||||
QodeAssistLogger
|
||||
)
|
||||
|
||||
target_include_directories(LLMCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
@ -21,13 +21,13 @@
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
struct ContextData
|
||||
{
|
||||
QString prefix;
|
||||
QString suffix;
|
||||
QString instriuctions;
|
||||
QString fileContext;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // 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
|
||||
@ -23,9 +23,9 @@
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
#include "QodeAssistData.hpp"
|
||||
#include "ContextData.hpp"
|
||||
|
||||
namespace QodeAssist::Templates {
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
enum class TemplateType { Chat, Fim };
|
||||
|
||||
@ -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::Templates
|
||||
} // namespace QodeAssist::LLMCore
|
||||
55
llmcore/PromptTemplateManager.cpp
Normal file
55
llmcore/PromptTemplateManager.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 "PromptTemplateManager.hpp"
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
PromptTemplateManager &PromptTemplateManager::instance()
|
||||
{
|
||||
static PromptTemplateManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
QStringList PromptTemplateManager::fimTemplatesNames() const
|
||||
{
|
||||
return m_fimTemplates.keys();
|
||||
}
|
||||
|
||||
QStringList PromptTemplateManager::chatTemplatesNames() const
|
||||
{
|
||||
return m_chatTemplates.keys();
|
||||
}
|
||||
|
||||
PromptTemplateManager::~PromptTemplateManager()
|
||||
{
|
||||
qDeleteAll(m_fimTemplates);
|
||||
}
|
||||
|
||||
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
|
||||
{
|
||||
return m_fimTemplates[templateName];
|
||||
}
|
||||
|
||||
PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName)
|
||||
{
|
||||
return m_chatTemplates[templateName];
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -22,9 +22,9 @@
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "templates/PromptTemplate.hpp"
|
||||
#include "PromptTemplate.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class PromptTemplateManager
|
||||
{
|
||||
@ -35,22 +35,18 @@ public:
|
||||
template<typename T>
|
||||
void registerTemplate()
|
||||
{
|
||||
static_assert(std::is_base_of<Templates::PromptTemplate, T>::value,
|
||||
static_assert(std::is_base_of<PromptTemplate, T>::value,
|
||||
"T must inherit from PromptTemplate");
|
||||
T *template_ptr = new T();
|
||||
QString name = template_ptr->name();
|
||||
if (template_ptr->type() == Templates::TemplateType::Fim) {
|
||||
m_fimTemplates[name] = template_ptr;
|
||||
} else if (template_ptr->type() == Templates::TemplateType::Chat) {
|
||||
m_fimTemplates[name] = template_ptr;
|
||||
if (template_ptr->type() == TemplateType::Chat) {
|
||||
m_chatTemplates[name] = template_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
void setCurrentFimTemplate(const QString &name);
|
||||
Templates::PromptTemplate *getCurrentFimTemplate();
|
||||
|
||||
void setCurrentChatTemplate(const QString &name);
|
||||
Templates::PromptTemplate *getCurrentChatTemplate();
|
||||
PromptTemplate *getFimTemplateByName(const QString &templateName);
|
||||
PromptTemplate *getChatTemplateByName(const QString &templateName);
|
||||
|
||||
QStringList fimTemplatesNames() const;
|
||||
QStringList chatTemplatesNames() const;
|
||||
@ -60,10 +56,8 @@ private:
|
||||
PromptTemplateManager(const PromptTemplateManager &) = delete;
|
||||
PromptTemplateManager &operator=(const PromptTemplateManager &) = delete;
|
||||
|
||||
QMap<QString, Templates::PromptTemplate *> m_fimTemplates;
|
||||
QMap<QString, Templates::PromptTemplate *> m_chatTemplates;
|
||||
Templates::PromptTemplate *m_currentFimTemplate;
|
||||
Templates::PromptTemplate *m_currentChatTemplate;
|
||||
QMap<QString, PromptTemplate *> m_fimTemplates;
|
||||
QMap<QString, PromptTemplate *> m_chatTemplates;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -19,27 +19,35 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <utils/environment.h>
|
||||
#include <QNetworkRequest>
|
||||
#include <QString>
|
||||
|
||||
#include "PromptTemplate.hpp"
|
||||
#include "RequestType.hpp"
|
||||
|
||||
class QNetworkReply;
|
||||
class QJsonObject;
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class LLMProvider
|
||||
class Provider
|
||||
{
|
||||
public:
|
||||
virtual ~LLMProvider() = default;
|
||||
virtual ~Provider() = default;
|
||||
|
||||
virtual QString name() const = 0;
|
||||
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) = 0;
|
||||
virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
|
||||
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0;
|
||||
virtual QList<QString> getInstalledModels(const Utils::Environment &env, const QString &url) = 0;
|
||||
virtual QList<QString> getInstalledModels(const QString &url) = 0;
|
||||
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
|
||||
virtual QString apiKey() const = 0;
|
||||
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
} // namespace QodeAssist::LLMCore
|
||||
45
llmcore/ProvidersManager.cpp
Normal file
45
llmcore/ProvidersManager.cpp
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 "ProvidersManager.hpp"
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
ProvidersManager &ProvidersManager::instance()
|
||||
{
|
||||
static ProvidersManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
QStringList ProvidersManager::providersNames() const
|
||||
{
|
||||
return m_providers.keys();
|
||||
}
|
||||
|
||||
ProvidersManager::~ProvidersManager()
|
||||
{
|
||||
qDeleteAll(m_providers);
|
||||
}
|
||||
|
||||
Provider *ProvidersManager::getProviderByName(const QString &providerName)
|
||||
{
|
||||
return m_providers[providerName];
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -21,42 +21,36 @@
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "providers/LLMProvider.hpp"
|
||||
#include <QMap>
|
||||
#include "Provider.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class LLMProvidersManager
|
||||
class ProvidersManager
|
||||
{
|
||||
public:
|
||||
static LLMProvidersManager &instance();
|
||||
~LLMProvidersManager();
|
||||
static ProvidersManager &instance();
|
||||
~ProvidersManager();
|
||||
|
||||
template<typename T>
|
||||
void registerProvider()
|
||||
{
|
||||
static_assert(std::is_base_of<Providers::LLMProvider, T>::value,
|
||||
"T must inherit from LLMProvider");
|
||||
static_assert(std::is_base_of<Provider, T>::value, "T must inherit from Provider");
|
||||
T *provider = new T();
|
||||
QString name = provider->name();
|
||||
m_providers[name] = provider;
|
||||
}
|
||||
|
||||
Providers::LLMProvider *setCurrentFimProvider(const QString &name);
|
||||
Providers::LLMProvider *setCurrentChatProvider(const QString &name);
|
||||
|
||||
Providers::LLMProvider *getCurrentFimProvider();
|
||||
Providers::LLMProvider *getCurrentChatProvider();
|
||||
Provider *getProviderByName(const QString &providerName);
|
||||
|
||||
QStringList providersNames() const;
|
||||
|
||||
private:
|
||||
LLMProvidersManager() = default;
|
||||
LLMProvidersManager(const LLMProvidersManager &) = delete;
|
||||
LLMProvidersManager &operator=(const LLMProvidersManager &) = delete;
|
||||
ProvidersManager() = default;
|
||||
ProvidersManager(const ProvidersManager &) = delete;
|
||||
ProvidersManager &operator=(const ProvidersManager &) = delete;
|
||||
|
||||
QMap<QString, Providers::LLMProvider *> m_providers;
|
||||
Providers::LLMProvider *m_currentFimProvider = nullptr;
|
||||
Providers::LLMProvider *m_currentChatProvider = nullptr;
|
||||
QMap<QString, Provider *> m_providers;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -21,20 +21,21 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QUrl>
|
||||
#include "providers/LLMProvider.hpp"
|
||||
#include "templates/PromptTemplate.hpp"
|
||||
#include "PromptTemplate.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "RequestType.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
enum class RequestType { Fim, Chat };
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
struct LLMConfig
|
||||
{
|
||||
QUrl url;
|
||||
Providers::LLMProvider *provider;
|
||||
Templates::PromptTemplate *promptTemplate;
|
||||
Provider *provider;
|
||||
PromptTemplate *promptTemplate;
|
||||
QJsonObject providerRequest;
|
||||
RequestType requestType;
|
||||
bool multiLineCompletion;
|
||||
QString apiKey;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
} // namespace QodeAssist::LLMCore
|
||||
175
llmcore/RequestHandler.cpp
Normal file
175
llmcore/RequestHandler.cpp
Normal file
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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 "RequestHandler.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
RequestHandler::RequestHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{}
|
||||
|
||||
void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request)
|
||||
{
|
||||
LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
|
||||
.arg(config.url.toString(),
|
||||
QString::fromUtf8(
|
||||
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QNetworkRequest networkRequest(config.url);
|
||||
config.provider->prepareNetworkRequest(networkRequest);
|
||||
|
||||
QNetworkReply *reply = m_manager->post(networkRequest,
|
||||
QJsonDocument(config.providerRequest).toJson());
|
||||
if (!reply) {
|
||||
LOG_MESSAGE("Error: Failed to create network reply");
|
||||
return;
|
||||
}
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
m_activeRequests[requestId] = reply;
|
||||
|
||||
connect(reply, &QNetworkReply::readyRead, this, [this, reply, request, config]() {
|
||||
handleLLMResponse(reply, request, config);
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId]() {
|
||||
reply->deleteLater();
|
||||
m_activeRequests.remove(requestId);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
LOG_MESSAGE(QString("Error in QodeAssist request: %1").arg(reply->errorString()));
|
||||
emit requestFinished(requestId, false, reply->errorString());
|
||||
} else {
|
||||
LOG_MESSAGE("Request finished successfully");
|
||||
emit requestFinished(requestId, true, QString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void RequestHandler::handleLLMResponse(QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const LLMConfig &config)
|
||||
{
|
||||
QString &accumulatedResponse = m_accumulatedResponses[reply];
|
||||
|
||||
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
|
||||
|
||||
if (config.requestType == RequestType::CodeCompletion) {
|
||||
if (!config.multiLineCompletion
|
||||
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
auto cleanedCompletion = removeStopWords(accumulatedResponse,
|
||||
config.promptTemplate->stopWords());
|
||||
|
||||
emit completionReceived(cleanedCompletion, request, true);
|
||||
}
|
||||
} else if (config.requestType == RequestType::Chat) {
|
||||
emit completionReceived(accumulatedResponse, request, isComplete);
|
||||
}
|
||||
|
||||
if (isComplete)
|
||||
m_accumulatedResponses.remove(reply);
|
||||
}
|
||||
|
||||
bool RequestHandler::cancelRequest(const QString &id)
|
||||
{
|
||||
if (m_activeRequests.contains(id)) {
|
||||
QNetworkReply *reply = m_activeRequests[id];
|
||||
reply->abort();
|
||||
m_activeRequests.remove(id);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
emit requestCancelled(id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RequestHandler::processSingleLineCompletion(
|
||||
QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedResponse,
|
||||
const LLMConfig &config)
|
||||
{
|
||||
QString cleanedResponse = accumulatedResponse;
|
||||
|
||||
int newlinePos = cleanedResponse.indexOf('\n');
|
||||
if (newlinePos != -1) {
|
||||
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;
|
||||
}
|
||||
|
||||
QString RequestHandler::removeStopWords(const QStringView &completion, const QStringList &stopWords)
|
||||
{
|
||||
QString filteredCompletion = completion.toString();
|
||||
|
||||
for (const QString &stopWord : stopWords) {
|
||||
filteredCompletion = filteredCompletion.replace(stopWord, "");
|
||||
}
|
||||
|
||||
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
|
||||
@ -23,19 +23,18 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
#include "QodeAssistData.hpp"
|
||||
#include "core/LLMRequestConfig.hpp"
|
||||
#include "RequestConfig.hpp"
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class LLMRequestHandler : public QObject
|
||||
class RequestHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LLMRequestHandler(QObject *parent = nullptr);
|
||||
explicit RequestHandler(QObject *parent = nullptr);
|
||||
|
||||
void sendLLMRequest(const LLMConfig &config, const QJsonObject &request);
|
||||
void handleLLMResponse(QNetworkReply *reply,
|
||||
@ -53,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
|
||||
} // namespace QodeAssist::LLMCore
|
||||
25
llmcore/RequestType.hpp
Normal file
25
llmcore/RequestType.hpp
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
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
|
||||
14
logger/CMakeLists.txt
Normal file
14
logger/CMakeLists.txt
Normal file
@ -0,0 +1,14 @@
|
||||
add_library(QodeAssistLogger STATIC
|
||||
Logger.cpp
|
||||
Logger.hpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistLogger
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
QtCreator::Core
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistLogger
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
55
logger/Logger.cpp
Normal file
55
logger/Logger.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
#include "Logger.hpp"
|
||||
#include <coreplugin/messagemanager.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
Logger &Logger::instance()
|
||||
{
|
||||
static Logger instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Logger::Logger()
|
||||
: m_loggingEnabled(false)
|
||||
{}
|
||||
|
||||
void Logger::setLoggingEnabled(bool enable)
|
||||
{
|
||||
m_loggingEnabled = enable;
|
||||
}
|
||||
|
||||
bool Logger::isLoggingEnabled() const
|
||||
{
|
||||
return m_loggingEnabled;
|
||||
}
|
||||
|
||||
void Logger::log(const QString &message, bool silent)
|
||||
{
|
||||
if (!m_loggingEnabled)
|
||||
return;
|
||||
|
||||
const QString prefixedMessage = QLatin1String("[Qode Assist] ") + message;
|
||||
if (silent) {
|
||||
Core::MessageManager::writeSilently(prefixedMessage);
|
||||
} else {
|
||||
Core::MessageManager::writeFlashing(prefixedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void Logger::logMessages(const QStringList &messages, bool silent)
|
||||
{
|
||||
if (!m_loggingEnabled)
|
||||
return;
|
||||
|
||||
QStringList prefixedMessages;
|
||||
for (const QString &message : messages) {
|
||||
prefixedMessages << (QLatin1String("[Qode Assist] ") + message);
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
Core::MessageManager::writeSilently(prefixedMessages);
|
||||
} else {
|
||||
Core::MessageManager::writeFlashing(prefixedMessages);
|
||||
}
|
||||
}
|
||||
} // namespace QodeAssist
|
||||
33
logger/Logger.hpp
Normal file
33
logger/Logger.hpp
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class Logger : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static Logger &instance();
|
||||
|
||||
void setLoggingEnabled(bool enable);
|
||||
bool isLoggingEnabled() const;
|
||||
|
||||
void log(const QString &message, bool silent = true);
|
||||
void logMessages(const QStringList &messages, bool silent = true);
|
||||
|
||||
private:
|
||||
Logger();
|
||||
~Logger() = default;
|
||||
Logger(const Logger &) = delete;
|
||||
Logger &operator=(const Logger &) = delete;
|
||||
|
||||
bool m_loggingEnabled;
|
||||
};
|
||||
|
||||
#define LOG_MESSAGE(msg) QodeAssist::Logger::instance().log(msg)
|
||||
#define LOG_MESSAGES(msgs) QodeAssist::Logger::instance().logMessages(msgs)
|
||||
|
||||
} // namespace QodeAssist
|
||||
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,8 +25,11 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "QodeAssistUtils.hpp"
|
||||
#include "settings/PresetPromptsSettings.hpp"
|
||||
#include "llmcore/OpenAIMessage.hpp"
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
@ -52,81 +55,104 @@ QString LMStudioProvider::chatEndpoint() const
|
||||
return "/v1/chat/completions";
|
||||
}
|
||||
|
||||
void LMStudioProvider::prepareRequest(QJsonObject &request)
|
||||
bool LMStudioProvider::supportsModelListing() const
|
||||
{
|
||||
auto &settings = Settings::presetPromptsSettings();
|
||||
QJsonArray messages;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.contains("system")) {
|
||||
QJsonObject systemMessage{{"role", "system"},
|
||||
{"content", request.take("system").toString()}};
|
||||
messages.append(systemMessage);
|
||||
}
|
||||
void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
|
||||
{
|
||||
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
|
||||
QJsonArray messages;
|
||||
if (req.contains("system")) {
|
||||
messages.append(
|
||||
QJsonObject{{"role", "system"}, {"content", req.take("system").toString()}});
|
||||
}
|
||||
if (req.contains("prompt")) {
|
||||
messages.append(
|
||||
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
|
||||
if (request.contains("prompt")) {
|
||||
QJsonObject userMessage{{"role", "user"}, {"content", request.take("prompt").toString()}};
|
||||
messages.append(userMessage);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
request["max_tokens"] = settings.maxTokens();
|
||||
request["temperature"] = settings.temperature();
|
||||
if (settings.useTopP())
|
||||
request["top_p"] = settings.topP();
|
||||
if (settings.useTopK())
|
||||
request["top_k"] = settings.topK();
|
||||
if (settings.useFrequencyPenalty())
|
||||
request["frequency_penalty"] = settings.frequencyPenalty();
|
||||
if (settings.usePresencePenalty())
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
}
|
||||
}
|
||||
|
||||
bool 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 Utils::Environment &env,
|
||||
const QString &url)
|
||||
QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
||||
{
|
||||
QList<QString> models;
|
||||
QNetworkAccessManager manager;
|
||||
@ -150,11 +176,39 @@ QList<QString> LMStudioProvider::getInstalledModels(const Utils::Environment &en
|
||||
models.append(modelId);
|
||||
}
|
||||
} else {
|
||||
logMessage(QString("Error fetching models: %1").arg(reply->errorString()));
|
||||
LOG_MESSAGE(QString("Error fetching models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user