mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-12 10:10:44 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbd47387be | |||
| 50e1276ab2 | |||
| 50c948ccfe | |||
| 949dad4fd2 | |||
| 01fd7dad6f | |||
| fd408ba415 | |||
| 14e7ea2ec3 | |||
| 9f050aec67 | |||
| 9e118ddfaf | |||
| 157498b770 | |||
| 5c8a8f305d | |||
| fc33bb60d0 | |||
| 498eb4d932 | |||
| fb941cea99 | |||
| a0af983bda | |||
| 4bd96e0718 | |||
| 7b0d3c2abb | |||
| 75d1551b00 | |||
| 406ba05bfb | |||
| 7a97d0aba5 | |||
| b19c4c0c0c | |||
| a466332822 | |||
| e1fa01d123 |
10
.github/workflows/build_cmake.yml
vendored
10
.github/workflows/build_cmake.yml
vendored
@ -45,17 +45,13 @@ jobs:
|
||||
cc: "clang", cxx: "clang++"
|
||||
}
|
||||
qt_config:
|
||||
- {
|
||||
qt_version: "6.8.3",
|
||||
qt_creator_version: "16.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.9.2",
|
||||
qt_creator_version: "17.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.10.0",
|
||||
qt_creator_version: "18.0.0"
|
||||
qt_version: "6.10.1",
|
||||
qt_creator_version: "18.0.1"
|
||||
}
|
||||
|
||||
steps:
|
||||
@ -165,7 +161,7 @@ jobs:
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
||||
endfunction()
|
||||
|
||||
foreach(package qtbase qtdeclarative qttools)
|
||||
foreach(package qtbase qtdeclarative qttools qtsvg)
|
||||
downloadAndExtract(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||
${package}.7z
|
||||
|
||||
@ -11,7 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
|
||||
find_package(GTest)
|
||||
|
||||
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
|
||||
@ -57,6 +57,7 @@ add_qtc_plugin(QodeAssist
|
||||
Qt::Quick
|
||||
Qt::Widgets
|
||||
Qt::Network
|
||||
Qt::Svg
|
||||
QtCreator::ExtensionSystem
|
||||
QtCreator::Utils
|
||||
QtCreator::CPlusPlus
|
||||
@ -89,6 +90,7 @@ add_qtc_plugin(QodeAssist
|
||||
templates/GoogleAI.hpp
|
||||
templates/LlamaCppFim.hpp
|
||||
templates/Qwen3CoderFIM.hpp
|
||||
templates/OpenAIResponses.hpp
|
||||
providers/Providers.hpp
|
||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||
@ -100,6 +102,17 @@ add_qtc_plugin(QodeAssist
|
||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
||||
providers/OpenAIResponses/ModelRequest.hpp
|
||||
providers/OpenAIResponses/ResponseObject.hpp
|
||||
providers/OpenAIResponses/GetResponseRequest.hpp
|
||||
providers/OpenAIResponses/DeleteResponseRequest.hpp
|
||||
providers/OpenAIResponses/CancelResponseRequest.hpp
|
||||
providers/OpenAIResponses/ListInputItemsRequest.hpp
|
||||
providers/OpenAIResponses/InputTokensRequest.hpp
|
||||
providers/OpenAIResponses/ItemTypesReference.hpp
|
||||
providers/OpenAIResponsesRequestBuilder.hpp
|
||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
||||
providers/OpenAIResponsesMessage.hpp providers/OpenAIResponsesMessage.cpp
|
||||
QodeAssist.qrc
|
||||
LSPCompletion.hpp
|
||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||
@ -141,6 +154,7 @@ add_qtc_plugin(QodeAssist
|
||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||
tools/TodoTool.hpp tools/TodoTool.cpp
|
||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
||||
|
||||
@ -21,7 +21,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
qml/controls/AttachedFilesPlace.qml
|
||||
qml/controls/BottomBar.qml
|
||||
qml/controls/FileEditsActionBar.qml
|
||||
qml/controls/RulesViewer.qml
|
||||
qml/controls/ContextViewer.qml
|
||||
qml/controls/Toast.qml
|
||||
qml/controls/TopBar.qml
|
||||
qml/controls/SplitDropZone.qml
|
||||
@ -43,6 +43,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
icons/chat-icon.svg
|
||||
icons/chat-pause-icon.svg
|
||||
icons/rules-icon.svg
|
||||
icons/context-icon.svg
|
||||
icons/open-in-editor.svg
|
||||
icons/apply-changes-button.svg
|
||||
icons/undo-changes-button.svg
|
||||
@ -51,6 +52,8 @@ qt_add_qml_module(QodeAssistChatView
|
||||
icons/thinking-icon-off.svg
|
||||
icons/tools-icon-on.svg
|
||||
icons/tools-icon-off.svg
|
||||
icons/settings-icon.svg
|
||||
icons/compress-icon.svg
|
||||
|
||||
SOURCES
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
@ -64,6 +67,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
ChatData.hpp
|
||||
FileItem.hpp FileItem.cpp
|
||||
ChatFileManager.hpp ChatFileManager.cpp
|
||||
ChatCompressor.hpp ChatCompressor.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
|
||||
310
ChatView/ChatCompressor.cpp
Normal file
310
ChatView/ChatCompressor.cpp
Normal file
@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "ChatCompressor.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatCompressor::ChatCompressor(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
||||
{
|
||||
if (m_isCompressing) {
|
||||
emit compressionFailed(tr("Compression already in progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (chatFilePath.isEmpty()) {
|
||||
emit compressionFailed(tr("No chat file to compress"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chatModel || chatModel->rowCount() == 0) {
|
||||
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
m_provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!m_provider) {
|
||||
emit compressionFailed(tr("No provider available"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||
templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
emit compressionFailed(tr("No template available"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_isCompressing = true;
|
||||
m_chatModel = chatModel;
|
||||
m_originalChatPath = chatFilePath;
|
||||
m_accumulatedSummary.clear();
|
||||
m_currentRequestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
emit compressionStarted();
|
||||
|
||||
connectProviderSignals();
|
||||
|
||||
QUrl requestUrl;
|
||||
QJsonObject payload;
|
||||
|
||||
if (m_provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
requestUrl = QUrl(QString("%1/models/%2:streamGenerateContent?alt=sse")
|
||||
.arg(Settings::generalSettings().caUrl(),
|
||||
Settings::generalSettings().caModel()));
|
||||
} else {
|
||||
requestUrl = QUrl(QString("%1%2").arg(Settings::generalSettings().caUrl(),
|
||||
m_provider->chatEndpoint()));
|
||||
payload["model"] = Settings::generalSettings().caModel();
|
||||
payload["stream"] = true;
|
||||
}
|
||||
|
||||
buildRequestPayload(payload, promptTemplate);
|
||||
|
||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||
m_provider->sendRequest(m_currentRequestId, requestUrl, payload);
|
||||
}
|
||||
|
||||
bool ChatCompressor::isCompressing() const
|
||||
{
|
||||
return m_isCompressing;
|
||||
}
|
||||
|
||||
void ChatCompressor::cancelCompression()
|
||||
{
|
||||
if (!m_isCompressing)
|
||||
return;
|
||||
|
||||
LOG_MESSAGE("Cancelling compression request");
|
||||
|
||||
if (m_provider && !m_currentRequestId.isEmpty())
|
||||
m_provider->cancelRequest(m_currentRequestId);
|
||||
|
||||
cleanupState();
|
||||
emit compressionFailed(tr("Compression cancelled"));
|
||||
}
|
||||
|
||||
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
||||
{
|
||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||
return;
|
||||
|
||||
m_accumulatedSummary += partialText;
|
||||
}
|
||||
|
||||
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
||||
{
|
||||
Q_UNUSED(fullText)
|
||||
|
||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||
return;
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
||||
|
||||
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
||||
handleCompressionError(tr("Failed to save compressed chat"));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||
cleanupState();
|
||||
emit compressionCompleted(compressedPath);
|
||||
}
|
||||
|
||||
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||
return;
|
||||
|
||||
LOG_MESSAGE(QString("Compression request failed: %1").arg(error));
|
||||
handleCompressionError(tr("Compression failed: %1").arg(error));
|
||||
}
|
||||
|
||||
void ChatCompressor::handleCompressionError(const QString &error)
|
||||
{
|
||||
cleanupState();
|
||||
emit compressionFailed(error);
|
||||
}
|
||||
|
||||
QString ChatCompressor::createCompressedChatPath(const QString &originalPath) const
|
||||
{
|
||||
QFileInfo fileInfo(originalPath);
|
||||
QString hash = QString::number(QDateTime::currentMSecsSinceEpoch() % 100000, 16);
|
||||
return QString("%1/%2_%3.%4")
|
||||
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
|
||||
}
|
||||
|
||||
QString ChatCompressor::buildCompressionPrompt() const
|
||||
{
|
||||
return QStringLiteral(
|
||||
"Please create a comprehensive summary of our entire conversation above. "
|
||||
"The summary should:\n"
|
||||
"1. Preserve all important context, decisions, and key information\n"
|
||||
"2. Maintain technical details, code snippets, file references, and specific examples\n"
|
||||
"3. Keep the chronological flow of the discussion\n"
|
||||
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
|
||||
"5. Be written in clear, structured format\n"
|
||||
"6. Use markdown formatting for better readability\n\n"
|
||||
"Create the summary now:");
|
||||
}
|
||||
|
||||
void ChatCompressor::buildRequestPayload(
|
||||
QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate)
|
||||
{
|
||||
LLMCore::ContextData context;
|
||||
|
||||
context.systemPrompt = QStringLiteral(
|
||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||
|
||||
QVector<LLMCore::Message> messages;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool
|
||||
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||
continue;
|
||||
|
||||
LLMCore::Message apiMessage;
|
||||
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
LLMCore::Message compressionRequest;
|
||||
compressionRequest.role = "user";
|
||||
compressionRequest.content = buildCompressionPrompt();
|
||||
messages.append(compressionRequest);
|
||||
|
||||
context.history = messages;
|
||||
|
||||
m_provider->prepareRequest(
|
||||
payload, promptTemplate, context, LLMCore::RequestType::Chat, false, false);
|
||||
}
|
||||
|
||||
bool ChatCompressor::createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||
{
|
||||
QFile sourceFile(sourcePath);
|
||||
if (!sourceFile.open(QIODevice::ReadOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
|
||||
sourceFile.close();
|
||||
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
|
||||
.arg(sourcePath, parseError.errorString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
|
||||
QJsonObject summaryMessage;
|
||||
summaryMessage["role"] = "assistant";
|
||||
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
summaryMessage["isRedacted"] = false;
|
||||
summaryMessage["attachments"] = QJsonArray();
|
||||
summaryMessage["images"] = QJsonArray();
|
||||
|
||||
root["messages"] = QJsonArray{summaryMessage};
|
||||
|
||||
if (QFile::exists(destPath))
|
||||
QFile::remove(destPath);
|
||||
|
||||
QFile destFile(destPath);
|
||||
if (!destFile.open(QIODevice::WriteOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to create compressed chat file: %1").arg(destPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
destFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatCompressor::connectProviderSignals()
|
||||
{
|
||||
m_connections.append(connect(
|
||||
m_provider,
|
||||
&LLMCore::Provider::partialResponseReceived,
|
||||
this,
|
||||
&ChatCompressor::onPartialResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
m_provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
this,
|
||||
&ChatCompressor::onFullResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
m_provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
this,
|
||||
&ChatCompressor::onRequestFailed,
|
||||
Qt::UniqueConnection));
|
||||
}
|
||||
|
||||
void ChatCompressor::disconnectAllSignals()
|
||||
{
|
||||
for (const auto &connection : std::as_const(m_connections))
|
||||
disconnect(connection);
|
||||
m_connections.clear();
|
||||
}
|
||||
|
||||
void ChatCompressor::cleanupState()
|
||||
{
|
||||
disconnectAllSignals();
|
||||
|
||||
m_isCompressing = false;
|
||||
m_currentRequestId.clear();
|
||||
m_originalChatPath.clear();
|
||||
m_accumulatedSummary.clear();
|
||||
m_chatModel = nullptr;
|
||||
m_provider = nullptr;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
79
ChatView/ChatCompressor.hpp
Normal file
79
ChatView/ChatCompressor.hpp
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QList>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
class Provider;
|
||||
class PromptTemplate;
|
||||
} // namespace QodeAssist::LLMCore
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel;
|
||||
|
||||
class ChatCompressor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatCompressor(QObject *parent = nullptr);
|
||||
|
||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
||||
|
||||
bool isCompressing() const;
|
||||
void cancelCompression();
|
||||
|
||||
signals:
|
||||
void compressionStarted();
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
void compressionFailed(const QString &error);
|
||||
|
||||
private slots:
|
||||
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
||||
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
||||
void onRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
QString createCompressedChatPath(const QString &originalPath) const;
|
||||
QString buildCompressionPrompt() const;
|
||||
bool createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||
void connectProviderSignals();
|
||||
void disconnectAllSignals();
|
||||
void cleanupState();
|
||||
void handleCompressionError(const QString &error);
|
||||
void buildRequestPayload(QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate);
|
||||
|
||||
bool m_isCompressing = false;
|
||||
QString m_currentRequestId;
|
||||
QString m_originalChatPath;
|
||||
QString m_accumulatedSummary;
|
||||
LLMCore::Provider *m_provider = nullptr;
|
||||
ChatModel *m_chatModel = nullptr;
|
||||
|
||||
QList<QMetaObject::Connection> m_connections;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@ -34,10 +34,13 @@
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatCompressor.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "ConfigurationManager.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
@ -56,6 +59,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||
, m_fileManager(new ChatFileManager(this))
|
||||
, m_isRequestInProgress(false)
|
||||
, m_chatCompressor(new ChatCompressor(this))
|
||||
{
|
||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||
connect(
|
||||
@ -116,6 +120,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::updateInputTokensCount);
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().systemPrompt,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::baseSystemPromptChanged);
|
||||
|
||||
auto editors = Core::EditorManager::instance();
|
||||
|
||||
@ -208,6 +217,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
updateInputTokensCount();
|
||||
refreshRules();
|
||||
loadAvailableConfigurations();
|
||||
loadAvailableAgentRoles();
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
@ -237,6 +247,27 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
m_lastErrorMessage = error;
|
||||
emit lastErrorMessageChanged();
|
||||
});
|
||||
|
||||
// ChatCompressor signals
|
||||
connect(m_chatCompressor, &ChatCompressor::compressionStarted, this, [this]() {
|
||||
emit isCompressingChanged();
|
||||
});
|
||||
|
||||
connect(m_chatCompressor, &ChatCompressor::compressionCompleted, this, [this](const QString &compressedChatPath) {
|
||||
emit isCompressingChanged();
|
||||
m_lastInfoMessage = tr("Chat compressed successfully!");
|
||||
emit lastInfoMessageChanged();
|
||||
emit compressionCompleted(compressedChatPath);
|
||||
|
||||
loadHistory(compressedChatPath);
|
||||
});
|
||||
|
||||
connect(m_chatCompressor, &ChatCompressor::compressionFailed, this, [this](const QString &error) {
|
||||
emit isCompressingChanged();
|
||||
m_lastErrorMessage = error;
|
||||
emit lastErrorMessageChanged();
|
||||
emit compressionFailed(error);
|
||||
});
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
@ -310,6 +341,12 @@ void ChatRootView::clearLinkedFiles()
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
|
||||
void ChatRootView::clearMessages()
|
||||
{
|
||||
m_clientInterface->clearMessages();
|
||||
clearLinkedFiles();
|
||||
}
|
||||
|
||||
QString ChatRootView::getChatsHistoryDir() const
|
||||
{
|
||||
QString path;
|
||||
@ -696,6 +733,11 @@ void ChatRootView::openRulesFolder()
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void ChatRootView::openSettings()
|
||||
{
|
||||
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID);
|
||||
}
|
||||
|
||||
void ChatRootView::updateInputTokensCount()
|
||||
{
|
||||
int inputTokens = m_messageTokensCount;
|
||||
@ -1354,4 +1396,130 @@ QString ChatRootView::currentConfiguration() const
|
||||
return m_currentConfiguration;
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableAgentRoles()
|
||||
{
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
m_availableAgentRoles.clear();
|
||||
m_availableAgentRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
||||
|
||||
for (const auto &role : roles)
|
||||
m_availableAgentRoles.append(role.name);
|
||||
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
m_currentAgentRole = Settings::AgentRolesManager::getNoRole().name;
|
||||
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
for (const auto &role : roles) {
|
||||
if (role.id == lastRoleId) {
|
||||
m_currentAgentRole = role.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit availableAgentRolesChanged();
|
||||
emit currentAgentRoleChanged();
|
||||
}
|
||||
|
||||
void ChatRootView::applyAgentRole(const QString &roleName)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
||||
settings.lastUsedRoleId.setValue("");
|
||||
settings.writeSettings();
|
||||
m_currentAgentRole = roleName;
|
||||
emit currentAgentRoleChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
for (const auto &role : roles) {
|
||||
if (role.name == roleName) {
|
||||
settings.lastUsedRoleId.setValue(role.id);
|
||||
settings.writeSettings();
|
||||
m_currentAgentRole = role.name;
|
||||
emit currentAgentRoleChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableAgentRoles() const
|
||||
{
|
||||
return m_availableAgentRoles;
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRole() const
|
||||
{
|
||||
return m_currentAgentRole;
|
||||
}
|
||||
|
||||
QString ChatRootView::baseSystemPrompt() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().systemPrompt();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleDescription() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
return role.description;
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return QString();
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return QString();
|
||||
|
||||
return role.systemPrompt;
|
||||
}
|
||||
|
||||
void ChatRootView::openAgentRolesSettings()
|
||||
{
|
||||
Core::ICore::showOptionsDialog(Utils::Id("QodeAssist.AgentRoles"));
|
||||
}
|
||||
|
||||
void ChatRootView::compressCurrentChat()
|
||||
{
|
||||
if (m_chatCompressor->isCompressing()) {
|
||||
m_lastErrorMessage = tr("Compression is already in progress");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_recentFilePath.isEmpty()) {
|
||||
m_lastErrorMessage = tr("No chat file to compress. Please save the chat first.");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
autosave();
|
||||
|
||||
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
||||
}
|
||||
|
||||
void ChatRootView::cancelCompression()
|
||||
{
|
||||
m_chatCompressor->cancelCompression();
|
||||
}
|
||||
|
||||
bool ChatRootView::isCompressing() const
|
||||
{
|
||||
return m_chatCompressor->isCompressing();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -21,14 +21,16 @@
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "llmcore/PromptProviderChat.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatCompressor;
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -59,6 +61,12 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||
|
||||
QML_ELEMENT
|
||||
|
||||
@ -94,6 +102,7 @@ public:
|
||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||
Q_INVOKABLE void openChatHistoryFolder();
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
Q_INVOKABLE void openSettings();
|
||||
|
||||
Q_INVOKABLE void updateInputTokensCount();
|
||||
int inputTokensCount() const;
|
||||
@ -144,6 +153,18 @@ public:
|
||||
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
||||
QStringList availableConfigurations() const;
|
||||
QString currentConfiguration() const;
|
||||
|
||||
Q_INVOKABLE void compressCurrentChat();
|
||||
Q_INVOKABLE void cancelCompression();
|
||||
|
||||
Q_INVOKABLE void loadAvailableAgentRoles();
|
||||
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
||||
Q_INVOKABLE void openAgentRolesSettings();
|
||||
QStringList availableAgentRoles() const;
|
||||
QString currentAgentRole() const;
|
||||
QString baseSystemPrompt() const;
|
||||
QString currentAgentRoleDescription() const;
|
||||
QString currentAgentRoleSystemPrompt() const;
|
||||
|
||||
int currentMessageTotalEdits() const;
|
||||
int currentMessageAppliedEdits() const;
|
||||
@ -153,6 +174,8 @@ public:
|
||||
QString lastInfoMessage() const;
|
||||
|
||||
bool isThinkingSupport() const;
|
||||
|
||||
bool isCompressing() const;
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message);
|
||||
@ -160,6 +183,7 @@ public slots:
|
||||
void cancelRequest();
|
||||
void clearAttachmentFiles();
|
||||
void clearLinkedFiles();
|
||||
void clearMessages();
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
@ -190,6 +214,14 @@ signals:
|
||||
void availableConfigurationsChanged();
|
||||
void currentConfigurationChanged();
|
||||
|
||||
void availableAgentRolesChanged();
|
||||
void currentAgentRoleChanged();
|
||||
void baseSystemPromptChanged();
|
||||
|
||||
void isCompressingChanged();
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
void compressionFailed(const QString &error);
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
QString getChatsHistoryDir() const;
|
||||
@ -222,6 +254,11 @@ private:
|
||||
|
||||
QStringList m_availableConfigurations;
|
||||
QString m_currentConfiguration;
|
||||
|
||||
QStringList m_availableAgentRoles;
|
||||
QString m_currentAgentRole;
|
||||
|
||||
ChatCompressor *m_chatCompressor;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -160,6 +160,13 @@ void ClientInterface::sendMessage(
|
||||
if (chatAssistantSettings.useSystemPrompt()) {
|
||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (!role.id.isEmpty())
|
||||
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
|
||||
}
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
|
||||
if (project) {
|
||||
@ -289,14 +296,14 @@ void ClientInterface::sendMessage(
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::toolExecutionStarted,
|
||||
m_chatModel,
|
||||
&ChatModel::addToolExecutionStatus,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionStarted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::toolExecutionCompleted,
|
||||
m_chatModel,
|
||||
&ChatModel::updateToolResult,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionCompleted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
@ -307,23 +314,34 @@ void ClientInterface::sendMessage(
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::thinkingBlockReceived,
|
||||
m_chatModel,
|
||||
&ChatModel::addThinkingBlock,
|
||||
this,
|
||||
&ClientInterface::handleThinkingBlockReceived,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::redactedThinkingBlockReceived,
|
||||
m_chatModel,
|
||||
&ChatModel::addRedactedThinkingBlock,
|
||||
this,
|
||||
&ClientInterface::handleRedactedThinkingBlockReceived,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||
|
||||
if (provider->supportsTools() && provider->toolsManager()) {
|
||||
provider->toolsManager()->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (provider && !m_chatFilePath.isEmpty() && provider->supportsTools()
|
||||
&& provider->toolsManager()) {
|
||||
provider->toolsManager()->clearTodoSession(m_chatFilePath);
|
||||
}
|
||||
|
||||
m_chatModel->clear();
|
||||
LOG_MESSAGE("Chat history cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
@ -475,6 +493,54 @@ void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
|
||||
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
void ClientInterface::handleThinkingBlockReceived(
|
||||
const QString &requestId, const QString &thinking, const QString &signature)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId)) {
|
||||
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
||||
}
|
||||
|
||||
void ClientInterface::handleRedactedThinkingBlockReceived(
|
||||
const QString &requestId, const QString &signature)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId)) {
|
||||
LOG_MESSAGE(
|
||||
QString("Ignoring redacted thinking block for non-chat request: %1").arg(requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
||||
}
|
||||
|
||||
void ClientInterface::handleToolExecutionStarted(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId)) {
|
||||
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
||||
}
|
||||
|
||||
void ClientInterface::handleToolExecutionCompleted(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &toolOutput)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId)) {
|
||||
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
|
||||
}
|
||||
|
||||
bool ClientInterface::isImageFile(const QString &filePath) const
|
||||
{
|
||||
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||
@ -548,6 +614,15 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||
|
||||
void ClientInterface::setChatFilePath(const QString &filePath)
|
||||
{
|
||||
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (provider && provider->supportsTools() && provider->toolsManager()) {
|
||||
provider->toolsManager()->clearTodoSession(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
m_chatFilePath = filePath;
|
||||
m_chatModel->setChatFilePath(filePath);
|
||||
}
|
||||
|
||||
@ -63,6 +63,16 @@ private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
void handleCleanAccumulatedData(const QString &requestId);
|
||||
void handleThinkingBlockReceived(
|
||||
const QString &requestId, const QString &thinking, const QString &signature);
|
||||
void handleRedactedThinkingBlockReceived(const QString &requestId, const QString &signature);
|
||||
void handleToolExecutionStarted(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||
void handleToolExecutionCompleted(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &toolOutput);
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||
|
||||
10
ChatView/icons/compress-icon.svg
Normal file
10
ChatView/icons/compress-icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Archive/compress icon: downward arrows pointing to center with horizontal lines -->
|
||||
<line x1="12" y1="3" x2="12" y2="10" />
|
||||
<polyline points="9 7 12 10 15 7" />
|
||||
|
||||
<line x1="12" y1="21" x2="12" y2="14" />
|
||||
<polyline points="9 17 12 14 15 17" />
|
||||
|
||||
<line x1="4" y1="12" x2="20" y2="12" stroke-width="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 487 B |
5
ChatView/icons/context-icon.svg
Normal file
5
ChatView/icons/context-icon.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
|
||||
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 233 B |
4
ChatView/icons/settings-icon.svg
Normal file
4
ChatView/icons/settings-icon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 962 B |
@ -26,6 +26,7 @@ import UIControls
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
import "./chatparts"
|
||||
import "./controls"
|
||||
|
||||
ChatRootView {
|
||||
id: root
|
||||
@ -87,9 +88,12 @@ ChatRootView {
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height + 10
|
||||
|
||||
isCompressing: root.isCompressing
|
||||
saveButton.onClicked: root.showSaveDialog()
|
||||
loadButton.onClicked: root.showLoadDialog()
|
||||
clearButton.onClicked: root.clearChat()
|
||||
compressButton.onClicked: compressConfirmDialog.open()
|
||||
cancelCompressButton.onClicked: root.cancelCompression()
|
||||
tokensBadge {
|
||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||
}
|
||||
@ -97,8 +101,7 @@ ChatRootView {
|
||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
}
|
||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||
rulesButton.onClicked: rulesViewer.open()
|
||||
activeRulesCount: root.activeRulesCount
|
||||
contextButton.onClicked: contextViewer.open()
|
||||
pinButton {
|
||||
visible: typeof _chatview !== 'undefined'
|
||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||
@ -117,6 +120,7 @@ ChatRootView {
|
||||
root.useThinking = thinkingMode.checked
|
||||
}
|
||||
}
|
||||
settingsButton.onClicked: root.openSettings()
|
||||
configSelector {
|
||||
model: root.availableConfigurations
|
||||
displayText: root.currentConfiguration
|
||||
@ -130,12 +134,24 @@ ChatRootView {
|
||||
root.loadAvailableConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
roleSelector {
|
||||
model: root.availableAgentRoles
|
||||
displayText: root.currentAgentRole
|
||||
onActivated: function(index) {
|
||||
root.applyAgentRole(root.availableAgentRoles[index])
|
||||
}
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableAgentRoles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: chatListView
|
||||
|
||||
signal hideServiceComponents(int itemIndex)
|
||||
property bool userScrolledUp: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
@ -146,6 +162,18 @@ ChatRootView {
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 2000
|
||||
|
||||
onMovingChanged: {
|
||||
if (moving) {
|
||||
userScrolledUp = !atYEnd
|
||||
}
|
||||
}
|
||||
|
||||
onAtYEndChanged: {
|
||||
if (atYEnd) {
|
||||
userScrolledUp = false
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Loader {
|
||||
id: componentLoader
|
||||
|
||||
@ -166,11 +194,6 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (componentLoader.sourceComponent == chatItemComponent) {
|
||||
chatListView.hideServiceComponents(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header: Item {
|
||||
@ -182,12 +205,53 @@ ChatRootView {
|
||||
id: scroll
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: scrollToBottomButton
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottomMargin: 10
|
||||
}
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
color: palette.button
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
visible: chatListView.userScrolledUp
|
||||
opacity: 0.9
|
||||
z: 100
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "▼"
|
||||
font.pixelSize: 14
|
||||
color: palette.buttonText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
chatListView.userScrolledUp = false
|
||||
root.scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on visible {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
root.scrollToBottom()
|
||||
if (!userScrolledUp) {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (atYEnd) {
|
||||
if (!userScrolledUp && atYEnd) {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
}
|
||||
@ -223,19 +287,8 @@ ChatRootView {
|
||||
id: toolMessageComponent
|
||||
|
||||
ToolBlock {
|
||||
id: toolsItem
|
||||
|
||||
width: parent.width
|
||||
toolContent: model.content
|
||||
|
||||
Connections {
|
||||
target: chatListView
|
||||
function onHideServiceComponents(itemIndex) {
|
||||
if (index !== itemIndex) {
|
||||
toolsItem.headerOpacity = 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,8 +321,6 @@ ChatRootView {
|
||||
id: thinkingMessageComponent
|
||||
|
||||
ThinkingBlock {
|
||||
id: thinking
|
||||
|
||||
width: parent.width
|
||||
thinkingContent: {
|
||||
let content = model.content
|
||||
@ -280,15 +331,6 @@ ChatRootView {
|
||||
return content
|
||||
}
|
||||
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
|
||||
|
||||
Connections {
|
||||
target: chatListView
|
||||
function onHideServiceComponents(itemIndex) {
|
||||
if (index !== itemIndex) {
|
||||
thinking.headerOpacity = 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -445,7 +487,7 @@ ChatRootView {
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.chatModel.clear()
|
||||
root.clearMessages()
|
||||
root.clearAttachmentFiles()
|
||||
root.updateInputTokensCount()
|
||||
}
|
||||
@ -460,6 +502,22 @@ ChatRootView {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: compressConfirmDialog
|
||||
|
||||
anchors.centerIn: parent
|
||||
title: qsTr("Compress Chat")
|
||||
modal: true
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
Label {
|
||||
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
onAccepted: root.compressCurrentChat()
|
||||
}
|
||||
|
||||
Toast {
|
||||
id: errorToast
|
||||
z: 1000
|
||||
@ -478,19 +536,28 @@ ChatRootView {
|
||||
toastTextColor: "#FFFFFF"
|
||||
}
|
||||
|
||||
RulesViewer {
|
||||
id: rulesViewer
|
||||
ContextViewer {
|
||||
id: contextViewer
|
||||
|
||||
width: parent.width * 0.8
|
||||
height: parent.height * 0.8
|
||||
width: Math.min(parent.width * 0.85, 800)
|
||||
height: Math.min(parent.height * 0.85, 700)
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
baseSystemPrompt: root.baseSystemPrompt
|
||||
currentAgentRole: root.currentAgentRole
|
||||
currentAgentRoleDescription: root.currentAgentRoleDescription
|
||||
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
||||
activeRules: root.activeRules
|
||||
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex)
|
||||
|
||||
onRefreshRules: root.refreshRules()
|
||||
activeRulesCount: root.activeRulesCount
|
||||
|
||||
onOpenSettings: root.openSettings()
|
||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||
onOpenRulesFolder: root.openRulesFolder()
|
||||
onRefreshRules: root.refreshRules()
|
||||
onRuleSelected: function(index) {
|
||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
558
ChatView/qml/controls/ContextViewer.qml
Normal file
558
ChatView/qml/controls/ContextViewer.qml
Normal file
@ -0,0 +1,558 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 QtQuick.Controls.Basic as QQC
|
||||
|
||||
import UIControls
|
||||
import ChatView
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property string baseSystemPrompt
|
||||
property string currentAgentRole
|
||||
property string currentAgentRoleDescription
|
||||
property string currentAgentRoleSystemPrompt
|
||||
property var activeRules
|
||||
property int activeRulesCount
|
||||
property string selectedRuleContent
|
||||
|
||||
signal openSettings()
|
||||
signal openAgentRolesSettings()
|
||||
signal openRulesFolder()
|
||||
signal refreshRules()
|
||||
signal ruleSelected(int index)
|
||||
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: palette.window
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 4
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: qsTr("Chat Context")
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Refresh")
|
||||
onClicked: root.refreshRules()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Close")
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: palette.mid
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: mainFlickable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
contentHeight: sectionsColumn.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
id: sectionsColumn
|
||||
|
||||
width: mainFlickable.width
|
||||
spacing: 8
|
||||
|
||||
CollapsibleSection {
|
||||
id: systemPromptSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Base System Prompt")
|
||||
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
|
||||
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
|
||||
|
||||
sectionContent: ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
Flickable {
|
||||
id: systemPromptFlickable
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
contentHeight: systemPromptText.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
TextEdit {
|
||||
id: systemPromptText
|
||||
|
||||
width: systemPromptFlickable.width
|
||||
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Copy")
|
||||
enabled: root.baseSystemPrompt.length > 0
|
||||
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Edit in Settings")
|
||||
onClicked: {
|
||||
root.openSettings()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: agentRoleSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Agent Role")
|
||||
badge: root.currentAgentRole
|
||||
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
|
||||
|
||||
sectionContent: ColumnLayout {
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.currentAgentRoleDescription
|
||||
font.pixelSize: 11
|
||||
font.italic: true
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
visible: root.currentAgentRoleDescription.length > 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
visible: root.currentAgentRoleSystemPrompt.length > 0
|
||||
|
||||
Flickable {
|
||||
id: agentPromptFlickable
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
contentHeight: agentPromptText.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
TextEdit {
|
||||
id: agentPromptText
|
||||
|
||||
width: agentPromptFlickable.width
|
||||
text: root.currentAgentRoleSystemPrompt
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
color: palette.text
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("No role selected. Using base system prompt only.")
|
||||
font.pixelSize: 11
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
visible: root.currentAgentRoleSystemPrompt.length === 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Copy")
|
||||
enabled: root.currentAgentRoleSystemPrompt.length > 0
|
||||
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Manage Roles")
|
||||
onClicked: {
|
||||
root.openAgentRolesSettings()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: projectRulesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Project Rules")
|
||||
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
|
||||
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
|
||||
|
||||
sectionContent: ColumnLayout {
|
||||
spacing: 8
|
||||
|
||||
SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 220
|
||||
orientation: Qt.Horizontal
|
||||
visible: root.activeRulesCount > 0
|
||||
|
||||
Rectangle {
|
||||
SplitView.minimumWidth: 120
|
||||
SplitView.preferredWidth: 180
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Rules (%1)").arg(rulesList.count)
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: rulesList
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: root.activeRules
|
||||
currentIndex: 0
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
delegate: ItemDelegate {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
height: ruleItemContent.implicitHeight + 8
|
||||
highlighted: ListView.isCurrentItem
|
||||
|
||||
background: Rectangle {
|
||||
color: {
|
||||
if (parent.highlighted)
|
||||
return palette.highlight
|
||||
if (parent.hovered)
|
||||
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
||||
return "transparent"
|
||||
}
|
||||
radius: 2
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: ruleItemContent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: modelData.fileName
|
||||
font.pixelSize: 10
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.category
|
||||
font.pixelSize: 9
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
rulesList.currentIndex = index
|
||||
root.ruleSelected(index)
|
||||
}
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumWidth: 200
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Content")
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Copy")
|
||||
enabled: root.selectedRuleContent.length > 0
|
||||
onClicked: utils.copyToClipboard(root.selectedRuleContent)
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: ruleContentFlickable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
contentHeight: ruleContentArea.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
TextEdit {
|
||||
id: ruleContentArea
|
||||
|
||||
width: ruleContentFlickable.width
|
||||
text: root.selectedRuleContent
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
|
||||
font.pixelSize: 11
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
visible: root.activeRulesCount === 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Open Rules Folder")
|
||||
onClicked: root.openRulesFolder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: palette.mid
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
|
||||
font.pixelSize: 9
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
component CollapsibleSection: ColumnLayout {
|
||||
id: sectionRoot
|
||||
|
||||
property string title
|
||||
property string badge
|
||||
property color badgeColor: palette.mid
|
||||
property Component sectionContent: null
|
||||
property bool expanded: false
|
||||
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
MouseArea {
|
||||
id: sectionMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: sectionRoot.expanded = !sectionRoot.expanded
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: sectionRoot.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
text: sectionRoot.title
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: badgeText.implicitWidth + 12
|
||||
implicitHeight: 18
|
||||
color: sectionRoot.badgeColor
|
||||
radius: 3
|
||||
|
||||
Text {
|
||||
id: badgeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: sectionRoot.badge
|
||||
font.pixelSize: 10
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 12
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 4
|
||||
sourceComponent: sectionRoot.sectionContent
|
||||
visible: sectionRoot.expanded
|
||||
active: sectionRoot.expanded
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
if (root.activeRulesCount > 0) {
|
||||
root.ruleSelected(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,251 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 QtQuick.Controls.Basic as QQC
|
||||
|
||||
import UIControls
|
||||
import ChatView
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property var activeRules
|
||||
|
||||
property alias rulesCurrentIndex: rulesList.currentIndex
|
||||
property alias ruleContentAreaText: ruleContentArea.text
|
||||
|
||||
signal refreshRules()
|
||||
signal openRulesFolder()
|
||||
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: palette.window
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 4
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: qsTr("Active Project Rules")
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Open Folder")
|
||||
onClicked: root.openRulesFolder()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Refresh")
|
||||
onClicked: root.refreshRules()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Close")
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: palette.mid
|
||||
}
|
||||
|
||||
SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
Rectangle {
|
||||
SplitView.minimumWidth: 200
|
||||
SplitView.preferredWidth: parent.width * 0.3
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Rules Files (%1)").arg(rulesList.count)
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: rulesList
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: root.activeRules
|
||||
currentIndex: 0
|
||||
|
||||
delegate: ItemDelegate {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
highlighted: ListView.isCurrentItem
|
||||
|
||||
background: Rectangle {
|
||||
color: {
|
||||
if (parent.highlighted) {
|
||||
return palette.highlight
|
||||
} else if (parent.hovered) {
|
||||
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
||||
}
|
||||
return "transparent"
|
||||
}
|
||||
radius: 2
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: modelData.fileName
|
||||
font.pixelSize: 11
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Category: %1").arg(modelData.category)
|
||||
font.pixelSize: 9
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
rulesList.currentIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: QQC.ScrollBar {
|
||||
id: scroll
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: rulesList.count === 0
|
||||
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
SplitView.fillWidth: true
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Content")
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Copy")
|
||||
enabled: ruleContentArea.text.length > 0
|
||||
onClicked: utils.copyToClipboard(ruleContentArea.text)
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
TextEdit {
|
||||
id: ruleContentArea
|
||||
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
|
||||
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
|
||||
font.pixelSize: 9
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,15 +29,20 @@ Rectangle {
|
||||
property alias saveButton: saveButtonId
|
||||
property alias loadButton: loadButtonId
|
||||
property alias clearButton: clearButtonId
|
||||
property alias compressButton: compressButtonId
|
||||
property alias cancelCompressButton: cancelCompressButtonId
|
||||
property alias tokensBadge: tokensBadgeId
|
||||
property alias recentPath: recentPathId
|
||||
property alias openChatHistory: openChatHistoryId
|
||||
property alias pinButton: pinButtonId
|
||||
property alias rulesButton: rulesButtonId
|
||||
property alias contextButton: contextButtonId
|
||||
property alias toolsButton: toolsButtonId
|
||||
property alias thinkingMode: thinkingModeId
|
||||
property alias activeRulesCount: activeRulesCountId.text
|
||||
property alias settingsButton: settingsButtonId
|
||||
property alias configSelector: configSelectorId
|
||||
property alias roleSelector: roleSelector
|
||||
|
||||
property bool isCompressing: false
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
@ -86,9 +91,26 @@ Rectangle {
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Switch AI configuration")
|
||||
ToolTip.text: qsTr("Switch saved AI configuration")
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: roleSelector
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
model: []
|
||||
currentIndex: 0
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Switch agent role (different system prompts)")
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: toolsButtonId
|
||||
|
||||
@ -139,8 +161,30 @@ Rectangle {
|
||||
: qsTr("Thinking Mode disabled"))
|
||||
: qsTr("Thinking Mode is not available for this provider")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: settingsButtonId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/settings-icon.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
||||
}
|
||||
|
||||
QoASeparator {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
height: firstRow.height
|
||||
width: recentPathId.width
|
||||
@ -199,19 +243,6 @@ Rectangle {
|
||||
ToolTip.text: qsTr("Load chat from *.json file")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: clearButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Clean chat")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: openChatHistoryId
|
||||
|
||||
@ -225,36 +256,70 @@ Rectangle {
|
||||
ToolTip.text: qsTr("Show in system")
|
||||
}
|
||||
|
||||
QoASeparator {}
|
||||
|
||||
QoAButton {
|
||||
id: rulesButtonId
|
||||
id: compressButtonId
|
||||
|
||||
visible: !root.isCompressing
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
|
||||
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||
}
|
||||
|
||||
Row {
|
||||
id: compressingRow
|
||||
|
||||
visible: root.isCompressing
|
||||
spacing: 6
|
||||
|
||||
BusyIndicator {
|
||||
id: compressBusyIndicator
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
running: root.isCompressing
|
||||
width: 16
|
||||
height: 16
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Compressing...")
|
||||
height: parent.height
|
||||
color: palette.text
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: cancelCompressButtonId
|
||||
|
||||
text: qsTr("Cancel")
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Cancel compression")
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: contextButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
text: " "
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: root.activeRulesCount > 0
|
||||
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
|
||||
: qsTr("View active project rules (no rules found)")
|
||||
|
||||
Text {
|
||||
id: activeRulesCountId
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 2
|
||||
right: parent.right
|
||||
rightMargin: 4
|
||||
}
|
||||
|
||||
color: palette.text
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
}
|
||||
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
|
||||
}
|
||||
|
||||
Badge {
|
||||
@ -264,6 +329,21 @@ Rectangle {
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||
}
|
||||
|
||||
QoASeparator {}
|
||||
|
||||
QoAButton {
|
||||
id: clearButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Clean chat")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,8 +125,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
QString requestId = request["id"].toString();
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
handleCompletion(request);
|
||||
} else if (method == "cancelRequest") {
|
||||
qDebug() << "Cancelling request";
|
||||
} else if (method == "$/cancelRequest") {
|
||||
handleCancelRequest();
|
||||
} else if (method == "exit") {
|
||||
// TODO make exit handler
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.9.3",
|
||||
"Version" : "0.9.7",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
|
||||
@ -362,7 +362,7 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
"\n- Preserve the original code structure when possible"
|
||||
"\n- Only change what is necessary to fulfill the user's request";
|
||||
|
||||
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
|
||||
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
|
||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||
}
|
||||
|
||||
|
||||
41
README.md
41
README.md
@ -132,14 +132,34 @@ QodeAssist supports multiple LLM providers. Choose your preferred provider and f
|
||||
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
|
||||
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
||||
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
|
||||
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider
|
||||
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider (includes Responses API support)
|
||||
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
|
||||
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
|
||||
- **LM Studio** - Local LLM provider
|
||||
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
|
||||
|
||||
### Recommended Models for Best Experience
|
||||
|
||||
For optimal coding assistance, we recommend using these top-tier models:
|
||||
|
||||
**Best for Code Completion & Refactoring:**
|
||||
- **Claude 4.5 Haiku or Sonnet** (Anthropic)
|
||||
- **GPT-5.1-codex or codex-mini** (OpenAI Responses API)
|
||||
- **Codestral** (Mistral)
|
||||
|
||||
**Best for Chat Assistant:**
|
||||
- **Claude 4.5 Sonnet** (Anthropic) - Outstanding reasoning and natural conversation flow
|
||||
- **GPT-5.1-codex** (OpenAI Responses API) - Latest model with advanced capabilities
|
||||
- **Gemini 2.5 or 3.0** (Google AI) - Latest models from Google
|
||||
- **Mistral large** (Mistral) - Fast and capable
|
||||
|
||||
**Local models:**
|
||||
- **Qwen3-coder** (Qwen) - Best local models
|
||||
|
||||
### Additional Configuration
|
||||
|
||||
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
||||
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
||||
|
||||
@ -176,6 +196,8 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
||||
- Multiple chat panels: side panel, bottom panel, and popup window
|
||||
- Chat history with auto-save and restore
|
||||
- Token usage monitoring
|
||||
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
|
||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
||||
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
||||
- Automatic syncing with open editor files (optional)
|
||||
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
||||
@ -260,22 +282,24 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
│ CHAT ASSISTANT │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 1. System Prompt (from Chat Assistant Settings) │
|
||||
│ 2. Project Rules: │
|
||||
│ 2. Agent Role (optional, from role selector): │
|
||||
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
|
||||
│ 3. Project Rules: │
|
||||
│ ├─ .qodeassist/rules/common/*.md │
|
||||
│ └─ .qodeassist/rules/chat/*.md │
|
||||
│ 3. File Context (optional): │
|
||||
│ 4. File Context (optional): │
|
||||
│ ├─ Attached files (manual) │
|
||||
│ ├─ Linked files (persistent) │
|
||||
│ └─ Open editor files (if auto-sync enabled) │
|
||||
│ 4. Tool Definitions (if enabled): │
|
||||
│ 5. Tool Definitions (if enabled): │
|
||||
│ ├─ ReadProjectFileByName │
|
||||
│ ├─ ListProjectFiles │
|
||||
│ ├─ SearchInProject │
|
||||
│ └─ GetIssuesList │
|
||||
│ 5. Conversation History │
|
||||
│ 6. User Message │
|
||||
│ 6. Conversation History │
|
||||
│ 7. User Message │
|
||||
│ │
|
||||
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │
|
||||
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
|
||||
│ [History: Previous messages] │
|
||||
│ [User: FileContext + UserMessage] │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
@ -321,6 +345,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
|
||||
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
||||
- **System Prompts** are configured independently for each feature in Settings
|
||||
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
|
||||
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
||||
- FIM models: Direct completion prompt
|
||||
- Non-FIM models: Prompt includes response formatting instructions
|
||||
@ -328,7 +353,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
- **Custom Instructions** provide reusable templates that can be augmented with specific details
|
||||
- **Tool Calling** is available for Chat and Quick Refactor when enabled
|
||||
|
||||
See [Project Rules Documentation](docs/project-rules.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||
|
||||
## QtCreator Version Compatibility
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ qt_add_qml_module(QodeAssistUIControls
|
||||
RESOURCES
|
||||
icons/dropdown-arrow-light.svg
|
||||
icons/dropdown-arrow-dark.svg
|
||||
QML_FILES qml/QoASeparator.qml
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistUIControls
|
||||
|
||||
@ -24,9 +24,26 @@ import QtQuick.Controls.Basic as Basic
|
||||
Basic.ComboBox {
|
||||
id: control
|
||||
|
||||
property real popupContentWidth: 100
|
||||
|
||||
implicitWidth: Math.min(contentItem.implicitWidth + 8, 300)
|
||||
implicitHeight: 30
|
||||
|
||||
function updatePopupWidth() {
|
||||
var maxWidth = 100;
|
||||
if (model) {
|
||||
for (var i = 0; i < model.length; i++) {
|
||||
textMetrics.text = model[i];
|
||||
maxWidth = Math.max(maxWidth, textMetrics.width + 40);
|
||||
}
|
||||
}
|
||||
popupContentWidth = Math.min(maxWidth, 350);
|
||||
}
|
||||
|
||||
onModelChanged: updatePopupWidth()
|
||||
Component.onCompleted: updatePopupWidth()
|
||||
clip: true
|
||||
|
||||
indicator: Image {
|
||||
id: dropdownIcon
|
||||
|
||||
@ -94,7 +111,7 @@ Basic.ComboBox {
|
||||
|
||||
popup: Popup {
|
||||
y: control.height + 2
|
||||
width: control.width
|
||||
width: Math.max(control.width, control.popupContentWidth)
|
||||
implicitHeight: Math.min(contentItem.implicitHeight, 300)
|
||||
padding: 4
|
||||
|
||||
@ -128,7 +145,7 @@ Basic.ComboBox {
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: control.width - 8
|
||||
width: control.popup.width - 8
|
||||
height: 32
|
||||
|
||||
contentItem: Text {
|
||||
@ -157,5 +174,10 @@ Basic.ComboBox {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
UIControls/qml/QoASeparator.qml
Normal file
28
UIControls/qml/QoASeparator.qml
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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
|
||||
|
||||
height: 15
|
||||
width: 1
|
||||
color: palette.mid
|
||||
}
|
||||
174
docs/agent-roles.md
Normal file
174
docs/agent-roles.md
Normal file
@ -0,0 +1,174 @@
|
||||
# Agent Roles
|
||||
|
||||
Agent Roles allow you to define different AI personas with specialized system prompts for various tasks. Switch between roles instantly in the chat interface to adapt the AI's behavior to your current needs.
|
||||
|
||||
## Overview
|
||||
|
||||
Agent Roles are reusable system prompt configurations that modify how the AI assistant responds. Instead of manually changing system prompts, you can create roles like "Developer", "Code Reviewer", or "Documentation Writer" and switch between them with a single click.
|
||||
|
||||
**Key Features:**
|
||||
- **Quick Switching**: Change roles from the chat toolbar dropdown
|
||||
- **Custom Prompts**: Each role has its own specialized system prompt
|
||||
- **Built-in Roles**: Pre-configured Developer and Code Reviewer roles
|
||||
- **Persistent**: Roles are saved locally and loaded on startup
|
||||
- **Extensible**: Create unlimited custom roles for different tasks
|
||||
|
||||
## Default Roles
|
||||
|
||||
QodeAssist comes with three built-in roles:
|
||||
|
||||
### Developer
|
||||
Experienced Qt/C++ developer with a structured workflow: analyze the problem, propose a solution, wait for approval, then implement. Best for implementation tasks where you want thoughtful, minimal code changes.
|
||||
|
||||
### Code Reviewer
|
||||
Expert C++/QML code reviewer specializing in C++20 and Qt6. Checks for bugs, memory leaks, thread safety, Qt patterns, and production readiness. Provides direct, specific feedback with code examples.
|
||||
|
||||
### Researcher
|
||||
Research-oriented developer who investigates problems and explores solutions. Analyzes problems, presents multiple approaches with trade-offs, and recommends the best option. Does not write implementation code — focuses on helping you make informed decisions.
|
||||
|
||||
## Using Agent Roles
|
||||
|
||||
### Switching Roles in Chat
|
||||
|
||||
1. Open the Chat Assistant (side panel, bottom panel, or popup window)
|
||||
2. Locate the **Role selector** dropdown in the top toolbar (next to the configuration selector)
|
||||
3. Select a role from the dropdown
|
||||
4. The AI will now use the selected role's system prompt
|
||||
|
||||
**Note**: Selecting "No Role" uses only the base system prompt without role specialization.
|
||||
|
||||
### Viewing Active Role
|
||||
|
||||
Click the **Context** button (📋) in the chat toolbar to view:
|
||||
- Base system prompt
|
||||
- Current agent role and its system prompt
|
||||
- Active project rules
|
||||
|
||||
## Managing Agent Roles
|
||||
|
||||
### Opening the Role Manager
|
||||
|
||||
Navigate to: `Qt Creator → Preferences → QodeAssist → Chat Assistant`
|
||||
|
||||
Scroll down to the **Agent Roles** section where you can manage all your roles.
|
||||
|
||||
### Creating a New Role
|
||||
|
||||
1. Click **Add...** button
|
||||
2. Fill in the role details:
|
||||
- **Name**: Display name shown in the dropdown (e.g., "Documentation Writer")
|
||||
- **ID**: Unique identifier for the role file (e.g., "doc_writer")
|
||||
- **Description**: Brief explanation of the role's purpose
|
||||
- **System Prompt**: The specialized instructions for this role
|
||||
3. Click **OK** to save
|
||||
|
||||
### Editing a Role
|
||||
|
||||
1. Select a role from the list
|
||||
2. Click **Edit...** or double-click the role
|
||||
3. Modify the fields as needed
|
||||
4. Click **OK** to save changes
|
||||
|
||||
**Note**: Built-in roles cannot be edited directly. Duplicate them to create a modifiable copy.
|
||||
|
||||
### Duplicating a Role
|
||||
|
||||
1. Select a role to duplicate
|
||||
2. Click **Duplicate...**
|
||||
3. Modify the copy as needed
|
||||
4. Click **OK** to save as a new role
|
||||
|
||||
### Deleting a Role
|
||||
|
||||
1. Select a custom role (built-in roles cannot be deleted)
|
||||
2. Click **Delete**
|
||||
3. Confirm deletion
|
||||
|
||||
## Creating Effective Roles
|
||||
|
||||
### System Prompt Tips
|
||||
|
||||
- **Be specific**: Clearly define the role's expertise and focus areas
|
||||
- **Set expectations**: Describe the desired response format and style
|
||||
- **Include guidelines**: Add specific rules or constraints for responses
|
||||
- **Use structured prompts**: Break down complex roles into bullet points
|
||||
|
||||
## Storage Location
|
||||
|
||||
Agent roles are stored as JSON files in:
|
||||
|
||||
```
|
||||
~/.config/QtProject/qtcreator/qodeassist/agent_roles/
|
||||
```
|
||||
|
||||
**On different platforms:**
|
||||
- **Linux**: `~/.config/QtProject/qtcreator/qodeassist/agent_roles/`
|
||||
- **macOS**: `~/Library/Application Support/QtProject/Qt Creator/qodeassist/agent_roles/`
|
||||
- **Windows**: `%APPDATA%\QtProject\qtcreator\qodeassist\agent_roles\`
|
||||
|
||||
### File Format
|
||||
|
||||
Each role is stored as a JSON file named `{id}.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "doc_writer",
|
||||
"name": "Documentation Writer",
|
||||
"description": "Technical documentation and code comments",
|
||||
"systemPrompt": "You are a technical documentation specialist...",
|
||||
"isBuiltin": false
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Editing
|
||||
|
||||
You can:
|
||||
- Edit JSON files directly in any text editor
|
||||
- Copy role files between machines
|
||||
- Share roles with team members
|
||||
- Version control your roles
|
||||
- Click **Open Roles Folder...** to quickly access the directory
|
||||
|
||||
## How Roles Work
|
||||
|
||||
When a role is selected, the final system prompt is composed as:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Final System Prompt = Base Prompt + Role Prompt │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 1. Base System Prompt (from Chat Settings) │
|
||||
│ 2. Agent Role System Prompt │
|
||||
│ 3. Project Rules (common/ + chat/) │
|
||||
│ 4. Linked Files Context │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
This allows roles to augment rather than replace your base configuration.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep roles focused**: Each role should have a clear, specific purpose
|
||||
2. **Use descriptive names**: Make it easy to identify roles at a glance
|
||||
3. **Test your prompts**: Verify roles produce the expected behavior
|
||||
4. **Iterate and improve**: Refine prompts based on AI responses
|
||||
5. **Share with team**: Export and share useful roles with colleagues
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Role Not Appearing in Dropdown
|
||||
- Restart Qt Creator after adding roles manually
|
||||
- Check JSON file format validity
|
||||
- Verify file is in the correct directory
|
||||
|
||||
### Role Behavior Not as Expected
|
||||
- Review the system prompt for clarity
|
||||
- Check if base system prompt conflicts with role prompt
|
||||
- Try a more specific or detailed prompt
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
|
||||
- [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality
|
||||
- [File Context](file-context.md) - Attaching files to chat context
|
||||
|
||||
115
docs/chat-summarization.md
Normal file
115
docs/chat-summarization.md
Normal file
@ -0,0 +1,115 @@
|
||||
# Chat Summarization
|
||||
|
||||
Chat Summarization allows you to compress long conversations into concise AI-generated summaries. This helps save context tokens and makes it easier to continue work on complex topics without losing important information.
|
||||
|
||||
## Overview
|
||||
|
||||
When conversations grow long, they consume more context tokens with each message. Chat Summarization uses your configured Chat Assistant provider to create an intelligent summary that preserves:
|
||||
|
||||
- Key decisions and conclusions
|
||||
- Technical details and code references
|
||||
- Important context for continuing the conversation
|
||||
|
||||
**Key Features:**
|
||||
- **One-click compression**: Summarize directly from the chat toolbar
|
||||
- **Preserves original**: Creates a new chat file, keeping the original intact
|
||||
- **Smart summaries**: AI extracts the most relevant information
|
||||
- **Markdown formatted**: Summaries are well-structured and readable
|
||||
|
||||
## Using Chat Summarization
|
||||
|
||||
### Compressing a Chat
|
||||
|
||||
1. Open any chat with conversation history
|
||||
2. Click the **Compress** button (📦) in the chat top bar
|
||||
3. Wait for the AI to generate the summary
|
||||
4. A new chat opens with the compressed summary
|
||||
|
||||
### What Gets Preserved
|
||||
|
||||
The summarization process:
|
||||
- Maintains chronological flow of the discussion
|
||||
- Keeps technical details, code snippets, and file references
|
||||
- Preserves key decisions and conclusions
|
||||
- Aims for 30-40% of the original conversation length
|
||||
|
||||
### What Gets Filtered
|
||||
|
||||
The following message types are excluded from summarization:
|
||||
- Tool call results (file reads, searches)
|
||||
- File edit blocks
|
||||
- Thinking/reasoning blocks
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CHAT SUMMARIZATION │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. Original chat messages are collected │
|
||||
│ 2. Tool/thinking messages are filtered out │
|
||||
│ 3. AI generates a structured summary │
|
||||
│ 4. New chat file is created with summary as first message │
|
||||
│ 5. Original chat remains unchanged │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### File Naming
|
||||
|
||||
Compressed chats are saved with a unique suffix:
|
||||
```
|
||||
original_chat.json → original_chat_a1b2c.json
|
||||
```
|
||||
|
||||
Both files appear in your chat history, allowing you to switch between the full conversation and the summary.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Summarize at natural breakpoints**: Compress after completing a major task or topic
|
||||
2. **Review the summary**: Ensure important details were captured before continuing
|
||||
3. **Keep originals**: Don't delete original chats until you've verified the summary is sufficient
|
||||
4. **Use for long sessions**: Most beneficial for conversations with 20+ messages
|
||||
|
||||
## When to Use
|
||||
|
||||
**Good candidates for summarization:**
|
||||
- Long debugging sessions with resolved issues
|
||||
- Feature implementation discussions with final decisions
|
||||
- Research conversations where conclusions were reached
|
||||
- Any chat approaching context limits
|
||||
|
||||
**Consider keeping full history for:**
|
||||
- Ongoing work that may need exact message references
|
||||
- Conversations with important code snippets you'll copy
|
||||
- Discussions where the reasoning process matters
|
||||
|
||||
## Configuration
|
||||
|
||||
Chat Summarization uses your current Chat Assistant settings:
|
||||
- **Provider**: Same as Chat Assistant (Settings → QodeAssist → General)
|
||||
- **Model**: Same as Chat Assistant
|
||||
- **Template**: Same as Chat Assistant
|
||||
|
||||
No additional configuration is required.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Compression Button Not Visible
|
||||
- Ensure you have an active chat with messages
|
||||
- Check that the chat top bar is visible
|
||||
|
||||
### Compression Fails
|
||||
- Verify your Chat Assistant provider is configured correctly
|
||||
- Check network connectivity
|
||||
- Ensure the model supports chat completions
|
||||
|
||||
### Summary Missing Details
|
||||
- The AI aims for 30-40% compression; some details may be condensed
|
||||
- For critical information, keep the original chat
|
||||
- Consider summarizing smaller conversation segments
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Agent Roles](agent-roles.md) - Switch between AI personas
|
||||
- [File Context](file-context.md) - Attach files to chat
|
||||
- [Project Rules](project-rules.md) - Customize AI behavior
|
||||
@ -1,11 +1,15 @@
|
||||
# Configure for OpenAI
|
||||
|
||||
QodeAssist supports both OpenAI's standard Chat Completions API and the new Responses API, giving you access to the latest GPT models including GPT-5.1 and GPT-5.1-codex.
|
||||
|
||||
## Standard OpenAI Configuration
|
||||
|
||||
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)
|
||||
- Select your preferred model (e.g., gpt-4o, gpt-5.1, gpt-5.1-codex)
|
||||
- Choose the OpenAI template for code completion or/and chat
|
||||
|
||||
<details>
|
||||
@ -14,3 +18,15 @@
|
||||
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
|
||||
</details>
|
||||
|
||||
## OpenAI Responses API Configuration
|
||||
|
||||
The Responses API is OpenAI's newer endpoint that provides enhanced capabilities and improved performance. It supports the latest GPT-5.1 models.
|
||||
|
||||
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 Responses" 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-5.1, gpt-5.1-codex)
|
||||
- Choose the OpenAI Responses template for code completion or/and chat
|
||||
|
||||
|
||||
51
llmcore/IToolsManager.hpp
Normal file
51
llmcore/IToolsManager.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "BaseTool.hpp"
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
class IToolsManager
|
||||
{
|
||||
public:
|
||||
virtual ~IToolsManager() = default;
|
||||
|
||||
virtual void executeToolCall(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &input) = 0;
|
||||
|
||||
virtual QJsonArray getToolsDefinitions(
|
||||
ToolSchemaFormat format,
|
||||
RunToolsFilter filter = RunToolsFilter::ALL) const = 0;
|
||||
|
||||
virtual void cleanupRequest(const QString &requestId) = 0;
|
||||
virtual void setCurrentSessionId(const QString &sessionId) = 0;
|
||||
virtual void clearTodoSession(const QString &sessionId) = 0;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
@ -27,6 +27,7 @@
|
||||
#include "ContextData.hpp"
|
||||
#include "DataBuffers.hpp"
|
||||
#include "HttpClient.hpp"
|
||||
#include "IToolsManager.hpp"
|
||||
#include "PromptTemplate.hpp"
|
||||
#include "RequestType.hpp"
|
||||
|
||||
@ -71,6 +72,8 @@ public:
|
||||
|
||||
virtual void cancelRequest(const RequestID &requestId);
|
||||
|
||||
virtual IToolsManager *toolsManager() const { return nullptr; }
|
||||
|
||||
HttpClient *httpClient() const;
|
||||
|
||||
public slots:
|
||||
|
||||
@ -26,6 +26,7 @@ enum class ProviderID {
|
||||
Claude,
|
||||
OpenAI,
|
||||
OpenAICompatible,
|
||||
OpenAIResponses,
|
||||
MistralAI,
|
||||
OpenRouter,
|
||||
GoogleAI,
|
||||
|
||||
@ -268,6 +268,11 @@ void ClaudeProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
LLMCore::IToolsManager *ClaudeProvider::toolsManager() const
|
||||
{
|
||||
return m_toolsManager;
|
||||
}
|
||||
|
||||
void ClaudeProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
@ -531,6 +536,7 @@ void ClaudeProvider::handleMessageComplete(const QString &requestId)
|
||||
for (auto toolContent : toolUseContent) {
|
||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
|
||||
@ -57,6 +57,8 @@ public:
|
||||
bool supportThinking() const override;
|
||||
bool supportImage() const override;
|
||||
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
||||
|
||||
LLMCore::IToolsManager *toolsManager() const override;
|
||||
|
||||
public slots:
|
||||
void onDataReceived(
|
||||
|
||||
54
providers/OpenAIResponses/CancelResponseRequest.hpp
Normal file
54
providers/OpenAIResponses/CancelResponseRequest.hpp
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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::OpenAIResponses {
|
||||
|
||||
struct CancelResponseRequest
|
||||
{
|
||||
QString responseId;
|
||||
|
||||
QString buildUrl(const QString &baseUrl) const
|
||||
{
|
||||
return QString("%1/v1/responses/%2/cancel").arg(baseUrl, responseId);
|
||||
}
|
||||
|
||||
bool isValid() const { return !responseId.isEmpty(); }
|
||||
};
|
||||
|
||||
class CancelResponseRequestBuilder
|
||||
{
|
||||
public:
|
||||
CancelResponseRequestBuilder &setResponseId(const QString &id)
|
||||
{
|
||||
m_request.responseId = id;
|
||||
return *this;
|
||||
}
|
||||
|
||||
CancelResponseRequest build() const { return m_request; }
|
||||
|
||||
private:
|
||||
CancelResponseRequest m_request;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
69
providers/OpenAIResponses/DeleteResponseRequest.hpp
Normal file
69
providers/OpenAIResponses/DeleteResponseRequest.hpp
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QString>
|
||||
|
||||
namespace QodeAssist::OpenAIResponses {
|
||||
|
||||
struct DeleteResponseRequest
|
||||
{
|
||||
QString responseId;
|
||||
|
||||
QString buildUrl(const QString &baseUrl) const
|
||||
{
|
||||
return QString("%1/v1/responses/%2").arg(baseUrl, responseId);
|
||||
}
|
||||
|
||||
bool isValid() const { return !responseId.isEmpty(); }
|
||||
};
|
||||
|
||||
class DeleteResponseRequestBuilder
|
||||
{
|
||||
public:
|
||||
DeleteResponseRequestBuilder &setResponseId(const QString &id)
|
||||
{
|
||||
m_request.responseId = id;
|
||||
return *this;
|
||||
}
|
||||
|
||||
DeleteResponseRequest build() const { return m_request; }
|
||||
|
||||
private:
|
||||
DeleteResponseRequest m_request;
|
||||
};
|
||||
|
||||
struct DeleteResponseResult
|
||||
{
|
||||
bool success = false;
|
||||
QString message;
|
||||
|
||||
static DeleteResponseResult fromJson(const QJsonObject &obj)
|
||||
{
|
||||
DeleteResponseResult result;
|
||||
result.success = obj["success"].toBool();
|
||||
result.message = obj["message"].toString();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
120
providers/OpenAIResponses/GetResponseRequest.hpp
Normal file
120
providers/OpenAIResponses/GetResponseRequest.hpp
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QString>
|
||||
#include <QStringList>
|
||||
#include <optional>
|
||||
|
||||
namespace QodeAssist::OpenAIResponses {
|
||||
|
||||
struct GetResponseRequest
|
||||
{
|
||||
QString responseId;
|
||||
std::optional<QStringList> include;
|
||||
std::optional<bool> includeObfuscation;
|
||||
std::optional<int> startingAfter;
|
||||
std::optional<bool> stream;
|
||||
|
||||
QString buildUrl(const QString &baseUrl) const
|
||||
{
|
||||
QString url = QString("%1/v1/responses/%2").arg(baseUrl, responseId);
|
||||
QStringList queryParams;
|
||||
|
||||
if (include && !include->isEmpty()) {
|
||||
for (const auto &item : *include) {
|
||||
queryParams.append(QString("include=%1").arg(item));
|
||||
}
|
||||
}
|
||||
|
||||
if (includeObfuscation) {
|
||||
queryParams.append(
|
||||
QString("include_obfuscation=%1").arg(*includeObfuscation ? "true" : "false"));
|
||||
}
|
||||
|
||||
if (startingAfter) {
|
||||
queryParams.append(QString("starting_after=%1").arg(*startingAfter));
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
queryParams.append(QString("stream=%1").arg(*stream ? "true" : "false"));
|
||||
}
|
||||
|
||||
if (!queryParams.isEmpty()) {
|
||||
url += "?" + queryParams.join("&");
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
bool isValid() const { return !responseId.isEmpty(); }
|
||||
};
|
||||
|
||||
class GetResponseRequestBuilder
|
||||
{
|
||||
public:
|
||||
GetResponseRequestBuilder &setResponseId(const QString &id)
|
||||
{
|
||||
m_request.responseId = id;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GetResponseRequestBuilder &setInclude(const QStringList &include)
|
||||
{
|
||||
m_request.include = include;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GetResponseRequestBuilder &addInclude(const QString &item)
|
||||
{
|
||||
if (!m_request.include) {
|
||||
m_request.include = QStringList();
|
||||
}
|
||||
m_request.include->append(item);
|
||||
return *this;
|
||||
}
|
||||
|
||||
GetResponseRequestBuilder &setIncludeObfuscation(bool enabled)
|
||||
{
|
||||
m_request.includeObfuscation = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GetResponseRequestBuilder &setStartingAfter(int sequence)
|
||||
{
|
||||
m_request.startingAfter = sequence;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GetResponseRequestBuilder &setStream(bool enabled)
|
||||
{
|
||||
m_request.stream = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GetResponseRequest build() const { return m_request; }
|
||||
|
||||
private:
|
||||
GetResponseRequest m_request;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
219
providers/OpenAIResponses/InputTokensRequest.hpp
Normal file
219
providers/OpenAIResponses/InputTokensRequest.hpp
Normal file
@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "ModelRequest.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::OpenAIResponses {
|
||||
|
||||
struct InputTokensRequest
|
||||
{
|
||||
std::optional<QString> conversation;
|
||||
std::optional<QJsonArray> input;
|
||||
std::optional<QString> instructions;
|
||||
std::optional<QString> model;
|
||||
std::optional<bool> parallelToolCalls;
|
||||
std::optional<QString> previousResponseId;
|
||||
std::optional<QJsonObject> reasoning;
|
||||
std::optional<QJsonObject> text;
|
||||
std::optional<QJsonValue> toolChoice;
|
||||
std::optional<QJsonArray> tools;
|
||||
std::optional<QString> truncation;
|
||||
|
||||
QString buildUrl(const QString &baseUrl) const
|
||||
{
|
||||
return QString("%1/v1/responses/input_tokens").arg(baseUrl);
|
||||
}
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
if (conversation)
|
||||
obj["conversation"] = *conversation;
|
||||
if (input)
|
||||
obj["input"] = *input;
|
||||
if (instructions)
|
||||
obj["instructions"] = *instructions;
|
||||
if (model)
|
||||
obj["model"] = *model;
|
||||
if (parallelToolCalls)
|
||||
obj["parallel_tool_calls"] = *parallelToolCalls;
|
||||
if (previousResponseId)
|
||||
obj["previous_response_id"] = *previousResponseId;
|
||||
if (reasoning)
|
||||
obj["reasoning"] = *reasoning;
|
||||
if (text)
|
||||
obj["text"] = *text;
|
||||
if (toolChoice)
|
||||
obj["tool_choice"] = *toolChoice;
|
||||
if (tools)
|
||||
obj["tools"] = *tools;
|
||||
if (truncation)
|
||||
obj["truncation"] = *truncation;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool isValid() const { return input.has_value() || previousResponseId.has_value(); }
|
||||
};
|
||||
|
||||
class InputTokensRequestBuilder
|
||||
{
|
||||
public:
|
||||
InputTokensRequestBuilder &setConversation(const QString &conversationId)
|
||||
{
|
||||
m_request.conversation = conversationId;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setInput(const QJsonArray &input)
|
||||
{
|
||||
m_request.input = input;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &addInputMessage(const Message &message)
|
||||
{
|
||||
if (!m_request.input) {
|
||||
m_request.input = QJsonArray();
|
||||
}
|
||||
m_request.input->append(message.toJson());
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setInstructions(const QString &instructions)
|
||||
{
|
||||
m_request.instructions = instructions;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setModel(const QString &model)
|
||||
{
|
||||
m_request.model = model;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setParallelToolCalls(bool enabled)
|
||||
{
|
||||
m_request.parallelToolCalls = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setPreviousResponseId(const QString &responseId)
|
||||
{
|
||||
m_request.previousResponseId = responseId;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setReasoning(const QJsonObject &reasoning)
|
||||
{
|
||||
m_request.reasoning = reasoning;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setReasoningEffort(ReasoningEffort effort)
|
||||
{
|
||||
QString effortStr;
|
||||
switch (effort) {
|
||||
case ReasoningEffort::None:
|
||||
effortStr = "none";
|
||||
break;
|
||||
case ReasoningEffort::Minimal:
|
||||
effortStr = "minimal";
|
||||
break;
|
||||
case ReasoningEffort::Low:
|
||||
effortStr = "low";
|
||||
break;
|
||||
case ReasoningEffort::Medium:
|
||||
effortStr = "medium";
|
||||
break;
|
||||
case ReasoningEffort::High:
|
||||
effortStr = "high";
|
||||
break;
|
||||
}
|
||||
m_request.reasoning = QJsonObject{{"effort", effortStr}};
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setText(const QJsonObject &text)
|
||||
{
|
||||
m_request.text = text;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setTextFormat(const TextFormatOptions &format)
|
||||
{
|
||||
m_request.text = format.toJson();
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setToolChoice(const QJsonValue &toolChoice)
|
||||
{
|
||||
m_request.toolChoice = toolChoice;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setTools(const QJsonArray &tools)
|
||||
{
|
||||
m_request.tools = tools;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &addTool(const Tool &tool)
|
||||
{
|
||||
if (!m_request.tools) {
|
||||
m_request.tools = QJsonArray();
|
||||
}
|
||||
m_request.tools->append(tool.toJson());
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequestBuilder &setTruncation(const QString &truncation)
|
||||
{
|
||||
m_request.truncation = truncation;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InputTokensRequest build() const { return m_request; }
|
||||
|
||||
private:
|
||||
InputTokensRequest m_request;
|
||||
};
|
||||
|
||||
struct InputTokensResponse
|
||||
{
|
||||
QString object;
|
||||
int inputTokens = 0;
|
||||
|
||||
static InputTokensResponse fromJson(const QJsonObject &obj)
|
||||
{
|
||||
InputTokensResponse result;
|
||||
result.object = obj["object"].toString();
|
||||
result.inputTokens = obj["input_tokens"].toInt();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
143
providers/OpenAIResponses/ItemTypesReference.hpp
Normal file
143
providers/OpenAIResponses/ItemTypesReference.hpp
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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::OpenAIResponses {
|
||||
|
||||
/*
|
||||
* REFERENCE: Item Types in List Input Items Response
|
||||
* ===================================================
|
||||
*
|
||||
* The `data` array in ListInputItemsResponse can contain various item types.
|
||||
* This file serves as a reference for all possible item types.
|
||||
*
|
||||
* EXISTING TYPES (already implemented):
|
||||
* -------------------------------------
|
||||
* - MessageOutput (in ResponseObject.hpp)
|
||||
* - FunctionCall (in ResponseObject.hpp)
|
||||
* - ReasoningOutput (in ResponseObject.hpp)
|
||||
* - FileSearchCall (in ResponseObject.hpp)
|
||||
* - CodeInterpreterCall (in ResponseObject.hpp)
|
||||
* - Message (in ModelRequest.hpp) - for input messages
|
||||
*
|
||||
* ADDITIONAL TYPES (to be implemented if needed):
|
||||
* -----------------------------------------------
|
||||
*
|
||||
* 1. Computer Tool Call (computer_call)
|
||||
* - Computer use tool for UI automation
|
||||
* - Properties: action, call_id, id, pending_safety_checks, status, type
|
||||
* - Actions: click, double_click, drag, keypress, move, screenshot, scroll, type, wait
|
||||
*
|
||||
* 2. Computer Tool Call Output (computer_call_output)
|
||||
* - Output from computer tool
|
||||
* - Properties: call_id, id, output, type, acknowledged_safety_checks, status
|
||||
*
|
||||
* 3. Web Search Tool Call (web_search_call)
|
||||
* - Web search results
|
||||
* - Properties: action, id, status, type
|
||||
* - Actions: search, open_page, find
|
||||
*
|
||||
* 4. Image Generation Call (image_generation_call)
|
||||
* - AI image generation request
|
||||
* - Properties: id, result (base64), status, type
|
||||
*
|
||||
* 5. Local Shell Call (local_shell_call)
|
||||
* - Execute shell commands locally
|
||||
* - Properties: action (exec), call_id, id, status, type
|
||||
* - Action properties: command, env, timeout_ms, user, working_directory
|
||||
*
|
||||
* 6. Local Shell Call Output (local_shell_call_output)
|
||||
* - Output from local shell execution
|
||||
* - Properties: id, output (JSON string), type, status
|
||||
*
|
||||
* 7. Shell Tool Call (shell_call)
|
||||
* - Managed shell environment execution
|
||||
* - Properties: action, call_id, id, status, type, created_by
|
||||
*
|
||||
* 8. Shell Call Output (shell_call_output)
|
||||
* - Output from shell tool
|
||||
* - Properties: call_id, id, max_output_length, output (array), type, created_by
|
||||
* - Output chunks: outcome (exit/timeout), stderr, stdout
|
||||
*
|
||||
* 9. Apply Patch Tool Call (apply_patch_call)
|
||||
* - File diff operations
|
||||
* - Properties: call_id, id, operation, status, type, created_by
|
||||
* - Operations: create_file, delete_file, update_file
|
||||
*
|
||||
* 10. Apply Patch Tool Call Output (apply_patch_call_output)
|
||||
* - Output from patch operations
|
||||
* - Properties: call_id, id, status, type, created_by, output
|
||||
*
|
||||
* 11. MCP List Tools (mcp_list_tools)
|
||||
* - List of tools from MCP server
|
||||
* - Properties: id, server_label, tools (array), type, error
|
||||
*
|
||||
* 12. MCP Approval Request (mcp_approval_request)
|
||||
* - Request for human approval
|
||||
* - Properties: arguments, id, name, server_label, type
|
||||
*
|
||||
* 13. MCP Approval Response (mcp_approval_response)
|
||||
* - Response to approval request
|
||||
* - Properties: approval_request_id, approve (bool), id, type, reason
|
||||
*
|
||||
* 14. MCP Tool Call (mcp_call)
|
||||
* - Tool invocation on MCP server
|
||||
* - Properties: arguments, id, name, server_label, type
|
||||
* - Optional: approval_request_id, error, output, status
|
||||
*
|
||||
* 15. Custom Tool Call (custom_tool_call)
|
||||
* - User-defined tool call
|
||||
* - Properties: call_id, input, name, type, id
|
||||
*
|
||||
* 16. Custom Tool Call Output (custom_tool_call_output)
|
||||
* - Output from custom tool
|
||||
* - Properties: call_id, output (string or array), type, id
|
||||
*
|
||||
* 17. Item Reference (item_reference)
|
||||
* - Internal reference to another item
|
||||
* - Properties: id, type
|
||||
*
|
||||
* USAGE:
|
||||
* ------
|
||||
* When parsing ListInputItemsResponse.data array:
|
||||
* 1. Check item["type"] field
|
||||
* 2. Use appropriate parser based on type
|
||||
* 3. For existing types, use ResponseObject.hpp or ModelRequest.hpp
|
||||
* 4. For additional types, implement parsers as needed
|
||||
*
|
||||
* EXAMPLE:
|
||||
* --------
|
||||
* for (const auto &itemValue : response.data) {
|
||||
* const QJsonObject itemObj = itemValue.toObject();
|
||||
* const QString type = itemObj["type"].toString();
|
||||
*
|
||||
* if (type == "message") {
|
||||
* // Use MessageOutput or Message
|
||||
* } else if (type == "function_call") {
|
||||
* // Use FunctionCall
|
||||
* } else if (type == "computer_call") {
|
||||
* // Implement ComputerCall parser
|
||||
* }
|
||||
* // ... handle other types
|
||||
* }
|
||||
*/
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
166
providers/OpenAIResponses/ListInputItemsRequest.hpp
Normal file
166
providers/OpenAIResponses/ListInputItemsRequest.hpp
Normal file
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QStringList>
|
||||
#include <optional>
|
||||
|
||||
namespace QodeAssist::OpenAIResponses {
|
||||
|
||||
enum class SortOrder { Ascending, Descending };
|
||||
|
||||
struct ListInputItemsRequest
|
||||
{
|
||||
QString responseId;
|
||||
std::optional<QString> after;
|
||||
std::optional<QStringList> include;
|
||||
std::optional<int> limit;
|
||||
std::optional<SortOrder> order;
|
||||
|
||||
QString buildUrl(const QString &baseUrl) const
|
||||
{
|
||||
QString url = QString("%1/v1/responses/%2/input_items").arg(baseUrl, responseId);
|
||||
QStringList queryParams;
|
||||
|
||||
if (after) {
|
||||
queryParams.append(QString("after=%1").arg(*after));
|
||||
}
|
||||
|
||||
if (include && !include->isEmpty()) {
|
||||
for (const auto &item : *include) {
|
||||
queryParams.append(QString("include=%1").arg(item));
|
||||
}
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
queryParams.append(QString("limit=%1").arg(*limit));
|
||||
}
|
||||
|
||||
if (order) {
|
||||
QString orderStr = (*order == SortOrder::Ascending) ? "asc" : "desc";
|
||||
queryParams.append(QString("order=%1").arg(orderStr));
|
||||
}
|
||||
|
||||
if (!queryParams.isEmpty()) {
|
||||
url += "?" + queryParams.join("&");
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
bool isValid() const
|
||||
{
|
||||
if (responseId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (limit && (*limit < 1 || *limit > 100)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class ListInputItemsRequestBuilder
|
||||
{
|
||||
public:
|
||||
ListInputItemsRequestBuilder &setResponseId(const QString &id)
|
||||
{
|
||||
m_request.responseId = id;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequestBuilder &setAfter(const QString &itemId)
|
||||
{
|
||||
m_request.after = itemId;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequestBuilder &setInclude(const QStringList &include)
|
||||
{
|
||||
m_request.include = include;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequestBuilder &addInclude(const QString &item)
|
||||
{
|
||||
if (!m_request.include) {
|
||||
m_request.include = QStringList();
|
||||
}
|
||||
m_request.include->append(item);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequestBuilder &setLimit(int limit)
|
||||
{
|
||||
m_request.limit = limit;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequestBuilder &setOrder(SortOrder order)
|
||||
{
|
||||
m_request.order = order;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequestBuilder &setAscendingOrder()
|
||||
{
|
||||
m_request.order = SortOrder::Ascending;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequestBuilder &setDescendingOrder()
|
||||
{
|
||||
m_request.order = SortOrder::Descending;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ListInputItemsRequest build() const { return m_request; }
|
||||
|
||||
private:
|
||||
ListInputItemsRequest m_request;
|
||||
};
|
||||
|
||||
struct ListInputItemsResponse
|
||||
{
|
||||
QJsonArray data;
|
||||
QString firstId;
|
||||
QString lastId;
|
||||
bool hasMore = false;
|
||||
QString object;
|
||||
|
||||
static ListInputItemsResponse fromJson(const QJsonObject &obj)
|
||||
{
|
||||
ListInputItemsResponse result;
|
||||
result.data = obj["data"].toArray();
|
||||
result.firstId = obj["first_id"].toString();
|
||||
result.lastId = obj["last_id"].toString();
|
||||
result.hasMore = obj["has_more"].toBool();
|
||||
result.object = obj["object"].toString();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
354
providers/OpenAIResponses/ModelRequest.hpp
Normal file
354
providers/OpenAIResponses/ModelRequest.hpp
Normal file
@ -0,0 +1,354 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QVariant>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
namespace QodeAssist::OpenAIResponses {
|
||||
|
||||
enum class Role { User, Assistant, System, Developer };
|
||||
|
||||
enum class MessageStatus { InProgress, Completed, Incomplete };
|
||||
|
||||
enum class ReasoningEffort { None, Minimal, Low, Medium, High };
|
||||
|
||||
enum class TextFormat { Text, JsonSchema, JsonObject };
|
||||
|
||||
struct InputText
|
||||
{
|
||||
QString text;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
return QJsonObject{{"type", "input_text"}, {"text", text}};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !text.isEmpty(); }
|
||||
};
|
||||
|
||||
struct InputImage
|
||||
{
|
||||
std::optional<QString> fileId;
|
||||
std::optional<QString> imageUrl;
|
||||
QString detail = "auto";
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj{{"type", "input_image"}, {"detail", detail}};
|
||||
if (fileId)
|
||||
obj["file_id"] = *fileId;
|
||||
if (imageUrl)
|
||||
obj["image_url"] = *imageUrl;
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return fileId.has_value() || imageUrl.has_value(); }
|
||||
};
|
||||
|
||||
struct InputFile
|
||||
{
|
||||
std::optional<QString> fileId;
|
||||
std::optional<QString> fileUrl;
|
||||
std::optional<QString> fileData;
|
||||
std::optional<QString> filename;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj{{"type", "input_file"}};
|
||||
if (fileId)
|
||||
obj["file_id"] = *fileId;
|
||||
if (fileUrl)
|
||||
obj["file_url"] = *fileUrl;
|
||||
if (fileData)
|
||||
obj["file_data"] = *fileData;
|
||||
if (filename)
|
||||
obj["filename"] = *filename;
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return fileId.has_value() || fileUrl.has_value() || fileData.has_value();
|
||||
}
|
||||
};
|
||||
|
||||
class MessageContent
|
||||
{
|
||||
public:
|
||||
MessageContent(QString text) : m_variant(std::move(text)) {}
|
||||
MessageContent(InputText text) : m_variant(std::move(text)) {}
|
||||
MessageContent(InputImage image) : m_variant(std::move(image)) {}
|
||||
MessageContent(InputFile file) : m_variant(std::move(file)) {}
|
||||
|
||||
QJsonValue toJson() const
|
||||
{
|
||||
return std::visit([](const auto &content) -> QJsonValue {
|
||||
using T = std::decay_t<decltype(content)>;
|
||||
if constexpr (std::is_same_v<T, QString>) {
|
||||
return content;
|
||||
} else {
|
||||
return content.toJson();
|
||||
}
|
||||
}, m_variant);
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return std::visit([](const auto &content) -> bool {
|
||||
using T = std::decay_t<decltype(content)>;
|
||||
if constexpr (std::is_same_v<T, QString>) {
|
||||
return !content.isEmpty();
|
||||
} else {
|
||||
return content.isValid();
|
||||
}
|
||||
}, m_variant);
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<QString, InputText, InputImage, InputFile> m_variant;
|
||||
};
|
||||
|
||||
struct Message
|
||||
{
|
||||
Role role;
|
||||
QList<MessageContent> content;
|
||||
std::optional<MessageStatus> status;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj["role"] = roleToString(role);
|
||||
|
||||
if (content.size() == 1) {
|
||||
obj["content"] = content[0].toJson();
|
||||
} else {
|
||||
QJsonArray arr;
|
||||
for (const auto &c : content) {
|
||||
arr.append(c.toJson());
|
||||
}
|
||||
obj["content"] = arr;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
obj["status"] = statusToString(*status);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
if (content.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto &c : content) {
|
||||
if (!c.isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static QString roleToString(Role r) noexcept
|
||||
{
|
||||
switch (r) {
|
||||
case Role::User:
|
||||
return "user";
|
||||
case Role::Assistant:
|
||||
return "assistant";
|
||||
case Role::System:
|
||||
return "system";
|
||||
case Role::Developer:
|
||||
return "developer";
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
|
||||
static QString statusToString(MessageStatus s) noexcept
|
||||
{
|
||||
switch (s) {
|
||||
case MessageStatus::InProgress:
|
||||
return "in_progress";
|
||||
case MessageStatus::Completed:
|
||||
return "completed";
|
||||
case MessageStatus::Incomplete:
|
||||
return "incomplete";
|
||||
}
|
||||
return "in_progress";
|
||||
}
|
||||
};
|
||||
|
||||
struct FunctionTool
|
||||
{
|
||||
QString name;
|
||||
QJsonObject parameters;
|
||||
std::optional<QString> description;
|
||||
bool strict = true;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj{{"type", "function"},
|
||||
{"name", name},
|
||||
{"parameters", parameters},
|
||||
{"strict", strict}};
|
||||
if (description)
|
||||
obj["description"] = *description;
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return !name.isEmpty() && !parameters.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
struct FileSearchTool
|
||||
{
|
||||
QStringList vectorStoreIds;
|
||||
std::optional<int> maxNumResults;
|
||||
std::optional<double> scoreThreshold;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj{{"type", "file_search"}};
|
||||
QJsonArray ids;
|
||||
for (const auto &id : vectorStoreIds) {
|
||||
ids.append(id);
|
||||
}
|
||||
obj["vector_store_ids"] = ids;
|
||||
|
||||
if (maxNumResults)
|
||||
obj["max_num_results"] = *maxNumResults;
|
||||
if (scoreThreshold)
|
||||
obj["score_threshold"] = *scoreThreshold;
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return !vectorStoreIds.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
struct WebSearchTool
|
||||
{
|
||||
QString searchContextSize = "medium";
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
return QJsonObject{{"type", "web_search"}, {"search_context_size", searchContextSize}};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return !searchContextSize.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
struct CodeInterpreterTool
|
||||
{
|
||||
QString container;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
return QJsonObject{{"type", "code_interpreter"}, {"container", container}};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return !container.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
class Tool
|
||||
{
|
||||
public:
|
||||
Tool(FunctionTool tool) : m_variant(std::move(tool)) {}
|
||||
Tool(FileSearchTool tool) : m_variant(std::move(tool)) {}
|
||||
Tool(WebSearchTool tool) : m_variant(std::move(tool)) {}
|
||||
Tool(CodeInterpreterTool tool) : m_variant(std::move(tool)) {}
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
return std::visit([](const auto &t) { return t.toJson(); }, m_variant);
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return std::visit([](const auto &t) { return t.isValid(); }, m_variant);
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<FunctionTool, FileSearchTool, WebSearchTool, CodeInterpreterTool> m_variant;
|
||||
};
|
||||
|
||||
struct TextFormatOptions
|
||||
{
|
||||
TextFormat type = TextFormat::Text;
|
||||
std::optional<QString> name;
|
||||
std::optional<QJsonObject> schema;
|
||||
std::optional<QString> description;
|
||||
std::optional<bool> strict;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
switch (type) {
|
||||
case TextFormat::Text:
|
||||
obj["type"] = "text";
|
||||
break;
|
||||
case TextFormat::JsonSchema:
|
||||
obj["type"] = "json_schema";
|
||||
if (name)
|
||||
obj["name"] = *name;
|
||||
if (schema)
|
||||
obj["schema"] = *schema;
|
||||
if (description)
|
||||
obj["description"] = *description;
|
||||
if (strict)
|
||||
obj["strict"] = *strict;
|
||||
break;
|
||||
case TextFormat::JsonObject:
|
||||
obj["type"] = "json_object";
|
||||
break;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
if (type == TextFormat::JsonSchema) {
|
||||
return name.has_value() && schema.has_value();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
562
providers/OpenAIResponses/ResponseObject.hpp
Normal file
562
providers/OpenAIResponses/ResponseObject.hpp
Normal file
@ -0,0 +1,562 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <optional>
|
||||
#include <variant>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::OpenAIResponses {
|
||||
|
||||
enum class ResponseStatus { Completed, Failed, InProgress, Cancelled, Queued, Incomplete };
|
||||
|
||||
enum class ItemStatus { InProgress, Completed, Incomplete };
|
||||
|
||||
struct FileCitation
|
||||
{
|
||||
QString fileId;
|
||||
QString filename;
|
||||
int index = 0;
|
||||
|
||||
static FileCitation fromJson(const QJsonObject &obj)
|
||||
{
|
||||
return {obj["file_id"].toString(), obj["filename"].toString(), obj["index"].toInt()};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !fileId.isEmpty(); }
|
||||
};
|
||||
|
||||
struct UrlCitation
|
||||
{
|
||||
QString url;
|
||||
QString title;
|
||||
int startIndex = 0;
|
||||
int endIndex = 0;
|
||||
|
||||
static UrlCitation fromJson(const QJsonObject &obj)
|
||||
{
|
||||
return {
|
||||
obj["url"].toString(),
|
||||
obj["title"].toString(),
|
||||
obj["start_index"].toInt(),
|
||||
obj["end_index"].toInt()};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !url.isEmpty(); }
|
||||
};
|
||||
|
||||
struct OutputText
|
||||
{
|
||||
QString text;
|
||||
QList<FileCitation> fileCitations;
|
||||
QList<UrlCitation> urlCitations;
|
||||
|
||||
static OutputText fromJson(const QJsonObject &obj)
|
||||
{
|
||||
OutputText result;
|
||||
result.text = obj["text"].toString();
|
||||
|
||||
if (obj.contains("annotations")) {
|
||||
const QJsonArray annotations = obj["annotations"].toArray();
|
||||
result.fileCitations.reserve(annotations.size());
|
||||
result.urlCitations.reserve(annotations.size());
|
||||
|
||||
for (const auto &annValue : annotations) {
|
||||
const QJsonObject ann = annValue.toObject();
|
||||
const QString type = ann["type"].toString();
|
||||
if (type == "file_citation") {
|
||||
result.fileCitations.append(FileCitation::fromJson(ann));
|
||||
} else if (type == "url_citation") {
|
||||
result.urlCitations.append(UrlCitation::fromJson(ann));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !text.isEmpty(); }
|
||||
};
|
||||
|
||||
struct Refusal
|
||||
{
|
||||
QString refusal;
|
||||
|
||||
static Refusal fromJson(const QJsonObject &obj)
|
||||
{
|
||||
return {obj["refusal"].toString()};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !refusal.isEmpty(); }
|
||||
};
|
||||
|
||||
struct MessageOutput
|
||||
{
|
||||
QString id;
|
||||
QString role;
|
||||
ItemStatus status = ItemStatus::InProgress;
|
||||
QList<OutputText> outputTexts;
|
||||
QList<Refusal> refusals;
|
||||
|
||||
static MessageOutput fromJson(const QJsonObject &obj)
|
||||
{
|
||||
MessageOutput result;
|
||||
result.id = obj["id"].toString();
|
||||
result.role = obj["role"].toString();
|
||||
|
||||
const QString statusStr = obj["status"].toString();
|
||||
if (statusStr == "in_progress")
|
||||
result.status = ItemStatus::InProgress;
|
||||
else if (statusStr == "completed")
|
||||
result.status = ItemStatus::Completed;
|
||||
else
|
||||
result.status = ItemStatus::Incomplete;
|
||||
|
||||
if (obj.contains("content")) {
|
||||
const QJsonArray content = obj["content"].toArray();
|
||||
result.outputTexts.reserve(content.size());
|
||||
result.refusals.reserve(content.size());
|
||||
|
||||
for (const auto &item : content) {
|
||||
const QJsonObject itemObj = item.toObject();
|
||||
const QString type = itemObj["type"].toString();
|
||||
|
||||
if (type == "output_text") {
|
||||
result.outputTexts.append(OutputText::fromJson(itemObj));
|
||||
} else if (type == "refusal") {
|
||||
result.refusals.append(Refusal::fromJson(itemObj));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||
bool hasContent() const noexcept { return !outputTexts.isEmpty() || !refusals.isEmpty(); }
|
||||
};
|
||||
|
||||
struct FunctionCall
|
||||
{
|
||||
QString id;
|
||||
QString callId;
|
||||
QString name;
|
||||
QString arguments;
|
||||
ItemStatus status = ItemStatus::InProgress;
|
||||
|
||||
static FunctionCall fromJson(const QJsonObject &obj)
|
||||
{
|
||||
FunctionCall result;
|
||||
result.id = obj["id"].toString();
|
||||
result.callId = obj["call_id"].toString();
|
||||
result.name = obj["name"].toString();
|
||||
result.arguments = obj["arguments"].toString();
|
||||
|
||||
const QString statusStr = obj["status"].toString();
|
||||
if (statusStr == "in_progress")
|
||||
result.status = ItemStatus::InProgress;
|
||||
else if (statusStr == "completed")
|
||||
result.status = ItemStatus::Completed;
|
||||
else
|
||||
result.status = ItemStatus::Incomplete;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !id.isEmpty() && !callId.isEmpty() && !name.isEmpty(); }
|
||||
};
|
||||
|
||||
struct ReasoningOutput
|
||||
{
|
||||
QString id;
|
||||
ItemStatus status = ItemStatus::InProgress;
|
||||
QString summaryText;
|
||||
QString encryptedContent;
|
||||
QList<QString> contentTexts;
|
||||
|
||||
static ReasoningOutput fromJson(const QJsonObject &obj)
|
||||
{
|
||||
ReasoningOutput result;
|
||||
result.id = obj["id"].toString();
|
||||
|
||||
const QString statusStr = obj["status"].toString();
|
||||
if (statusStr == "in_progress")
|
||||
result.status = ItemStatus::InProgress;
|
||||
else if (statusStr == "completed")
|
||||
result.status = ItemStatus::Completed;
|
||||
else
|
||||
result.status = ItemStatus::Incomplete;
|
||||
|
||||
if (obj.contains("summary")) {
|
||||
const QJsonArray summary = obj["summary"].toArray();
|
||||
for (const auto &item : summary) {
|
||||
const QJsonObject itemObj = item.toObject();
|
||||
if (itemObj["type"].toString() == "summary_text") {
|
||||
result.summaryText = itemObj["text"].toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contains("content")) {
|
||||
const QJsonArray content = obj["content"].toArray();
|
||||
result.contentTexts.reserve(content.size());
|
||||
|
||||
for (const auto &item : content) {
|
||||
const QJsonObject itemObj = item.toObject();
|
||||
if (itemObj["type"].toString() == "reasoning_text") {
|
||||
result.contentTexts.append(itemObj["text"].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contains("encrypted_content")) {
|
||||
result.encryptedContent = obj["encrypted_content"].toString();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||
bool hasContent() const noexcept
|
||||
{
|
||||
return !summaryText.isEmpty() || !contentTexts.isEmpty() || !encryptedContent.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
struct FileSearchResult
|
||||
{
|
||||
QString fileId;
|
||||
QString filename;
|
||||
QString text;
|
||||
double score = 0.0;
|
||||
|
||||
static FileSearchResult fromJson(const QJsonObject &obj)
|
||||
{
|
||||
return {
|
||||
obj["file_id"].toString(),
|
||||
obj["filename"].toString(),
|
||||
obj["text"].toString(),
|
||||
obj["score"].toDouble()};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !fileId.isEmpty(); }
|
||||
};
|
||||
|
||||
struct FileSearchCall
|
||||
{
|
||||
QString id;
|
||||
QString status;
|
||||
QStringList queries;
|
||||
QList<FileSearchResult> results;
|
||||
|
||||
static FileSearchCall fromJson(const QJsonObject &obj)
|
||||
{
|
||||
FileSearchCall result;
|
||||
result.id = obj["id"].toString();
|
||||
result.status = obj["status"].toString();
|
||||
|
||||
if (obj.contains("queries")) {
|
||||
const QJsonArray queries = obj["queries"].toArray();
|
||||
result.queries.reserve(queries.size());
|
||||
|
||||
for (const auto &q : queries) {
|
||||
result.queries.append(q.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contains("results")) {
|
||||
const QJsonArray results = obj["results"].toArray();
|
||||
result.results.reserve(results.size());
|
||||
|
||||
for (const auto &r : results) {
|
||||
result.results.append(FileSearchResult::fromJson(r.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||
};
|
||||
|
||||
struct CodeInterpreterOutput
|
||||
{
|
||||
QString type;
|
||||
QString logs;
|
||||
QString imageUrl;
|
||||
|
||||
static CodeInterpreterOutput fromJson(const QJsonObject &obj)
|
||||
{
|
||||
CodeInterpreterOutput result;
|
||||
result.type = obj["type"].toString();
|
||||
if (result.type == "logs") {
|
||||
result.logs = obj["logs"].toString();
|
||||
} else if (result.type == "image") {
|
||||
result.imageUrl = obj["url"].toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept
|
||||
{
|
||||
return !type.isEmpty() && (!logs.isEmpty() || !imageUrl.isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
struct CodeInterpreterCall
|
||||
{
|
||||
QString id;
|
||||
QString containerId;
|
||||
std::optional<QString> code;
|
||||
QString status;
|
||||
QList<CodeInterpreterOutput> outputs;
|
||||
|
||||
static CodeInterpreterCall fromJson(const QJsonObject &obj)
|
||||
{
|
||||
CodeInterpreterCall result;
|
||||
result.id = obj["id"].toString();
|
||||
result.containerId = obj["container_id"].toString();
|
||||
result.status = obj["status"].toString();
|
||||
|
||||
if (obj.contains("code") && !obj["code"].isNull()) {
|
||||
result.code = obj["code"].toString();
|
||||
}
|
||||
|
||||
if (obj.contains("outputs")) {
|
||||
const QJsonArray outputs = obj["outputs"].toArray();
|
||||
result.outputs.reserve(outputs.size());
|
||||
|
||||
for (const auto &o : outputs) {
|
||||
result.outputs.append(CodeInterpreterOutput::fromJson(o.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !id.isEmpty() && !containerId.isEmpty(); }
|
||||
};
|
||||
|
||||
class OutputItem
|
||||
{
|
||||
public:
|
||||
enum class Type { Message, FunctionCall, Reasoning, FileSearch, CodeInterpreter, Unknown };
|
||||
|
||||
explicit OutputItem(const MessageOutput &msg)
|
||||
: m_type(Type::Message)
|
||||
, m_data(msg)
|
||||
{}
|
||||
explicit OutputItem(const FunctionCall &call)
|
||||
: m_type(Type::FunctionCall)
|
||||
, m_data(call)
|
||||
{}
|
||||
explicit OutputItem(const ReasoningOutput &reasoning)
|
||||
: m_type(Type::Reasoning)
|
||||
, m_data(reasoning)
|
||||
{}
|
||||
explicit OutputItem(const FileSearchCall &search)
|
||||
: m_type(Type::FileSearch)
|
||||
, m_data(search)
|
||||
{}
|
||||
explicit OutputItem(const CodeInterpreterCall &interpreter)
|
||||
: m_type(Type::CodeInterpreter)
|
||||
, m_data(interpreter)
|
||||
{}
|
||||
|
||||
Type type() const { return m_type; }
|
||||
|
||||
const MessageOutput *asMessage() const
|
||||
{
|
||||
return std::holds_alternative<MessageOutput>(m_data) ? &std::get<MessageOutput>(m_data)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
const FunctionCall *asFunctionCall() const
|
||||
{
|
||||
return std::holds_alternative<FunctionCall>(m_data) ? &std::get<FunctionCall>(m_data)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
const ReasoningOutput *asReasoning() const
|
||||
{
|
||||
return std::holds_alternative<ReasoningOutput>(m_data) ? &std::get<ReasoningOutput>(m_data)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
const FileSearchCall *asFileSearch() const
|
||||
{
|
||||
return std::holds_alternative<FileSearchCall>(m_data) ? &std::get<FileSearchCall>(m_data)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
const CodeInterpreterCall *asCodeInterpreter() const
|
||||
{
|
||||
return std::holds_alternative<CodeInterpreterCall>(m_data)
|
||||
? &std::get<CodeInterpreterCall>(m_data)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
static OutputItem fromJson(const QJsonObject &obj)
|
||||
{
|
||||
const QString type = obj["type"].toString();
|
||||
|
||||
if (type == "message") {
|
||||
return OutputItem(MessageOutput::fromJson(obj));
|
||||
} else if (type == "function_call") {
|
||||
return OutputItem(FunctionCall::fromJson(obj));
|
||||
} else if (type == "reasoning") {
|
||||
return OutputItem(ReasoningOutput::fromJson(obj));
|
||||
} else if (type == "file_search_call") {
|
||||
return OutputItem(FileSearchCall::fromJson(obj));
|
||||
} else if (type == "code_interpreter_call") {
|
||||
return OutputItem(CodeInterpreterCall::fromJson(obj));
|
||||
}
|
||||
|
||||
return OutputItem(MessageOutput{});
|
||||
}
|
||||
|
||||
private:
|
||||
Type m_type;
|
||||
std::variant<MessageOutput, FunctionCall, ReasoningOutput, FileSearchCall, CodeInterpreterCall>
|
||||
m_data;
|
||||
};
|
||||
|
||||
struct Usage
|
||||
{
|
||||
int inputTokens = 0;
|
||||
int outputTokens = 0;
|
||||
int totalTokens = 0;
|
||||
|
||||
static Usage fromJson(const QJsonObject &obj)
|
||||
{
|
||||
return {
|
||||
obj["input_tokens"].toInt(),
|
||||
obj["output_tokens"].toInt(),
|
||||
obj["total_tokens"].toInt()
|
||||
};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return totalTokens > 0; }
|
||||
};
|
||||
|
||||
struct ResponseError
|
||||
{
|
||||
QString code;
|
||||
QString message;
|
||||
|
||||
static ResponseError fromJson(const QJsonObject &obj)
|
||||
{
|
||||
return {obj["code"].toString(), obj["message"].toString()};
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !code.isEmpty() && !message.isEmpty(); }
|
||||
};
|
||||
|
||||
struct Response
|
||||
{
|
||||
QString id;
|
||||
qint64 createdAt = 0;
|
||||
QString model;
|
||||
ResponseStatus status = ResponseStatus::InProgress;
|
||||
QList<OutputItem> output;
|
||||
QString outputText;
|
||||
std::optional<Usage> usage;
|
||||
std::optional<ResponseError> error;
|
||||
std::optional<QString> conversationId;
|
||||
|
||||
static Response fromJson(const QJsonObject &obj)
|
||||
{
|
||||
Response result;
|
||||
result.id = obj["id"].toString();
|
||||
result.createdAt = obj["created_at"].toInteger();
|
||||
result.model = obj["model"].toString();
|
||||
|
||||
const QString statusStr = obj["status"].toString();
|
||||
if (statusStr == "completed")
|
||||
result.status = ResponseStatus::Completed;
|
||||
else if (statusStr == "failed")
|
||||
result.status = ResponseStatus::Failed;
|
||||
else if (statusStr == "in_progress")
|
||||
result.status = ResponseStatus::InProgress;
|
||||
else if (statusStr == "cancelled")
|
||||
result.status = ResponseStatus::Cancelled;
|
||||
else if (statusStr == "queued")
|
||||
result.status = ResponseStatus::Queued;
|
||||
else
|
||||
result.status = ResponseStatus::Incomplete;
|
||||
|
||||
if (obj.contains("output")) {
|
||||
const QJsonArray output = obj["output"].toArray();
|
||||
result.output.reserve(output.size());
|
||||
|
||||
for (const auto &item : output) {
|
||||
result.output.append(OutputItem::fromJson(item.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contains("output_text")) {
|
||||
result.outputText = obj["output_text"].toString();
|
||||
}
|
||||
|
||||
if (obj.contains("usage")) {
|
||||
result.usage = Usage::fromJson(obj["usage"].toObject());
|
||||
}
|
||||
|
||||
if (obj.contains("error")) {
|
||||
result.error = ResponseError::fromJson(obj["error"].toObject());
|
||||
}
|
||||
|
||||
if (obj.contains("conversation")) {
|
||||
const QJsonObject conv = obj["conversation"].toObject();
|
||||
result.conversationId = conv["id"].toString();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString getAggregatedText() const
|
||||
{
|
||||
if (!outputText.isEmpty()) {
|
||||
return outputText;
|
||||
}
|
||||
|
||||
QString aggregated;
|
||||
for (const auto &item : output) {
|
||||
if (const auto *msg = item.asMessage()) {
|
||||
for (const auto &text : msg->outputTexts) {
|
||||
aggregated += text.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||
bool hasError() const noexcept { return error.has_value(); }
|
||||
bool isCompleted() const noexcept { return status == ResponseStatus::Completed; }
|
||||
bool isFailed() const noexcept { return status == ResponseStatus::Failed; }
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
246
providers/OpenAIResponsesMessage.cpp
Normal file
246
providers/OpenAIResponsesMessage.cpp
Normal file
@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "OpenAIResponsesMessage.hpp"
|
||||
#include "OpenAIResponses/ResponseObject.hpp"
|
||||
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
OpenAIResponsesMessage::OpenAIResponsesMessage(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void OpenAIResponsesMessage::handleItemDelta(const QJsonObject &item)
|
||||
{
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
|
||||
const QString itemType = item["type"].toString();
|
||||
|
||||
if (itemType == "message" || (itemType.isEmpty() && item.contains("content"))) {
|
||||
OutputItem outputItem = OutputItem::fromJson(item);
|
||||
|
||||
if (const auto *msg = outputItem.asMessage()) {
|
||||
for (const auto &outputText : msg->outputTexts) {
|
||||
if (!outputText.text.isEmpty()) {
|
||||
auto textItem = getOrCreateTextItem();
|
||||
textItem->appendText(outputText.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::handleToolCallStart(const QString &callId, const QString &name)
|
||||
{
|
||||
auto toolContent = new LLMCore::ToolUseContent(callId, name);
|
||||
toolContent->setParent(this);
|
||||
m_items.append(toolContent);
|
||||
m_toolCalls[callId] = toolContent;
|
||||
m_pendingToolArguments[callId] = "";
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::handleToolCallDelta(const QString &callId, const QString &argumentsDelta)
|
||||
{
|
||||
if (m_pendingToolArguments.contains(callId)) {
|
||||
m_pendingToolArguments[callId] += argumentsDelta;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::handleToolCallComplete(const QString &callId)
|
||||
{
|
||||
if (m_pendingToolArguments.contains(callId) && m_toolCalls.contains(callId)) {
|
||||
QString jsonArgs = m_pendingToolArguments[callId];
|
||||
QJsonObject argsObject;
|
||||
|
||||
if (!jsonArgs.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonArgs.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
argsObject = doc.object();
|
||||
}
|
||||
}
|
||||
|
||||
m_toolCalls[callId]->setInput(argsObject);
|
||||
m_pendingToolArguments.remove(callId);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::handleReasoningStart(const QString &itemId)
|
||||
{
|
||||
auto thinkingContent = new LLMCore::ThinkingContent();
|
||||
thinkingContent->setParent(this);
|
||||
m_items.append(thinkingContent);
|
||||
m_thinkingBlocks[itemId] = thinkingContent;
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::handleReasoningDelta(const QString &itemId, const QString &text)
|
||||
{
|
||||
if (m_thinkingBlocks.contains(itemId)) {
|
||||
m_thinkingBlocks[itemId]->appendThinking(text);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::handleReasoningComplete(const QString &itemId)
|
||||
{
|
||||
Q_UNUSED(itemId);
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::handleStatus(const QString &status)
|
||||
{
|
||||
m_status = status;
|
||||
updateStateFromStatus();
|
||||
}
|
||||
|
||||
QList<QJsonObject> OpenAIResponsesMessage::toItemsFormat() const
|
||||
{
|
||||
QList<QJsonObject> items;
|
||||
|
||||
QString textContent;
|
||||
QList<LLMCore::ToolUseContent *> toolCalls;
|
||||
|
||||
for (const auto *block : m_items) {
|
||||
if (const auto *text = qobject_cast<const LLMCore::TextContent *>(block)) {
|
||||
textContent += text->text();
|
||||
} else if (auto *tool = qobject_cast<LLMCore::ToolUseContent *>(
|
||||
const_cast<LLMCore::ContentBlock *>(block))) {
|
||||
toolCalls.append(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if (!textContent.isEmpty()) {
|
||||
QJsonObject message;
|
||||
message["role"] = "assistant";
|
||||
message["content"] = textContent;
|
||||
items.append(message);
|
||||
}
|
||||
|
||||
for (const auto *tool : toolCalls) {
|
||||
QJsonObject functionCallItem;
|
||||
functionCallItem["type"] = "function_call";
|
||||
functionCallItem["call_id"] = tool->id();
|
||||
functionCallItem["name"] = tool->name();
|
||||
functionCallItem["arguments"] = QString::fromUtf8(
|
||||
QJsonDocument(tool->input()).toJson(QJsonDocument::Compact));
|
||||
items.append(functionCallItem);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
QList<LLMCore::ToolUseContent *> OpenAIResponsesMessage::getCurrentToolUseContent() const
|
||||
{
|
||||
QList<LLMCore::ToolUseContent *> toolBlocks;
|
||||
for (auto *block : m_items) {
|
||||
if (auto *toolContent = qobject_cast<LLMCore::ToolUseContent *>(block)) {
|
||||
toolBlocks.append(toolContent);
|
||||
}
|
||||
}
|
||||
return toolBlocks;
|
||||
}
|
||||
|
||||
QList<LLMCore::ThinkingContent *> OpenAIResponsesMessage::getCurrentThinkingContent() const
|
||||
{
|
||||
QList<LLMCore::ThinkingContent *> thinkingBlocks;
|
||||
for (auto *block : m_items) {
|
||||
if (auto *thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(block)) {
|
||||
thinkingBlocks.append(thinkingContent);
|
||||
}
|
||||
}
|
||||
return thinkingBlocks;
|
||||
}
|
||||
|
||||
QJsonArray OpenAIResponsesMessage::createToolResultItems(const QHash<QString, QString> &toolResults) const
|
||||
{
|
||||
QJsonArray items;
|
||||
|
||||
for (const auto *toolContent : getCurrentToolUseContent()) {
|
||||
if (toolResults.contains(toolContent->id())) {
|
||||
QJsonObject toolResultItem;
|
||||
toolResultItem["type"] = "function_call_output";
|
||||
toolResultItem["call_id"] = toolContent->id();
|
||||
toolResultItem["output"] = toolResults[toolContent->id()];
|
||||
items.append(toolResultItem);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
QString OpenAIResponsesMessage::accumulatedText() const
|
||||
{
|
||||
QString text;
|
||||
for (const auto *block : m_items) {
|
||||
if (const auto *textContent = qobject_cast<const LLMCore::TextContent *>(block)) {
|
||||
text += textContent->text();
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::updateStateFromStatus()
|
||||
{
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
|
||||
if (m_status == "completed") {
|
||||
if (!getCurrentToolUseContent().isEmpty()) {
|
||||
m_state = LLMCore::MessageState::RequiresToolExecution;
|
||||
} else {
|
||||
m_state = LLMCore::MessageState::Complete;
|
||||
}
|
||||
} else if (m_status == "in_progress") {
|
||||
m_state = LLMCore::MessageState::Building;
|
||||
} else if (m_status == "failed" || m_status == "cancelled" || m_status == "incomplete") {
|
||||
m_state = LLMCore::MessageState::Final;
|
||||
} else {
|
||||
m_state = LLMCore::MessageState::Building;
|
||||
}
|
||||
}
|
||||
|
||||
LLMCore::TextContent *OpenAIResponsesMessage::getOrCreateTextItem()
|
||||
{
|
||||
for (auto *block : m_items) {
|
||||
if (auto *textContent = qobject_cast<LLMCore::TextContent *>(block)) {
|
||||
return textContent;
|
||||
}
|
||||
}
|
||||
|
||||
auto *textContent = new LLMCore::TextContent();
|
||||
textContent->setParent(this);
|
||||
m_items.append(textContent);
|
||||
return textContent;
|
||||
}
|
||||
|
||||
void OpenAIResponsesMessage::startNewContinuation()
|
||||
{
|
||||
m_toolCalls.clear();
|
||||
m_thinkingBlocks.clear();
|
||||
|
||||
qDeleteAll(m_items);
|
||||
m_items.clear();
|
||||
|
||||
m_pendingToolArguments.clear();
|
||||
m_status.clear();
|
||||
m_state = LLMCore::MessageState::Building;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
67
providers/OpenAIResponsesMessage.hpp
Normal file
67
providers/OpenAIResponsesMessage.hpp
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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/ContentBlocks.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class OpenAIResponsesMessage : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OpenAIResponsesMessage(QObject *parent = nullptr);
|
||||
|
||||
void handleItemDelta(const QJsonObject &item);
|
||||
void handleToolCallStart(const QString &callId, const QString &name);
|
||||
void handleToolCallDelta(const QString &callId, const QString &argumentsDelta);
|
||||
void handleToolCallComplete(const QString &callId);
|
||||
void handleReasoningStart(const QString &itemId);
|
||||
void handleReasoningDelta(const QString &itemId, const QString &text);
|
||||
void handleReasoningComplete(const QString &itemId);
|
||||
void handleStatus(const QString &status);
|
||||
|
||||
QList<QJsonObject> toItemsFormat() const;
|
||||
QJsonArray createToolResultItems(const QHash<QString, QString> &toolResults) const;
|
||||
|
||||
LLMCore::MessageState state() const noexcept { return m_state; }
|
||||
QString accumulatedText() const;
|
||||
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||
QList<LLMCore::ThinkingContent *> getCurrentThinkingContent() const;
|
||||
|
||||
bool hasToolCalls() const noexcept { return !m_toolCalls.isEmpty(); }
|
||||
bool hasThinkingContent() const noexcept { return !m_thinkingBlocks.isEmpty(); }
|
||||
|
||||
void startNewContinuation();
|
||||
|
||||
private:
|
||||
QString m_status;
|
||||
LLMCore::MessageState m_state = LLMCore::MessageState::Building;
|
||||
QList<LLMCore::ContentBlock *> m_items;
|
||||
QHash<QString, QString> m_pendingToolArguments;
|
||||
QHash<QString, LLMCore::ToolUseContent *> m_toolCalls;
|
||||
QHash<QString, LLMCore::ThinkingContent *> m_thinkingBlocks;
|
||||
|
||||
void updateStateFromStatus();
|
||||
LLMCore::TextContent *getOrCreateTextItem();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
666
providers/OpenAIResponsesProvider.cpp
Normal file
666
providers/OpenAIResponsesProvider.cpp
Normal file
@ -0,0 +1,666 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "OpenAIResponsesProvider.hpp"
|
||||
#include "OpenAIResponses/ResponseObject.hpp"
|
||||
|
||||
#include "llmcore/ValidationUtils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
OpenAIResponsesProvider::OpenAIResponsesProvider(QObject *parent)
|
||||
: LLMCore::Provider(parent)
|
||||
, m_toolsManager(new Tools::ToolsManager(this))
|
||||
{
|
||||
connect(
|
||||
m_toolsManager,
|
||||
&Tools::ToolsManager::toolExecutionComplete,
|
||||
this,
|
||||
&OpenAIResponsesProvider::onToolExecutionComplete);
|
||||
}
|
||||
|
||||
QString OpenAIResponsesProvider::name() const
|
||||
{
|
||||
return "OpenAI Responses";
|
||||
}
|
||||
|
||||
QString OpenAIResponsesProvider::url() const
|
||||
{
|
||||
return "https://api.openai.com";
|
||||
}
|
||||
|
||||
QString OpenAIResponsesProvider::completionEndpoint() const
|
||||
{
|
||||
return "/v1/responses";
|
||||
}
|
||||
|
||||
QString OpenAIResponsesProvider::chatEndpoint() const
|
||||
{
|
||||
return "/v1/responses";
|
||||
}
|
||||
|
||||
bool OpenAIResponsesProvider::supportsModelListing() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
bool isThinkingEnabled)
|
||||
{
|
||||
if (!prompt->isSupportProvider(providerID())) {
|
||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||
}
|
||||
|
||||
prompt->prepareRequest(request, context);
|
||||
|
||||
auto applyModelParams = [&request](const auto &settings) {
|
||||
request["max_output_tokens"] = settings.maxTokens();
|
||||
|
||||
if (settings.useTopP()) {
|
||||
request["top_p"] = settings.topP();
|
||||
}
|
||||
};
|
||||
|
||||
auto applyThinkingMode = [&request](const auto &settings) {
|
||||
QString effortStr = settings.openAIResponsesReasoningEffort.stringValue().toLower();
|
||||
if (effortStr.isEmpty()) {
|
||||
effortStr = "medium";
|
||||
}
|
||||
|
||||
QJsonObject reasoning;
|
||||
reasoning["effort"] = effortStr;
|
||||
request["reasoning"] = reasoning;
|
||||
request["max_output_tokens"] = settings.thinkingMaxTokens();
|
||||
request["store"] = true;
|
||||
|
||||
QJsonArray include;
|
||||
include.append("reasoning.encrypted_content");
|
||||
request["include"] = include;
|
||||
};
|
||||
|
||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == LLMCore::RequestType::QuickRefactoring) {
|
||||
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||
applyModelParams(qrSettings);
|
||||
|
||||
if (isThinkingEnabled) {
|
||||
applyThinkingMode(qrSettings);
|
||||
}
|
||||
} else {
|
||||
const auto &chatSettings = Settings::chatAssistantSettings();
|
||||
applyModelParams(chatSettings);
|
||||
|
||||
if (isThinkingEnabled) {
|
||||
applyThinkingMode(chatSettings);
|
||||
}
|
||||
}
|
||||
|
||||
if (isToolsEnabled) {
|
||||
const LLMCore::RunToolsFilter filter = (type == LLMCore::RequestType::QuickRefactoring)
|
||||
? LLMCore::RunToolsFilter::OnlyRead
|
||||
: LLMCore::RunToolsFilter::ALL;
|
||||
|
||||
const auto toolsDefinitions
|
||||
= m_toolsManager->getToolsDefinitions(LLMCore::ToolSchemaFormat::OpenAI, filter);
|
||||
if (!toolsDefinitions.isEmpty()) {
|
||||
QJsonArray responsesTools;
|
||||
|
||||
for (const QJsonValue &toolValue : toolsDefinitions) {
|
||||
const QJsonObject tool = toolValue.toObject();
|
||||
if (tool.contains("function")) {
|
||||
const QJsonObject functionObj = tool["function"].toObject();
|
||||
QJsonObject responsesTool;
|
||||
responsesTool["type"] = "function";
|
||||
responsesTool["name"] = functionObj["name"];
|
||||
responsesTool["description"] = functionObj["description"];
|
||||
responsesTool["parameters"] = functionObj["parameters"];
|
||||
responsesTools.append(responsesTool);
|
||||
}
|
||||
}
|
||||
request["tools"] = responsesTools;
|
||||
}
|
||||
}
|
||||
|
||||
request["stream"] = true;
|
||||
}
|
||||
|
||||
QList<QString> OpenAIResponsesProvider::getInstalledModels(const QString &url)
|
||||
{
|
||||
QList<QString> models;
|
||||
QNetworkAccessManager manager;
|
||||
QNetworkRequest request(QString("%1/v1/models").arg(url));
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
if (!apiKey().isEmpty()) {
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
const QByteArray responseData = reply->readAll();
|
||||
const QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
const QJsonObject jsonObject = jsonResponse.object();
|
||||
|
||||
if (jsonObject.contains("data")) {
|
||||
const QJsonArray modelArray = jsonObject["data"].toArray();
|
||||
models.reserve(modelArray.size());
|
||||
|
||||
static const QStringList modelPrefixes = {"gpt-5", "o1", "o2", "o3", "o4"};
|
||||
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
const QJsonObject modelObject = value.toObject();
|
||||
if (!modelObject.contains("id")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString modelId = modelObject["id"].toString();
|
||||
for (const QString &prefix : modelPrefixes) {
|
||||
if (modelId.contains(prefix)) {
|
||||
models.append(modelId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
}
|
||||
|
||||
QList<QString> OpenAIResponsesProvider::validateRequest(
|
||||
const QJsonObject &request, LLMCore::TemplateType type)
|
||||
{
|
||||
Q_UNUSED(type);
|
||||
|
||||
QList<QString> errors;
|
||||
|
||||
if (!request.contains("input")) {
|
||||
errors.append("Missing required field: input");
|
||||
return errors;
|
||||
}
|
||||
|
||||
const QJsonValue inputValue = request["input"];
|
||||
if (!inputValue.isString() && !inputValue.isArray()) {
|
||||
errors.append("Field 'input' must be either a string or an array");
|
||||
}
|
||||
|
||||
if (request.contains("max_output_tokens") && !request["max_output_tokens"].isDouble()) {
|
||||
errors.append("Field 'max_output_tokens' must be a number");
|
||||
}
|
||||
|
||||
if (request.contains("top_p") && !request["top_p"].isDouble()) {
|
||||
errors.append("Field 'top_p' must be a number");
|
||||
}
|
||||
|
||||
if (request.contains("reasoning") && !request["reasoning"].isObject()) {
|
||||
errors.append("Field 'reasoning' must be an object");
|
||||
}
|
||||
|
||||
if (request.contains("stream") && !request["stream"].isBool()) {
|
||||
errors.append("Field 'stream' must be a boolean");
|
||||
}
|
||||
|
||||
if (request.contains("tools") && !request["tools"].isArray()) {
|
||||
errors.append("Field 'tools' must be an array");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
QString OpenAIResponsesProvider::apiKey() const
|
||||
{
|
||||
return Settings::providerSettings().openAiApiKey();
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||
{
|
||||
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
if (!apiKey().isEmpty()) {
|
||||
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
LLMCore::ProviderID OpenAIResponsesProvider::providerID() const
|
||||
{
|
||||
return LLMCore::ProviderID::OpenAIResponses;
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
m_dataBuffers[requestId].clear();
|
||||
}
|
||||
|
||||
m_requestUrls[requestId] = url;
|
||||
m_originalRequests[requestId] = payload;
|
||||
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
}
|
||||
|
||||
bool OpenAIResponsesProvider::supportsTools() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenAIResponsesProvider::supportImage() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenAIResponsesProvider::supportThinking() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LLMCore::Provider::cancelRequest(requestId);
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
{
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
const QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||
|
||||
QString currentEventType;
|
||||
|
||||
for (const QString &line : lines) {
|
||||
const QString trimmedLine = line.trimmed();
|
||||
if (trimmedLine.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "data: [DONE]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("event: ")) {
|
||||
currentEventType = line.mid(7).trimmed();
|
||||
continue;
|
||||
}
|
||||
|
||||
QString dataLine = line;
|
||||
if (line.startsWith("data: ")) {
|
||||
dataLine = line.mid(6);
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(dataLine.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
const QJsonObject obj = doc.object();
|
||||
processStreamEvent(requestId, currentEventType, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
if (!buffers.responseContent.isEmpty()) {
|
||||
emit fullResponseReceived(requestId, buffers.responseContent);
|
||||
} else {
|
||||
LOG_MESSAGE(QString("WARNING: OpenAIResponses - Response content is empty for %1, "
|
||||
"emitting empty response")
|
||||
.arg(requestId));
|
||||
emit fullResponseReceived(requestId, "");
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(
|
||||
QString("WARNING: OpenAIResponses - No data buffer found for %1").arg(requestId));
|
||||
}
|
||||
|
||||
cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::processStreamEvent(
|
||||
const QString &requestId, const QString &eventType, const QJsonObject &data)
|
||||
{
|
||||
OpenAIResponsesMessage *message = m_messages.value(requestId);
|
||||
if (!message) {
|
||||
message = new OpenAIResponsesMessage(this);
|
||||
m_messages[requestId] = message;
|
||||
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
emit continuationStarted(requestId);
|
||||
}
|
||||
} else if (
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
emit continuationStarted(requestId);
|
||||
}
|
||||
|
||||
if (eventType == "response.content_part.added") {
|
||||
} else if (eventType == "response.output_text.delta") {
|
||||
const QString delta = data["delta"].toString();
|
||||
if (!delta.isEmpty()) {
|
||||
m_dataBuffers[requestId].responseContent += delta;
|
||||
emit partialResponseReceived(requestId, delta);
|
||||
}
|
||||
} else if (eventType == "response.output_text.done") {
|
||||
const QString fullText = data["text"].toString();
|
||||
if (!fullText.isEmpty()) {
|
||||
m_dataBuffers[requestId].responseContent = fullText;
|
||||
}
|
||||
} else if (eventType == "response.content_part.done") {
|
||||
} else if (eventType == "response.output_item.added") {
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
const QJsonObject item = data["item"].toObject();
|
||||
OutputItem outputItem = OutputItem::fromJson(item);
|
||||
|
||||
if (const auto *functionCall = outputItem.asFunctionCall()) {
|
||||
if (!functionCall->callId.isEmpty() && !functionCall->name.isEmpty()) {
|
||||
if (!m_itemIdToCallId.contains(requestId)) {
|
||||
m_itemIdToCallId[requestId] = QHash<QString, QString>();
|
||||
}
|
||||
m_itemIdToCallId[requestId][functionCall->id] = functionCall->callId;
|
||||
message->handleToolCallStart(functionCall->callId, functionCall->name);
|
||||
}
|
||||
} else if (const auto *reasoning = outputItem.asReasoning()) {
|
||||
if (!reasoning->id.isEmpty()) {
|
||||
message->handleReasoningStart(reasoning->id);
|
||||
}
|
||||
}
|
||||
} else if (eventType == "response.reasoning_content.delta") {
|
||||
const QString itemId = data["item_id"].toString();
|
||||
const QString delta = data["delta"].toString();
|
||||
if (!itemId.isEmpty() && !delta.isEmpty()) {
|
||||
message->handleReasoningDelta(itemId, delta);
|
||||
}
|
||||
} else if (eventType == "response.reasoning_content.done") {
|
||||
const QString itemId = data["item_id"].toString();
|
||||
if (!itemId.isEmpty()) {
|
||||
message->handleReasoningComplete(itemId);
|
||||
emitPendingThinkingBlocks(requestId);
|
||||
}
|
||||
} else if (eventType == "response.function_call_arguments.delta") {
|
||||
const QString itemId = data["item_id"].toString();
|
||||
const QString delta = data["delta"].toString();
|
||||
if (!itemId.isEmpty() && !delta.isEmpty()) {
|
||||
const QString callId = m_itemIdToCallId.value(requestId).value(itemId);
|
||||
if (!callId.isEmpty()) {
|
||||
message->handleToolCallDelta(callId, delta);
|
||||
} else {
|
||||
LOG_MESSAGE(QString("ERROR: No call_id mapping found for item_id: %1").arg(itemId));
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
eventType == "response.function_call_arguments.done"
|
||||
|| eventType == "response.output_item.done") {
|
||||
const QString itemId = data["item_id"].toString();
|
||||
const QJsonObject item = data["item"].toObject();
|
||||
|
||||
if (!item.isEmpty() && item["type"].toString() == "reasoning") {
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
|
||||
const QString finalItemId = itemId.isEmpty() ? item["id"].toString() : itemId;
|
||||
|
||||
ReasoningOutput reasoningOutput = ReasoningOutput::fromJson(item);
|
||||
QString reasoningText;
|
||||
|
||||
if (!reasoningOutput.summaryText.isEmpty()) {
|
||||
reasoningText = reasoningOutput.summaryText;
|
||||
} else if (!reasoningOutput.contentTexts.isEmpty()) {
|
||||
reasoningText = reasoningOutput.contentTexts.join("\n");
|
||||
}
|
||||
|
||||
if (reasoningText.isEmpty()) {
|
||||
reasoningText = QString(
|
||||
"[Reasoning process completed, but detailed thinking is not available in "
|
||||
"streaming mode. The model has processed your request with extended reasoning.]");
|
||||
}
|
||||
|
||||
if (!finalItemId.isEmpty()) {
|
||||
message->handleReasoningDelta(finalItemId, reasoningText);
|
||||
message->handleReasoningComplete(finalItemId);
|
||||
emitPendingThinkingBlocks(requestId);
|
||||
}
|
||||
} else if (item.isEmpty() && !itemId.isEmpty()) {
|
||||
const QString callId = m_itemIdToCallId.value(requestId).value(itemId);
|
||||
if (!callId.isEmpty()) {
|
||||
message->handleToolCallComplete(callId);
|
||||
} else {
|
||||
LOG_MESSAGE(
|
||||
QString("ERROR: OpenAIResponses - No call_id mapping found for item_id: %1")
|
||||
.arg(itemId));
|
||||
}
|
||||
} else if (!item.isEmpty() && item["type"].toString() == "function_call") {
|
||||
const QString callId = item["call_id"].toString();
|
||||
if (!callId.isEmpty()) {
|
||||
message->handleToolCallComplete(callId);
|
||||
} else {
|
||||
LOG_MESSAGE(
|
||||
QString("ERROR: OpenAIResponses - Function call done but call_id is empty"));
|
||||
}
|
||||
}
|
||||
} else if (eventType == "response.created") {
|
||||
} else if (eventType == "response.in_progress") {
|
||||
} else if (eventType == "response.completed") {
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
const QJsonObject responseObj = data["response"].toObject();
|
||||
Response response = Response::fromJson(responseObj);
|
||||
|
||||
const QString statusStr = responseObj["status"].toString();
|
||||
|
||||
if (m_dataBuffers[requestId].responseContent.isEmpty()) {
|
||||
const QString aggregatedText = response.getAggregatedText();
|
||||
if (!aggregatedText.isEmpty()) {
|
||||
m_dataBuffers[requestId].responseContent = aggregatedText;
|
||||
}
|
||||
}
|
||||
|
||||
message->handleStatus(statusStr);
|
||||
handleMessageComplete(requestId);
|
||||
} else if (eventType == "response.incomplete") {
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
const QJsonObject responseObj = data["response"].toObject();
|
||||
|
||||
if (!responseObj.isEmpty()) {
|
||||
Response response = Response::fromJson(responseObj);
|
||||
const QString statusStr = responseObj["status"].toString();
|
||||
|
||||
if (m_dataBuffers[requestId].responseContent.isEmpty()) {
|
||||
const QString aggregatedText = response.getAggregatedText();
|
||||
if (!aggregatedText.isEmpty()) {
|
||||
m_dataBuffers[requestId].responseContent = aggregatedText;
|
||||
}
|
||||
}
|
||||
|
||||
message->handleStatus(statusStr);
|
||||
} else {
|
||||
message->handleStatus("incomplete");
|
||||
}
|
||||
|
||||
handleMessageComplete(requestId);
|
||||
} else if (!eventType.isEmpty()) {
|
||||
LOG_MESSAGE(QString("WARNING: OpenAIResponses - Unhandled event type '%1' for request %2\nData: %3")
|
||||
.arg(eventType)
|
||||
.arg(requestId)
|
||||
.arg(QString::fromUtf8(QJsonDocument(data).toJson(QJsonDocument::Compact))));
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::emitPendingThinkingBlocks(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||
const auto thinkingBlocks = message->getCurrentThinkingContent();
|
||||
|
||||
if (thinkingBlocks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int alreadyEmitted = m_emittedThinkingBlocksCount.value(requestId, 0);
|
||||
const int totalBlocks = thinkingBlocks.size();
|
||||
|
||||
for (int i = alreadyEmitted; i < totalBlocks; ++i) {
|
||||
const auto *thinkingContent = thinkingBlocks[i];
|
||||
|
||||
if (thinkingContent->thinking().trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
emit thinkingBlockReceived(
|
||||
requestId, thinkingContent->thinking(), thinkingContent->signature());
|
||||
}
|
||||
|
||||
m_emittedThinkingBlocksCount[requestId] = totalBlocks;
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::handleMessageComplete(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||
|
||||
emitPendingThinkingBlocks(requestId);
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
const auto toolUseContent = message->getCurrentToolUseContent();
|
||||
|
||||
if (toolUseContent.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto *toolContent : toolUseContent) {
|
||||
const auto toolStringName = m_toolsManager->toolsFactory()->getStringName(
|
||||
toolContent->name());
|
||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||
m_toolsManager->executeToolCall(
|
||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults)
|
||||
{
|
||||
if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) {
|
||||
LOG_MESSAGE(QString("ERROR: OpenAIResponses - Missing data for continuation request %1")
|
||||
.arg(requestId));
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||
const auto toolContent = message->getCurrentToolUseContent();
|
||||
|
||||
for (auto it = toolResults.constBegin(); it != toolResults.constEnd(); ++it) {
|
||||
for (const auto *tool : toolContent) {
|
||||
if (tool->id() == it.key()) {
|
||||
const auto toolStringName = m_toolsManager->toolsFactory()->getStringName(
|
||||
tool->name());
|
||||
emit toolExecutionCompleted(
|
||||
requestId, tool->id(), toolStringName, toolResults[tool->id()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||
QJsonArray input = continuationRequest["input"].toArray();
|
||||
|
||||
const QList<QJsonObject> assistantItems = message->toItemsFormat();
|
||||
for (const QJsonObject &item : assistantItems) {
|
||||
input.append(item);
|
||||
}
|
||||
|
||||
const QJsonArray toolResultItems = message->createToolResultItems(toolResults);
|
||||
for (const QJsonValue &item : toolResultItems) {
|
||||
input.append(item);
|
||||
}
|
||||
|
||||
continuationRequest["input"] = input;
|
||||
|
||||
m_dataBuffers[requestId].responseContent.clear();
|
||||
|
||||
sendRequest(requestId, m_requestUrls[requestId], continuationRequest);
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
if (m_messages.contains(requestId)) {
|
||||
OpenAIResponsesMessage *message = m_messages.take(requestId);
|
||||
message->deleteLater();
|
||||
}
|
||||
|
||||
m_dataBuffers.remove(requestId);
|
||||
m_requestUrls.remove(requestId);
|
||||
m_originalRequests.remove(requestId);
|
||||
m_itemIdToCallId.remove(requestId);
|
||||
m_emittedThinkingBlocksCount.remove(requestId);
|
||||
m_toolsManager->cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
87
providers/OpenAIResponsesProvider.hpp
Normal file
87
providers/OpenAIResponsesProvider.hpp
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "OpenAIResponsesMessage.hpp"
|
||||
#include "tools/ToolsManager.hpp"
|
||||
#include <llmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
class OpenAIResponsesProvider : public LLMCore::Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OpenAIResponsesProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString url() const override;
|
||||
QString completionEndpoint() const override;
|
||||
QString chatEndpoint() const override;
|
||||
bool supportsModelListing() const override;
|
||||
void prepareRequest(
|
||||
QJsonObject &request,
|
||||
LLMCore::PromptTemplate *prompt,
|
||||
LLMCore::ContextData context,
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
bool isThinkingEnabled) override;
|
||||
QList<QString> getInstalledModels(const QString &url) override;
|
||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
LLMCore::ProviderID providerID() const override;
|
||||
|
||||
void sendRequest(
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override;
|
||||
|
||||
bool supportsTools() const override;
|
||||
bool supportImage() const override;
|
||||
bool supportThinking() const override;
|
||||
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
||||
|
||||
public slots:
|
||||
void onDataReceived(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||
|
||||
private:
|
||||
void processStreamEvent(const QString &requestId, const QString &eventType, const QJsonObject &data);
|
||||
void emitPendingThinkingBlocks(const QString &requestId);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, OpenAIResponsesMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
QHash<LLMCore::RequestID, QHash<QString, QString>> m_itemIdToCallId;
|
||||
QHash<LLMCore::RequestID, int> m_emittedThinkingBlocksCount;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
|
||||
255
providers/OpenAIResponsesRequestBuilder.hpp
Normal file
255
providers/OpenAIResponsesRequestBuilder.hpp
Normal file
@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "OpenAIResponses/ModelRequest.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::OpenAIResponses {
|
||||
|
||||
class RequestBuilder
|
||||
{
|
||||
public:
|
||||
RequestBuilder() = default;
|
||||
|
||||
RequestBuilder &setModel(QString model)
|
||||
{
|
||||
m_model = std::move(model);
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &addMessage(Role role, QString content)
|
||||
{
|
||||
Message msg;
|
||||
msg.role = role;
|
||||
msg.content.append(MessageContent(std::move(content)));
|
||||
m_messages.append(std::move(msg));
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &addMessage(Message msg)
|
||||
{
|
||||
m_messages.append(std::move(msg));
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setInstructions(QString instructions)
|
||||
{
|
||||
m_instructions = std::move(instructions);
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &addTool(Tool tool)
|
||||
{
|
||||
m_tools.append(std::move(tool));
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setTemperature(double temp) noexcept
|
||||
{
|
||||
m_temperature = temp;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setTopP(double topP) noexcept
|
||||
{
|
||||
m_topP = topP;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setMaxOutputTokens(int tokens) noexcept
|
||||
{
|
||||
m_maxOutputTokens = tokens;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setStream(bool stream) noexcept
|
||||
{
|
||||
m_stream = stream;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setStore(bool store) noexcept
|
||||
{
|
||||
m_store = store;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setTextFormat(TextFormatOptions format)
|
||||
{
|
||||
m_textFormat = std::move(format);
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setReasoningEffort(ReasoningEffort effort) noexcept
|
||||
{
|
||||
m_reasoningEffort = effort;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setMetadata(QMap<QString, QVariant> metadata)
|
||||
{
|
||||
m_metadata = std::move(metadata);
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &setIncludeReasoningContent(bool include) noexcept
|
||||
{
|
||||
m_includeReasoningContent = include;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder &clear() noexcept
|
||||
{
|
||||
m_model.clear();
|
||||
m_messages.clear();
|
||||
m_instructions.reset();
|
||||
m_tools.clear();
|
||||
m_temperature.reset();
|
||||
m_topP.reset();
|
||||
m_maxOutputTokens.reset();
|
||||
m_stream = false;
|
||||
m_store.reset();
|
||||
m_textFormat.reset();
|
||||
m_reasoningEffort.reset();
|
||||
m_includeReasoningContent = false;
|
||||
m_metadata.clear();
|
||||
return *this;
|
||||
}
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
if (!m_model.isEmpty()) {
|
||||
obj["model"] = m_model;
|
||||
}
|
||||
|
||||
if (!m_messages.isEmpty()) {
|
||||
if (m_messages.size() == 1 && m_messages[0].role == Role::User
|
||||
&& m_messages[0].content.size() == 1) {
|
||||
obj["input"] = m_messages[0].content[0].toJson();
|
||||
} else {
|
||||
QJsonArray input;
|
||||
for (const auto &msg : m_messages) {
|
||||
input.append(msg.toJson());
|
||||
}
|
||||
obj["input"] = input;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_instructions) {
|
||||
obj["instructions"] = *m_instructions;
|
||||
}
|
||||
|
||||
if (!m_tools.isEmpty()) {
|
||||
QJsonArray tools;
|
||||
for (const auto &tool : m_tools) {
|
||||
tools.append(tool.toJson());
|
||||
}
|
||||
obj["tools"] = tools;
|
||||
}
|
||||
|
||||
if (m_temperature) {
|
||||
obj["temperature"] = *m_temperature;
|
||||
}
|
||||
|
||||
if (m_topP) {
|
||||
obj["top_p"] = *m_topP;
|
||||
}
|
||||
|
||||
if (m_maxOutputTokens) {
|
||||
obj["max_output_tokens"] = *m_maxOutputTokens;
|
||||
}
|
||||
|
||||
obj["stream"] = m_stream;
|
||||
|
||||
if (m_store) {
|
||||
obj["store"] = *m_store;
|
||||
}
|
||||
|
||||
if (m_textFormat) {
|
||||
QJsonObject textObj;
|
||||
textObj["format"] = m_textFormat->toJson();
|
||||
obj["text"] = textObj;
|
||||
}
|
||||
|
||||
if (m_reasoningEffort) {
|
||||
QJsonObject reasoning;
|
||||
reasoning["effort"] = effortToString(*m_reasoningEffort);
|
||||
obj["reasoning"] = reasoning;
|
||||
}
|
||||
|
||||
if (m_includeReasoningContent) {
|
||||
QJsonArray include;
|
||||
include.append("reasoning.encrypted_content");
|
||||
obj["include"] = include;
|
||||
}
|
||||
|
||||
if (!m_metadata.isEmpty()) {
|
||||
QJsonObject metadata;
|
||||
for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
|
||||
metadata[it.key()] = QJsonValue::fromVariant(it.value());
|
||||
}
|
||||
obj["metadata"] = metadata;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_model;
|
||||
QList<Message> m_messages;
|
||||
std::optional<QString> m_instructions;
|
||||
QList<Tool> m_tools;
|
||||
std::optional<double> m_temperature;
|
||||
std::optional<double> m_topP;
|
||||
std::optional<int> m_maxOutputTokens;
|
||||
bool m_stream = false;
|
||||
std::optional<bool> m_store;
|
||||
std::optional<TextFormatOptions> m_textFormat;
|
||||
std::optional<ReasoningEffort> m_reasoningEffort;
|
||||
bool m_includeReasoningContent = false;
|
||||
QMap<QString, QVariant> m_metadata;
|
||||
|
||||
static QString effortToString(ReasoningEffort e)
|
||||
{
|
||||
switch (e) {
|
||||
case ReasoningEffort::None:
|
||||
return "none";
|
||||
case ReasoningEffort::Minimal:
|
||||
return "minimal";
|
||||
case ReasoningEffort::Low:
|
||||
return "low";
|
||||
case ReasoningEffort::Medium:
|
||||
return "medium";
|
||||
case ReasoningEffort::High:
|
||||
return "high";
|
||||
}
|
||||
return "medium";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::OpenAIResponses
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
#include "providers/OllamaProvider.hpp"
|
||||
#include "providers/OpenAICompatProvider.hpp"
|
||||
#include "providers/OpenAIProvider.hpp"
|
||||
#include "providers/OpenAIResponsesProvider.hpp"
|
||||
#include "providers/OpenRouterAIProvider.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
@ -39,6 +40,7 @@ inline void registerProviders()
|
||||
providerManager.registerProvider<OllamaProvider>();
|
||||
providerManager.registerProvider<ClaudeProvider>();
|
||||
providerManager.registerProvider<OpenAIProvider>();
|
||||
providerManager.registerProvider<OpenAIResponsesProvider>();
|
||||
providerManager.registerProvider<OpenAICompatProvider>();
|
||||
providerManager.registerProvider<LMStudioProvider>();
|
||||
providerManager.registerProvider<OpenRouterProvider>();
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProjectSettingsPanel.hpp"
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
#include "settings/SettingsConstants.hpp"
|
||||
#include "templates/Templates.hpp"
|
||||
#include "widgets/CustomInstructionsManager.hpp"
|
||||
|
||||
226
settings/AgentRole.cpp
Normal file
226
settings/AgentRole.cpp
Normal file
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "AgentRole.hpp"
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
QString AgentRolesManager::getConfigurationDirectory()
|
||||
{
|
||||
QString path = QString("%1/qodeassist/agent_roles")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||
QDir().mkpath(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
QList<AgentRole> AgentRolesManager::loadAllRoles()
|
||||
{
|
||||
QList<AgentRole> roles;
|
||||
QString configDir = getConfigurationDirectory();
|
||||
QDir dir(configDir);
|
||||
|
||||
ensureDefaultRoles();
|
||||
|
||||
const QStringList jsonFiles = dir.entryList({"*.json"}, QDir::Files);
|
||||
for (const QString &fileName : jsonFiles) {
|
||||
AgentRole role = loadRoleFromFile(dir.absoluteFilePath(fileName));
|
||||
if (!role.id.isEmpty()) {
|
||||
roles.append(role);
|
||||
}
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::loadRole(const QString &roleId)
|
||||
{
|
||||
if (roleId.isEmpty())
|
||||
return {};
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||
if (!QFile::exists(filePath))
|
||||
return {};
|
||||
|
||||
return loadRoleFromFile(filePath);
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::loadRoleFromFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (doc.isNull() || !doc.isObject())
|
||||
return {};
|
||||
|
||||
return AgentRole::fromJson(doc.object());
|
||||
}
|
||||
|
||||
bool AgentRolesManager::saveRole(const AgentRole &role)
|
||||
{
|
||||
if (role.id.isEmpty())
|
||||
return false;
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(role.id + ".json");
|
||||
QFile file(filePath);
|
||||
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
|
||||
QJsonDocument doc(role.toJson());
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AgentRolesManager::deleteRole(const QString &roleId)
|
||||
{
|
||||
if (roleId.isEmpty())
|
||||
return false;
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||
return QFile::remove(filePath);
|
||||
}
|
||||
|
||||
bool AgentRolesManager::roleExists(const QString &roleId)
|
||||
{
|
||||
if (roleId.isEmpty())
|
||||
return false;
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||
return QFile::exists(filePath);
|
||||
}
|
||||
|
||||
void AgentRolesManager::ensureDefaultRoles()
|
||||
{
|
||||
QDir dir(getConfigurationDirectory());
|
||||
|
||||
if (!dir.exists("developer.json"))
|
||||
saveRole(getDefaultDeveloperRole());
|
||||
|
||||
if (!dir.exists("reviewer.json"))
|
||||
saveRole(getDefaultReviewerRole());
|
||||
|
||||
if (!dir.exists("researcher.json"))
|
||||
saveRole(getDefaultResearcherRole());
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::getDefaultDeveloperRole()
|
||||
{
|
||||
return AgentRole{
|
||||
"developer",
|
||||
"Developer",
|
||||
"Experienced Qt/C++ developer for implementation tasks",
|
||||
"You are an experienced Qt/C++ developer working on a Qt Creator plugin.\n\n"
|
||||
"Your workflow:\n"
|
||||
"1. **Analyze** - understand the problem and what needs to be done\n"
|
||||
"2. **Propose solution** - explain your approach in 2-3 sentences\n"
|
||||
"3. **Wait for approval** - don't write code until the solution is confirmed\n"
|
||||
"4. **Implement** - write clean, minimal code that solves the task\n\n"
|
||||
"When analyzing:\n"
|
||||
"- Ask clarifying questions if requirements are unclear\n"
|
||||
"- Check existing code for similar patterns\n"
|
||||
"- Consider edge cases and potential issues\n\n"
|
||||
"When proposing:\n"
|
||||
"- Explain what you'll change and why\n"
|
||||
"- Mention files you'll modify\n"
|
||||
"- Note any architectural implications\n\n"
|
||||
"When implementing:\n"
|
||||
"- Use C++20, Qt6, follow existing codebase style\n"
|
||||
"- Write only what's needed (MVP approach)\n"
|
||||
"- Include file paths and necessary changes\n"
|
||||
"- Handle errors properly\n"
|
||||
"- Make sure it compiles\n\n"
|
||||
"Keep it practical:\n"
|
||||
"- Short explanations, let code speak\n"
|
||||
"- No over-engineering or unnecessary refactoring\n"
|
||||
"- No TODOs, debug code, or unfinished work\n"
|
||||
"- Point out non-obvious things\n\n"
|
||||
"You're a pragmatic team member who thinks before coding.",
|
||||
true};
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::getDefaultReviewerRole()
|
||||
{
|
||||
return AgentRole{
|
||||
"reviewer",
|
||||
"Code Reviewer",
|
||||
"Expert C++/QML code reviewer for quality assurance",
|
||||
"You are an expert C++/QML code reviewer specializing in C++20 and Qt6.\n\n"
|
||||
"What you check:\n"
|
||||
"- Bugs, memory leaks, undefined behavior\n"
|
||||
"- C++20 compliance and Qt6 patterns\n"
|
||||
"- RAII, move semantics, smart pointers\n"
|
||||
"- Qt parent-child ownership and signal/slot correctness\n"
|
||||
"- Thread safety and Qt concurrent usage\n"
|
||||
"- const-correctness and Qt container usage\n"
|
||||
"- Performance bottlenecks\n"
|
||||
"- Production readiness: error handling, no debug leftovers\n\n"
|
||||
"What you do:\n"
|
||||
"- Point out problems with clear explanations\n"
|
||||
"- Suggest specific fixes with code examples\n"
|
||||
"- Remove unnecessary comments, keep essential docs only\n"
|
||||
"- Flag anything that's not production-ready\n"
|
||||
"- Recommend optimizations when you spot them\n\n"
|
||||
"Focus on: correctness, performance, maintainability, Qt idioms.\n\n"
|
||||
"Be direct and specific. Show, don't just tell.",
|
||||
true};
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::getDefaultResearcherRole()
|
||||
{
|
||||
return AgentRole{
|
||||
"researcher",
|
||||
"Researcher",
|
||||
"Research-oriented developer for exploring solutions",
|
||||
"You are a research-oriented Qt/C++ developer who investigates problems and explores "
|
||||
"solutions.\n\n"
|
||||
"Your job is to think, not to code:\n"
|
||||
"- Deep dive into the problem before suggesting anything\n"
|
||||
"- Research Qt docs, patterns, and best practices\n"
|
||||
"- Find multiple ways to solve it\n"
|
||||
"- Compare trade-offs: performance, complexity, maintainability\n"
|
||||
"- Look for relevant Qt APIs and modules\n"
|
||||
"- Think about architectural consequences\n\n"
|
||||
"How you work:\n"
|
||||
"1. **Problem Analysis** - what exactly needs solving\n"
|
||||
"2. **Research Findings** - what you learned about this problem space\n"
|
||||
"3. **Solution Options** - present 2-3 approaches with honest pros/cons\n"
|
||||
"4. **Recommendation** - which one fits best and why\n"
|
||||
"5. **Next Steps** - what to consider before implementing\n\n"
|
||||
"What you provide:\n"
|
||||
"- Clear comparison of different approaches\n"
|
||||
"- Code snippets as examples (not ready-to-use patches)\n"
|
||||
"- Links to docs, examples, similar implementations\n"
|
||||
"- Questions to clarify requirements\n"
|
||||
"- Warning about potential problems\n\n"
|
||||
"You DO NOT write implementation code. You explore options and let the developer choose.\n\n"
|
||||
"Think like a consultant: research thoroughly, present clearly, stay objective.",
|
||||
true};
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
82
settings/AgentRole.hpp
Normal file
82
settings/AgentRole.hpp
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QList>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
struct AgentRole
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString description;
|
||||
QString systemPrompt;
|
||||
bool isBuiltin = false;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
return QJsonObject{
|
||||
{"id", id},
|
||||
{"name", name},
|
||||
{"description", description},
|
||||
{"systemPrompt", systemPrompt},
|
||||
{"isBuiltin", isBuiltin}};
|
||||
}
|
||||
|
||||
static AgentRole fromJson(const QJsonObject &json)
|
||||
{
|
||||
return AgentRole{
|
||||
json["id"].toString(),
|
||||
json["name"].toString(),
|
||||
json["description"].toString(),
|
||||
json["systemPrompt"].toString(),
|
||||
json["isBuiltin"].toBool(false)};
|
||||
}
|
||||
|
||||
bool operator==(const AgentRole &other) const { return id == other.id; }
|
||||
};
|
||||
|
||||
class AgentRolesManager
|
||||
{
|
||||
public:
|
||||
static QString getConfigurationDirectory();
|
||||
static QList<AgentRole> loadAllRoles();
|
||||
static AgentRole loadRole(const QString &roleId);
|
||||
static AgentRole loadRoleFromFile(const QString &filePath);
|
||||
static bool saveRole(const AgentRole &role);
|
||||
static bool deleteRole(const QString &roleId);
|
||||
static bool roleExists(const QString &roleId);
|
||||
static void ensureDefaultRoles();
|
||||
|
||||
static AgentRole getNoRole()
|
||||
{
|
||||
return AgentRole{"", "No Role", "Use base system prompt without role specialization", "", false};
|
||||
}
|
||||
|
||||
private:
|
||||
static AgentRole getDefaultDeveloperRole();
|
||||
static AgentRole getDefaultReviewerRole();
|
||||
static AgentRole getDefaultResearcherRole();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
122
settings/AgentRoleDialog.cpp
Normal file
122
settings/AgentRoleDialog.cpp
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "AgentRoleDialog.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentRoleDialog::AgentRoleDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_editMode(false)
|
||||
{
|
||||
setWindowTitle(tr("Add Agent Role"));
|
||||
setupUI();
|
||||
}
|
||||
|
||||
AgentRoleDialog::AgentRoleDialog(const AgentRole &role, bool editMode, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_editMode(editMode)
|
||||
{
|
||||
setWindowTitle(editMode ? tr("Edit Agent Role") : tr("Duplicate Agent Role"));
|
||||
setupUI();
|
||||
setRole(role);
|
||||
}
|
||||
|
||||
void AgentRoleDialog::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
auto *formLayout = new QFormLayout();
|
||||
|
||||
m_nameEdit = new QLineEdit(this);
|
||||
m_nameEdit->setPlaceholderText(tr("e.g., Developer, Code Reviewer"));
|
||||
formLayout->addRow(tr("Name:"), m_nameEdit);
|
||||
|
||||
m_idEdit = new QLineEdit(this);
|
||||
m_idEdit->setPlaceholderText(tr("e.g., developer, code_reviewer"));
|
||||
formLayout->addRow(tr("ID:"), m_idEdit);
|
||||
|
||||
m_descriptionEdit = new QTextEdit(this);
|
||||
m_descriptionEdit->setPlaceholderText(tr("Brief description of this role..."));
|
||||
m_descriptionEdit->setMaximumHeight(80);
|
||||
formLayout->addRow(tr("Description:"), m_descriptionEdit);
|
||||
|
||||
mainLayout->addLayout(formLayout);
|
||||
|
||||
auto *promptLabel = new QLabel(tr("System Prompt:"), this);
|
||||
mainLayout->addWidget(promptLabel);
|
||||
|
||||
m_systemPromptEdit = new QTextEdit(this);
|
||||
m_systemPromptEdit->setPlaceholderText(
|
||||
tr("You are an expert in...\n\nYour role is to:\n- Task 1\n- Task 2\n- Task 3"));
|
||||
mainLayout->addWidget(m_systemPromptEdit);
|
||||
|
||||
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
mainLayout->addWidget(m_buttonBox);
|
||||
|
||||
connect(m_buttonBox, &QDialogButtonBox::accepted, this, &AgentRoleDialog::accept);
|
||||
connect(m_buttonBox, &QDialogButtonBox::rejected, this, &AgentRoleDialog::reject);
|
||||
connect(m_nameEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||
connect(m_idEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||
connect(m_systemPromptEdit, &QTextEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||
|
||||
if (m_editMode) {
|
||||
m_idEdit->setEnabled(false);
|
||||
m_idEdit->setToolTip(tr("ID cannot be changed for existing roles"));
|
||||
}
|
||||
|
||||
setMinimumSize(600, 500);
|
||||
validateInput();
|
||||
}
|
||||
|
||||
void AgentRoleDialog::validateInput()
|
||||
{
|
||||
bool valid = !m_nameEdit->text().trimmed().isEmpty()
|
||||
&& !m_idEdit->text().trimmed().isEmpty()
|
||||
&& !m_systemPromptEdit->toPlainText().trimmed().isEmpty();
|
||||
|
||||
m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
|
||||
}
|
||||
|
||||
AgentRole AgentRoleDialog::getRole() const
|
||||
{
|
||||
return AgentRole{
|
||||
m_idEdit->text().trimmed(),
|
||||
m_nameEdit->text().trimmed(),
|
||||
m_descriptionEdit->toPlainText().trimmed(),
|
||||
m_systemPromptEdit->toPlainText().trimmed(),
|
||||
false};
|
||||
}
|
||||
|
||||
void AgentRoleDialog::setRole(const AgentRole &role)
|
||||
{
|
||||
m_idEdit->setText(role.id);
|
||||
m_nameEdit->setText(role.name);
|
||||
m_descriptionEdit->setPlainText(role.description);
|
||||
m_systemPromptEdit->setPlainText(role.systemPrompt);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
55
settings/AgentRoleDialog.hpp
Normal file
55
settings/AgentRoleDialog.hpp
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QDialog>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
|
||||
class QLineEdit;
|
||||
class QTextEdit;
|
||||
class QDialogButtonBox;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentRoleDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRoleDialog(QWidget *parent = nullptr);
|
||||
explicit AgentRoleDialog(const AgentRole &role, bool editMode = true, QWidget *parent = nullptr);
|
||||
|
||||
AgentRole getRole() const;
|
||||
void setRole(const AgentRole &role);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void validateInput();
|
||||
|
||||
QLineEdit *m_nameEdit = nullptr;
|
||||
QLineEdit *m_idEdit = nullptr;
|
||||
QTextEdit *m_descriptionEdit = nullptr;
|
||||
QTextEdit *m_systemPromptEdit = nullptr;
|
||||
QDialogButtonBox *m_buttonBox = nullptr;
|
||||
bool m_editMode = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
264
settings/AgentRolesWidget.cpp
Normal file
264
settings/AgentRolesWidget.cpp
Normal file
@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "AgentRolesWidget.hpp"
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "AgentRoleDialog.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentRolesWidget::AgentRolesWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUI();
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
void AgentRolesWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
auto *headerLayout = new QHBoxLayout();
|
||||
|
||||
auto *infoLabel = new QLabel(
|
||||
Tr::tr("Agent roles define different system prompts for specific tasks."), this);
|
||||
infoLabel->setWordWrap(true);
|
||||
headerLayout->addWidget(infoLabel, 1);
|
||||
|
||||
auto *openFolderButton = new QPushButton(Tr::tr("Open Roles Folder..."), this);
|
||||
connect(openFolderButton, &QPushButton::clicked, this, &AgentRolesWidget::onOpenRolesFolder);
|
||||
headerLayout->addWidget(openFolderButton);
|
||||
|
||||
mainLayout->addLayout(headerLayout);
|
||||
|
||||
auto *contentLayout = new QHBoxLayout();
|
||||
|
||||
m_rolesList = new QListWidget(this);
|
||||
m_rolesList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
connect(m_rolesList, &QListWidget::itemSelectionChanged, this, &AgentRolesWidget::updateButtons);
|
||||
connect(m_rolesList, &QListWidget::itemDoubleClicked, this, &AgentRolesWidget::onEditRole);
|
||||
contentLayout->addWidget(m_rolesList, 1);
|
||||
|
||||
auto *buttonsLayout = new QVBoxLayout();
|
||||
|
||||
m_addButton = new QPushButton(Tr::tr("Add..."), this);
|
||||
connect(m_addButton, &QPushButton::clicked, this, &AgentRolesWidget::onAddRole);
|
||||
buttonsLayout->addWidget(m_addButton);
|
||||
|
||||
m_editButton = new QPushButton(Tr::tr("Edit..."), this);
|
||||
connect(m_editButton, &QPushButton::clicked, this, &AgentRolesWidget::onEditRole);
|
||||
buttonsLayout->addWidget(m_editButton);
|
||||
|
||||
m_duplicateButton = new QPushButton(Tr::tr("Duplicate..."), this);
|
||||
connect(m_duplicateButton, &QPushButton::clicked, this, &AgentRolesWidget::onDuplicateRole);
|
||||
buttonsLayout->addWidget(m_duplicateButton);
|
||||
|
||||
m_deleteButton = new QPushButton(Tr::tr("Delete"), this);
|
||||
connect(m_deleteButton, &QPushButton::clicked, this, &AgentRolesWidget::onDeleteRole);
|
||||
buttonsLayout->addWidget(m_deleteButton);
|
||||
|
||||
buttonsLayout->addStretch();
|
||||
|
||||
contentLayout->addLayout(buttonsLayout);
|
||||
mainLayout->addLayout(contentLayout);
|
||||
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
void AgentRolesWidget::loadRoles()
|
||||
{
|
||||
m_rolesList->clear();
|
||||
|
||||
const QList<AgentRole> roles = AgentRolesManager::loadAllRoles();
|
||||
for (const AgentRole &role : roles) {
|
||||
auto *item = new QListWidgetItem(role.name, m_rolesList);
|
||||
item->setData(Qt::UserRole, role.id);
|
||||
|
||||
QString tooltip = role.description;
|
||||
if (role.isBuiltin) {
|
||||
tooltip += "\n\n" + Tr::tr("(Built-in role)");
|
||||
item->setForeground(Qt::darkGray);
|
||||
}
|
||||
item->setToolTip(tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::updateButtons()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
bool hasSelection = selectedItem != nullptr;
|
||||
bool isBuiltin = false;
|
||||
|
||||
if (hasSelection) {
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
isBuiltin = role.isBuiltin;
|
||||
}
|
||||
|
||||
m_editButton->setEnabled(hasSelection);
|
||||
m_duplicateButton->setEnabled(hasSelection);
|
||||
m_deleteButton->setEnabled(hasSelection && !isBuiltin);
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onAddRole()
|
||||
{
|
||||
AgentRoleDialog dialog(this);
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
AgentRole newRole = dialog.getRole();
|
||||
|
||||
if (AgentRolesManager::roleExists(newRole.id)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("Role Already Exists"),
|
||||
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
|
||||
.arg(newRole.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentRolesManager::saveRole(newRole)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
this, Tr::tr("Error"), Tr::tr("Failed to save role '%1'.").arg(newRole.name));
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onEditRole()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
|
||||
if (role.isBuiltin) {
|
||||
QMessageBox::information(
|
||||
this,
|
||||
Tr::tr("Cannot Edit Built-in Role"),
|
||||
Tr::tr(
|
||||
"Built-in roles cannot be edited. You can duplicate this role and modify the copy."));
|
||||
return;
|
||||
}
|
||||
|
||||
AgentRoleDialog dialog(role, this);
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
AgentRole updatedRole = dialog.getRole();
|
||||
|
||||
if (AgentRolesManager::saveRole(updatedRole)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
this, Tr::tr("Error"), Tr::tr("Failed to update role '%1'.").arg(updatedRole.name));
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onDuplicateRole()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
|
||||
role.name += " (Copy)";
|
||||
role.id += "_copy";
|
||||
role.isBuiltin = false;
|
||||
|
||||
int counter = 1;
|
||||
QString baseId = role.id;
|
||||
while (AgentRolesManager::roleExists(role.id)) {
|
||||
role.id = baseId + QString::number(counter++);
|
||||
}
|
||||
|
||||
AgentRoleDialog dialog(role, false, this);
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
AgentRole newRole = dialog.getRole();
|
||||
|
||||
if (AgentRolesManager::roleExists(newRole.id)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("Role Already Exists"),
|
||||
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
|
||||
.arg(newRole.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentRolesManager::saveRole(newRole)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(this, Tr::tr("Error"), Tr::tr("Failed to duplicate role."));
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onDeleteRole()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
|
||||
if (role.isBuiltin) {
|
||||
QMessageBox::information(
|
||||
this, Tr::tr("Cannot Delete Built-in Role"), Tr::tr("Built-in roles cannot be deleted."));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
this,
|
||||
Tr::tr("Delete Role"),
|
||||
Tr::tr("Are you sure you want to delete the role '%1'?").arg(role.name),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
if (AgentRolesManager::deleteRole(roleId)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
this, Tr::tr("Error"), Tr::tr("Failed to delete role '%1'.").arg(role.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onOpenRolesFolder()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(AgentRolesManager::getConfigurationDirectory()));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
54
settings/AgentRolesWidget.hpp
Normal file
54
settings/AgentRolesWidget.hpp
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 <QWidget>
|
||||
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentRolesWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRolesWidget(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void loadRoles();
|
||||
void updateButtons();
|
||||
|
||||
void onAddRole();
|
||||
void onEditRole();
|
||||
void onDuplicateRole();
|
||||
void onDeleteRole();
|
||||
void onOpenRolesFolder();
|
||||
|
||||
QListWidget *m_rolesList = nullptr;
|
||||
QPushButton *m_addButton = nullptr;
|
||||
QPushButton *m_editButton = nullptr;
|
||||
QPushButton *m_duplicateButton = nullptr;
|
||||
QPushButton *m_deleteButton = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
@ -16,11 +16,15 @@ add_library(QodeAssistSettings STATIC
|
||||
ProviderSettings.hpp ProviderSettings.cpp
|
||||
PluginUpdater.hpp PluginUpdater.cpp
|
||||
UpdateDialog.hpp UpdateDialog.cpp
|
||||
AgentRole.hpp AgentRole.cpp
|
||||
AgentRoleDialog.hpp AgentRoleDialog.cpp
|
||||
AgentRolesWidget.hpp AgentRolesWidget.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistSettings
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
Qt::Widgets
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
#include "AgentRolesWidget.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
@ -173,6 +174,25 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
thinkingMaxTokens.setRange(-1, 200000);
|
||||
thinkingMaxTokens.setDefaultValue(16000);
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
openAIResponsesReasoningEffort.setSettingsKey(Constants::CA_OPENAI_RESPONSES_REASONING_EFFORT);
|
||||
openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:"));
|
||||
openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
openAIResponsesReasoningEffort.addOption("None");
|
||||
openAIResponsesReasoningEffort.addOption("Minimal");
|
||||
openAIResponsesReasoningEffort.addOption("Low");
|
||||
openAIResponsesReasoningEffort.addOption("Medium");
|
||||
openAIResponsesReasoningEffort.addOption("High");
|
||||
openAIResponsesReasoningEffort.setDefaultValue("Medium");
|
||||
openAIResponsesReasoningEffort.setToolTip(
|
||||
Tr::tr("Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n"
|
||||
"None: No reasoning (gpt-5.1 only)\n"
|
||||
"Minimal: Minimal reasoning effort (o-series only)\n"
|
||||
"Low: Low reasoning effort\n"
|
||||
"Medium: Balanced reasoning (default for most models)\n"
|
||||
"High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n"
|
||||
"Note: Reducing effort = faster responses + fewer tokens"));
|
||||
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
|
||||
@ -243,6 +263,9 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
chatRenderer.setDefaultValue("rhi");
|
||||
#endif
|
||||
|
||||
lastUsedRoleId.setSettingsKey(Constants::CA_LAST_USED_ROLE);
|
||||
lastUsedRoleId.setDefaultValue("");
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
|
||||
readSettings();
|
||||
@ -270,6 +293,9 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
thinkingGrid.addRow({thinkingBudgetTokens});
|
||||
thinkingGrid.addRow({thinkingMaxTokens});
|
||||
|
||||
auto openAIResponsesGrid = Grid{};
|
||||
openAIResponsesGrid.addRow({openAIResponsesReasoningEffort});
|
||||
|
||||
auto chatViewSettingsGrid = Grid{};
|
||||
chatViewSettingsGrid.addRow({textFontFamily, textFontSize});
|
||||
chatViewSettingsGrid.addRow({codeFontFamily, codeFontSize});
|
||||
@ -293,9 +319,13 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
Column{enableChatTools}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Extended Thinking (if provider/model supports)")),
|
||||
title(Tr::tr("Extended Thinking (Claude)")),
|
||||
Column{enableThinkingMode, Row{thinkingGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("OpenAI Responses API")),
|
||||
Column{Row{openAIResponsesGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Row{genGrid, Stretch{1}},
|
||||
@ -352,6 +382,7 @@ void ChatAssistantSettings::resetSettingsToDefaults()
|
||||
resetAspect(enableThinkingMode);
|
||||
resetAspect(thinkingBudgetTokens);
|
||||
resetAspect(thinkingMaxTokens);
|
||||
resetAspect(openAIResponsesReasoningEffort);
|
||||
resetAspect(linkOpenFiles);
|
||||
resetAspect(enableChatTools);
|
||||
resetAspect(textFontFamily);
|
||||
@ -378,4 +409,18 @@ public:
|
||||
|
||||
const ChatAssistantSettingsPage chatAssistantSettingsPage;
|
||||
|
||||
class AgentRolesSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
AgentRolesSettingsPage()
|
||||
{
|
||||
setId("QodeAssist.AgentRoles");
|
||||
setDisplayName(Tr::tr("Agent Roles"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setWidgetCreator([]() { return new AgentRolesWidget(); });
|
||||
}
|
||||
};
|
||||
|
||||
const AgentRolesSettingsPage agentRolesSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "ButtonAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
@ -70,6 +71,9 @@ public:
|
||||
Utils::IntegerAspect thinkingBudgetTokens{this};
|
||||
Utils::IntegerAspect thinkingMaxTokens{this};
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
Utils::SelectionAspect openAIResponsesReasoningEffort{this};
|
||||
|
||||
// Visuals settings
|
||||
Utils::SelectionAspect textFontFamily{this};
|
||||
Utils::IntegerAspect textFontSize{this};
|
||||
@ -79,6 +83,8 @@ public:
|
||||
|
||||
Utils::SelectionAspect chatRenderer{this};
|
||||
|
||||
Utils::StringAspect lastUsedRoleId{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
|
||||
@ -151,7 +151,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS);
|
||||
maxTokens.setLabelText(Tr::tr("Max Tokens:"));
|
||||
maxTokens.setRange(-1, 900000);
|
||||
maxTokens.setDefaultValue(100);
|
||||
maxTokens.setDefaultValue(500);
|
||||
|
||||
// Advanced Parameters
|
||||
useTopP.setSettingsKey(Constants::CC_USE_TOP_P);
|
||||
@ -287,18 +287,6 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
maxChangesCacheSize.setRange(2, 1000);
|
||||
maxChangesCacheSize.setDefaultValue(10);
|
||||
|
||||
// Quick refactor command settings
|
||||
useOpenFilesInQuickRefactor.setSettingsKey(Constants::CC_USE_OPEN_FILES_IN_QUICK_REFACTOR);
|
||||
useOpenFilesInQuickRefactor.setLabelText(
|
||||
Tr::tr("Include context from open files in quick refactor"));
|
||||
useOpenFilesInQuickRefactor.setDefaultValue(false);
|
||||
quickRefactorSystemPrompt.setSettingsKey(Constants::CC_QUICK_REFACTOR_SYSTEM_PROMPT);
|
||||
quickRefactorSystemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
quickRefactorSystemPrompt.setDefaultValue(
|
||||
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide"
|
||||
"precise and contextually appropriate code completions to insert depending on user "
|
||||
"instructions.\n\n");
|
||||
|
||||
// Ollama Settings
|
||||
ollamaLivetime.setSettingsKey(Constants::CC_OLLAMA_LIVETIME);
|
||||
ollamaLivetime.setToolTip(
|
||||
@ -313,6 +301,25 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
contextWindow.setRange(-1, 10000);
|
||||
contextWindow.setDefaultValue(2048);
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
openAIResponsesReasoningEffort.setSettingsKey(Constants::CC_OPENAI_RESPONSES_REASONING_EFFORT);
|
||||
openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:"));
|
||||
openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
openAIResponsesReasoningEffort.addOption("None");
|
||||
openAIResponsesReasoningEffort.addOption("Minimal");
|
||||
openAIResponsesReasoningEffort.addOption("Low");
|
||||
openAIResponsesReasoningEffort.addOption("Medium");
|
||||
openAIResponsesReasoningEffort.addOption("High");
|
||||
openAIResponsesReasoningEffort.setDefaultValue("Medium");
|
||||
openAIResponsesReasoningEffort.setToolTip(
|
||||
Tr::tr("Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n"
|
||||
"None: No reasoning (gpt-5.1 only)\n"
|
||||
"Minimal: Minimal reasoning effort (o-series only)\n"
|
||||
"Low: Low reasoning effort\n"
|
||||
"Medium: Balanced reasoning (default for most models)\n"
|
||||
"High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n"
|
||||
"Note: Reducing effort = faster responses + fewer tokens"));
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
readSettings();
|
||||
@ -338,6 +345,9 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
auto openAIResponsesGrid = Grid{};
|
||||
openAIResponsesGrid.addRow({openAIResponsesReasoningEffort});
|
||||
|
||||
auto contextGrid = Grid{};
|
||||
contextGrid.addRow({Row{readFullFile}});
|
||||
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
|
||||
@ -395,8 +405,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), contextItem},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Quick Refactor Settings")),
|
||||
Column{useOpenFilesInQuickRefactor, quickRefactorSystemPrompt}},
|
||||
Group{title(Tr::tr("OpenAI Responses API")), Column{Row{openAIResponsesGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Stretch{1}};
|
||||
@ -458,14 +467,13 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
resetAspect(maxChangesCacheSize);
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(contextWindow);
|
||||
resetAspect(openAIResponsesReasoningEffort);
|
||||
resetAspect(useUserMessageTemplateForCC);
|
||||
resetAspect(userMessageTemplateForCC);
|
||||
resetAspect(systemPromptForNonFimModels);
|
||||
resetAspect(customLanguages);
|
||||
resetAspect(showProgressWidget);
|
||||
resetAspect(useOpenFilesContext);
|
||||
resetAspect(useOpenFilesInQuickRefactor);
|
||||
resetAspect(quickRefactorSystemPrompt);
|
||||
resetAspect(modelOutputHandler);
|
||||
resetAspect(completionTriggerMode);
|
||||
resetAspect(hintCharThreshold);
|
||||
|
||||
@ -82,14 +82,13 @@ public:
|
||||
Utils::BoolAspect useProjectChangesCache{this};
|
||||
Utils::IntegerAspect maxChangesCacheSize{this};
|
||||
|
||||
// Quick refactor command settings
|
||||
Utils::BoolAspect useOpenFilesInQuickRefactor{this};
|
||||
Utils::StringAspect quickRefactorSystemPrompt{this};
|
||||
|
||||
// Ollama Settings
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::IntegerAspect contextWindow{this};
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
Utils::SelectionAspect openAIResponsesReasoningEffort{this};
|
||||
|
||||
QString processMessageToFIM(const QString &prefix, const QString &suffix) const;
|
||||
|
||||
private:
|
||||
|
||||
@ -93,8 +93,9 @@ QuickRefactorSettings::QuickRefactorSettings()
|
||||
// Ollama Settings
|
||||
ollamaLivetime.setSettingsKey(Constants::QR_OLLAMA_LIVETIME);
|
||||
ollamaLivetime.setToolTip(
|
||||
Tr::tr("Time to suspend Ollama after completion request (in minutes), "
|
||||
"Only Ollama, -1 to disable"));
|
||||
Tr::tr(
|
||||
"Time to suspend Ollama after completion request (in minutes), "
|
||||
"Only Ollama, -1 to disable"));
|
||||
ollamaLivetime.setLabelText("Livetime:");
|
||||
ollamaLivetime.setDefaultValue("5m");
|
||||
ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
@ -107,32 +108,56 @@ QuickRefactorSettings::QuickRefactorSettings()
|
||||
useTools.setSettingsKey(Constants::QR_USE_TOOLS);
|
||||
useTools.setLabelText(Tr::tr("Enable Tools"));
|
||||
useTools.setToolTip(
|
||||
Tr::tr("Enable AI tools/functions for quick refactoring (allows reading project files, "
|
||||
"searching code, etc.)"));
|
||||
Tr::tr(
|
||||
"Enable AI tools/functions for quick refactoring (allows reading project files, "
|
||||
"searching code, etc.)"));
|
||||
useTools.setDefaultValue(false);
|
||||
|
||||
useThinking.setSettingsKey(Constants::QR_USE_THINKING);
|
||||
useThinking.setLabelText(Tr::tr("Enable Thinking Mode"));
|
||||
useThinking.setToolTip(
|
||||
Tr::tr("Enable extended thinking mode for complex refactoring tasks (supported by "
|
||||
"compatible models like Claude and Google AI)"));
|
||||
Tr::tr(
|
||||
"Enable extended thinking mode for complex refactoring tasks (supported by "
|
||||
"compatible models like Claude and Google AI)"));
|
||||
useThinking.setDefaultValue(false);
|
||||
|
||||
thinkingBudgetTokens.setSettingsKey(Constants::QR_THINKING_BUDGET_TOKENS);
|
||||
thinkingBudgetTokens.setLabelText(Tr::tr("Thinking Budget Tokens:"));
|
||||
thinkingBudgetTokens.setToolTip(
|
||||
Tr::tr("Number of tokens allocated for thinking process. Use -1 for dynamic thinking "
|
||||
"(model decides), 0 to disable, or positive value for custom budget"));
|
||||
Tr::tr(
|
||||
"Number of tokens allocated for thinking process. Use -1 for dynamic thinking "
|
||||
"(model decides), 0 to disable, or positive value for custom budget"));
|
||||
thinkingBudgetTokens.setRange(-1, 100000);
|
||||
thinkingBudgetTokens.setDefaultValue(10000);
|
||||
|
||||
thinkingMaxTokens.setSettingsKey(Constants::QR_THINKING_MAX_TOKENS);
|
||||
thinkingMaxTokens.setLabelText(Tr::tr("Thinking Max Output Tokens:"));
|
||||
thinkingMaxTokens.setToolTip(
|
||||
Tr::tr("Maximum output tokens when thinking mode is enabled (includes thinking + response)"));
|
||||
Tr::tr(
|
||||
"Maximum output tokens when thinking mode is enabled (includes thinking + response)"));
|
||||
thinkingMaxTokens.setRange(1000, 200000);
|
||||
thinkingMaxTokens.setDefaultValue(16000);
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
openAIResponsesReasoningEffort.setSettingsKey(Constants::QR_OPENAI_RESPONSES_REASONING_EFFORT);
|
||||
openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:"));
|
||||
openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
openAIResponsesReasoningEffort.addOption("None");
|
||||
openAIResponsesReasoningEffort.addOption("Minimal");
|
||||
openAIResponsesReasoningEffort.addOption("Low");
|
||||
openAIResponsesReasoningEffort.addOption("Medium");
|
||||
openAIResponsesReasoningEffort.addOption("High");
|
||||
openAIResponsesReasoningEffort.setDefaultValue("Medium");
|
||||
openAIResponsesReasoningEffort.setToolTip(
|
||||
Tr::tr(
|
||||
"Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n"
|
||||
"None: No reasoning (gpt-5.1 only)\n"
|
||||
"Minimal: Minimal reasoning effort (o-series only)\n"
|
||||
"Low: Low reasoning effort\n"
|
||||
"Medium: Balanced reasoning (default for most models)\n"
|
||||
"High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n"
|
||||
"Note: Reducing effort = faster responses + fewer tokens"));
|
||||
|
||||
// Context Settings
|
||||
readFullFile.setSettingsKey(Constants::QR_READ_FULL_FILE);
|
||||
readFullFile.setLabelText(Tr::tr("Read Full File"));
|
||||
@ -158,9 +183,11 @@ QuickRefactorSettings::QuickRefactorSettings()
|
||||
displayMode.setSettingsKey(Constants::QR_DISPLAY_MODE);
|
||||
displayMode.setLabelText(Tr::tr("Display Mode:"));
|
||||
displayMode.setToolTip(
|
||||
Tr::tr("Choose how to display refactoring suggestions:\n"
|
||||
"- Inline Widget: Shows refactor in a widget overlay with Apply/Decline buttons (default)\n"
|
||||
"- Qt Creator Suggestion: Uses Qt Creator's built-in suggestion system"));
|
||||
Tr::tr(
|
||||
"Choose how to display refactoring suggestions:\n"
|
||||
"- Inline Widget: Shows refactor in a widget overlay with Apply/Decline buttons "
|
||||
"(default)\n"
|
||||
"- Qt Creator Suggestion: Uses Qt Creator's built-in suggestion system"));
|
||||
displayMode.addOption(Tr::tr("Inline Widget"));
|
||||
displayMode.addOption(Tr::tr("Qt Creator Suggestion"));
|
||||
displayMode.setDefaultValue(0);
|
||||
@ -168,9 +195,10 @@ QuickRefactorSettings::QuickRefactorSettings()
|
||||
widgetOrientation.setSettingsKey(Constants::QR_WIDGET_ORIENTATION);
|
||||
widgetOrientation.setLabelText(Tr::tr("Widget Orientation:"));
|
||||
widgetOrientation.setToolTip(
|
||||
Tr::tr("Choose default orientation for refactor widget:\n"
|
||||
"- Horizontal: Original and refactored code side by side (default)\n"
|
||||
"- Vertical: Original and refactored code stacked vertically"));
|
||||
Tr::tr(
|
||||
"Choose default orientation for refactor widget:\n"
|
||||
"- Horizontal: Original and refactored code side by side (default)\n"
|
||||
"- Vertical: Original and refactored code stacked vertically"));
|
||||
widgetOrientation.addOption(Tr::tr("Horizontal"));
|
||||
widgetOrientation.addOption(Tr::tr("Vertical"));
|
||||
widgetOrientation.setDefaultValue(0);
|
||||
@ -207,6 +235,11 @@ QuickRefactorSettings::QuickRefactorSettings()
|
||||
"precise and contextually appropriate code completions to insert depending on user "
|
||||
"instructions.\n\n");
|
||||
|
||||
useOpenFilesInQuickRefactor.setSettingsKey(Constants::QR_USE_OPEN_FILES_IN_QUICK_REFACTOR);
|
||||
useOpenFilesInQuickRefactor.setLabelText(
|
||||
Tr::tr("Include context from open files in quick refactor"));
|
||||
useOpenFilesInQuickRefactor.setDefaultValue(false);
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
|
||||
readSettings();
|
||||
@ -238,9 +271,15 @@ QuickRefactorSettings::QuickRefactorSettings()
|
||||
toolsGrid.addRow({thinkingBudgetTokens});
|
||||
toolsGrid.addRow({thinkingMaxTokens});
|
||||
|
||||
auto openAIResponsesGrid = Grid{};
|
||||
openAIResponsesGrid.addRow({openAIResponsesReasoningEffort});
|
||||
|
||||
auto contextGrid = Grid{};
|
||||
contextGrid.addRow({Row{readFullFile}});
|
||||
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
|
||||
contextGrid.addRow({
|
||||
Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor},
|
||||
});
|
||||
contextGrid.addRow({Row{useOpenFilesInQuickRefactor}});
|
||||
|
||||
auto displayGrid = Grid{};
|
||||
displayGrid.addRow({Row{displayMode}});
|
||||
@ -260,6 +299,8 @@ QuickRefactorSettings::QuickRefactorSettings()
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Tools Settings")), Column{Row{toolsGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("OpenAI Responses API")), Column{Row{openAIResponsesGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), Column{Row{contextGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Display Settings")), Column{Row{displayGrid, Stretch{1}}}},
|
||||
@ -299,10 +340,13 @@ void QuickRefactorSettings::setupConnections()
|
||||
bool isInlineWidget = (displayMode.volatileValue() == 0);
|
||||
widgetOrientation.setEnabled(isInlineWidget);
|
||||
};
|
||||
|
||||
connect(&displayMode, &Utils::SelectionAspect::volatileValueChanged,
|
||||
this, updateWidgetOrientationEnabled);
|
||||
|
||||
|
||||
connect(
|
||||
&displayMode,
|
||||
&Utils::SelectionAspect::volatileValueChanged,
|
||||
this,
|
||||
updateWidgetOrientationEnabled);
|
||||
|
||||
updateWidgetOrientationEnabled();
|
||||
|
||||
auto validateWidgetSizes = [this]() {
|
||||
@ -346,6 +390,7 @@ void QuickRefactorSettings::resetSettingsToDefaults()
|
||||
resetAspect(useThinking);
|
||||
resetAspect(thinkingBudgetTokens);
|
||||
resetAspect(thinkingMaxTokens);
|
||||
resetAspect(openAIResponsesReasoningEffort);
|
||||
resetAspect(readFullFile);
|
||||
resetAspect(readFileParts);
|
||||
resetAspect(readStringsBeforeCursor);
|
||||
@ -357,6 +402,7 @@ void QuickRefactorSettings::resetSettingsToDefaults()
|
||||
resetAspect(widgetMinHeight);
|
||||
resetAspect(widgetMaxHeight);
|
||||
resetAspect(systemPrompt);
|
||||
resetAspect(useOpenFilesInQuickRefactor);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
@ -376,4 +422,3 @@ public:
|
||||
const QuickRefactorSettingsPage quickRefactorSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
|
||||
@ -61,6 +61,9 @@ public:
|
||||
Utils::IntegerAspect thinkingBudgetTokens{this};
|
||||
Utils::IntegerAspect thinkingMaxTokens{this};
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
Utils::SelectionAspect openAIResponsesReasoningEffort{this};
|
||||
|
||||
// Context Settings
|
||||
Utils::BoolAspect readFullFile{this};
|
||||
Utils::BoolAspect readFileParts{this};
|
||||
@ -77,6 +80,7 @@ public:
|
||||
|
||||
// Prompt Settings
|
||||
Utils::StringAspect systemPrompt{this};
|
||||
Utils::BoolAspect useOpenFilesInQuickRefactor{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
|
||||
@ -111,6 +111,7 @@ const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideP
|
||||
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
||||
const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool";
|
||||
const char CA_ENABLE_TERMINAL_COMMAND_TOOL[] = "QodeAssist.caEnableTerminalCommandTool";
|
||||
const char CA_ENABLE_TODO_TOOL[] = "QodeAssist.caEnableTodoTool";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS[] = "QodeAssist.caAllowedTerminalCommands";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_LINUX[] = "QodeAssist.caAllowedTerminalCommandsLinux";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS";
|
||||
@ -165,10 +166,6 @@ const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize";
|
||||
const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt";
|
||||
const char CA_SYSTEM_PROMPT[] = "QodeAssist.chatSystemPrompt";
|
||||
|
||||
// quick refactor command settings
|
||||
const char CC_QUICK_REFACTOR_SYSTEM_PROMPT[] = "QodeAssist.ccQuickRefactorSystemPrompt";
|
||||
const char CC_USE_OPEN_FILES_IN_QUICK_REFACTOR[] = "QodeAssist.ccUseOpenFilesInQuickRefactor";
|
||||
|
||||
// preset prompt settings
|
||||
const char CC_TEMPERATURE[] = "QodeAssist.ccTemperature";
|
||||
const char CC_MAX_TOKENS[] = "QodeAssist.ccMaxTokens";
|
||||
@ -182,6 +179,10 @@ const char CC_USE_FREQUENCY_PENALTY[] = "QodeAssist.fimUseFrequencyPenalty";
|
||||
const char CC_FREQUENCY_PENALTY[] = "QodeAssist.fimFrequencyPenalty";
|
||||
const char CC_OLLAMA_LIVETIME[] = "QodeAssist.fimOllamaLivetime";
|
||||
const char CC_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.ccOllamaContextWindow";
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
const char CC_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.ccOpenAIResponsesReasoningEffort";
|
||||
|
||||
const char CA_TEMPERATURE[] = "QodeAssist.chatTemperature";
|
||||
const char CA_MAX_TOKENS[] = "QodeAssist.chatMaxTokens";
|
||||
const char CA_USE_TOP_P[] = "QodeAssist.chatUseTopP";
|
||||
@ -197,6 +198,10 @@ const char CA_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.caOllamaContextWindow";
|
||||
const char CA_ENABLE_THINKING_MODE[] = "QodeAssist.caEnableThinkingMode";
|
||||
const char CA_THINKING_BUDGET_TOKENS[] = "QodeAssist.caThinkingBudgetTokens";
|
||||
const char CA_THINKING_MAX_TOKENS[] = "QodeAssist.caThinkingMaxTokens";
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
const char CA_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.caOpenAIResponsesReasoningEffort";
|
||||
|
||||
const char CA_TEXT_FONT_FAMILY[] = "QodeAssist.caTextFontFamily";
|
||||
const char CA_TEXT_FONT_SIZE[] = "QodeAssist.caTextFontSize";
|
||||
const char CA_CODE_FONT_FAMILY[] = "QodeAssist.caCodeFontFamily";
|
||||
@ -204,6 +209,8 @@ const char CA_CODE_FONT_SIZE[] = "QodeAssist.caCodeFontSize";
|
||||
const char CA_TEXT_FORMAT[] = "QodeAssist.caTextFormat";
|
||||
const char CA_CHAT_RENDERER[] = "QodeAssist.caChatRenderer";
|
||||
|
||||
const char CA_LAST_USED_ROLE[] = "QodeAssist.caLastUsedRole";
|
||||
|
||||
// quick refactor preset prompt settings
|
||||
const char QR_TEMPERATURE[] = "QodeAssist.qrTemperature";
|
||||
const char QR_MAX_TOKENS[] = "QodeAssist.qrMaxTokens";
|
||||
@ -221,10 +228,15 @@ const char QR_USE_TOOLS[] = "QodeAssist.qrUseTools";
|
||||
const char QR_USE_THINKING[] = "QodeAssist.qrUseThinking";
|
||||
const char QR_THINKING_BUDGET_TOKENS[] = "QodeAssist.qrThinkingBudgetTokens";
|
||||
const char QR_THINKING_MAX_TOKENS[] = "QodeAssist.qrThinkingMaxTokens";
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
const char QR_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.qrOpenAIResponsesReasoningEffort";
|
||||
|
||||
const char QR_READ_FULL_FILE[] = "QodeAssist.qrReadFullFile";
|
||||
const char QR_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.qrReadStringsBeforeCursor";
|
||||
const char QR_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.qrReadStringsAfterCursor";
|
||||
const char QR_SYSTEM_PROMPT[] = "QodeAssist.qrSystemPrompt";
|
||||
const char QR_USE_OPEN_FILES_IN_QUICK_REFACTOR[] = "QodeAssist.qrUseOpenFilesInQuickRefactor";
|
||||
const char QR_DISPLAY_MODE[] = "QodeAssist.qrDisplayMode";
|
||||
const char QR_WIDGET_ORIENTATION[] = "QodeAssist.qrWidgetOrientation";
|
||||
const char QR_WIDGET_MIN_WIDTH[] = "QodeAssist.qrWidgetMinWidth";
|
||||
|
||||
@ -97,6 +97,13 @@ ToolsSettings::ToolsSettings()
|
||||
"unexpected behavior."));
|
||||
enableTerminalCommandTool.setDefaultValue(false);
|
||||
|
||||
enableTodoTool.setSettingsKey(Constants::CA_ENABLE_TODO_TOOL);
|
||||
enableTodoTool.setLabelText(Tr::tr("Enable Todo Tool"));
|
||||
enableTodoTool.setToolTip(
|
||||
Tr::tr("Enable the todo_tool that helps AI track and organize multi-step tasks. "
|
||||
"Useful for complex refactoring, debugging, and feature implementation workflows."));
|
||||
enableTodoTool.setDefaultValue(true);
|
||||
|
||||
allowedTerminalCommandsLinux.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_LINUX);
|
||||
allowedTerminalCommandsLinux.setLabelText(Tr::tr("Allowed Commands (Linux)"));
|
||||
allowedTerminalCommandsLinux.setToolTip(
|
||||
@ -158,6 +165,7 @@ ToolsSettings::ToolsSettings()
|
||||
enableEditFileTool,
|
||||
enableBuildProjectTool,
|
||||
enableTerminalCommandTool,
|
||||
enableTodoTool,
|
||||
currentOsCommands,
|
||||
autoApplyFileEdits}},
|
||||
Stretch{1}};
|
||||
@ -191,6 +199,7 @@ void ToolsSettings::resetSettingsToDefaults()
|
||||
resetAspect(enableEditFileTool);
|
||||
resetAspect(enableBuildProjectTool);
|
||||
resetAspect(enableTerminalCommandTool);
|
||||
resetAspect(enableTodoTool);
|
||||
resetAspect(allowedTerminalCommandsLinux);
|
||||
resetAspect(allowedTerminalCommandsMacOS);
|
||||
resetAspect(allowedTerminalCommandsWindows);
|
||||
|
||||
@ -41,6 +41,7 @@ public:
|
||||
Utils::BoolAspect enableEditFileTool{this};
|
||||
Utils::BoolAspect enableBuildProjectTool{this};
|
||||
Utils::BoolAspect enableTerminalCommandTool{this};
|
||||
Utils::BoolAspect enableTodoTool{this};
|
||||
Utils::StringAspect allowedTerminalCommandsLinux{this};
|
||||
Utils::StringAspect allowedTerminalCommandsMacOS{this};
|
||||
Utils::StringAspect allowedTerminalCommandsWindows{this};
|
||||
|
||||
135
templates/OpenAIResponses.hpp
Normal file
135
templates/OpenAIResponses.hpp
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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/PromptTemplate.hpp"
|
||||
#include "providers/OpenAIResponsesRequestBuilder.hpp"
|
||||
|
||||
namespace QodeAssist::Templates {
|
||||
|
||||
class OpenAIResponses : public LLMCore::PromptTemplate
|
||||
{
|
||||
public:
|
||||
LLMCore::TemplateType type() const noexcept override
|
||||
{
|
||||
return LLMCore::TemplateType::Chat;
|
||||
}
|
||||
|
||||
QString name() const override { return "OpenAI Responses"; }
|
||||
|
||||
QStringList stopWords() const override { return {}; }
|
||||
|
||||
void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
|
||||
{
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
RequestBuilder builder;
|
||||
|
||||
if (context.systemPrompt) {
|
||||
builder.setInstructions(context.systemPrompt.value());
|
||||
}
|
||||
|
||||
if (!context.history || context.history->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &history = context.history.value();
|
||||
|
||||
for (const auto &msg : history) {
|
||||
if (msg.role == "system") {
|
||||
continue;
|
||||
}
|
||||
|
||||
Message message;
|
||||
message.role = roleFromString(msg.role);
|
||||
|
||||
if (msg.images && !msg.images->isEmpty()) {
|
||||
const auto &images = msg.images.value();
|
||||
message.content.reserve(1 + images.size());
|
||||
|
||||
if (!msg.content.isEmpty()) {
|
||||
message.content.append(MessageContent(InputText{msg.content}));
|
||||
}
|
||||
|
||||
for (const auto &image : images) {
|
||||
InputImage imgInput;
|
||||
imgInput.detail = "auto";
|
||||
|
||||
if (image.isUrl) {
|
||||
imgInput.imageUrl = image.data;
|
||||
} else {
|
||||
imgInput.imageUrl
|
||||
= QString("data:%1;base64,%2").arg(image.mediaType, image.data);
|
||||
}
|
||||
|
||||
message.content.append(MessageContent(std::move(imgInput)));
|
||||
}
|
||||
} else {
|
||||
message.content.append(MessageContent(msg.content));
|
||||
}
|
||||
|
||||
builder.addMessage(std::move(message));
|
||||
}
|
||||
|
||||
const QJsonObject builtRequest = builder.toJson();
|
||||
for (auto it = builtRequest.constBegin(); it != builtRequest.constEnd(); ++it) {
|
||||
request[it.key()] = it.value();
|
||||
}
|
||||
}
|
||||
QString description() const override
|
||||
{
|
||||
return "Template for OpenAI Responses API:\n\n"
|
||||
"Simple request:\n"
|
||||
"{\n"
|
||||
" \"input\": \"<user message>\"\n"
|
||||
"}\n\n"
|
||||
"Multi-turn conversation:\n"
|
||||
"{\n"
|
||||
" \"instructions\": \"<system prompt>\",\n"
|
||||
" \"input\": [\n"
|
||||
" {\"role\": \"user\", \"content\": \"<message>\"}\n"
|
||||
" ]\n"
|
||||
"}\n\n"
|
||||
"Uses type-safe RequestBuilder for OpenAI Responses API.";
|
||||
}
|
||||
bool isSupportProvider(LLMCore::ProviderID id) const noexcept override
|
||||
{
|
||||
return id == QodeAssist::LLMCore::ProviderID::OpenAIResponses;
|
||||
}
|
||||
|
||||
private:
|
||||
static QodeAssist::OpenAIResponses::Role roleFromString(const QString &roleStr) noexcept
|
||||
{
|
||||
using namespace QodeAssist::OpenAIResponses;
|
||||
|
||||
if (roleStr == "user")
|
||||
return Role::User;
|
||||
if (roleStr == "assistant")
|
||||
return Role::Assistant;
|
||||
if (roleStr == "system")
|
||||
return Role::System;
|
||||
if (roleStr == "developer")
|
||||
return Role::Developer;
|
||||
|
||||
return Role::User;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Templates
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
#include "templates/Ollama.hpp"
|
||||
#include "templates/OpenAI.hpp"
|
||||
#include "templates/OpenAICompatible.hpp"
|
||||
#include "templates/OpenAIResponses.hpp"
|
||||
// #include "templates/CustomFimTemplate.hpp"
|
||||
// #include "templates/DeepSeekCoderFim.hpp"
|
||||
#include "templates/GoogleAI.hpp"
|
||||
@ -49,6 +50,7 @@ inline void registerTemplates()
|
||||
templateManager.registerTemplate<CodeLlamaFim>();
|
||||
templateManager.registerTemplate<Claude>();
|
||||
templateManager.registerTemplate<OpenAI>();
|
||||
templateManager.registerTemplate<OpenAIResponses>();
|
||||
templateManager.registerTemplate<MistralAIFim>();
|
||||
templateManager.registerTemplate<MistralAIChat>();
|
||||
templateManager.registerTemplate<CodeLlamaQMLFim>();
|
||||
|
||||
360
tools/TodoTool.cpp
Normal file
360
tools/TodoTool.cpp
Normal file
@ -0,0 +1,360 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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 "TodoTool.hpp"
|
||||
#include "ToolExceptions.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMutexLocker>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
TodoTool::TodoTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{}
|
||||
|
||||
QString TodoTool::name() const
|
||||
{
|
||||
return "todo_tool";
|
||||
}
|
||||
|
||||
QString TodoTool::stringName() const
|
||||
{
|
||||
return "Managing TODO list for task tracking";
|
||||
}
|
||||
|
||||
QString TodoTool::description() const
|
||||
{
|
||||
return "Track and organize multi-step tasks during complex operations that require multiple "
|
||||
"sequential steps. "
|
||||
"**Use when planning 3+ step workflows.** "
|
||||
"Operations: 'add' - provide array of task descriptions to create full plan at once, "
|
||||
"'complete' - provide array of task IDs to mark finished steps, 'list' - review "
|
||||
"progress. "
|
||||
"Helpful for: large refactorings, feature implementations, debugging workflows. "
|
||||
"The list persists throughout the conversation.";
|
||||
}
|
||||
|
||||
QJsonObject TodoTool::getDefinition(LLMCore::ToolSchemaFormat format) const
|
||||
{
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
|
||||
QJsonObject properties;
|
||||
|
||||
QJsonObject operationProp;
|
||||
operationProp["type"] = "string";
|
||||
operationProp["description"] = "Operation: 'add' (create tasks), 'complete' (mark tasks as "
|
||||
"done), 'list' (show all tasks)";
|
||||
QJsonArray operationEnum;
|
||||
operationEnum.append("add");
|
||||
operationEnum.append("complete");
|
||||
operationEnum.append("list");
|
||||
operationProp["enum"] = operationEnum;
|
||||
properties["operation"] = operationProp;
|
||||
|
||||
QJsonObject tasksProp;
|
||||
tasksProp["type"] = "array";
|
||||
QJsonObject tasksItems;
|
||||
tasksItems["type"] = "string";
|
||||
tasksProp["items"] = tasksItems;
|
||||
tasksProp["description"]
|
||||
= "Array of task descriptions to create (required for 'add' operation). "
|
||||
"Create all subtasks at once, e.g.: ['Step 1: ...', 'Step 2: ...', 'Step 3: ...']";
|
||||
properties["tasks"] = tasksProp;
|
||||
|
||||
QJsonObject todoIdsProp;
|
||||
todoIdsProp["type"] = "array";
|
||||
QJsonObject todoIdsItems;
|
||||
todoIdsItems["type"] = "integer";
|
||||
todoIdsProp["items"] = todoIdsItems;
|
||||
todoIdsProp["description"]
|
||||
= "Array of todo item IDs to mark as completed (required for 'complete' operation). "
|
||||
"Example: [1, 2, 5] to complete tasks #1, #2, and #5";
|
||||
properties["todo_ids"] = todoIdsProp;
|
||||
|
||||
definition["properties"] = properties;
|
||||
|
||||
QJsonArray required;
|
||||
required.append("operation");
|
||||
definition["required"] = required;
|
||||
|
||||
switch (format) {
|
||||
case LLMCore::ToolSchemaFormat::OpenAI:
|
||||
return customizeForOpenAI(definition);
|
||||
case LLMCore::ToolSchemaFormat::Claude:
|
||||
return customizeForClaude(definition);
|
||||
case LLMCore::ToolSchemaFormat::Ollama:
|
||||
return customizeForOllama(definition);
|
||||
case LLMCore::ToolSchemaFormat::Google:
|
||||
return customizeForGoogle(definition);
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
LLMCore::ToolPermissions TodoTool::requiredPermissions() const
|
||||
{
|
||||
return LLMCore::ToolPermission::None;
|
||||
}
|
||||
|
||||
QFuture<QString> TodoTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> QString {
|
||||
QString sessionId = input.value("session_id").toString();
|
||||
if (sessionId.isEmpty()) {
|
||||
sessionId = "current";
|
||||
}
|
||||
|
||||
const QString operation = input.value("operation").toString();
|
||||
|
||||
if (operation == "add") {
|
||||
if (!input.contains("tasks") || !input.value("tasks").isArray()) {
|
||||
throw ToolRuntimeError(
|
||||
tr("Error: 'tasks' parameter (array) is required for 'add' operation. "
|
||||
"Example: {\"operation\": \"add\", \"tasks\": [\"Task 1\", \"Task 2\"]}"));
|
||||
}
|
||||
|
||||
const QJsonArray tasksArray = input.value("tasks").toArray();
|
||||
if (tasksArray.isEmpty()) {
|
||||
throw ToolRuntimeError(
|
||||
tr("Error: 'tasks' array cannot be empty. Provide at least one task."));
|
||||
}
|
||||
|
||||
QStringList tasks;
|
||||
for (const QJsonValue &taskValue : tasksArray) {
|
||||
QString task = taskValue.toString().trimmed();
|
||||
if (!task.isEmpty()) {
|
||||
tasks.append(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.isEmpty()) {
|
||||
throw ToolRuntimeError(
|
||||
tr("Error: All tasks in 'tasks' array are empty strings."));
|
||||
}
|
||||
|
||||
return addTodos(sessionId, tasks);
|
||||
|
||||
} else if (operation == "complete") {
|
||||
if (!input.contains("todo_ids") || !input.value("todo_ids").isArray()) {
|
||||
throw ToolRuntimeError(
|
||||
tr("Error: 'todo_ids' parameter (array) is required for 'complete' operation. "
|
||||
"Example: {\"operation\": \"complete\", \"todo_ids\": [1, 2, 3]}"));
|
||||
}
|
||||
|
||||
const QJsonArray idsArray = input.value("todo_ids").toArray();
|
||||
if (idsArray.isEmpty()) {
|
||||
throw ToolRuntimeError(
|
||||
tr("Error: 'todo_ids' array cannot be empty. Provide at least one ID."));
|
||||
}
|
||||
|
||||
QList<int> ids;
|
||||
for (const QJsonValue &idValue : idsArray) {
|
||||
int id = idValue.toInt(-1);
|
||||
if (id > 0) {
|
||||
ids.append(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.isEmpty()) {
|
||||
throw ToolRuntimeError(
|
||||
tr("Error: All IDs in 'todo_ids' array are invalid. IDs must be positive "
|
||||
"integers."));
|
||||
}
|
||||
|
||||
return completeTodos(sessionId, ids);
|
||||
|
||||
} else if (operation == "list") {
|
||||
return listTodos(sessionId);
|
||||
|
||||
} else {
|
||||
throw ToolRuntimeError(
|
||||
tr("Error: Unknown operation '%1'. Valid operations: 'add', 'complete', 'list'")
|
||||
.arg(operation));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TodoTool::clearSession(const QString &sessionId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_sessionTodos.remove(sessionId);
|
||||
m_sessionNextId.remove(sessionId);
|
||||
}
|
||||
|
||||
QString TodoTool::addTodos(const QString &sessionId, const QStringList &tasks)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
if (!m_sessionTodos.contains(sessionId)) {
|
||||
m_sessionTodos[sessionId] = QHash<int, TodoItem>();
|
||||
m_sessionNextId[sessionId] = 1;
|
||||
}
|
||||
|
||||
for (const QString &task : tasks) {
|
||||
const int newId = m_sessionNextId[sessionId]++;
|
||||
m_sessionTodos[sessionId][newId] = {newId, task, false};
|
||||
}
|
||||
|
||||
const QString summary = (tasks.size() == 1) ? tr("✓ Added 1 new task")
|
||||
: tr("✓ Added %1 new tasks").arg(tasks.size());
|
||||
|
||||
return QString("%1\n\n%2").arg(summary, listTodosLocked(sessionId));
|
||||
}
|
||||
|
||||
QString TodoTool::completeTodos(const QString &sessionId, const QList<int> &todoIds)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
if (!m_sessionTodos.contains(sessionId)) {
|
||||
throw ToolRuntimeError(tr("Error: No todos found in this session"));
|
||||
}
|
||||
|
||||
auto &todos = m_sessionTodos[sessionId];
|
||||
int completedCount = 0;
|
||||
int alreadyCompletedCount = 0;
|
||||
QStringList notFound;
|
||||
|
||||
for (const int todoId : todoIds) {
|
||||
if (!todos.contains(todoId)) {
|
||||
notFound.append(QString("#%1").arg(todoId));
|
||||
continue;
|
||||
}
|
||||
|
||||
TodoItem &item = todos[todoId];
|
||||
if (item.completed) {
|
||||
alreadyCompletedCount++;
|
||||
} else {
|
||||
item.completed = true;
|
||||
completedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
QStringList messages;
|
||||
if (completedCount > 0) {
|
||||
messages.append((completedCount == 1) ? tr("✓ Marked 1 task as completed")
|
||||
: tr("✓ Marked %1 tasks as completed")
|
||||
.arg(completedCount));
|
||||
}
|
||||
|
||||
if (alreadyCompletedCount > 0) {
|
||||
messages.append(tr("⚠ %1 already completed").arg(alreadyCompletedCount));
|
||||
}
|
||||
|
||||
if (!notFound.isEmpty()) {
|
||||
messages.append(tr("❌ Not found: %1").arg(notFound.join(", ")));
|
||||
}
|
||||
|
||||
const QString summary = messages.join(", ");
|
||||
return QString("%1\n\n%2").arg(summary, listRemainingTodosLocked(sessionId));
|
||||
}
|
||||
|
||||
QString TodoTool::listTodos(const QString &sessionId) const
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
return listTodosLocked(sessionId);
|
||||
}
|
||||
|
||||
QString TodoTool::listTodosLocked(const QString &sessionId) const
|
||||
{
|
||||
const auto it = m_sessionTodos.constFind(sessionId);
|
||||
if (it == m_sessionTodos.constEnd() || it->isEmpty()) {
|
||||
return tr("📋 TODO List: (empty)");
|
||||
}
|
||||
|
||||
const auto &todos = *it;
|
||||
QList<int> ids = todos.keys();
|
||||
std::sort(ids.begin(), ids.end());
|
||||
|
||||
QStringList lines;
|
||||
lines.reserve(ids.size() + 4);
|
||||
lines.append(tr("📋 TODO List:"));
|
||||
lines.append("");
|
||||
|
||||
int completedCount = 0;
|
||||
for (const int id : ids) {
|
||||
const TodoItem &item = todos[id];
|
||||
const QString checkbox = item.completed ? "[x]" : "[ ]";
|
||||
const QString strikethrough = item.completed ? QString("~~") : QString("");
|
||||
|
||||
lines.append(QString("%1 **#%2** %3%4%5")
|
||||
.arg(checkbox)
|
||||
.arg(id)
|
||||
.arg(strikethrough, item.task, strikethrough));
|
||||
|
||||
if (item.completed) {
|
||||
completedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
lines.append("");
|
||||
const int totalCount = ids.size();
|
||||
const int percentage = totalCount > 0 ? (completedCount * 100) / totalCount : 0;
|
||||
lines.append(
|
||||
tr("Progress: %1/%2 completed (%3%)").arg(completedCount).arg(totalCount).arg(percentage));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
QString TodoTool::listRemainingTodosLocked(const QString &sessionId) const
|
||||
{
|
||||
const auto it = m_sessionTodos.constFind(sessionId);
|
||||
if (it == m_sessionTodos.constEnd() || it->isEmpty()) {
|
||||
return tr("📋 All tasks completed! 🎉");
|
||||
}
|
||||
|
||||
const auto &todos = *it;
|
||||
QList<int> ids = todos.keys();
|
||||
std::sort(ids.begin(), ids.end());
|
||||
|
||||
int completedCount = 0;
|
||||
QStringList remainingLines;
|
||||
|
||||
for (const int id : ids) {
|
||||
const TodoItem &item = todos[id];
|
||||
if (item.completed) {
|
||||
completedCount++;
|
||||
} else {
|
||||
remainingLines.append(QString("[ ] **#%1** %2").arg(id).arg(item.task));
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingLines.isEmpty()) {
|
||||
return tr("📋 All tasks completed! 🎉");
|
||||
}
|
||||
|
||||
QStringList lines;
|
||||
lines.reserve(remainingLines.size() + 4);
|
||||
lines.append(tr("📋 Remaining tasks:"));
|
||||
lines.append("");
|
||||
lines.append(remainingLines);
|
||||
lines.append("");
|
||||
|
||||
const int totalCount = ids.size();
|
||||
const int percentage = totalCount > 0 ? (completedCount * 100) / totalCount : 0;
|
||||
lines.append(
|
||||
tr("Progress: %1/%2 completed (%3%)").arg(completedCount).arg(totalCount).arg(percentage));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
66
tools/TodoTool.hpp
Normal file
66
tools/TodoTool.hpp
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 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/BaseTool.hpp>
|
||||
|
||||
#include <QHash>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
struct TodoItem
|
||||
{
|
||||
int id;
|
||||
QString task;
|
||||
bool completed;
|
||||
};
|
||||
|
||||
class TodoTool : public LLMCore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TodoTool(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString stringName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
|
||||
LLMCore::ToolPermissions requiredPermissions() const override;
|
||||
|
||||
QFuture<QString> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
void clearSession(const QString &sessionId);
|
||||
|
||||
private:
|
||||
QString addTodos(const QString &sessionId, const QStringList &tasks);
|
||||
QString completeTodos(const QString &sessionId, const QList<int> &todoIds);
|
||||
QString listTodos(const QString &sessionId) const;
|
||||
QString listTodosLocked(const QString &sessionId) const;
|
||||
QString listRemainingTodosLocked(const QString &sessionId) const;
|
||||
|
||||
mutable QMutex m_mutex;
|
||||
QHash<QString, QHash<int, TodoItem>> m_sessionTodos;
|
||||
QHash<QString, int> m_sessionNextId;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
@ -34,6 +34,7 @@
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
#include "ProjectSearchTool.hpp"
|
||||
#include "ReadVisibleFilesTool.hpp"
|
||||
#include "TodoTool.hpp"
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
@ -54,6 +55,7 @@ void ToolsFactory::registerTools()
|
||||
registerTool(new ExecuteTerminalCommandTool(this));
|
||||
registerTool(new ProjectSearchTool(this));
|
||||
registerTool(new FindAndReadFileTool(this));
|
||||
registerTool(new TodoTool(this));
|
||||
|
||||
LOG_MESSAGE(QString("Registered %1 tools").arg(m_tools.size()));
|
||||
}
|
||||
@ -107,6 +109,10 @@ QJsonArray ToolsFactory::getToolsDefinitions(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (it.value()->name() == "todo_tool" && !settings.enableTodoTool()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto requiredPerms = it.value()->requiredPermissions();
|
||||
|
||||
if (filter != LLMCore::RunToolsFilter::ALL) {
|
||||
|
||||
@ -18,7 +18,13 @@
|
||||
*/
|
||||
|
||||
#include "ToolsManager.hpp"
|
||||
#include "TodoTool.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include <QTimer>
|
||||
|
||||
namespace {
|
||||
constexpr int kToolExecutionDelayMs = 300;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
@ -74,6 +80,10 @@ void ToolsManager::executeToolCall(
|
||||
|
||||
QJsonObject modifiedInput = input;
|
||||
modifiedInput["_request_id"] = requestId;
|
||||
|
||||
if (!m_currentSessionId.isEmpty()) {
|
||||
modifiedInput["session_id"] = m_currentSessionId;
|
||||
}
|
||||
|
||||
PendingTool pendingTool{toolId, toolName, modifiedInput, "", false};
|
||||
queue.queue.append(pendingTool);
|
||||
@ -140,27 +150,21 @@ QJsonArray ToolsManager::getToolsDefinitions(
|
||||
void ToolsManager::cleanupRequest(const QString &requestId)
|
||||
{
|
||||
if (m_toolQueues.contains(requestId)) {
|
||||
LOG_MESSAGE(QString("ToolsManager: Canceling pending tools for request %1").arg(requestId));
|
||||
m_toolHandler->cleanupRequest(requestId);
|
||||
m_toolQueues.remove(requestId);
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("ToolsManager: Cleaned up request %1").arg(requestId));
|
||||
}
|
||||
|
||||
void ToolsManager::onToolFinished(
|
||||
const QString &requestId, const QString &toolId, const QString &result, bool success)
|
||||
{
|
||||
if (!m_toolQueues.contains(requestId)) {
|
||||
LOG_MESSAGE(QString("ToolsManager: Tool result for unknown request %1").arg(requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
auto &queue = m_toolQueues[requestId];
|
||||
|
||||
if (!queue.completed.contains(toolId)) {
|
||||
LOG_MESSAGE(QString("ToolsManager: Tool result for unknown tool %1 in request %2")
|
||||
.arg(toolId, requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -173,7 +177,13 @@ void ToolsManager::onToolFinished(
|
||||
.arg(success ? QString("completed") : QString("failed"))
|
||||
.arg(requestId));
|
||||
|
||||
executeNextTool(requestId);
|
||||
if (kToolExecutionDelayMs > 0 && !queue.queue.isEmpty()) {
|
||||
QTimer::singleShot(kToolExecutionDelayMs, this, [this, requestId]() {
|
||||
executeNextTool(requestId);
|
||||
});
|
||||
} else {
|
||||
executeNextTool(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
ToolsFactory *ToolsManager::toolsFactory() const
|
||||
@ -197,4 +207,17 @@ QHash<QString, QString> ToolsManager::getToolResults(const QString &requestId) c
|
||||
return results;
|
||||
}
|
||||
|
||||
void ToolsManager::clearTodoSession(const QString &sessionId)
|
||||
{
|
||||
auto *todoTool = qobject_cast<TodoTool *>(m_toolsFactory->getToolByName("todo_tool"));
|
||||
if (todoTool) {
|
||||
todoTool->clearSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
void ToolsManager::setCurrentSessionId(const QString &sessionId)
|
||||
{
|
||||
m_currentSessionId = sessionId;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
#include "ToolHandler.hpp"
|
||||
#include "ToolsFactory.hpp"
|
||||
#include <llmcore/BaseTool.hpp>
|
||||
#include <llmcore/IToolsManager.hpp>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
@ -46,7 +47,7 @@ struct ToolQueue
|
||||
bool isExecuting = false;
|
||||
};
|
||||
|
||||
class ToolsManager : public QObject
|
||||
class ToolsManager : public QObject, public LLMCore::IToolsManager
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@ -57,12 +58,15 @@ public:
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &input);
|
||||
const QJsonObject &input) override;
|
||||
|
||||
QJsonArray getToolsDefinitions(
|
||||
LLMCore::ToolSchemaFormat format,
|
||||
LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL) const;
|
||||
void cleanupRequest(const QString &requestId);
|
||||
LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL) const override;
|
||||
|
||||
void cleanupRequest(const QString &requestId) override;
|
||||
void setCurrentSessionId(const QString &sessionId) override;
|
||||
void clearTodoSession(const QString &sessionId) override;
|
||||
|
||||
ToolsFactory *toolsFactory() const;
|
||||
|
||||
@ -77,6 +81,7 @@ private:
|
||||
ToolsFactory *m_toolsFactory;
|
||||
ToolHandler *m_toolHandler;
|
||||
QHash<QString, ToolQueue> m_toolQueues;
|
||||
QString m_currentSessionId;
|
||||
|
||||
void executeNextTool(const QString &requestId);
|
||||
QHash<QString, QString> getToolResults(const QString &requestId) const;
|
||||
|
||||
@ -22,6 +22,11 @@
|
||||
#include "CustomInstructionsManager.hpp"
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
#include "settings/ConfigurationManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
#include "settings/SettingsConstants.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
@ -30,12 +35,15 @@
|
||||
#include <QDir>
|
||||
#include <QFontMetrics>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QScreen>
|
||||
#include <QStringListModel>
|
||||
#include <QSvgRenderer>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QUrl>
|
||||
@ -48,6 +56,43 @@
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
static QIcon createThemedIcon(const QString &svgPath, const QColor &color)
|
||||
{
|
||||
QSvgRenderer renderer(svgPath);
|
||||
if (!renderer.isValid()) {
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
QSize iconSize(16, 16);
|
||||
QPixmap pixmap(iconSize);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
renderer.render(&painter);
|
||||
painter.end();
|
||||
|
||||
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
|
||||
|
||||
uchar *bits = image.bits();
|
||||
const int bytesPerPixel = 4;
|
||||
const int totalBytes = image.width() * image.height() * bytesPerPixel;
|
||||
|
||||
const int newR = color.red();
|
||||
const int newG = color.green();
|
||||
const int newB = color.blue();
|
||||
|
||||
for (int i = 0; i < totalBytes; i += bytesPerPixel) {
|
||||
int alpha = bits[i + 3];
|
||||
if (alpha > 0) {
|
||||
bits[i] = newB;
|
||||
bits[i + 1] = newG;
|
||||
bits[i + 2] = newR;
|
||||
}
|
||||
}
|
||||
|
||||
return QIcon(QPixmap::fromImage(image));
|
||||
}
|
||||
|
||||
QuickRefactorDialog::QuickRefactorDialog(QWidget *parent, const QString &lastInstructions)
|
||||
: QDialog(parent)
|
||||
, m_lastInstructions(lastInstructions)
|
||||
@ -75,6 +120,57 @@ void QuickRefactorDialog::setupUi()
|
||||
actionsLayout->addWidget(m_improveButton);
|
||||
actionsLayout->addWidget(m_alternativeButton);
|
||||
actionsLayout->addStretch();
|
||||
|
||||
m_configComboBox = new QComboBox(this);
|
||||
m_configComboBox->setMinimumWidth(200);
|
||||
m_configComboBox->setToolTip(Tr::tr("Switch AI configuration"));
|
||||
actionsLayout->addWidget(m_configComboBox);
|
||||
|
||||
Utils::Theme *theme = Utils::creatorTheme();
|
||||
QColor iconColor = theme ? theme->color(Utils::Theme::TextColorNormal) : QColor(Qt::white);
|
||||
|
||||
m_toolsIconOn = createThemedIcon(":/qt/qml/ChatView/icons/tools-icon-on.svg", iconColor);
|
||||
m_toolsIconOff = createThemedIcon(":/qt/qml/ChatView/icons/tools-icon-off.svg", iconColor);
|
||||
|
||||
m_toolsButton = new QToolButton(this);
|
||||
m_toolsButton->setCheckable(true);
|
||||
m_toolsButton->setChecked(Settings::quickRefactorSettings().useTools());
|
||||
m_toolsButton->setIcon(m_toolsButton->isChecked() ? m_toolsIconOn : m_toolsIconOff);
|
||||
m_toolsButton->setToolTip(Tr::tr("Enable/Disable AI Tools"));
|
||||
m_toolsButton->setIconSize(QSize(16, 16));
|
||||
actionsLayout->addWidget(m_toolsButton);
|
||||
|
||||
connect(m_toolsButton, &QToolButton::toggled, this, [this](bool checked) {
|
||||
m_toolsButton->setIcon(checked ? m_toolsIconOn : m_toolsIconOff);
|
||||
Settings::quickRefactorSettings().useTools.setValue(checked);
|
||||
Settings::quickRefactorSettings().writeSettings();
|
||||
});
|
||||
|
||||
m_thinkingIconOn = createThemedIcon(":/qt/qml/ChatView/icons/thinking-icon-on.svg", iconColor);
|
||||
m_thinkingIconOff = createThemedIcon(":/qt/qml/ChatView/icons/thinking-icon-off.svg", iconColor);
|
||||
|
||||
m_thinkingButton = new QToolButton(this);
|
||||
m_thinkingButton->setCheckable(true);
|
||||
m_thinkingButton->setChecked(Settings::quickRefactorSettings().useThinking());
|
||||
m_thinkingButton->setIcon(m_thinkingButton->isChecked() ? m_thinkingIconOn : m_thinkingIconOff);
|
||||
m_thinkingButton->setToolTip(Tr::tr("Enable/Disable Thinking Mode"));
|
||||
m_thinkingButton->setIconSize(QSize(16, 16));
|
||||
actionsLayout->addWidget(m_thinkingButton);
|
||||
|
||||
connect(m_thinkingButton, &QToolButton::toggled, this, [this](bool checked) {
|
||||
m_thinkingButton->setIcon(checked ? m_thinkingIconOn : m_thinkingIconOff);
|
||||
Settings::quickRefactorSettings().useThinking.setValue(checked);
|
||||
Settings::quickRefactorSettings().writeSettings();
|
||||
});
|
||||
|
||||
m_settingsButton = new QToolButton(this);
|
||||
m_settingsButton->setIcon(Utils::Icons::SETTINGS_TOOLBAR.icon());
|
||||
m_settingsButton->setToolTip(Tr::tr("Open Quick Refactor Settings"));
|
||||
m_settingsButton->setIconSize(QSize(16, 16));
|
||||
actionsLayout->addWidget(m_settingsButton);
|
||||
|
||||
connect(m_settingsButton, &QToolButton::clicked, this, &QuickRefactorDialog::onOpenSettings);
|
||||
|
||||
mainLayout->addLayout(actionsLayout);
|
||||
|
||||
QHBoxLayout *instructionsLayout = new QHBoxLayout();
|
||||
@ -149,6 +245,13 @@ void QuickRefactorDialog::setupUi()
|
||||
mainLayout->addWidget(m_textEdit);
|
||||
|
||||
loadCustomCommands();
|
||||
loadAvailableConfigurations();
|
||||
|
||||
connect(
|
||||
m_configComboBox,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this,
|
||||
&QuickRefactorDialog::onConfigurationChanged);
|
||||
|
||||
QDialogButtonBox *buttonBox
|
||||
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
@ -476,4 +579,65 @@ void QuickRefactorDialog::onOpenInstructionsFolder()
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onOpenSettings()
|
||||
{
|
||||
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID);
|
||||
}
|
||||
|
||||
QString QuickRefactorDialog::selectedConfiguration() const
|
||||
{
|
||||
return m_selectedConfiguration;
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::loadAvailableConfigurations()
|
||||
{
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
QVector<Settings::AIConfiguration> configs
|
||||
= manager.configurations(Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
m_configComboBox->clear();
|
||||
m_configComboBox->addItem(Tr::tr("Current"), QString());
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
m_configComboBox->addItem(config.name, config.id);
|
||||
}
|
||||
|
||||
auto &settings = Settings::generalSettings();
|
||||
QString currentProvider = settings.qrProvider.value();
|
||||
QString currentModel = settings.qrModel.value();
|
||||
QString currentConfigText = QString("%1/%2").arg(currentProvider, currentModel);
|
||||
m_configComboBox->setItemText(0, Tr::tr("Current (%1)").arg(currentConfigText));
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onConfigurationChanged(int index)
|
||||
{
|
||||
if (index == 0) {
|
||||
m_selectedConfiguration.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
QString configId = m_configComboBox->itemData(index).toString();
|
||||
m_selectedConfiguration = m_configComboBox->itemText(index);
|
||||
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
Settings::AIConfiguration config
|
||||
= manager.getConfigurationById(configId, Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
if (!config.id.isEmpty()) {
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
settings.qrProvider.setValue(config.provider);
|
||||
settings.qrModel.setValue(config.model);
|
||||
settings.qrTemplate.setValue(config.templateName);
|
||||
settings.qrUrl.setValue(config.url);
|
||||
settings.qrEndpointMode.setValue(
|
||||
settings.qrEndpointMode.indexForDisplay(config.endpointMode));
|
||||
settings.qrCustomEndpoint.setValue(config.customEndpoint);
|
||||
|
||||
settings.writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -46,6 +46,8 @@ public:
|
||||
|
||||
Action selectedAction() const;
|
||||
|
||||
QString selectedConfiguration() const;
|
||||
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
@ -58,7 +60,10 @@ private slots:
|
||||
void onEditCustomCommand();
|
||||
void onDeleteCustomCommand();
|
||||
void onOpenInstructionsFolder();
|
||||
void onOpenSettings();
|
||||
void loadCustomCommands();
|
||||
void loadAvailableConfigurations();
|
||||
void onConfigurationChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
@ -73,11 +78,21 @@ private:
|
||||
QToolButton *m_editCommandButton;
|
||||
QToolButton *m_deleteCommandButton;
|
||||
QToolButton *m_openFolderButton;
|
||||
QToolButton *m_settingsButton;
|
||||
QToolButton *m_toolsButton;
|
||||
QToolButton *m_thinkingButton;
|
||||
QComboBox *m_commandsComboBox;
|
||||
QComboBox *m_configComboBox;
|
||||
QLabel *m_instructionsLabel;
|
||||
|
||||
Action m_selectedAction = Action::Custom;
|
||||
QString m_lastInstructions;
|
||||
QString m_selectedConfiguration;
|
||||
|
||||
QIcon m_toolsIconOn;
|
||||
QIcon m_toolsIconOff;
|
||||
QIcon m_thinkingIconOn;
|
||||
QIcon m_thinkingIconOff;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
Reference in New Issue
Block a user